domma-cms 0.88.0 → 0.88.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- .dashboard-layout{display:flex;flex-direction:column;height:100vh;min-height:100vh;overflow:hidden}.dashboard-wrapper{display:flex;flex:1;min-height:0;overflow:hidden}.dashboard-main{flex:1;min-width:0;overflow-y:auto}#admin-sidebar.dashboard-sidebar{display:flex;flex-direction:column;flex-shrink:0;overflow:hidden}#admin-sidebar .dm-admin-sidebar{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;scrollbar-width:thin;scrollbar-color:var(--dm-border) transparent}#admin-sidebar .dm-admin-sidebar::-webkit-scrollbar{width:8px}#admin-sidebar .dm-admin-sidebar::-webkit-scrollbar-track{background:transparent}#admin-sidebar .dm-admin-sidebar::-webkit-scrollbar-thumb{background:var(--dm-border);border-radius:4px}#admin-sidebar .dm-admin-sidebar::-webkit-scrollbar-thumb:hover{background:var(--dm-text-muted)}.view-container{padding:1.5rem 2rem;max-width:1200px}.view-container:has(>.dm-plugin-shell){max-width:100%}.view-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:1.5rem;gap:1rem}.view-header h1{font-size:1.5rem;font-weight:700;margin:0;display:flex;align-items:center;gap:.5rem}.view-header-actions{display:flex;gap:.75rem;flex-wrap:wrap}.view-header-actions .btn-primary,.view-header-actions .btn-success,.view-header-actions .btn-danger{margin-left:.5rem}.stat-card .card-body{display:flex;align-items:center;gap:1rem}.stat-icon{width:44px;height:44px;border-radius:8px;background:color-mix(in srgb,var(--dm-primary) 14%,transparent);color:var(--dm-primary, #5b8cff);display:flex;align-items:center;justify-content:center;font-size:1.25rem;flex-shrink:0}.stat-icon--success{background:color-mix(in srgb,var(--dm-success) 15%,transparent);color:var(--dm-success)}.stat-icon--warning{background:color-mix(in srgb,var(--dm-warning) 15%,transparent);color:var(--dm-warning)}.stat-value{font-size:1.75rem;font-weight:700;line-height:1}.stat-label{font-size:.8rem;color:var(--dm-text-muted, #888);margin-top:.25rem}.toolbar{display:flex;align-items:center;gap:.75rem}.form-check-label{display:flex;align-items:center;gap:.5rem;cursor:pointer;font-size:.9rem}.form-hint{display:block;font-size:.8rem;color:var(--dm-text-muted, #888);margin-top:.25rem}.dashboard-main:has(#editor-meta-tabs){overflow:hidden}.view-container:has(#editor-meta-tabs){display:flex;flex-direction:column;height:calc(100dvh - 60px);max-width:100%;padding-bottom:0}.view-container:has(#block-editor-body){max-width:100%}.view-container:has(#ep-editor-tabs){max-width:100%}.ep-editor-grid{display:grid;grid-template-columns:minmax(0,1.25fr) minmax(0,1fr) minmax(0,1.3fr);gap:1rem;align-items:start}.ep-editor-grid>.ep-tryit-col,.ep-editor-grid>.ep-results-col{position:sticky;top:1rem;display:flex;flex-direction:column;gap:1rem;min-width:0}@media(max-width:1500px){.ep-editor-grid{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.ep-editor-grid>:first-child{grid-column:1 / -1}.ep-editor-grid>.ep-tryit-col,.ep-editor-grid>.ep-results-col{position:static}}@media(max-width:1024px){.ep-editor-grid{grid-template-columns:minmax(0,1fr)}}#editor-meta-tabs{flex:1;min-height:0;display:flex;flex-direction:column}#editor-meta-tabs .tab-content{flex:1;min-height:0;overflow:hidden}#editor-meta-tabs .tab-panel{height:100%;overflow-y:auto}#editor-meta-tabs .tab-panel:has(.editor-card),#live-preview-panel{overflow:hidden}.live-preview-bar{display:flex;align-items:center;gap:var(--dm-space-4);flex-wrap:wrap;padding:var(--dm-space-2) 0;border-bottom:1px solid var(--dm-border);margin-bottom:var(--dm-space-2)}.live-preview-widths{display:inline-flex;border:1px solid var(--dm-border);border-radius:var(--dm-radius-sm);overflow:hidden}.live-preview-width{display:inline-flex;align-items:center;justify-content:center;padding:.3rem .55rem;border:none;background:transparent;color:inherit;cursor:pointer;line-height:1}.live-preview-width+.live-preview-width{border-left:1px solid var(--dm-border)}.live-preview-width:hover{background:var(--dm-hover-bg)}.live-preview-width.active{background:var(--dm-active-bg)}.live-preview-status{flex:1 1 auto;font-size:.8rem;opacity:.75}.live-preview-actions{display:flex;gap:var(--dm-space-1)}.live-preview-stage{display:flex;justify-content:center;height:calc(100vh - 230px);overflow:auto;background:var(--dm-background-alt);border-radius:var(--dm-radius-md)}.page-live-preview-frame{display:block;width:100%;max-width:100%;height:100%;border:none;border-radius:6px;background:var(--dm-background);transition:max-width .18s ease}.page-live-preview-frame[data-width="1024"]{max-width:1024px}.page-live-preview-frame[data-width="390"]{max-width:390px}.page-live-preview-frame[data-width="1024"],.page-live-preview-frame[data-width="390"]{border-left:1px solid var(--dm-border);border-right:1px solid var(--dm-border)}.editor-card{display:flex;flex-direction:column;height:100%}.editor-card .card-body{display:flex;flex-direction:column;flex:1;min-height:0;padding:0!important}.editor-toolbar{display:flex;align-items:center;gap:2px;padding:6px 10px;border-bottom:1px solid var(--dm-border, rgba(255, 255, 255, .08));flex-wrap:wrap;flex-shrink:0}.editor-toolbar-btn{background:none;border:none;color:var(--dm-text-muted, #888);padding:6px 8px;border-radius:4px;cursor:pointer;display:flex;align-items:center;transition:color .15s,background .15s}.editor-toolbar-btn:hover{color:var(--dm-text, #eee);background:var(--dm-hover-bg)}.editor-toolbar-sep{width:1px;height:20px;background:var(--dm-border, rgba(255, 255, 255, .08));margin:0 4px;flex-shrink:0}.editor-toolbar-right{margin-left:auto;display:flex;align-items:center;gap:2px}.editor-view-btn{background:none;border:none;color:var(--dm-text-muted, #888);padding:6px 8px;border-radius:4px;cursor:pointer;display:flex;align-items:center;transition:color .15s,background .15s}.editor-view-btn:hover{color:var(--dm-text, #eee);background:var(--dm-hover-bg)}.editor-view-btn.active{color:var(--dm-primary, #5b8cff);background:color-mix(in srgb,var(--dm-primary) 14%,transparent)}.editor-body{display:flex;flex:1;min-height:0;--dm-editor-split: 50%}@media(min-width:769px){.editor-body:not(.editor-mode-write):not(.editor-mode-preview) .editor-pane--write{flex:0 0 var(--dm-editor-split, 50%)}.editor-body:not(.editor-mode-write):not(.editor-mode-preview) .editor-pane--preview{flex:1 1 0}}.editor-pane{flex:1;display:flex;flex-direction:column;min-width:0}.editor-pane--write,.editor-pane--preview{overflow:hidden}.editor-pane--write{flex-direction:row}.editor-line-numbers{padding:1rem .6rem 1rem .75rem;background:var(--dm-background-alt, rgba(0, 0, 0, .15));border-right:1px solid var(--dm-border, rgba(255, 255, 255, .08));color:var(--dm-text-muted, #666);font-family:Fira Code,Courier New,monospace;font-size:.9rem;line-height:1.6;text-align:right;user-select:none;white-space:pre;overflow:hidden;flex-shrink:0;min-width:2.5rem}.editor-line-numbers--foldable{padding:1rem 0;min-width:3rem;white-space:normal;text-align:left;display:flex;flex-direction:column}.editor-line-number-row{display:flex;align-items:center;justify-content:flex-end;padding-right:.6rem;gap:.15rem}.fold-toggle{display:inline-flex;align-items:center;justify-content:center;width:1em;font-size:.6rem;cursor:default;color:transparent}.fold-toggle--open,.fold-toggle--folded{cursor:pointer;color:var(--dm-text-muted, #666);transition:color .1s}.fold-toggle--open:hover,.fold-toggle--folded:hover{color:var(--dm-primary, #4a9eff)}.editor-line-num{min-width:1.5rem;text-align:right}.editor-mode-write .editor-pane--preview,.editor-mode-write .editor-divider,.editor-mode-preview .editor-pane--write,.editor-mode-preview .editor-divider{display:none}.editor-divider{width:7px;background:linear-gradient(to right,transparent 0,transparent 3px,var(--dm-border, rgba(255,255,255,.08)) 3px,var(--dm-border, rgba(255,255,255,.08)) 4px,transparent 4px,transparent 100%);cursor:col-resize;flex-shrink:0;touch-action:none;transition:background .12s ease}.editor-divider:hover,.editor-divider:focus-visible,.editor-divider.is-dragging{background:linear-gradient(to right,transparent 0,transparent 2px,var(--dm-primary, #5b8cff) 2px,var(--dm-primary, #5b8cff) 5px,transparent 5px,transparent 100%);outline:none}.editor-body.is-resizing .editor-pane{pointer-events:none}.editor-body.is-resizing{cursor:col-resize;user-select:none}.editor-textarea{flex:1;resize:none;border:none;border-radius:0;background:transparent;font-family:Fira Code,Courier New,monospace;font-size:.9rem;padding:1rem;outline:none;color:inherit;line-height:1.6}.editor-preview{flex:1;display:block;width:100%;border:none;background:var(--dm-background)}.editor-fullscreen{position:fixed;inset:0;z-index:1000;border-radius:0;margin:0}.editor-fullscreen .card-body{height:100vh}.media-picker-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:.75rem;max-height:400px;overflow-y:auto;padding:.25rem}.media-picker-item{cursor:pointer;border:2px solid transparent;border-radius:6px;overflow:hidden;transition:border-color .15s}.media-picker-item:hover{border-color:var(--dm-primary, #5b8cff)}.media-picker-item img{width:100%;height:80px;object-fit:cover;display:block}.media-picker-item span{display:block;font-size:.75rem;padding:4px 6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.docs-body h3{margin-top:1.25rem;margin-bottom:.5rem}.docs-body p{margin-bottom:.5rem;line-height:1.6}.docs-body ol,.docs-body ul{margin-bottom:.75rem;padding-left:1.25rem}.docs-body li{margin-bottom:.25rem}.docs-body hr{margin:1.25rem 0;border-color:var(--dm-border, rgba(255, 255, 255, .08))}.docs-body .table{margin-bottom:.75rem}.nav-items-header,.nav-item-row{display:flex;gap:.5rem;align-items:center;padding:.35rem .75rem}.nav-items-header{border-bottom:1px solid var(--dm-border, rgba(255,255,255,.08));padding-bottom:.4rem;margin-bottom:.25rem}.nav-item-row{border-bottom:1px solid var(--dm-border, rgba(255,255,255,.04))}.nav-item-row:last-child{border-bottom:none}.nav-col-indent{width:18px;flex-shrink:0;color:var(--dm-text-muted, #888);text-align:center;font-size:.85rem}.nav-col-main{flex:2;min-width:0}.nav-col-icon{flex:0 0 90px}.nav-col-parent{flex:2;min-width:0}.nav-col-action{flex-shrink:0}.nav-item-row--child{background:color-mix(in srgb,var(--dm-text) 3%,transparent)}.nav-col-action{display:flex;align-items:center;gap:.25rem}.nav-item-row--hidden{opacity:.45}.nav-item-row--hidden .item-text,.nav-item-row--hidden .footer-link-text{text-decoration:line-through}.btn-toggle-hidden{color:var(--dm-text-muted, #888)}.btn-toggle-hidden.active{color:var(--dm-warning, #f59e0b)}.fb-field-card.fb-drag-over{outline:2px dashed var(--dm-primary, #6366f1);outline-offset:-2px}.btn-ghost span[data-icon=trash]{color:var(--dm-danger, #dc2626)}.view-container:has(.fb-fields-layout){max-width:none}.fb-fields-layout{display:grid;grid-template-columns:minmax(0,6fr) minmax(320px,4fr);gap:1.25rem;align-items:start}.fb-preview-pane{position:sticky;top:1rem;min-width:0}@media(max-width:1100px){.fb-fields-layout{grid-template-columns:1fr}.fb-preview-pane{position:static}}.toggle-label{display:flex;align-items:center;gap:.4rem;font-size:.9rem;cursor:pointer;margin-right:1rem}.preset-toggles{display:flex;flex-wrap:wrap;margin-top:.75rem}.presets-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:1rem}@media(max-width:900px){.presets-grid{grid-template-columns:repeat(2,1fr)}}@media(max-width:600px){.presets-grid{grid-template-columns:1fr}}.card-header{padding:.5rem 1rem}.card-header h2{margin:0;font-size:.9rem;font-weight:600;letter-spacing:.02em}.preset-card .card-header{display:flex;align-items:center;justify-content:space-between}.preset-card .card-header h3{margin:0;font-size:1rem}.media-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:1rem}.media-card{background:var(--dm-card-bg, rgba(255,255,255,.04));border:1px solid var(--dm-border, rgba(255,255,255,.08));border-radius:8px;overflow:hidden}.media-preview{height:130px;display:flex;align-items:center;justify-content:center;background:#0003;overflow:hidden}.media-thumb{width:100%;height:100%;object-fit:cover}.media-thumb--file{font-size:2rem;color:var(--dm-text-muted, #888)}.media-info{padding:.5rem .75rem;display:flex;flex-direction:column;gap:.15rem}.media-name{font-size:.8rem;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:default}.media-rename-input{width:100%;font-size:.8rem;font-weight:500;padding:.1rem .25rem;border:1px solid var(--dm-primary, #7c6af7);border-radius:3px;background:var(--dm-input-bg, transparent);color:inherit;outline:none}.media-size{font-size:.75rem;color:var(--dm-text-muted, #888)}.media-actions{padding:.5rem .75rem;display:flex;gap:.4rem;border-top:1px solid var(--dm-border, rgba(255,255,255,.08))}#admin-topbar{height:60px;display:flex;align-items:center;gap:1rem;padding:0 1.25rem;background:var(--dm-navbar-bg, rgba(0,0,0,.3));border-bottom:1px solid var(--dm-border, rgba(255,255,255,.08));position:sticky;top:0;z-index:200;flex-shrink:0}.topbar-brand{display:flex;align-items:center;gap:.5rem;font-weight:700;font-size:.95rem;color:inherit;flex-shrink:0;margin-right:.5rem}.topbar-brand span[data-icon],.topbar-brand svg{color:var(--dm-primary, #5b8cff);width:22px;height:22px}.topbar-brand-text{opacity:.85}.topbar-brand-logo{height:28px;width:auto;display:inline-block;vertical-align:middle;margin-right:.4em;object-fit:contain}.topbar-user{display:flex;align-items:center;gap:.6rem;flex:1}.topbar-user-name{font-size:.875rem;font-weight:500}.topbar-avatar{width:1.75rem;height:1.75rem;border-radius:50%;object-fit:cover;flex-shrink:0}.topbar-role-badge{font-size:.7rem;font-weight:600;letter-spacing:.04em;text-transform:uppercase;padding:.15rem .45rem;border-radius:4px}.topbar-role-badge--admin{background:color-mix(in srgb,var(--dm-primary) 15%,transparent);color:var(--dm-primary)}.topbar-role-badge--manager{background:color-mix(in srgb,var(--dm-success) 15%,transparent);color:var(--dm-success)}.topbar-role-badge--editor{background:color-mix(in srgb,var(--dm-warning) 15%,transparent);color:var(--dm-warning)}.topbar-role-badge--subscriber{background:color-mix(in srgb,var(--dm-text-muted) 15%,transparent);color:var(--dm-text-muted)}.topbar-actions{display:flex;align-items:center;gap:.5rem;flex-shrink:0}.topbar-action-link{display:flex;align-items:center;gap:.35rem;font-size:.875rem;color:var(--dm-text-muted, #888);text-decoration:none;padding:.4rem .65rem;border-radius:6px;transition:color .15s,background .15s}.topbar-action-link:hover{color:var(--dm-text, #eee);background:var(--dm-hover-bg)}.topbar-action-link span[data-icon],.topbar-action-link svg{width:16px;height:16px}.topbar-signout{display:flex;align-items:center;gap:.35rem;font-size:.875rem}.topbar-signout span[data-icon],.topbar-signout svg{width:16px;height:16px}.login-wrap{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;padding:1rem;background:var(--dm-background, #0f1117);z-index:100;overflow-y:auto}.login-card{width:100%;max-width:460px}.ob-done-icon{font-size:3rem;color:var(--dm-success);margin-bottom:1rem;text-align:center}.btn-skip{display:block;text-align:center;margin-top:.75rem;font-size:.85rem;color:var(--dm-text-muted, #888);text-decoration:none}.btn-skip:hover{text-decoration:underline}.theme-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:.6rem}.theme-swatch{border:2px solid var(--dm-border, rgba(255,255,255,.1));border-radius:8px;overflow:hidden;cursor:pointer;transition:border-color .15s,transform .1s}.theme-swatch:hover{border-color:var(--dm-primary, #5b8cff);transform:translateY(-1px)}.theme-swatch.selected{border-color:var(--dm-primary, #5b8cff);box-shadow:0 0 0 2px var(--dm-primary, #5b8cff)}.theme-swatch-preview{height:52px;position:relative;display:flex;align-items:flex-end;padding:.35rem}.theme-swatch-accent{width:28px;height:6px;border-radius:3px}.theme-swatch-label{display:flex;justify-content:space-between;align-items:center;padding:.3rem .45rem;font-size:.7rem;background:var(--dm-card-bg, rgba(255,255,255,.04))}.theme-swatch-mode{color:var(--dm-text-muted, #888);font-size:.65rem}.login-logo{display:flex;align-items:center;gap:.75rem;margin-bottom:1.75rem;font-size:1.5rem}.login-logo span[data-icon]{font-size:2rem;color:var(--dm-primary, #5b8cff)}.login-logo h1{margin:0;font-size:1.5rem;font-weight:700}.login-heading{font-size:1.1rem;font-weight:600;margin-bottom:1.25rem}.login-rules{margin:.25rem 0 .75rem;padding-left:1.1rem;color:var(--dm-danger);font-size:.85rem}.login-rules li+li{margin-top:.2rem}.login-small{font-size:.8rem;margin-top:.75rem}.login-icon-warn{color:var(--dm-warning)}.pwf{position:relative;display:block}.pwf>input{width:100%;padding-right:2.5rem}.pwf-toggle{position:absolute;top:50%;right:.35rem;transform:translateY(-50%);display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;padding:0;border:0;border-radius:var(--dm-radius-sm);background:transparent;color:var(--dm-text-muted);cursor:pointer}.pwf-toggle:hover,.pwf-toggle:focus-visible{color:var(--dm-text);background:var(--dm-hover-bg)}.pwf-toggle svg{width:16px;height:16px}.pwf-toggle [data-icon=eye-off],.pwf.is-shown .pwf-toggle [data-icon=eye]{display:none}.pwf.is-shown .pwf-toggle [data-icon=eye-off]{display:inline-block}.pwf-caps{display:block;margin-top:.25rem;color:var(--dm-warning);font-size:.8rem}.pwf-caps[hidden]{display:none}.btn-block{width:100%}.plugins-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:1rem}.plugin-card{display:flex;flex-direction:column;--plugin-accent: var(--dm-primary, #5b8cff);position:relative;overflow:hidden;background-image:radial-gradient(120% 90% at 100% 0%,color-mix(in srgb,var(--plugin-accent) 13%,transparent) 0%,color-mix(in srgb,var(--plugin-accent) 4%,transparent) 42%,transparent 72%)}.plugin-card .plugin-icon{background:color-mix(in srgb,var(--plugin-accent) 16%,transparent);color:var(--plugin-accent)}.plugin-watermark{position:absolute;top:-34px;right:-30px;width:148px;height:148px;display:block;color:var(--plugin-accent);opacity:.09;pointer-events:none;z-index:0}.plugin-card>.card-body,.plugin-card>.plugin-footer{position:relative;z-index:1}.plugin-card:has(.badge-secondary) .plugin-watermark{opacity:.05}@media(prefers-reduced-motion:no-preference){.plugin-card{transition:background-image .2s ease}}.plugin-card .card-body{flex:1}.plugin-header{display:flex;align-items:flex-start;gap:.75rem;margin-bottom:.75rem;flex-wrap:wrap}.plugin-icon{width:40px;height:40px;border-radius:8px;background:color-mix(in srgb,var(--dm-primary) 14%,transparent);color:var(--dm-primary, #5b8cff);display:flex;align-items:center;justify-content:center;flex-shrink:0}.plugin-meta{flex:1 1 140px;min-width:0}.plugin-name{font-weight:600;margin-bottom:.15rem}.plugin-version{font-size:.75rem;color:var(--dm-text-muted, #888)}.plugin-desc{font-size:.875rem;color:var(--dm-text-muted, #888);margin-bottom:1rem}.plugin-footer{display:flex;align-items:center;justify-content:space-between;padding:.75rem 1rem;border-top:1px solid var(--dm-border, rgba(255,255,255,.08))}.plugin-footer-actions{display:flex;align-items:center;gap:.5rem}#admin-sidebar .sidebar-link{position:relative!important}#admin-sidebar .sidebar-badge{position:absolute!important;right:.75rem!important;top:50%!important;transform:translateY(-50%)!important;margin-left:0!important;padding-left:.45rem!important;padding-right:.45rem!important}#admin-sidebar .sidebar-text{padding-right:2rem!important}#admin-sidebar.sidebar-dark{background:var(--dm-surface);border-color:var(--dm-border)}#admin-sidebar.sidebar-dark .sidebar-header{background:var(--dm-surface-raised);border-color:var(--dm-border)}#admin-sidebar.sidebar-dark .sidebar-header-title{color:var(--dm-text)}#admin-sidebar.sidebar-dark .sidebar-link{color:var(--dm-text-muted)}#admin-sidebar.sidebar-dark .sidebar-link:hover{color:var(--dm-text);background:var(--dm-surface-overlay)}#admin-sidebar.sidebar-dark .sidebar-link.active{color:var(--dm-primary);background:color-mix(in srgb,var(--dm-primary) 14%,transparent);border-left-color:var(--dm-primary)}#admin-sidebar.sidebar-dark .sidebar-heading{color:var(--dm-text-muted)}#admin-sidebar.sidebar-dark .sidebar-divider{background:var(--dm-border)}#admin-sidebar.sidebar-dark .sidebar-footer{background:var(--dm-surface-raised);border-color:var(--dm-border)}.sidebar-heading--collapsible{display:flex!important;align-items:center;justify-content:space-between;cursor:pointer;user-select:none}.sidebar-heading-toggle{display:flex;align-items:center;opacity:.5;flex-shrink:0;transition:transform .2s ease}.sidebar-heading--collapsible.is-collapsed .sidebar-heading-toggle{transform:rotate(-90deg)}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.p-0{padding:0!important}.text-muted{color:var(--dm-text-muted, #888)}@media(max-width:768px){.view-container{padding:1rem}.editor-body{flex-direction:column}.editor-divider{width:auto;height:1px;cursor:default;background:var(--dm-border, rgba(255,255,255,.08))}}.image-editor{display:flex;flex-direction:column;gap:0}.image-editor-toolbar{display:flex;flex-direction:row;align-items:center;gap:.25rem;padding:.5rem .75rem;border-bottom:1px solid var(--dm-border, rgba(255, 255, 255, .1))}.image-editor-sep{display:inline-block;width:1px;height:1.5rem;background:var(--dm-border, rgba(255, 255, 255, .15));margin:0 .25rem;flex-shrink:0}.editor-toolbar-btn.active{color:var(--dm-primary, #7c6af7);background:color-mix(in srgb,var(--dm-primary, #7c6af7) 15%,transparent)}.editor-toolbar-caret{font-size:.6rem;line-height:1;margin-left:2px;opacity:.6}.editor-toolbar-dropdown{position:absolute;z-index:1000;background:var(--dm-surface, #1e1e2e);border:1px solid var(--dm-border, rgba(255, 255, 255, .1));border-radius:8px;box-shadow:0 8px 24px #0006;padding:4px;min-width:160px;max-height:70vh;overflow-y:auto}.editor-toolbar-dropdown-heading{padding:6px 10px 3px;color:var(--dm-text-muted, #8a8a9a);font-size:.66rem;text-transform:uppercase;letter-spacing:.06em}.editor-toolbar-dropdown-item{display:flex;align-items:center;gap:8px;width:100%;text-align:left;background:none;border:none;color:var(--dm-text, #ddd);padding:7px 10px;border-radius:5px;cursor:pointer;font-size:.85rem;transition:background .12s,color .12s}.editor-toolbar-dropdown-item:hover{background:var(--dm-hover-bg);color:var(--dm-text, #fff)}.editor-toolbar-dropdown-item span[data-icon],.editor-toolbar-dropdown-item svg{width:15px;height:15px;flex-shrink:0;opacity:.75}.editor-toolbar{position:relative}.editor-effects-dropdown-menu{position:absolute;top:100%;z-index:1000;background:var(--dm-surface, #1e1e2e);border:1px solid var(--dm-border, rgba(255, 255, 255, .1));border-radius:8px;box-shadow:0 8px 24px #0006;padding:6px;min-width:180px;max-height:340px;overflow-y:auto}.editor-effects-category{padding:6px 8px 3px;font-size:.7rem;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:var(--dm-text-muted, #888)}.editor-effects-item{display:block;width:100%;text-align:left;background:none;border:none;color:var(--dm-text, #ddd);padding:6px 10px;border-radius:5px;cursor:pointer;font-size:.85rem;transition:background .12s,color .12s}.editor-effects-item:hover{background:var(--dm-hover-bg);color:var(--dm-text, #fff)}.editor-icon-picker{position:absolute;z-index:1000;background:var(--dm-surface, #1e1e2e);border:1px solid var(--dm-border, rgba(255, 255, 255, .1));border-radius:8px;box-shadow:0 8px 24px #0006;padding:8px;width:320px}.editor-icon-picker-search{width:100%;margin-bottom:8px;font-size:.8rem}.editor-spacer-picker{position:absolute;z-index:9999;background:var(--dm-surface, #1e1e2e);border:1px solid var(--dm-border, #333);border-radius:8px;padding:12px;width:240px;box-shadow:0 8px 24px #00000073}.editor-spacer-picker-label{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--dm-text-muted, #888);margin-bottom:10px}.editor-spacer-picker-row{display:flex;align-items:center;gap:10px;margin-bottom:12px}.editor-spacer-slider{flex:1;accent-color:var(--dm-primary, #6366f1);cursor:pointer}.editor-spacer-slider-value{font-size:13px;font-weight:600;min-width:38px;text-align:right;color:var(--dm-text, #cdd6f4)}.editor-spacer-insert-btn{width:100%}.editor-icon-picker-size-row{display:flex;align-items:center;gap:8px;padding:0 8px 8px}.editor-icon-picker-size-row label{font-size:11px;color:var(--dm-text-muted, #888);white-space:nowrap}.editor-icon-picker-size{width:70px;font-size:12px;padding:3px 6px}.editor-icon-picker-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(52px,1fr));gap:2px;max-height:280px;overflow-y:auto}.editor-icon-picker-item{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:6px 2px;border-radius:5px;cursor:pointer;background:none;border:none;color:var(--dm-text, #ddd);font-size:.58rem;text-align:center;gap:4px;overflow:hidden;transition:background .12s,color .12s}.editor-icon-picker-item:hover{background:var(--dm-hover-bg);color:var(--dm-text, #fff)}.editor-icon-picker-item span[data-icon],.editor-icon-picker-item svg{width:16px;height:16px;flex-shrink:0}.editor-icon-picker-item>span:last-child{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:1.2}.editor-icon-picker-empty{grid-column:1 / -1;text-align:center;color:var(--dm-text-muted, #888);font-size:.8rem;padding:1.5rem}.image-editor-canvas{min-height:320px;max-height:520px;background:#111;overflow:hidden;display:flex;align-items:center;justify-content:center}.image-editor-canvas img{display:block;max-width:100%;max-height:520px}.image-editor-resize{display:flex;flex-direction:row;align-items:center;gap:.5rem;padding:.6rem .75rem;border-top:1px solid var(--dm-border, rgba(255, 255, 255, .1));font-size:.85rem}.image-editor-resize label{font-weight:600;color:var(--dm-text-muted, #888);font-size:.8rem}.image-editor-resize .form-input{width:5rem;padding:.25rem .5rem;font-size:.85rem}.image-editor-footer{display:flex;flex-direction:row;align-items:center;justify-content:space-between;padding:.75rem;border-top:1px solid var(--dm-border, rgba(255, 255, 255, .1));gap:.5rem}.image-editor-effects{border-top:1px solid var(--dm-border, rgba(255, 255, 255, .1))}.image-editor-tab-bar{display:flex;flex-direction:row;overflow-x:auto;border-bottom:1px solid var(--dm-border, rgba(255, 255, 255, .1));scrollbar-width:none}.image-editor-tab-bar::-webkit-scrollbar{display:none}.image-editor-tab-btn{display:inline-flex;align-items:center;gap:.35rem;padding:.5rem .85rem;font-size:.8rem;font-weight:500;border:none;border-bottom:2px solid transparent;background:transparent;cursor:pointer;color:var(--dm-text-muted, #888);white-space:nowrap;transition:color .15s,border-color .15s;flex-shrink:0}.image-editor-tab-btn:hover{color:var(--dm-text, #eee)}.image-editor-tab-btn.active{color:var(--dm-primary, #7c6af7);border-bottom-color:var(--dm-primary, #7c6af7)}.image-editor-tab-panel{padding:.75rem;max-height:200px;overflow-y:auto}.ie-presets-grid{display:flex;flex-wrap:wrap;gap:.35rem;margin-bottom:.5rem}.ie-preset-btn.active{color:var(--dm-primary, #7c6af7);background:color-mix(in srgb,var(--dm-primary, #7c6af7) 15%,transparent);border-color:var(--dm-primary, #7c6af7)}.ie-server-note{font-size:.73rem;color:var(--dm-text-muted, #888);font-style:italic;margin:.25rem 0 0;line-height:1.4}.ie-slider-row{display:flex;align-items:center;gap:.5rem;margin-bottom:.4rem}.ie-slider-label{width:6rem;font-size:.8rem;color:var(--dm-text-muted, #888);font-weight:600;flex-shrink:0}.ie-slider{flex:1;accent-color:var(--dm-primary, #7c6af7);cursor:pointer}.ie-slider-val{width:3rem;font-size:.8rem;text-align:right;color:var(--dm-text, #eee);flex-shrink:0}.ie-select{flex:1;font-size:.85rem;padding:.25rem .5rem}.ie-colour-input{width:40px;height:28px;padding:0;border:1px solid var(--dm-border, rgba(255, 255, 255, .15));border-radius:4px;cursor:pointer;background:transparent;flex-shrink:0}.ie-wm-preview-row{display:flex;align-items:center;gap:.5rem;margin:.4rem 0}.ie-wm-preview-img{width:48px;height:48px;object-fit:contain;border-radius:4px;background:var(--dm-surface-raised, rgba(255, 255, 255, .06));flex-shrink:0}.ie-wm-preview-name{font-size:.8rem;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dm-text, #eee)}.ie-media-picker{padding:.25rem 0}.ie-media-picker-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(80px,1fr));gap:.5rem;max-height:300px;overflow-y:auto}.ie-media-thumb{display:flex;flex-direction:column;align-items:center;gap:.25rem;padding:.4rem;border-radius:6px;cursor:pointer;border:2px solid transparent;transition:border-color .15s,background .15s}.ie-media-thumb:hover{background:var(--dm-surface-raised, rgba(255, 255, 255, .08));border-color:var(--dm-primary, #7c6af7)}.ie-media-thumb img{width:72px;height:72px;object-fit:cover;border-radius:4px}.ie-media-thumb-name{font-size:.7rem;color:var(--dm-text-muted, #888);text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:72px}.dm-slideover-left,.dm-slideover-right{width:min(40rem,95vw)!important;min-width:min(36rem,100vw)!important;max-width:100vw!important}.dm-slideover-resizer{position:absolute;top:0;bottom:0;width:6px;cursor:col-resize;z-index:2;background:transparent}.dm-slideover-right>.dm-slideover-resizer{left:0}.dm-slideover-left>.dm-slideover-resizer{right:0}.dm-slideover-resizer:hover,.is-resizing>.dm-slideover-resizer{background:var(--dm-primary);opacity:.7}.dm-slideover.is-resizing{user-select:none;transition:none!important}.dm-slideover-header{display:flex;align-items:center;justify-content:space-between;padding:1rem 1.25rem;border-bottom:1px solid var(--dm-border, #333);flex-shrink:0}.dm-slideover-title{margin:0;font-size:1rem;font-weight:600}.dm-slideover-close{padding:.2rem .35rem!important;line-height:1;font-size:.75rem}.dm-slideover-body{flex:1;overflow:auto;overscroll-behavior:contain;min-height:0;padding:1.5rem!important;box-sizing:border-box}.dm-slideover-body:empty{flex:0 0 auto;padding:0!important}.dm-slideover>div:not(.dm-slideover-header):not(.dm-slideover-body):not(.dm-slideover-close){padding:1.5rem!important;box-sizing:border-box;flex:1 1 auto;min-height:0;overflow:auto;overscroll-behavior:contain}.dm-slideover-body,.dm-slideover>div:not(.dm-slideover-header):not(.dm-slideover-close){scrollbar-width:thin;scrollbar-color:var(--dm-border) transparent}.dm-slideover-body::-webkit-scrollbar,.dm-slideover>div:not(.dm-slideover-header):not(.dm-slideover-close)::-webkit-scrollbar{width:8px;height:8px}.dm-slideover-body::-webkit-scrollbar-track,.dm-slideover>div:not(.dm-slideover-header):not(.dm-slideover-close)::-webkit-scrollbar-track{background:transparent}.dm-slideover-body::-webkit-scrollbar-thumb,.dm-slideover>div:not(.dm-slideover-header):not(.dm-slideover-close)::-webkit-scrollbar-thumb{background:var(--dm-border);border-radius:4px}.dm-slideover-body::-webkit-scrollbar-thumb:hover,.dm-slideover>div:not(.dm-slideover-header):not(.dm-slideover-close)::-webkit-scrollbar-thumb:hover{background:var(--dm-text-muted)}.dm-slideover-body>div{padding-left:0!important;padding-right:0!important}.dm-slideover-body>button.btn:last-child{margin-top:.5rem}.dconfig-textarea{font-family:monospace;min-height:100px;resize:vertical}.dm-editor-line-numbers{white-space:pre}.dm-code-inline{font-family:Fira Code,Courier New,monospace;font-size:.82em;padding:.15em .35em;border-radius:3px;background:var(--dm-background-alt, rgba(0, 0, 0, .18));color:var(--dm-text, inherit);border:1px solid var(--dm-border, rgba(255, 255, 255, .08))}.dm-code-block{font-family:Fira Code,Courier New,monospace;font-size:.82rem;line-height:1.6;padding:.65rem .9rem;border-radius:6px;background:var(--dm-background-alt, rgba(0, 0, 0, .18));color:var(--dm-text, inherit);border:1px solid var(--dm-border, rgba(255, 255, 255, .08));white-space:pre;overflow-x:auto;display:block;margin:.4rem 0}.tabs-centered{text-align:center}.tabs-centered .tab-list{display:inline-flex}.tabs-centered .tab-content{text-align:left}@media(max-width:768px){.view-header{flex-direction:column;align-items:flex-start}.theme-grid{grid-template-columns:repeat(2,1fr)}.nav-item-row{flex-wrap:wrap}.nav-col-icon,.nav-col-parent{display:none}#admin-topbar{padding:0 .75rem}.topbar-brand-text{display:none}.image-editor-toolbar{flex-wrap:wrap}}#topbar-bell{position:relative}.topbar-action-link[hidden],.topbar-bell-badge[hidden]{display:none}.topbar-bell-badge{position:absolute;top:2px;right:2px;min-width:16px;height:16px;padding:0 4px;background:var(--dm-danger, #dc2626);color:var(--dm-danger-text, #fff);font-size:10px;font-weight:700;line-height:16px;border-radius:9999px;text-align:center;pointer-events:none}.cb-scaffolder{padding:1rem 1.1rem;background:var(--dm-background-alt);border:1px solid var(--dm-primary);border-radius:8px;color:var(--dm-text)}.cb-scaffolder-head{display:flex;align-items:center;gap:.5rem;margin-bottom:.5rem}.cb-scaffolder-head .cb-icon{flex-shrink:0;color:var(--dm-primary)}.cb-scaffolder-head h2{margin:0;font-size:1.05rem;font-weight:700;color:var(--dm-text)}.cb-scaffolder-blurb{margin:0 0 .75rem;font-size:.88rem;line-height:1.5;color:var(--dm-text-muted)}.cb-recipe-list{display:flex;flex-direction:column;gap:.5rem}.cb-recipe-card{display:flex;align-items:flex-start;gap:.75rem;text-align:left;padding:.75rem 1rem;background:var(--dm-surface);border:1px solid var(--dm-border);border-radius:6px;cursor:pointer;transition:border-color .12s;width:100%;color:var(--dm-text)}.cb-recipe-card:hover{border-color:var(--dm-primary)}.cb-recipe-card .cb-icon{flex-shrink:0;font-size:1.5rem;color:var(--dm-primary);margin-top:.1rem}.cb-recipe-meta{flex:1}.cb-recipe-name{font-weight:600;margin-bottom:.15rem;font-size:.95rem;color:var(--dm-text)}.cb-recipe-desc{font-size:.83rem;line-height:1.45;color:var(--dm-text-muted)}.cb-apply-panel{padding:1rem 1.1rem;background:var(--dm-surface);border:1px solid var(--dm-primary);border-radius:8px;color:var(--dm-text)}.cb-apply-head{display:flex;align-items:center;gap:.5rem;margin-bottom:.5rem}.cb-apply-head .cb-icon{color:var(--dm-primary);font-size:1.4rem}.cb-apply-head h3{margin:0;font-size:1rem;font-weight:600;color:var(--dm-text)}.cb-apply-desc,.cb-apply-hint{margin:.25rem 0 .75rem;font-size:.85rem;line-height:1.5;color:var(--dm-text-muted)}.cb-apply-hint{font-size:.75rem;margin-top:.2rem}.cb-apply-field{margin-bottom:.6rem}.cb-apply-field label{display:block;font-size:.82rem;font-weight:600;margin-bottom:.2rem;color:var(--dm-text)}.cb-apply-field input{width:100%;padding:.4rem .55rem;border:1px solid var(--dm-border);border-radius:4px;font-size:.88rem;background:var(--dm-surface);color:var(--dm-text)}.cb-apply-status{margin:.5rem 0;font-size:.85rem;min-height:1.2rem;color:var(--dm-text)}.cb-apply-status.cb-error{color:var(--dm-danger, #dc2626)}.cb-apply-buttons{display:flex;gap:.5rem;justify-content:flex-end;margin-top:.5rem}.cb-success-panel{padding:1rem 1.1rem;background:var(--dm-surface);border:1px solid var(--dm-success, #16a34a);border-radius:8px;color:var(--dm-text)}.cb-success-panel .cb-icon{color:var(--dm-success, #16a34a);font-size:1.4rem}.cb-success-summary{margin:.25rem 0 .5rem;padding-left:1.2rem;font-size:.86rem;line-height:1.6;color:var(--dm-text)}.cb-success-warning{margin:.5rem 0;padding:.5rem .75rem;background:var(--dm-background-alt);border-left:3px solid var(--dm-warning, #eab308);border-radius:0 4px 4px 0;font-size:.82rem;color:var(--dm-text-muted)}.cb-success-snippet-label{margin:0 0 .25rem;font-size:.82rem;font-weight:600;color:var(--dm-text)}.cb-success-actions{display:flex;gap:.5rem;margin-top:.5rem;flex-wrap:wrap}.cb-code{position:relative}.cb-code pre{background:var(--dm-background-alt);color:var(--dm-text);display:block;padding:.6rem 2.4rem .6rem .75rem;white-space:pre;overflow-x:auto;margin:0;border-radius:6px;font-size:.82em;font-family:monospace;line-height:1.45}.cb-code .cb-copy{position:absolute;top:.35rem;right:.35rem;background:none;border:none;cursor:pointer;opacity:.45;padding:.2rem;line-height:1;color:var(--dm-text)}.cb-code .cb-copy:hover{opacity:1}.cb-intro{padding:.75rem 1rem;border-left:3px solid var(--dm-primary);background:var(--dm-background-alt);border-radius:0 6px 6px 0}.cb-intro p{margin:0;font-size:.92rem;line-height:1.5;color:var(--dm-text)}.cb-tutorial-link{display:flex;align-items:center;gap:.6rem;padding:.65rem .85rem;border:1px solid var(--dm-border);border-radius:6px;text-decoration:none;color:var(--dm-text);font-size:.88rem;transition:border-color .12s}.cb-tutorial-link:hover{border-color:var(--dm-primary)}.cb-tutorial-link .cb-icon{flex-shrink:0;color:var(--dm-primary)}.cb-tutorial-link .cb-arrow{color:var(--dm-text-muted);font-size:1.1rem;margin-left:auto}.menu-tree-row{display:grid;grid-template-columns:16px 1.4fr 1.4fr .9fr .7fr .9fr repeat(6,auto);gap:6px;align-items:center;padding:4px 0;transition:opacity .15s ease}.menu-tree-row--hidden{opacity:.5}.menu-tree-row--tool .me-tool-badge{margin-left:.4rem;font-size:.7rem;vertical-align:middle}.menu-tree-row--default .me-row-label{font-style:italic;opacity:.8}.menu-tree-grip{cursor:grab;color:var(--dm-text-muted)}.menu-tree{padding:8px 0}.menu-tree-row--separator{display:grid;grid-template-columns:16px minmax(80px,1fr) repeat(7,auto);gap:6px;align-items:center;padding:2px 0;opacity:.85}.menu-tree-row--separator:hover{opacity:1}.menu-tree-row--separator .me-sep-line{display:flex;align-items:center;gap:10px;width:100%}.menu-tree-row--separator .me-sep-line:before,.menu-tree-row--separator .me-sep-line:after{content:"";flex:1;border-top:1px dashed var(--dm-border, rgba(128, 128, 128, .55))}.menu-tree-row--separator .me-sep-label{font-size:.66rem;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:var(--dm-text-muted, #8a8a8a);white-space:nowrap}.dm-admin-sidebar .sidebar-divider{border:0;border-top:1px solid var(--dm-border, rgba(128, 128, 128, .5));margin:.5rem .75rem;opacity:.8}.dm-admin-sidebar .sidebar-spacer--xs{height:var(--dm-space-1, .25rem)}.dm-admin-sidebar .sidebar-spacer--sm{height:var(--dm-space-2, .5rem)}.dm-admin-sidebar .sidebar-spacer--md{height:var(--dm-space-4, 1rem)}.dm-admin-sidebar .sidebar-spacer--lg{height:var(--dm-space-6, 1.5rem)}.dm-admin-sidebar .sidebar-spacer--xl{height:var(--dm-space-10, 2.5rem)}.dm-admin-sidebar .sidebar-spacer--flex{flex:1 1 auto;min-height:var(--dm-space-4, 1rem)}.dm-admin-sidebar--has-flex-spacer{display:flex;flex-direction:column;min-height:100%}.menu-tree-row--spacer{display:grid;grid-template-columns:16px minmax(80px,1fr) repeat(7,auto);gap:6px;align-items:center;padding:2px 0;opacity:.85}.menu-tree-row--spacer:hover{opacity:1}.menu-tree-row--spacer .me-spacer-line{display:flex;align-items:center;gap:8px;width:100%}.menu-tree-row--spacer .me-spacer-line:before,.menu-tree-row--spacer .me-spacer-line:after{content:"";flex:1;border-top:1px dotted var(--dm-border, rgba(128, 128, 128, .55))}.menu-tree-row--spacer .me-spacer-label{font-size:.66rem;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:var(--dm-text-muted, #8a8a8a);white-space:nowrap}.menu-tree-row--spacer .me-spacer-size{width:auto;min-width:5.5rem;height:26px;padding:0 .35rem;font-size:.75rem}.menu-tree-row .me-icon-wrap{display:flex;gap:2px}.menu-tree-row .me-icon-wrap .me-icon{flex:1;min-width:0}.menu-tree-row .me-icon-wrap .me-icon-pick{flex-shrink:0;padding:.2rem .45rem;font-size:.85em}.dm-admin-sidebar{display:flex;flex-direction:column;gap:2px;padding:8px 0;font-size:.875rem}.dm-admin-sidebar details{margin:0}.dm-admin-sidebar details>summary{list-style:none;cursor:pointer;display:flex;align-items:center;gap:.5rem;padding:.4rem .75rem;border-radius:4px;user-select:none;font-size:.78rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--dm-text-muted, #888)}.dm-admin-sidebar details details>summary{font-size:.875rem;font-weight:500;text-transform:none;letter-spacing:0;color:inherit}.dm-admin-sidebar .dm-menu-badge{font-size:.7rem;font-weight:600}.dm-admin-sidebar details>summary::-webkit-details-marker{display:none}.dm-admin-sidebar details>summary:after{content:"\25b8";margin-left:auto;transition:transform .15s ease;color:var(--dm-text-muted, #999);font-size:.85em}.dm-admin-sidebar details[open]>summary:after{transform:rotate(90deg)}.dm-admin-sidebar details>div{padding-left:1rem;border-left:1px solid var(--dm-border-light, rgba(0, 0, 0, .08));margin-left:.95rem}.dm-admin-sidebar .sidebar-link{display:flex;align-items:center;gap:.5rem;padding:.35rem .75rem;text-decoration:none;color:var(--dm-text, inherit);border-radius:4px}.dm-admin-sidebar .sidebar-link:hover,.dm-admin-sidebar details>summary:hover{background:var(--dm-hover-bg, rgba(0, 0, 0, .05))}.dm-admin-sidebar .sidebar-link.active{background:var(--dm-active-bg, rgba(0, 0, 0, .08));font-weight:600}.dm-admin-sidebar .sidebar-text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dm-admin-sidebar [data-icon]{flex-shrink:0}.dm-admin-sidebar-header{position:sticky;top:0;z-index:2;background:var(--dm-surface);display:flex;align-items:center;gap:.5rem;padding:.65rem .85rem;margin:0 0 .5rem;font-weight:600;font-size:.95rem;letter-spacing:.01em;color:var(--dm-text, inherit);border-bottom:1px solid var(--dm-border-light, rgba(0, 0, 0, .08))}.dm-admin-sidebar--dark .dm-admin-sidebar-header{background:#1a1d24}.dm-admin-sidebar--light .dm-admin-sidebar-header{background:#fff}.dm-admin-sidebar-header [data-icon]{color:var(--dm-text-muted, #999)}.dm-admin-sidebar .sidebar-badge{margin-left:auto;padding:0 .4rem;min-width:1.25rem;height:1.1rem;line-height:1.1rem;text-align:center;font-size:.7rem;font-weight:600;border-radius:999px;background:var(--dm-active-bg, rgba(0, 0, 0, .12));color:var(--dm-text-muted, #888)}.dm-admin-sidebar .sidebar-link.active .sidebar-badge{background:var(--dm-primary, #4a86e8);color:var(--dm-primary-text, #fff)}.dm-admin-sidebar .sidebar-children{overflow:hidden;transition:max-height .22s ease,opacity .18s ease}.dm-admin-sidebar details:not([open])>.sidebar-children{opacity:0}.dm-admin-sidebar{position:relative}.dm-admin-sidebar-handle{position:absolute;top:0;right:0;width:6px;height:100%;cursor:col-resize;background:transparent;z-index:10;transition:background .15s}.dm-admin-sidebar-handle:hover,.dm-admin-sidebar-handle:active{background:var(--dm-primary, #4a86e8);opacity:.35}.dm-admin-sidebar--dark{background:#1a1d24;color:#e3e6eb}.dm-admin-sidebar--dark .sidebar-link,.dm-admin-sidebar--dark summary{color:#e3e6eb}.dm-admin-sidebar--dark .sidebar-link:hover,.dm-admin-sidebar--dark details>summary:hover{background:#ffffff0f}.dm-admin-sidebar--dark .sidebar-link.active{background:#4a86e840}.dm-admin-sidebar--dark .sidebar-badge{background:#ffffff1f;color:#cbd0d8}.dm-admin-sidebar--light{background:#fff;color:#1a1d24;border-right:1px solid rgba(0,0,0,.08)}.dm-admin-sidebar--light .sidebar-link,.dm-admin-sidebar--light summary{color:#1a1d24}.dm-admin-sidebar--light .sidebar-link:hover,.dm-admin-sidebar--light details>summary:hover{background:#0000000a}.dm-admin-sidebar--light .sidebar-link.active{background:#4a86e826}.dm-admin-sidebar--transparent{background:transparent}.page-header{display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap;margin:0 0 1.25rem;padding-bottom:.75rem;border-bottom:1px solid var(--dm-border-light, rgba(0, 0, 0, .08))}.page-header h2{margin:0;display:flex;align-items:center;gap:.5rem;font-size:1.4rem;line-height:1.2}.page-header h2 [data-icon]{color:var(--dm-text-muted, #888)}.header-actions{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}.card+.card,.card+p,p+.card,.card+.toolbar,.toolbar+.card,.card+.grid{margin-top:1rem}.toolbar{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap;margin:0 0 1rem}.btn+.btn,.btn+a.btn,a.btn+.btn,a.btn+a.btn{margin-left:.35rem}[style*=inline-flex] .btn+.btn,[style*=inline-flex] .btn+a.btn,[style*=inline-flex] a.btn+.btn{margin-left:0}.card-body>[id$=-form],.card-body>form{margin:.25rem 0}.card-header{display:flex;align-items:center;gap:.5rem}.card-header h3{margin:0;font-size:1.05rem}.card-header [data-icon]{color:var(--dm-text-muted, #888)}.page-header+p.text-muted{margin:-.5rem 0 1rem}.dm-pe-grid{gap:.75rem 1rem}.dm-pe-grid .form-group{display:flex;flex-direction:column;gap:.25rem;margin:0}.dm-pe-grid .form-group.col-span-2{grid-column:1 / -1}.dm-pe-grid label{font-size:.85rem;font-weight:600;color:var(--dm-text, inherit)}.dm-pe-grid label .required{color:var(--dm-danger, #c33);margin-left:2px}.dm-pe-grid .form-hint{font-size:.75rem;color:var(--dm-text-muted, #888);margin-top:.1rem}.dm-pe-grid .form-input{width:100%;padding:.4rem .6rem}.dm-pe-grid textarea.form-input{resize:vertical;min-height:2.5rem}.dm-menu-badge{display:inline-block;margin-left:6px;padding:1px 7px;font-size:.7em;font-weight:700;line-height:1.4;border-radius:999px;background:var(--dm-text-muted);color:var(--dm-text-inverse, #fff);vertical-align:middle}.menu-tree-row--slim{display:grid;grid-template-columns:16px 1fr auto repeat(9,auto);gap:6px;align-items:center}.menu-tree-row--slim .me-row-label{font-weight:600}.menu-tree-row--slim .me-row-url{color:var(--dm-text-muted);font-size:.85em}.menu-tree-row.me-drop-above{box-shadow:inset 0 2px 0 0 var(--dm-primary)}.menu-tree-row.me-drop-below{box-shadow:inset 0 -2px 0 0 var(--dm-primary)}.menu-tree-row.me-drop-into{background:color-mix(in srgb,var(--dm-primary) 12%,transparent);box-shadow:inset 0 0 0 1px var(--dm-primary);border-radius:var(--dm-radius-sm, 4px)}.menu-tree-row[draggable=true] .menu-tree-grip{cursor:grabbing}.me-pages-picker{display:flex;gap:6px;align-items:center;margin-bottom:6px}.me-pages-picker .form-input{flex:1;min-width:0}.me-colour-control{display:flex;gap:6px;align-items:center}.me-colour-control .me-colour-hex{width:48px;height:32px;padding:2px;flex:none}.me-bind-summary{padding:.6rem .8rem;border:1px solid var(--dm-border, rgba(128, 128, 128, .35));border-radius:var(--dm-radius-md, 6px);background:color-mix(in srgb,var(--dm-text) 4%,transparent);font-size:.9em}.me-bind-summary code{font-size:.9em}.me-item-form{display:flex;flex-direction:column;gap:10px;padding:4px 2px}.me-item-form .form-field{display:flex;flex-direction:column;gap:4px}.me-item-form .form-label{font-size:.85em;color:var(--dm-text-muted)}.me-form-section{margin:12px 0 2px;padding-top:10px;border-top:1px solid var(--dm-border-light, rgba(0,0,0,.15));font-size:.78em;text-transform:uppercase;letter-spacing:.06em;color:var(--dm-text-muted)}.me-item-form .me-form-section:first-child{margin-top:0;padding-top:0;border-top:0}.me-item-form .me-colour-hex{width:48px;height:32px;padding:2px}.me-item-form .me-field-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}.me-item-form .me-field-grid .form-field{margin:0}.me-item-form .me-icon-field{display:flex;align-items:center;gap:8px}.me-item-form .me-icon-field .form-input{flex:1}.me-item-form .me-icon-preview{width:24px;height:24px;flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;color:var(--dm-text-muted)}#admin-sidebar.dm-menu-floating{border:1px solid var(--dm-border);border-radius:var(--dm-radius-md, 8px);box-shadow:var(--dm-shadow-lg);overflow:hidden}@media(max-width:768px){#admin-sidebar.dm-menu-floating{position:static!important;inset:auto!important;max-height:none!important;transform:none!important}.dashboard-wrapper{padding-left:0!important;padding-right:0!important}}[data-logic-section]{border:1px solid var(--dm-border);border-radius:var(--dm-radius-md, 6px);padding:.9rem 1rem 1rem;margin-bottom:1rem;background:color-mix(in srgb,var(--dm-text) 3%,transparent)}[data-logic-section] .fb-logic-cond-row,[data-logic-section] .fb-logic-val-rule{padding:.55rem .65rem;border-radius:var(--dm-radius-sm, 4px);border:1px solid var(--dm-border);background:var(--dm-surface, transparent)}[data-logic-section] .fb-logic-cond-row:last-of-type,[data-logic-section] .fb-logic-val-rule:last-of-type{margin-bottom:.75rem}[data-logic-section]>.btn{margin-top:.15rem}[data-logic-section] textarea{min-height:5rem}.project-filter-notice{display:flex;align-items:center;gap:.4rem;flex-wrap:wrap;margin:0 0 1rem;padding:.6rem .85rem;border:1px solid var(--dm-border);border-left:3px solid var(--dm-info, var(--dm-primary));border-radius:var(--dm-radius-md, 6px);background:color-mix(in srgb,var(--dm-info, var(--dm-primary)) 6%,transparent);font-size:.85rem;color:var(--dm-text-muted)}.project-filter-notice a{color:var(--dm-primary);font-weight:600}.ctx-help{display:inline-flex;align-items:center;justify-content:center;width:1.05rem;height:1.05rem;margin-left:.35rem;padding:0;border:1px solid var(--dm-border-dark, var(--dm-border));border-radius:var(--dm-radius-full, 9999px);background:transparent;color:var(--dm-text-muted);font-size:.68rem;font-weight:700;line-height:1;cursor:pointer;vertical-align:middle;flex-shrink:0}.ctx-help:hover,.ctx-help:focus-visible{border-color:var(--dm-primary);color:var(--dm-primary);outline:none}.card-header h3 .ctx-help{align-self:center}.ctx-preview-card{position:sticky;top:1rem}@media(max-width:1100px){.ctx-workspace>.col-8,.ctx-workspace>.col-4{flex:0 0 auto;width:100%}.ctx-preview-card{position:static}}.dm-plugin-shell{border:1px solid var(--dm-border);border-radius:var(--dm-radius-lg, 12px);background:var(--dm-surface);overflow:hidden}.dm-plugin-banner{display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap;padding:.7rem 1rem;background:var(--dm-background-alt);border-bottom:1px solid var(--dm-border)}.dm-plugin-banner-identity{display:flex;align-items:center;gap:.55rem;min-width:0;margin-right:auto}.dm-plugin-banner-actions{display:flex;align-items:center;gap:.25rem;flex:0 0 auto;margin-right:.15rem;padding-right:.4rem;border-right:1px solid var(--dm-border)}.dm-plugin-banner-action{padding:.25rem .4rem;line-height:1;color:var(--dm-text-muted)}.dm-plugin-banner-action:hover{color:var(--dm-primary)}.dm-plugin-banner-icon{display:inline-flex;align-items:center;color:var(--dm-primary);flex:0 0 auto}.dm-plugin-banner-name{margin:0;font-size:1.05rem;font-weight:600;letter-spacing:.01em;color:var(--dm-text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dm-activity{display:inline-flex;align-items:center;flex:0 0 auto;margin-left:.35rem}.dm-activity domma-loader{display:inline-block;line-height:0}.dm-activity[hidden]{display:none!important}#admin-topbar>.dm-activity{margin:0 .75rem 0 0}.dm-plugin-banner-parent{font-weight:500;color:var(--dm-text-muted)}.dm-plugin-banner-sep{margin:0 .4rem;color:var(--dm-text-muted)}.dm-plugin-banner-facts{display:flex;align-items:center;gap:.4rem;flex-wrap:wrap}.dm-plugin-fact{font-size:.72rem;line-height:1.6;padding:1px 8px;border-radius:999px;border:1px solid var(--dm-border);color:var(--dm-text-muted);background:var(--dm-surface);white-space:nowrap}.dm-plugin-fact--version{font-variant-numeric:tabular-nums;color:var(--dm-text)}.dm-plugin-fact--licence.is-included{color:var(--dm-info);border-color:var(--dm-info)}.dm-plugin-fact--licence.is-licensed{color:var(--dm-success);border-color:var(--dm-success)}.dm-plugin-fact--expiry{font-variant-numeric:tabular-nums}.dm-plugin-fact--expiry.is-warning{color:var(--dm-warning);border-color:var(--dm-warning)}.dm-plugin-fact--expiry.is-danger{color:var(--dm-danger);border-color:var(--dm-danger)}.dm-plugin-body{padding:1rem}.dm-plugin-body .card.dm-plugin-unwrap{border:0;background:none;box-shadow:none;margin:0}.dm-plugin-body .card.dm-plugin-unwrap>.card-body{padding:0}.dm-plugin-body>.p-4{padding:0}@media(max-width:640px){.dm-plugin-banner{align-items:flex-start;flex-direction:column;gap:.5rem}.dm-plugin-body{padding:.75rem}}.analytics-filter{max-width:240px}.dm-help-btn{display:inline-flex;align-items:center;justify-content:center;vertical-align:middle;margin-left:.3rem;padding:0;border:0;background:none;line-height:1;color:var(--dm-text-muted);opacity:.75;cursor:pointer}.dm-help-btn:hover,.dm-help-btn:focus-visible{color:var(--dm-primary);opacity:1}.dm-help-pop{position:fixed;z-index:10050;max-width:300px;padding:.6rem .75rem;background:var(--dm-surface, var(--dm-background));color:var(--dm-text);border:1px solid var(--dm-border);border-radius:var(--dm-radius-md);box-shadow:0 8px 24px #00000040;font-size:.85rem;line-height:1.45;text-align:left;font-weight:400}.dm-help-pop strong{display:block;margin-bottom:.2rem;font-weight:600}.dm-help-pop p{margin:0}.dm-admin-sidebar .sidebar-badge[data-badge-label]{cursor:help}.dm-admin-sidebar .sidebar-badge--danger,.dm-admin-sidebar .sidebar-link.active .sidebar-badge--danger{background:var(--dm-danger, #dc2626);color:#fff}.dm-admin-sidebar .sidebar-badge--live{background:color-mix(in srgb,var(--dm-success, #16a34a) 20%,transparent);color:var(--dm-success, #16a34a)}#admin-sidebar .sidebar-badge--live{padding-left:1.05rem!important}#admin-sidebar summary>.sidebar-badge--sum{position:static!important;transform:none!important;margin-left:auto!important;margin-right:.45rem;text-transform:none;letter-spacing:0}#admin-sidebar summary:has(>.sidebar-badge--sum):after{margin-left:0}#admin-sidebar details[open]>summary>.sidebar-badge--sum{display:none}.dm-admin-sidebar .sidebar-badge--live:before{content:"";position:absolute;left:.4rem;top:50%;width:.4rem;height:.4rem;margin-top:-.2rem;border-radius:50%;background:currentColor;animation:dm-badge-pulse 2s ease-in-out infinite}@keyframes dm-badge-pulse{50%{opacity:.35}}@media(prefers-reduced-motion:reduce){.dm-admin-sidebar .sidebar-badge--live:before{animation:none}}.dm-badge-pop{width:290px;max-width:calc(100vw - 16px);font-size:.8rem;pointer-events:none}.dm-badge-pop-title{display:block;font-size:.85rem}.dm-badge-pop-sub{margin:.1rem 0 .5rem;color:var(--dm-text-muted)}.dm-badge-pop-rows{display:grid;grid-template-columns:1fr auto;gap:.2rem .75rem;margin:0 0 .5rem;padding:.45rem 0;border-top:1px solid var(--dm-border);border-bottom:1px solid var(--dm-border)}.dm-badge-pop-rows dt{color:var(--dm-text-muted)}.dm-badge-pop-rows dd{margin:0;text-align:right;font-weight:600;font-variant-numeric:tabular-nums}.dm-badge-pop-items{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:.4rem}.dm-badge-pop-items li{display:flex;flex-direction:column;min-width:0}.dm-badge-pop-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dm-badge-pop-meta{font-size:.72rem;color:var(--dm-text-muted)}.dm-badge-pop .is-danger,.dm-badge-pop li.is-danger .dm-badge-pop-meta{color:var(--dm-danger, #dc2626)}.dm-badge-pop-empty{margin:0;color:var(--dm-text-muted)}.dm-plugin-body,.dm-slideover{font-family:var(--dm-font-sans, inherit)}.dm-plugin-body :is(button,input,select,textarea,.btn,.form-input,.form-select,.form-textarea),.dm-slideover :is(button,input,select,textarea,.btn,.form-input,.form-select,.form-textarea){font-family:inherit}.dm-plugin-body :is(input,select,textarea,.form-input,.form-select,.form-textarea):not(.form-input-lg),.dm-slideover :is(input,select,textarea,.form-input,.form-select,.form-textarea):not(.form-input-lg){font-size:var(--dm-text-sm)}.dm-plugin-body .form-input-lg{font-size:var(--dm-text-base)}.dm-plugin-body :is(.font-mono,code,pre,kbd),.dm-slideover :is(.font-mono,code,pre,kbd){font-family:var(--dm-font-mono, monospace)}.dm-plugin-body .form-label,.dm-slideover .form-label{font-size:var(--dm-text-sm)}.related-box{display:flex;flex-direction:column;gap:.6rem;border-top:1px solid var(--dm-border);padding-top:.75rem}.related-box[hidden]{display:none}.related-head{display:flex;align-items:center;gap:.4rem}.related-head strong{flex:1}.related-list{list-style:none;margin:.3rem 0 0;padding:0;display:flex;flex-direction:column;gap:.25rem}.related-list li{display:flex;flex-direction:column}.related-list li.is-past{opacity:.7}.related-meta,.related-note,.related-loading{color:var(--dm-text-muted);font-size:var(--dm-text-xs);margin:0}.login-challenge{display:flex;flex-direction:column;gap:.6rem;margin-bottom:.75rem}.login-challenge #challenge-code{font-size:1.25rem;letter-spacing:.2em;text-align:center}.login-trust{display:flex;align-items:center;gap:.45rem;font-size:.875rem}.login-enrol{display:flex;flex-direction:column;align-items:center;gap:.6rem;margin-bottom:1rem}.login-enrol img{background:#fff;padding:.5rem;border-radius:.5rem}.login-secret{display:flex;flex-direction:column;align-items:center;gap:.25rem;text-align:center}.login-secret code{font-size:.95rem;letter-spacing:.08em;word-break:break-all}.usx-secret{display:flex;flex-direction:column;gap:.75rem;padding:1rem}.usx-secret-link{display:block;padding:.6rem .75rem;border:1px solid var(--dm-border);border-radius:var(--dm-radius-sm);background:var(--dm-background-alt);word-break:break-all;font-size:.8rem;user-select:all}.usx-secret-actions{display:flex;align-items:center;justify-content:flex-end;gap:.5rem}.usx-secret-actions .btn{display:inline-flex;align-items:center;gap:.35rem}.usx-secret-note{margin-right:auto;color:var(--dm-success)}.usx-secret-foot{color:var(--dm-text-muted);font-size:.8rem;margin:0}.uex-reset{display:flex;flex-wrap:wrap;align-items:center;gap:.35rem .5rem;margin-top:.5rem}.uex-reset>small:first-child{flex-basis:100%;color:var(--dm-text-muted)}.uex-reset .btn{display:inline-flex;align-items:center;gap:.35rem}.uex-reset-open{flex-basis:100%;color:var(--dm-info)}
1
+ .dashboard-layout{display:flex;flex-direction:column;height:100vh;min-height:100vh;overflow:hidden}.dashboard-wrapper{display:flex;flex:1;min-height:0;overflow:hidden}.dashboard-main{flex:1;min-width:0;overflow-y:auto}#admin-sidebar.dashboard-sidebar{display:flex;flex-direction:column;flex-shrink:0;overflow:hidden}#admin-sidebar .dm-admin-sidebar{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;scrollbar-width:thin;scrollbar-color:var(--dm-border) transparent}#admin-sidebar .dm-admin-sidebar::-webkit-scrollbar{width:8px}#admin-sidebar .dm-admin-sidebar::-webkit-scrollbar-track{background:transparent}#admin-sidebar .dm-admin-sidebar::-webkit-scrollbar-thumb{background:var(--dm-border);border-radius:4px}#admin-sidebar .dm-admin-sidebar::-webkit-scrollbar-thumb:hover{background:var(--dm-text-muted)}.view-container{padding:1.5rem 2rem;max-width:1200px}.view-container:has(>.dm-plugin-shell){max-width:100%}.view-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:1.5rem;gap:1rem}.view-header h1{font-size:1.5rem;font-weight:700;margin:0;display:flex;align-items:center;gap:.5rem}.view-header-actions{display:flex;gap:.75rem;flex-wrap:wrap}.view-header-actions .btn-primary,.view-header-actions .btn-success,.view-header-actions .btn-danger{margin-left:.5rem}.stat-card .card-body{display:flex;align-items:center;gap:1rem}.stat-icon{width:44px;height:44px;border-radius:8px;background:color-mix(in srgb,var(--dm-primary) 14%,transparent);color:var(--dm-primary, #5b8cff);display:flex;align-items:center;justify-content:center;font-size:1.25rem;flex-shrink:0}.stat-icon--success{background:color-mix(in srgb,var(--dm-success) 15%,transparent);color:var(--dm-success)}.stat-icon--warning{background:color-mix(in srgb,var(--dm-warning) 15%,transparent);color:var(--dm-warning)}.stat-value{font-size:1.75rem;font-weight:700;line-height:1}.stat-label{font-size:.8rem;color:var(--dm-text-muted, #888);margin-top:.25rem}.toolbar{display:flex;align-items:center;gap:.75rem}.form-check-label{display:flex;align-items:center;gap:.5rem;cursor:pointer;font-size:.9rem}.form-hint{display:block;font-size:.8rem;color:var(--dm-text-muted, #888);margin-top:.25rem}.dashboard-main:has(#editor-meta-tabs){overflow:hidden}.view-container:has(#editor-meta-tabs){display:flex;flex-direction:column;height:calc(100dvh - 60px);max-width:100%;padding-bottom:0}.view-container:has(#block-editor-body){max-width:100%}.view-container:has(#ep-editor-tabs){max-width:100%}.ep-editor-grid{display:grid;grid-template-columns:minmax(0,1.25fr) minmax(0,1fr) minmax(0,1.3fr);gap:1rem;align-items:start}.ep-editor-grid>.ep-tryit-col,.ep-editor-grid>.ep-results-col{position:sticky;top:1rem;display:flex;flex-direction:column;gap:1rem;min-width:0}@media(max-width:1500px){.ep-editor-grid{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.ep-editor-grid>:first-child{grid-column:1 / -1}.ep-editor-grid>.ep-tryit-col,.ep-editor-grid>.ep-results-col{position:static}}@media(max-width:1024px){.ep-editor-grid{grid-template-columns:minmax(0,1fr)}}#editor-meta-tabs{flex:1;min-height:0;display:flex;flex-direction:column}#editor-meta-tabs .tab-content{flex:1;min-height:0;overflow:hidden}#editor-meta-tabs .tab-panel{height:100%;overflow-y:auto}#editor-meta-tabs .tab-panel:has(.editor-card),#live-preview-panel{overflow:hidden}.live-preview-bar{display:flex;align-items:center;gap:var(--dm-space-4);flex-wrap:wrap;padding:var(--dm-space-2) 0;border-bottom:1px solid var(--dm-border);margin-bottom:var(--dm-space-2)}.live-preview-widths{display:inline-flex;border:1px solid var(--dm-border);border-radius:var(--dm-radius-sm);overflow:hidden}.live-preview-width{display:inline-flex;align-items:center;justify-content:center;padding:.3rem .55rem;border:none;background:transparent;color:inherit;cursor:pointer;line-height:1}.live-preview-width+.live-preview-width{border-left:1px solid var(--dm-border)}.live-preview-width:hover{background:var(--dm-hover-bg)}.live-preview-width.active{background:var(--dm-active-bg)}.live-preview-status{flex:1 1 auto;font-size:.8rem;opacity:.75}.live-preview-actions{display:flex;gap:var(--dm-space-1)}.live-preview-stage{display:flex;justify-content:center;height:calc(100vh - 230px);overflow:auto;background:var(--dm-background-alt);border-radius:var(--dm-radius-md)}.page-live-preview-frame{display:block;width:100%;max-width:100%;height:100%;border:none;border-radius:6px;background:var(--dm-background);transition:max-width .18s ease}.page-live-preview-frame[data-width="1024"]{max-width:1024px}.page-live-preview-frame[data-width="390"]{max-width:390px}.page-live-preview-frame[data-width="1024"],.page-live-preview-frame[data-width="390"]{border-left:1px solid var(--dm-border);border-right:1px solid var(--dm-border)}.editor-card{display:flex;flex-direction:column;height:100%}.editor-card .card-body{display:flex;flex-direction:column;flex:1;min-height:0;padding:0!important}.editor-toolbar{display:flex;align-items:center;gap:2px;padding:6px 10px;border-bottom:1px solid var(--dm-border, rgba(255, 255, 255, .08));flex-wrap:wrap;flex-shrink:0}.editor-toolbar-btn{background:none;border:none;color:var(--dm-text-muted, #888);padding:6px 8px;border-radius:4px;cursor:pointer;display:flex;align-items:center;transition:color .15s,background .15s}.editor-toolbar-btn:hover{color:var(--dm-text, #eee);background:var(--dm-hover-bg)}.editor-toolbar-sep{width:1px;height:20px;background:var(--dm-border, rgba(255, 255, 255, .08));margin:0 4px;flex-shrink:0}.editor-toolbar-right{margin-left:auto;display:flex;align-items:center;gap:2px}.editor-view-btn{background:none;border:none;color:var(--dm-text-muted, #888);padding:6px 8px;border-radius:4px;cursor:pointer;display:flex;align-items:center;transition:color .15s,background .15s}.editor-view-btn:hover{color:var(--dm-text, #eee);background:var(--dm-hover-bg)}.editor-view-btn.active{color:var(--dm-primary, #5b8cff);background:color-mix(in srgb,var(--dm-primary) 14%,transparent)}.editor-body{display:flex;flex:1;min-height:0;--dm-editor-split: 50%}@media(min-width:769px){.editor-body:not(.editor-mode-write):not(.editor-mode-preview) .editor-pane--write{flex:0 0 var(--dm-editor-split, 50%)}.editor-body:not(.editor-mode-write):not(.editor-mode-preview) .editor-pane--preview{flex:1 1 0}}.editor-pane{flex:1;display:flex;flex-direction:column;min-width:0}.editor-pane--write,.editor-pane--preview{overflow:hidden}.editor-pane--write{flex-direction:row}.editor-line-numbers{padding:1rem .6rem 1rem .75rem;background:var(--dm-background-alt, rgba(0, 0, 0, .15));border-right:1px solid var(--dm-border, rgba(255, 255, 255, .08));color:var(--dm-text-muted, #666);font-family:Fira Code,Courier New,monospace;font-size:.9rem;line-height:1.6;text-align:right;user-select:none;white-space:pre;overflow:hidden;flex-shrink:0;min-width:2.5rem}.editor-line-numbers--foldable{padding:1rem 0;min-width:3rem;white-space:normal;text-align:left;display:flex;flex-direction:column}.editor-line-number-row{display:flex;align-items:center;justify-content:flex-end;padding-right:.6rem;gap:.15rem}.fold-toggle{display:inline-flex;align-items:center;justify-content:center;width:1em;font-size:.6rem;cursor:default;color:transparent}.fold-toggle--open,.fold-toggle--folded{cursor:pointer;color:var(--dm-text-muted, #666);transition:color .1s}.fold-toggle--open:hover,.fold-toggle--folded:hover{color:var(--dm-primary, #4a9eff)}.editor-line-num{min-width:1.5rem;text-align:right}.editor-mode-write .editor-pane--preview,.editor-mode-write .editor-divider,.editor-mode-preview .editor-pane--write,.editor-mode-preview .editor-divider{display:none}.editor-divider{width:7px;background:linear-gradient(to right,transparent 0,transparent 3px,var(--dm-border, rgba(255,255,255,.08)) 3px,var(--dm-border, rgba(255,255,255,.08)) 4px,transparent 4px,transparent 100%);cursor:col-resize;flex-shrink:0;touch-action:none;transition:background .12s ease}.editor-divider:hover,.editor-divider:focus-visible,.editor-divider.is-dragging{background:linear-gradient(to right,transparent 0,transparent 2px,var(--dm-primary, #5b8cff) 2px,var(--dm-primary, #5b8cff) 5px,transparent 5px,transparent 100%);outline:none}.editor-body.is-resizing .editor-pane{pointer-events:none}.editor-body.is-resizing{cursor:col-resize;user-select:none}.editor-textarea{flex:1;resize:none;border:none;border-radius:0;background:transparent;font-family:Fira Code,Courier New,monospace;font-size:.9rem;padding:1rem;outline:none;color:inherit;line-height:1.6}.editor-preview{flex:1;display:block;width:100%;border:none;background:var(--dm-background)}.editor-fullscreen{position:fixed;inset:0;z-index:1000;border-radius:0;margin:0}.editor-fullscreen .card-body{height:100vh}.media-picker-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:.75rem;max-height:400px;overflow-y:auto;padding:.25rem}.media-picker-item{cursor:pointer;border:2px solid transparent;border-radius:6px;overflow:hidden;transition:border-color .15s}.media-picker-item:hover{border-color:var(--dm-primary, #5b8cff)}.media-picker-item img{width:100%;height:80px;object-fit:cover;display:block}.media-picker-item span{display:block;font-size:.75rem;padding:4px 6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.docs-body h3{margin-top:1.25rem;margin-bottom:.5rem}.docs-body p{margin-bottom:.5rem;line-height:1.6}.docs-body ol,.docs-body ul{margin-bottom:.75rem;padding-left:1.25rem}.docs-body li{margin-bottom:.25rem}.docs-body hr{margin:1.25rem 0;border-color:var(--dm-border, rgba(255, 255, 255, .08))}.docs-body .table{margin-bottom:.75rem}.nav-items-header,.nav-item-row{display:flex;gap:.5rem;align-items:center;padding:.35rem .75rem}.nav-items-header{border-bottom:1px solid var(--dm-border, rgba(255,255,255,.08));padding-bottom:.4rem;margin-bottom:.25rem}.nav-item-row{border-bottom:1px solid var(--dm-border, rgba(255,255,255,.04))}.nav-item-row:last-child{border-bottom:none}.nav-col-indent{width:18px;flex-shrink:0;color:var(--dm-text-muted, #888);text-align:center;font-size:.85rem}.nav-col-main{flex:2;min-width:0}.nav-col-icon{flex:0 0 90px}.nav-col-parent{flex:2;min-width:0}.nav-col-action{flex-shrink:0}.nav-item-row--child{background:color-mix(in srgb,var(--dm-text) 3%,transparent)}.nav-col-action{display:flex;align-items:center;gap:.25rem}.nav-item-row--hidden{opacity:.45}.nav-item-row--hidden .item-text,.nav-item-row--hidden .footer-link-text{text-decoration:line-through}.btn-toggle-hidden{color:var(--dm-text-muted, #888)}.btn-toggle-hidden.active{color:var(--dm-warning, #f59e0b)}.fb-field-card.fb-drag-over{outline:2px dashed var(--dm-primary, #6366f1);outline-offset:-2px}.btn-ghost span[data-icon=trash]{color:var(--dm-danger, #dc2626)}.view-container:has(.fb-fields-layout){max-width:none}.fb-fields-layout{display:grid;grid-template-columns:minmax(0,6fr) minmax(320px,4fr);gap:1.25rem;align-items:start}.fb-preview-pane{position:sticky;top:1rem;min-width:0}@media(max-width:1100px){.fb-fields-layout{grid-template-columns:1fr}.fb-preview-pane{position:static}}.toggle-label{display:flex;align-items:center;gap:.4rem;font-size:.9rem;cursor:pointer;margin-right:1rem}.preset-toggles{display:flex;flex-wrap:wrap;margin-top:.75rem}.presets-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:1rem}@media(max-width:900px){.presets-grid{grid-template-columns:repeat(2,1fr)}}@media(max-width:600px){.presets-grid{grid-template-columns:1fr}}.card-header{padding:.5rem 1rem}.card-header h2{margin:0;font-size:.9rem;font-weight:600;letter-spacing:.02em}.preset-card .card-header{display:flex;align-items:center;justify-content:space-between}.preset-card .card-header h3{margin:0;font-size:1rem}.media-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:1rem}.media-card{background:var(--dm-card-bg, rgba(255,255,255,.04));border:1px solid var(--dm-border, rgba(255,255,255,.08));border-radius:8px;overflow:hidden}.media-preview{height:130px;display:flex;align-items:center;justify-content:center;background:#0003;overflow:hidden}.media-thumb{width:100%;height:100%;object-fit:cover}.media-thumb--file{font-size:2rem;color:var(--dm-text-muted, #888)}.media-info{padding:.5rem .75rem;display:flex;flex-direction:column;gap:.15rem}.media-name{font-size:.8rem;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:default}.media-rename-input{width:100%;font-size:.8rem;font-weight:500;padding:.1rem .25rem;border:1px solid var(--dm-primary, #7c6af7);border-radius:3px;background:var(--dm-input-bg, transparent);color:inherit;outline:none}.media-size{font-size:.75rem;color:var(--dm-text-muted, #888)}.media-actions{padding:.5rem .75rem;display:flex;gap:.4rem;border-top:1px solid var(--dm-border, rgba(255,255,255,.08))}#admin-topbar{height:60px;display:flex;align-items:center;gap:1rem;padding:0 1.25rem;background:var(--dm-navbar-bg, rgba(0,0,0,.3));border-bottom:1px solid var(--dm-border, rgba(255,255,255,.08));position:sticky;top:0;z-index:200;flex-shrink:0}.topbar-brand{display:flex;align-items:center;gap:.5rem;font-weight:700;font-size:.95rem;color:inherit;flex-shrink:0;margin-right:.5rem}.topbar-brand span[data-icon],.topbar-brand svg{color:var(--dm-primary, #5b8cff);width:22px;height:22px}.topbar-brand-text{opacity:.85}.topbar-brand-logo{height:28px;width:auto;display:inline-block;vertical-align:middle;margin-right:.4em;object-fit:contain}.topbar-user{display:flex;align-items:center;gap:.6rem;flex:1}.topbar-user-name{font-size:.875rem;font-weight:500}.topbar-avatar{width:1.75rem;height:1.75rem;border-radius:50%;object-fit:cover;flex-shrink:0}.topbar-role-badge{font-size:.7rem;font-weight:600;letter-spacing:.04em;text-transform:uppercase;padding:.15rem .45rem;border-radius:4px}.topbar-role-badge--admin{background:color-mix(in srgb,var(--dm-primary) 15%,transparent);color:var(--dm-primary)}.topbar-role-badge--manager{background:color-mix(in srgb,var(--dm-success) 15%,transparent);color:var(--dm-success)}.topbar-role-badge--editor{background:color-mix(in srgb,var(--dm-warning) 15%,transparent);color:var(--dm-warning)}.topbar-role-badge--subscriber{background:color-mix(in srgb,var(--dm-text-muted) 15%,transparent);color:var(--dm-text-muted)}.topbar-actions{display:flex;align-items:center;gap:.5rem;flex-shrink:0}.topbar-action-link{display:flex;align-items:center;gap:.35rem;font-size:.875rem;color:var(--dm-text-muted, #888);text-decoration:none;padding:.4rem .65rem;border-radius:6px;transition:color .15s,background .15s}.topbar-action-link:hover{color:var(--dm-text, #eee);background:var(--dm-hover-bg)}.topbar-action-link span[data-icon],.topbar-action-link svg{width:16px;height:16px}.topbar-signout{display:flex;align-items:center;gap:.35rem;font-size:.875rem}.topbar-signout span[data-icon],.topbar-signout svg{width:16px;height:16px}.login-wrap{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;padding:1rem;background:var(--dm-background, #0f1117);z-index:100;overflow-y:auto}.login-card{width:100%;max-width:460px}.ob-done-icon{font-size:3rem;color:var(--dm-success);margin-bottom:1rem;text-align:center}.btn-skip{display:block;text-align:center;margin-top:.75rem;font-size:.85rem;color:var(--dm-text-muted, #888);text-decoration:none}.btn-skip:hover{text-decoration:underline}.theme-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:.6rem}.theme-swatch{border:2px solid var(--dm-border, rgba(255,255,255,.1));border-radius:8px;overflow:hidden;cursor:pointer;transition:border-color .15s,transform .1s}.theme-swatch:hover{border-color:var(--dm-primary, #5b8cff);transform:translateY(-1px)}.theme-swatch.selected{border-color:var(--dm-primary, #5b8cff);box-shadow:0 0 0 2px var(--dm-primary, #5b8cff)}.theme-swatch-preview{height:52px;position:relative;display:flex;align-items:flex-end;padding:.35rem}.theme-swatch-accent{width:28px;height:6px;border-radius:3px}.theme-swatch-label{display:flex;justify-content:space-between;align-items:center;padding:.3rem .45rem;font-size:.7rem;background:var(--dm-card-bg, rgba(255,255,255,.04))}.theme-swatch-mode{color:var(--dm-text-muted, #888);font-size:.65rem}.login-logo{display:flex;align-items:center;gap:.75rem;margin-bottom:1.75rem;font-size:1.5rem}.login-logo span[data-icon]{font-size:2rem;color:var(--dm-primary, #5b8cff)}.login-logo h1{margin:0;font-size:1.5rem;font-weight:700}.login-card [hidden]{display:none!important}.login-heading{font-size:1.1rem;font-weight:600;margin-bottom:1.25rem}.login-rules{margin:.25rem 0 .75rem;padding-left:1.1rem;color:var(--dm-danger);font-size:.85rem}.login-rules li+li{margin-top:.2rem}.login-small{font-size:.8rem;margin-top:.75rem}.login-icon-warn{color:var(--dm-warning)}.pwf{position:relative;display:block}.pwf>input{width:100%;padding-right:2.5rem}.pwf-toggle{position:absolute;top:50%;right:.35rem;transform:translateY(-50%);display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;padding:0;border:0;border-radius:var(--dm-radius-sm);background:transparent;color:var(--dm-text-muted);cursor:pointer}.pwf-toggle:hover,.pwf-toggle:focus-visible{color:var(--dm-text);background:var(--dm-hover-bg)}.pwf-toggle svg{width:16px;height:16px}.pwf-toggle [data-icon=eye-off],.pwf.is-shown .pwf-toggle [data-icon=eye]{display:none}.pwf.is-shown .pwf-toggle [data-icon=eye-off]{display:inline-block}.pwf-caps{display:block;margin-top:.25rem;color:var(--dm-warning);font-size:.8rem}.pwf-caps[hidden]{display:none}.btn-block{width:100%}.plugins-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:1rem}.plugin-card{display:flex;flex-direction:column;--plugin-accent: var(--dm-primary, #5b8cff);position:relative;overflow:hidden;background-image:radial-gradient(120% 90% at 100% 0%,color-mix(in srgb,var(--plugin-accent) 13%,transparent) 0%,color-mix(in srgb,var(--plugin-accent) 4%,transparent) 42%,transparent 72%)}.plugin-card .plugin-icon{background:color-mix(in srgb,var(--plugin-accent) 16%,transparent);color:var(--plugin-accent)}.plugin-watermark{position:absolute;top:-34px;right:-30px;width:148px;height:148px;display:block;color:var(--plugin-accent);opacity:.09;pointer-events:none;z-index:0}.plugin-card>.card-body,.plugin-card>.plugin-footer{position:relative;z-index:1}.plugin-card:has(.badge-secondary) .plugin-watermark{opacity:.05}@media(prefers-reduced-motion:no-preference){.plugin-card{transition:background-image .2s ease}}.plugin-card .card-body{flex:1}.plugin-header{display:flex;align-items:flex-start;gap:.75rem;margin-bottom:.75rem;flex-wrap:wrap}.plugin-icon{width:40px;height:40px;border-radius:8px;background:color-mix(in srgb,var(--dm-primary) 14%,transparent);color:var(--dm-primary, #5b8cff);display:flex;align-items:center;justify-content:center;flex-shrink:0}.plugin-meta{flex:1 1 140px;min-width:0}.plugin-name{font-weight:600;margin-bottom:.15rem}.plugin-version{font-size:.75rem;color:var(--dm-text-muted, #888)}.plugin-desc{font-size:.875rem;color:var(--dm-text-muted, #888);margin-bottom:1rem}.plugin-footer{display:flex;align-items:center;justify-content:space-between;padding:.75rem 1rem;border-top:1px solid var(--dm-border, rgba(255,255,255,.08))}.plugin-footer-actions{display:flex;align-items:center;gap:.5rem}#admin-sidebar .sidebar-link{position:relative!important}#admin-sidebar .sidebar-badge{position:absolute!important;right:.75rem!important;top:50%!important;transform:translateY(-50%)!important;margin-left:0!important;padding-left:.45rem!important;padding-right:.45rem!important}#admin-sidebar .sidebar-text{padding-right:2rem!important}#admin-sidebar.sidebar-dark{background:var(--dm-surface);border-color:var(--dm-border)}#admin-sidebar.sidebar-dark .sidebar-header{background:var(--dm-surface-raised);border-color:var(--dm-border)}#admin-sidebar.sidebar-dark .sidebar-header-title{color:var(--dm-text)}#admin-sidebar.sidebar-dark .sidebar-link{color:var(--dm-text-muted)}#admin-sidebar.sidebar-dark .sidebar-link:hover{color:var(--dm-text);background:var(--dm-surface-overlay)}#admin-sidebar.sidebar-dark .sidebar-link.active{color:var(--dm-primary);background:color-mix(in srgb,var(--dm-primary) 14%,transparent);border-left-color:var(--dm-primary)}#admin-sidebar.sidebar-dark .sidebar-heading{color:var(--dm-text-muted)}#admin-sidebar.sidebar-dark .sidebar-divider{background:var(--dm-border)}#admin-sidebar.sidebar-dark .sidebar-footer{background:var(--dm-surface-raised);border-color:var(--dm-border)}.sidebar-heading--collapsible{display:flex!important;align-items:center;justify-content:space-between;cursor:pointer;user-select:none}.sidebar-heading-toggle{display:flex;align-items:center;opacity:.5;flex-shrink:0;transition:transform .2s ease}.sidebar-heading--collapsible.is-collapsed .sidebar-heading-toggle{transform:rotate(-90deg)}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.p-0{padding:0!important}.text-muted{color:var(--dm-text-muted, #888)}@media(max-width:768px){.view-container{padding:1rem}.editor-body{flex-direction:column}.editor-divider{width:auto;height:1px;cursor:default;background:var(--dm-border, rgba(255,255,255,.08))}}.image-editor{display:flex;flex-direction:column;gap:0}.image-editor-toolbar{display:flex;flex-direction:row;align-items:center;gap:.25rem;padding:.5rem .75rem;border-bottom:1px solid var(--dm-border, rgba(255, 255, 255, .1))}.image-editor-sep{display:inline-block;width:1px;height:1.5rem;background:var(--dm-border, rgba(255, 255, 255, .15));margin:0 .25rem;flex-shrink:0}.editor-toolbar-btn.active{color:var(--dm-primary, #7c6af7);background:color-mix(in srgb,var(--dm-primary, #7c6af7) 15%,transparent)}.editor-toolbar-caret{font-size:.6rem;line-height:1;margin-left:2px;opacity:.6}.editor-toolbar-dropdown{position:absolute;z-index:1000;background:var(--dm-surface, #1e1e2e);border:1px solid var(--dm-border, rgba(255, 255, 255, .1));border-radius:8px;box-shadow:0 8px 24px #0006;padding:4px;min-width:160px;max-height:70vh;overflow-y:auto}.editor-toolbar-dropdown-heading{padding:6px 10px 3px;color:var(--dm-text-muted, #8a8a9a);font-size:.66rem;text-transform:uppercase;letter-spacing:.06em}.editor-toolbar-dropdown-item{display:flex;align-items:center;gap:8px;width:100%;text-align:left;background:none;border:none;color:var(--dm-text, #ddd);padding:7px 10px;border-radius:5px;cursor:pointer;font-size:.85rem;transition:background .12s,color .12s}.editor-toolbar-dropdown-item:hover{background:var(--dm-hover-bg);color:var(--dm-text, #fff)}.editor-toolbar-dropdown-item span[data-icon],.editor-toolbar-dropdown-item svg{width:15px;height:15px;flex-shrink:0;opacity:.75}.editor-toolbar{position:relative}.editor-effects-dropdown-menu{position:absolute;top:100%;z-index:1000;background:var(--dm-surface, #1e1e2e);border:1px solid var(--dm-border, rgba(255, 255, 255, .1));border-radius:8px;box-shadow:0 8px 24px #0006;padding:6px;min-width:180px;max-height:340px;overflow-y:auto}.editor-effects-category{padding:6px 8px 3px;font-size:.7rem;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:var(--dm-text-muted, #888)}.editor-effects-item{display:block;width:100%;text-align:left;background:none;border:none;color:var(--dm-text, #ddd);padding:6px 10px;border-radius:5px;cursor:pointer;font-size:.85rem;transition:background .12s,color .12s}.editor-effects-item:hover{background:var(--dm-hover-bg);color:var(--dm-text, #fff)}.editor-icon-picker{position:absolute;z-index:1000;background:var(--dm-surface, #1e1e2e);border:1px solid var(--dm-border, rgba(255, 255, 255, .1));border-radius:8px;box-shadow:0 8px 24px #0006;padding:8px;width:320px}.editor-icon-picker-search{width:100%;margin-bottom:8px;font-size:.8rem}.editor-spacer-picker{position:absolute;z-index:9999;background:var(--dm-surface, #1e1e2e);border:1px solid var(--dm-border, #333);border-radius:8px;padding:12px;width:240px;box-shadow:0 8px 24px #00000073}.editor-spacer-picker-label{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--dm-text-muted, #888);margin-bottom:10px}.editor-spacer-picker-row{display:flex;align-items:center;gap:10px;margin-bottom:12px}.editor-spacer-slider{flex:1;accent-color:var(--dm-primary, #6366f1);cursor:pointer}.editor-spacer-slider-value{font-size:13px;font-weight:600;min-width:38px;text-align:right;color:var(--dm-text, #cdd6f4)}.editor-spacer-insert-btn{width:100%}.editor-icon-picker-size-row{display:flex;align-items:center;gap:8px;padding:0 8px 8px}.editor-icon-picker-size-row label{font-size:11px;color:var(--dm-text-muted, #888);white-space:nowrap}.editor-icon-picker-size{width:70px;font-size:12px;padding:3px 6px}.editor-icon-picker-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(52px,1fr));gap:2px;max-height:280px;overflow-y:auto}.editor-icon-picker-item{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:6px 2px;border-radius:5px;cursor:pointer;background:none;border:none;color:var(--dm-text, #ddd);font-size:.58rem;text-align:center;gap:4px;overflow:hidden;transition:background .12s,color .12s}.editor-icon-picker-item:hover{background:var(--dm-hover-bg);color:var(--dm-text, #fff)}.editor-icon-picker-item span[data-icon],.editor-icon-picker-item svg{width:16px;height:16px;flex-shrink:0}.editor-icon-picker-item>span:last-child{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:1.2}.editor-icon-picker-empty{grid-column:1 / -1;text-align:center;color:var(--dm-text-muted, #888);font-size:.8rem;padding:1.5rem}.image-editor-canvas{min-height:320px;max-height:520px;background:#111;overflow:hidden;display:flex;align-items:center;justify-content:center}.image-editor-canvas img{display:block;max-width:100%;max-height:520px}.image-editor-resize{display:flex;flex-direction:row;align-items:center;gap:.5rem;padding:.6rem .75rem;border-top:1px solid var(--dm-border, rgba(255, 255, 255, .1));font-size:.85rem}.image-editor-resize label{font-weight:600;color:var(--dm-text-muted, #888);font-size:.8rem}.image-editor-resize .form-input{width:5rem;padding:.25rem .5rem;font-size:.85rem}.image-editor-footer{display:flex;flex-direction:row;align-items:center;justify-content:space-between;padding:.75rem;border-top:1px solid var(--dm-border, rgba(255, 255, 255, .1));gap:.5rem}.image-editor-effects{border-top:1px solid var(--dm-border, rgba(255, 255, 255, .1))}.image-editor-tab-bar{display:flex;flex-direction:row;overflow-x:auto;border-bottom:1px solid var(--dm-border, rgba(255, 255, 255, .1));scrollbar-width:none}.image-editor-tab-bar::-webkit-scrollbar{display:none}.image-editor-tab-btn{display:inline-flex;align-items:center;gap:.35rem;padding:.5rem .85rem;font-size:.8rem;font-weight:500;border:none;border-bottom:2px solid transparent;background:transparent;cursor:pointer;color:var(--dm-text-muted, #888);white-space:nowrap;transition:color .15s,border-color .15s;flex-shrink:0}.image-editor-tab-btn:hover{color:var(--dm-text, #eee)}.image-editor-tab-btn.active{color:var(--dm-primary, #7c6af7);border-bottom-color:var(--dm-primary, #7c6af7)}.image-editor-tab-panel{padding:.75rem;max-height:200px;overflow-y:auto}.ie-presets-grid{display:flex;flex-wrap:wrap;gap:.35rem;margin-bottom:.5rem}.ie-preset-btn.active{color:var(--dm-primary, #7c6af7);background:color-mix(in srgb,var(--dm-primary, #7c6af7) 15%,transparent);border-color:var(--dm-primary, #7c6af7)}.ie-server-note{font-size:.73rem;color:var(--dm-text-muted, #888);font-style:italic;margin:.25rem 0 0;line-height:1.4}.ie-slider-row{display:flex;align-items:center;gap:.5rem;margin-bottom:.4rem}.ie-slider-label{width:6rem;font-size:.8rem;color:var(--dm-text-muted, #888);font-weight:600;flex-shrink:0}.ie-slider{flex:1;accent-color:var(--dm-primary, #7c6af7);cursor:pointer}.ie-slider-val{width:3rem;font-size:.8rem;text-align:right;color:var(--dm-text, #eee);flex-shrink:0}.ie-select{flex:1;font-size:.85rem;padding:.25rem .5rem}.ie-colour-input{width:40px;height:28px;padding:0;border:1px solid var(--dm-border, rgba(255, 255, 255, .15));border-radius:4px;cursor:pointer;background:transparent;flex-shrink:0}.ie-wm-preview-row{display:flex;align-items:center;gap:.5rem;margin:.4rem 0}.ie-wm-preview-img{width:48px;height:48px;object-fit:contain;border-radius:4px;background:var(--dm-surface-raised, rgba(255, 255, 255, .06));flex-shrink:0}.ie-wm-preview-name{font-size:.8rem;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dm-text, #eee)}.ie-media-picker{padding:.25rem 0}.ie-media-picker-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(80px,1fr));gap:.5rem;max-height:300px;overflow-y:auto}.ie-media-thumb{display:flex;flex-direction:column;align-items:center;gap:.25rem;padding:.4rem;border-radius:6px;cursor:pointer;border:2px solid transparent;transition:border-color .15s,background .15s}.ie-media-thumb:hover{background:var(--dm-surface-raised, rgba(255, 255, 255, .08));border-color:var(--dm-primary, #7c6af7)}.ie-media-thumb img{width:72px;height:72px;object-fit:cover;border-radius:4px}.ie-media-thumb-name{font-size:.7rem;color:var(--dm-text-muted, #888);text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:72px}.dm-slideover-left,.dm-slideover-right{width:min(40rem,95vw)!important;min-width:min(36rem,100vw)!important;max-width:100vw!important}.dm-slideover-resizer{position:absolute;top:0;bottom:0;width:6px;cursor:col-resize;z-index:2;background:transparent}.dm-slideover-right>.dm-slideover-resizer{left:0}.dm-slideover-left>.dm-slideover-resizer{right:0}.dm-slideover-resizer:hover,.is-resizing>.dm-slideover-resizer{background:var(--dm-primary);opacity:.7}.dm-slideover.is-resizing{user-select:none;transition:none!important}.dm-slideover-header{display:flex;align-items:center;justify-content:space-between;padding:1rem 1.25rem;border-bottom:1px solid var(--dm-border, #333);flex-shrink:0}.dm-slideover-title{margin:0;font-size:1rem;font-weight:600}.dm-slideover-close{padding:.2rem .35rem!important;line-height:1;font-size:.75rem}.dm-slideover-body{flex:1;overflow:auto;overscroll-behavior:contain;min-height:0;padding:1.5rem!important;box-sizing:border-box}.dm-slideover-body:empty{flex:0 0 auto;padding:0!important}.dm-slideover>div:not(.dm-slideover-header):not(.dm-slideover-body):not(.dm-slideover-close){padding:1.5rem!important;box-sizing:border-box;flex:1 1 auto;min-height:0;overflow:auto;overscroll-behavior:contain}.dm-slideover-body,.dm-slideover>div:not(.dm-slideover-header):not(.dm-slideover-close){scrollbar-width:thin;scrollbar-color:var(--dm-border) transparent}.dm-slideover-body::-webkit-scrollbar,.dm-slideover>div:not(.dm-slideover-header):not(.dm-slideover-close)::-webkit-scrollbar{width:8px;height:8px}.dm-slideover-body::-webkit-scrollbar-track,.dm-slideover>div:not(.dm-slideover-header):not(.dm-slideover-close)::-webkit-scrollbar-track{background:transparent}.dm-slideover-body::-webkit-scrollbar-thumb,.dm-slideover>div:not(.dm-slideover-header):not(.dm-slideover-close)::-webkit-scrollbar-thumb{background:var(--dm-border);border-radius:4px}.dm-slideover-body::-webkit-scrollbar-thumb:hover,.dm-slideover>div:not(.dm-slideover-header):not(.dm-slideover-close)::-webkit-scrollbar-thumb:hover{background:var(--dm-text-muted)}.dm-slideover-body>div{padding-left:0!important;padding-right:0!important}.dm-slideover-body>button.btn:last-child{margin-top:.5rem}.dconfig-textarea{font-family:monospace;min-height:100px;resize:vertical}.dm-editor-line-numbers{white-space:pre}.dm-code-inline{font-family:Fira Code,Courier New,monospace;font-size:.82em;padding:.15em .35em;border-radius:3px;background:var(--dm-background-alt, rgba(0, 0, 0, .18));color:var(--dm-text, inherit);border:1px solid var(--dm-border, rgba(255, 255, 255, .08))}.dm-code-block{font-family:Fira Code,Courier New,monospace;font-size:.82rem;line-height:1.6;padding:.65rem .9rem;border-radius:6px;background:var(--dm-background-alt, rgba(0, 0, 0, .18));color:var(--dm-text, inherit);border:1px solid var(--dm-border, rgba(255, 255, 255, .08));white-space:pre;overflow-x:auto;display:block;margin:.4rem 0}.tabs-centered{text-align:center}.tabs-centered .tab-list{display:inline-flex}.tabs-centered .tab-content{text-align:left}@media(max-width:768px){.view-header{flex-direction:column;align-items:flex-start}.theme-grid{grid-template-columns:repeat(2,1fr)}.nav-item-row{flex-wrap:wrap}.nav-col-icon,.nav-col-parent{display:none}#admin-topbar{padding:0 .75rem}.topbar-brand-text{display:none}.image-editor-toolbar{flex-wrap:wrap}}#topbar-bell{position:relative}.topbar-action-link[hidden],.topbar-bell-badge[hidden]{display:none}.topbar-bell-badge{position:absolute;top:2px;right:2px;min-width:16px;height:16px;padding:0 4px;background:var(--dm-danger, #dc2626);color:var(--dm-danger-text, #fff);font-size:10px;font-weight:700;line-height:16px;border-radius:9999px;text-align:center;pointer-events:none}.cb-scaffolder{padding:1rem 1.1rem;background:var(--dm-background-alt);border:1px solid var(--dm-primary);border-radius:8px;color:var(--dm-text)}.cb-scaffolder-head{display:flex;align-items:center;gap:.5rem;margin-bottom:.5rem}.cb-scaffolder-head .cb-icon{flex-shrink:0;color:var(--dm-primary)}.cb-scaffolder-head h2{margin:0;font-size:1.05rem;font-weight:700;color:var(--dm-text)}.cb-scaffolder-blurb{margin:0 0 .75rem;font-size:.88rem;line-height:1.5;color:var(--dm-text-muted)}.cb-recipe-list{display:flex;flex-direction:column;gap:.5rem}.cb-recipe-card{display:flex;align-items:flex-start;gap:.75rem;text-align:left;padding:.75rem 1rem;background:var(--dm-surface);border:1px solid var(--dm-border);border-radius:6px;cursor:pointer;transition:border-color .12s;width:100%;color:var(--dm-text)}.cb-recipe-card:hover{border-color:var(--dm-primary)}.cb-recipe-card .cb-icon{flex-shrink:0;font-size:1.5rem;color:var(--dm-primary);margin-top:.1rem}.cb-recipe-meta{flex:1}.cb-recipe-name{font-weight:600;margin-bottom:.15rem;font-size:.95rem;color:var(--dm-text)}.cb-recipe-desc{font-size:.83rem;line-height:1.45;color:var(--dm-text-muted)}.cb-apply-panel{padding:1rem 1.1rem;background:var(--dm-surface);border:1px solid var(--dm-primary);border-radius:8px;color:var(--dm-text)}.cb-apply-head{display:flex;align-items:center;gap:.5rem;margin-bottom:.5rem}.cb-apply-head .cb-icon{color:var(--dm-primary);font-size:1.4rem}.cb-apply-head h3{margin:0;font-size:1rem;font-weight:600;color:var(--dm-text)}.cb-apply-desc,.cb-apply-hint{margin:.25rem 0 .75rem;font-size:.85rem;line-height:1.5;color:var(--dm-text-muted)}.cb-apply-hint{font-size:.75rem;margin-top:.2rem}.cb-apply-field{margin-bottom:.6rem}.cb-apply-field label{display:block;font-size:.82rem;font-weight:600;margin-bottom:.2rem;color:var(--dm-text)}.cb-apply-field input{width:100%;padding:.4rem .55rem;border:1px solid var(--dm-border);border-radius:4px;font-size:.88rem;background:var(--dm-surface);color:var(--dm-text)}.cb-apply-status{margin:.5rem 0;font-size:.85rem;min-height:1.2rem;color:var(--dm-text)}.cb-apply-status.cb-error{color:var(--dm-danger, #dc2626)}.cb-apply-buttons{display:flex;gap:.5rem;justify-content:flex-end;margin-top:.5rem}.cb-success-panel{padding:1rem 1.1rem;background:var(--dm-surface);border:1px solid var(--dm-success, #16a34a);border-radius:8px;color:var(--dm-text)}.cb-success-panel .cb-icon{color:var(--dm-success, #16a34a);font-size:1.4rem}.cb-success-summary{margin:.25rem 0 .5rem;padding-left:1.2rem;font-size:.86rem;line-height:1.6;color:var(--dm-text)}.cb-success-warning{margin:.5rem 0;padding:.5rem .75rem;background:var(--dm-background-alt);border-left:3px solid var(--dm-warning, #eab308);border-radius:0 4px 4px 0;font-size:.82rem;color:var(--dm-text-muted)}.cb-success-snippet-label{margin:0 0 .25rem;font-size:.82rem;font-weight:600;color:var(--dm-text)}.cb-success-actions{display:flex;gap:.5rem;margin-top:.5rem;flex-wrap:wrap}.cb-code{position:relative}.cb-code pre{background:var(--dm-background-alt);color:var(--dm-text);display:block;padding:.6rem 2.4rem .6rem .75rem;white-space:pre;overflow-x:auto;margin:0;border-radius:6px;font-size:.82em;font-family:monospace;line-height:1.45}.cb-code .cb-copy{position:absolute;top:.35rem;right:.35rem;background:none;border:none;cursor:pointer;opacity:.45;padding:.2rem;line-height:1;color:var(--dm-text)}.cb-code .cb-copy:hover{opacity:1}.cb-intro{padding:.75rem 1rem;border-left:3px solid var(--dm-primary);background:var(--dm-background-alt);border-radius:0 6px 6px 0}.cb-intro p{margin:0;font-size:.92rem;line-height:1.5;color:var(--dm-text)}.cb-tutorial-link{display:flex;align-items:center;gap:.6rem;padding:.65rem .85rem;border:1px solid var(--dm-border);border-radius:6px;text-decoration:none;color:var(--dm-text);font-size:.88rem;transition:border-color .12s}.cb-tutorial-link:hover{border-color:var(--dm-primary)}.cb-tutorial-link .cb-icon{flex-shrink:0;color:var(--dm-primary)}.cb-tutorial-link .cb-arrow{color:var(--dm-text-muted);font-size:1.1rem;margin-left:auto}.menu-tree-row{display:grid;grid-template-columns:16px 1.4fr 1.4fr .9fr .7fr .9fr repeat(6,auto);gap:6px;align-items:center;padding:4px 0;transition:opacity .15s ease}.menu-tree-row--hidden{opacity:.5}.menu-tree-row--tool .me-tool-badge{margin-left:.4rem;font-size:.7rem;vertical-align:middle}.menu-tree-row--default .me-row-label{font-style:italic;opacity:.8}.menu-tree-grip{cursor:grab;color:var(--dm-text-muted)}.menu-tree{padding:8px 0}.menu-tree-row--separator{display:grid;grid-template-columns:16px minmax(80px,1fr) repeat(7,auto);gap:6px;align-items:center;padding:2px 0;opacity:.85}.menu-tree-row--separator:hover{opacity:1}.menu-tree-row--separator .me-sep-line{display:flex;align-items:center;gap:10px;width:100%}.menu-tree-row--separator .me-sep-line:before,.menu-tree-row--separator .me-sep-line:after{content:"";flex:1;border-top:1px dashed var(--dm-border, rgba(128, 128, 128, .55))}.menu-tree-row--separator .me-sep-label{font-size:.66rem;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:var(--dm-text-muted, #8a8a8a);white-space:nowrap}.dm-admin-sidebar .sidebar-divider{border:0;border-top:1px solid var(--dm-border, rgba(128, 128, 128, .5));margin:.5rem .75rem;opacity:.8}.dm-admin-sidebar .sidebar-spacer--xs{height:var(--dm-space-1, .25rem)}.dm-admin-sidebar .sidebar-spacer--sm{height:var(--dm-space-2, .5rem)}.dm-admin-sidebar .sidebar-spacer--md{height:var(--dm-space-4, 1rem)}.dm-admin-sidebar .sidebar-spacer--lg{height:var(--dm-space-6, 1.5rem)}.dm-admin-sidebar .sidebar-spacer--xl{height:var(--dm-space-10, 2.5rem)}.dm-admin-sidebar .sidebar-spacer--flex{flex:1 1 auto;min-height:var(--dm-space-4, 1rem)}.dm-admin-sidebar--has-flex-spacer{display:flex;flex-direction:column;min-height:100%}.menu-tree-row--spacer{display:grid;grid-template-columns:16px minmax(80px,1fr) repeat(7,auto);gap:6px;align-items:center;padding:2px 0;opacity:.85}.menu-tree-row--spacer:hover{opacity:1}.menu-tree-row--spacer .me-spacer-line{display:flex;align-items:center;gap:8px;width:100%}.menu-tree-row--spacer .me-spacer-line:before,.menu-tree-row--spacer .me-spacer-line:after{content:"";flex:1;border-top:1px dotted var(--dm-border, rgba(128, 128, 128, .55))}.menu-tree-row--spacer .me-spacer-label{font-size:.66rem;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:var(--dm-text-muted, #8a8a8a);white-space:nowrap}.menu-tree-row--spacer .me-spacer-size{width:auto;min-width:5.5rem;height:26px;padding:0 .35rem;font-size:.75rem}.menu-tree-row .me-icon-wrap{display:flex;gap:2px}.menu-tree-row .me-icon-wrap .me-icon{flex:1;min-width:0}.menu-tree-row .me-icon-wrap .me-icon-pick{flex-shrink:0;padding:.2rem .45rem;font-size:.85em}.dm-admin-sidebar{display:flex;flex-direction:column;gap:2px;padding:8px 0;font-size:.875rem}.dm-admin-sidebar details{margin:0}.dm-admin-sidebar details>summary{list-style:none;cursor:pointer;display:flex;align-items:center;gap:.5rem;padding:.4rem .75rem;border-radius:4px;user-select:none;font-size:.78rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--dm-text-muted, #888)}.dm-admin-sidebar details details>summary{font-size:.875rem;font-weight:500;text-transform:none;letter-spacing:0;color:inherit}.dm-admin-sidebar .dm-menu-badge{font-size:.7rem;font-weight:600}.dm-admin-sidebar details>summary::-webkit-details-marker{display:none}.dm-admin-sidebar details>summary:after{content:"\25b8";margin-left:auto;transition:transform .15s ease;color:var(--dm-text-muted, #999);font-size:.85em}.dm-admin-sidebar details[open]>summary:after{transform:rotate(90deg)}.dm-admin-sidebar details>div{padding-left:1rem;border-left:1px solid var(--dm-border-light, rgba(0, 0, 0, .08));margin-left:.95rem}.dm-admin-sidebar .sidebar-link{display:flex;align-items:center;gap:.5rem;padding:.35rem .75rem;text-decoration:none;color:var(--dm-text, inherit);border-radius:4px}.dm-admin-sidebar .sidebar-link:hover,.dm-admin-sidebar details>summary:hover{background:var(--dm-hover-bg, rgba(0, 0, 0, .05))}.dm-admin-sidebar .sidebar-link.active{background:var(--dm-active-bg, rgba(0, 0, 0, .08));font-weight:600}.dm-admin-sidebar .sidebar-text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dm-admin-sidebar [data-icon]{flex-shrink:0}.dm-admin-sidebar-header{position:sticky;top:0;z-index:2;background:var(--dm-surface);display:flex;align-items:center;gap:.5rem;padding:.65rem .85rem;margin:0 0 .5rem;font-weight:600;font-size:.95rem;letter-spacing:.01em;color:var(--dm-text, inherit);border-bottom:1px solid var(--dm-border-light, rgba(0, 0, 0, .08))}.dm-admin-sidebar--dark .dm-admin-sidebar-header{background:#1a1d24}.dm-admin-sidebar--light .dm-admin-sidebar-header{background:#fff}.dm-admin-sidebar-header [data-icon]{color:var(--dm-text-muted, #999)}.dm-admin-sidebar .sidebar-badge{margin-left:auto;padding:0 .4rem;min-width:1.25rem;height:1.1rem;line-height:1.1rem;text-align:center;font-size:.7rem;font-weight:600;border-radius:999px;background:var(--dm-active-bg, rgba(0, 0, 0, .12));color:var(--dm-text-muted, #888)}.dm-admin-sidebar .sidebar-link.active .sidebar-badge{background:var(--dm-primary, #4a86e8);color:var(--dm-primary-text, #fff)}.dm-admin-sidebar .sidebar-children{overflow:hidden;transition:max-height .22s ease,opacity .18s ease}.dm-admin-sidebar details:not([open])>.sidebar-children{opacity:0}.dm-admin-sidebar{position:relative}.dm-admin-sidebar-handle{position:absolute;top:0;right:0;width:6px;height:100%;cursor:col-resize;background:transparent;z-index:10;transition:background .15s}.dm-admin-sidebar-handle:hover,.dm-admin-sidebar-handle:active{background:var(--dm-primary, #4a86e8);opacity:.35}.dm-admin-sidebar--dark{background:#1a1d24;color:#e3e6eb}.dm-admin-sidebar--dark .sidebar-link,.dm-admin-sidebar--dark summary{color:#e3e6eb}.dm-admin-sidebar--dark .sidebar-link:hover,.dm-admin-sidebar--dark details>summary:hover{background:#ffffff0f}.dm-admin-sidebar--dark .sidebar-link.active{background:#4a86e840}.dm-admin-sidebar--dark .sidebar-badge{background:#ffffff1f;color:#cbd0d8}.dm-admin-sidebar--light{background:#fff;color:#1a1d24;border-right:1px solid rgba(0,0,0,.08)}.dm-admin-sidebar--light .sidebar-link,.dm-admin-sidebar--light summary{color:#1a1d24}.dm-admin-sidebar--light .sidebar-link:hover,.dm-admin-sidebar--light details>summary:hover{background:#0000000a}.dm-admin-sidebar--light .sidebar-link.active{background:#4a86e826}.dm-admin-sidebar--transparent{background:transparent}.page-header{display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap;margin:0 0 1.25rem;padding-bottom:.75rem;border-bottom:1px solid var(--dm-border-light, rgba(0, 0, 0, .08))}.page-header h2{margin:0;display:flex;align-items:center;gap:.5rem;font-size:1.4rem;line-height:1.2}.page-header h2 [data-icon]{color:var(--dm-text-muted, #888)}.header-actions{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}.card+.card,.card+p,p+.card,.card+.toolbar,.toolbar+.card,.card+.grid{margin-top:1rem}.toolbar{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap;margin:0 0 1rem}.btn+.btn,.btn+a.btn,a.btn+.btn,a.btn+a.btn{margin-left:.35rem}[style*=inline-flex] .btn+.btn,[style*=inline-flex] .btn+a.btn,[style*=inline-flex] a.btn+.btn{margin-left:0}.card-body>[id$=-form],.card-body>form{margin:.25rem 0}.card-header{display:flex;align-items:center;gap:.5rem}.card-header h3{margin:0;font-size:1.05rem}.card-header [data-icon]{color:var(--dm-text-muted, #888)}.page-header+p.text-muted{margin:-.5rem 0 1rem}.dm-pe-grid{gap:.75rem 1rem}.dm-pe-grid .form-group{display:flex;flex-direction:column;gap:.25rem;margin:0}.dm-pe-grid .form-group.col-span-2{grid-column:1 / -1}.dm-pe-grid label{font-size:.85rem;font-weight:600;color:var(--dm-text, inherit)}.dm-pe-grid label .required{color:var(--dm-danger, #c33);margin-left:2px}.dm-pe-grid .form-hint{font-size:.75rem;color:var(--dm-text-muted, #888);margin-top:.1rem}.dm-pe-grid .form-input{width:100%;padding:.4rem .6rem}.dm-pe-grid textarea.form-input{resize:vertical;min-height:2.5rem}.dm-menu-badge{display:inline-block;margin-left:6px;padding:1px 7px;font-size:.7em;font-weight:700;line-height:1.4;border-radius:999px;background:var(--dm-text-muted);color:var(--dm-text-inverse, #fff);vertical-align:middle}.menu-tree-row--slim{display:grid;grid-template-columns:16px 1fr auto repeat(9,auto);gap:6px;align-items:center}.menu-tree-row--slim .me-row-label{font-weight:600}.menu-tree-row--slim .me-row-url{color:var(--dm-text-muted);font-size:.85em}.menu-tree-row.me-drop-above{box-shadow:inset 0 2px 0 0 var(--dm-primary)}.menu-tree-row.me-drop-below{box-shadow:inset 0 -2px 0 0 var(--dm-primary)}.menu-tree-row.me-drop-into{background:color-mix(in srgb,var(--dm-primary) 12%,transparent);box-shadow:inset 0 0 0 1px var(--dm-primary);border-radius:var(--dm-radius-sm, 4px)}.menu-tree-row[draggable=true] .menu-tree-grip{cursor:grabbing}.me-pages-picker{display:flex;gap:6px;align-items:center;margin-bottom:6px}.me-pages-picker .form-input{flex:1;min-width:0}.me-colour-control{display:flex;gap:6px;align-items:center}.me-colour-control .me-colour-hex{width:48px;height:32px;padding:2px;flex:none}.me-bind-summary{padding:.6rem .8rem;border:1px solid var(--dm-border, rgba(128, 128, 128, .35));border-radius:var(--dm-radius-md, 6px);background:color-mix(in srgb,var(--dm-text) 4%,transparent);font-size:.9em}.me-bind-summary code{font-size:.9em}.me-item-form{display:flex;flex-direction:column;gap:10px;padding:4px 2px}.me-item-form .form-field{display:flex;flex-direction:column;gap:4px}.me-item-form .form-label{font-size:.85em;color:var(--dm-text-muted)}.me-form-section{margin:12px 0 2px;padding-top:10px;border-top:1px solid var(--dm-border-light, rgba(0,0,0,.15));font-size:.78em;text-transform:uppercase;letter-spacing:.06em;color:var(--dm-text-muted)}.me-item-form .me-form-section:first-child{margin-top:0;padding-top:0;border-top:0}.me-item-form .me-colour-hex{width:48px;height:32px;padding:2px}.me-item-form .me-field-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}.me-item-form .me-field-grid .form-field{margin:0}.me-item-form .me-icon-field{display:flex;align-items:center;gap:8px}.me-item-form .me-icon-field .form-input{flex:1}.me-item-form .me-icon-preview{width:24px;height:24px;flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;color:var(--dm-text-muted)}#admin-sidebar.dm-menu-floating{border:1px solid var(--dm-border);border-radius:var(--dm-radius-md, 8px);box-shadow:var(--dm-shadow-lg);overflow:hidden}@media(max-width:768px){#admin-sidebar.dm-menu-floating{position:static!important;inset:auto!important;max-height:none!important;transform:none!important}.dashboard-wrapper{padding-left:0!important;padding-right:0!important}}[data-logic-section]{border:1px solid var(--dm-border);border-radius:var(--dm-radius-md, 6px);padding:.9rem 1rem 1rem;margin-bottom:1rem;background:color-mix(in srgb,var(--dm-text) 3%,transparent)}[data-logic-section] .fb-logic-cond-row,[data-logic-section] .fb-logic-val-rule{padding:.55rem .65rem;border-radius:var(--dm-radius-sm, 4px);border:1px solid var(--dm-border);background:var(--dm-surface, transparent)}[data-logic-section] .fb-logic-cond-row:last-of-type,[data-logic-section] .fb-logic-val-rule:last-of-type{margin-bottom:.75rem}[data-logic-section]>.btn{margin-top:.15rem}[data-logic-section] textarea{min-height:5rem}.project-filter-notice{display:flex;align-items:center;gap:.4rem;flex-wrap:wrap;margin:0 0 1rem;padding:.6rem .85rem;border:1px solid var(--dm-border);border-left:3px solid var(--dm-info, var(--dm-primary));border-radius:var(--dm-radius-md, 6px);background:color-mix(in srgb,var(--dm-info, var(--dm-primary)) 6%,transparent);font-size:.85rem;color:var(--dm-text-muted)}.project-filter-notice a{color:var(--dm-primary);font-weight:600}.ctx-help{display:inline-flex;align-items:center;justify-content:center;width:1.05rem;height:1.05rem;margin-left:.35rem;padding:0;border:1px solid var(--dm-border-dark, var(--dm-border));border-radius:var(--dm-radius-full, 9999px);background:transparent;color:var(--dm-text-muted);font-size:.68rem;font-weight:700;line-height:1;cursor:pointer;vertical-align:middle;flex-shrink:0}.ctx-help:hover,.ctx-help:focus-visible{border-color:var(--dm-primary);color:var(--dm-primary);outline:none}.card-header h3 .ctx-help{align-self:center}.ctx-preview-card{position:sticky;top:1rem}@media(max-width:1100px){.ctx-workspace>.col-8,.ctx-workspace>.col-4{flex:0 0 auto;width:100%}.ctx-preview-card{position:static}}.dm-plugin-shell{border:1px solid var(--dm-border);border-radius:var(--dm-radius-lg, 12px);background:var(--dm-surface);overflow:hidden}.dm-plugin-banner{display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap;padding:.7rem 1rem;background:var(--dm-background-alt);border-bottom:1px solid var(--dm-border)}.dm-plugin-banner-identity{display:flex;align-items:center;gap:.55rem;min-width:0;margin-right:auto}.dm-plugin-banner-actions{display:flex;align-items:center;gap:.25rem;flex:0 0 auto;margin-right:.15rem;padding-right:.4rem;border-right:1px solid var(--dm-border)}.dm-plugin-banner-action{padding:.25rem .4rem;line-height:1;color:var(--dm-text-muted)}.dm-plugin-banner-action:hover{color:var(--dm-primary)}.dm-plugin-banner-icon{display:inline-flex;align-items:center;color:var(--dm-primary);flex:0 0 auto}.dm-plugin-banner-name{margin:0;font-size:1.05rem;font-weight:600;letter-spacing:.01em;color:var(--dm-text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dm-activity{display:inline-flex;align-items:center;flex:0 0 auto;margin-left:.35rem}.dm-activity domma-loader{display:inline-block;line-height:0}.dm-activity[hidden]{display:none!important}#admin-topbar>.dm-activity{margin:0 .75rem 0 0}.dm-plugin-banner-parent{font-weight:500;color:var(--dm-text-muted)}.dm-plugin-banner-sep{margin:0 .4rem;color:var(--dm-text-muted)}.dm-plugin-banner-facts{display:flex;align-items:center;gap:.4rem;flex-wrap:wrap}.dm-plugin-fact{font-size:.72rem;line-height:1.6;padding:1px 8px;border-radius:999px;border:1px solid var(--dm-border);color:var(--dm-text-muted);background:var(--dm-surface);white-space:nowrap}.dm-plugin-fact--version{font-variant-numeric:tabular-nums;color:var(--dm-text)}.dm-plugin-fact--licence.is-included{color:var(--dm-info);border-color:var(--dm-info)}.dm-plugin-fact--licence.is-licensed{color:var(--dm-success);border-color:var(--dm-success)}.dm-plugin-fact--expiry{font-variant-numeric:tabular-nums}.dm-plugin-fact--expiry.is-warning{color:var(--dm-warning);border-color:var(--dm-warning)}.dm-plugin-fact--expiry.is-danger{color:var(--dm-danger);border-color:var(--dm-danger)}.dm-plugin-body{padding:1rem}.dm-plugin-body .card.dm-plugin-unwrap{border:0;background:none;box-shadow:none;margin:0}.dm-plugin-body .card.dm-plugin-unwrap>.card-body{padding:0}.dm-plugin-body>.p-4{padding:0}@media(max-width:640px){.dm-plugin-banner{align-items:flex-start;flex-direction:column;gap:.5rem}.dm-plugin-body{padding:.75rem}}.analytics-filter{max-width:240px}.dm-help-btn{display:inline-flex;align-items:center;justify-content:center;vertical-align:middle;margin-left:.3rem;padding:0;border:0;background:none;line-height:1;color:var(--dm-text-muted);opacity:.75;cursor:pointer}.dm-help-btn:hover,.dm-help-btn:focus-visible{color:var(--dm-primary);opacity:1}.dm-help-pop{position:fixed;z-index:10050;max-width:300px;padding:.6rem .75rem;background:var(--dm-surface, var(--dm-background));color:var(--dm-text);border:1px solid var(--dm-border);border-radius:var(--dm-radius-md);box-shadow:0 8px 24px #00000040;font-size:.85rem;line-height:1.45;text-align:left;font-weight:400}.dm-help-pop strong{display:block;margin-bottom:.2rem;font-weight:600}.dm-help-pop p{margin:0}.dm-admin-sidebar .sidebar-badge[data-badge-label]{cursor:help}.dm-admin-sidebar .sidebar-badge--danger,.dm-admin-sidebar .sidebar-link.active .sidebar-badge--danger{background:var(--dm-danger, #dc2626);color:#fff}.dm-admin-sidebar .sidebar-badge--live{background:color-mix(in srgb,var(--dm-success, #16a34a) 20%,transparent);color:var(--dm-success, #16a34a)}#admin-sidebar .sidebar-badge--live{padding-left:1.05rem!important}#admin-sidebar summary>.sidebar-badge--sum{position:static!important;transform:none!important;margin-left:auto!important;margin-right:.45rem;text-transform:none;letter-spacing:0}#admin-sidebar summary:has(>.sidebar-badge--sum):after{margin-left:0}#admin-sidebar details[open]>summary>.sidebar-badge--sum{display:none}.dm-admin-sidebar .sidebar-badge--live:before{content:"";position:absolute;left:.4rem;top:50%;width:.4rem;height:.4rem;margin-top:-.2rem;border-radius:50%;background:currentColor;animation:dm-badge-pulse 2s ease-in-out infinite}@keyframes dm-badge-pulse{50%{opacity:.35}}@media(prefers-reduced-motion:reduce){.dm-admin-sidebar .sidebar-badge--live:before{animation:none}}.dm-badge-pop{width:290px;max-width:calc(100vw - 16px);font-size:.8rem;pointer-events:none}.dm-badge-pop-title{display:block;font-size:.85rem}.dm-badge-pop-sub{margin:.1rem 0 .5rem;color:var(--dm-text-muted)}.dm-badge-pop-rows{display:grid;grid-template-columns:1fr auto;gap:.2rem .75rem;margin:0 0 .5rem;padding:.45rem 0;border-top:1px solid var(--dm-border);border-bottom:1px solid var(--dm-border)}.dm-badge-pop-rows dt{color:var(--dm-text-muted)}.dm-badge-pop-rows dd{margin:0;text-align:right;font-weight:600;font-variant-numeric:tabular-nums}.dm-badge-pop-items{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:.4rem}.dm-badge-pop-items li{display:flex;flex-direction:column;min-width:0}.dm-badge-pop-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dm-badge-pop-meta{font-size:.72rem;color:var(--dm-text-muted)}.dm-badge-pop .is-danger,.dm-badge-pop li.is-danger .dm-badge-pop-meta{color:var(--dm-danger, #dc2626)}.dm-badge-pop-empty{margin:0;color:var(--dm-text-muted)}.dm-plugin-body,.dm-slideover{font-family:var(--dm-font-sans, inherit)}.dm-plugin-body :is(button,input,select,textarea,.btn,.form-input,.form-select,.form-textarea),.dm-slideover :is(button,input,select,textarea,.btn,.form-input,.form-select,.form-textarea){font-family:inherit}.dm-plugin-body :is(input,select,textarea,.form-input,.form-select,.form-textarea):not(.form-input-lg),.dm-slideover :is(input,select,textarea,.form-input,.form-select,.form-textarea):not(.form-input-lg){font-size:var(--dm-text-sm)}.dm-plugin-body .form-input-lg{font-size:var(--dm-text-base)}.dm-plugin-body :is(.font-mono,code,pre,kbd),.dm-slideover :is(.font-mono,code,pre,kbd){font-family:var(--dm-font-mono, monospace)}.dm-plugin-body .form-label,.dm-slideover .form-label{font-size:var(--dm-text-sm)}.related-box{display:flex;flex-direction:column;gap:.6rem;border-top:1px solid var(--dm-border);padding-top:.75rem}.related-box[hidden]{display:none}.related-head{display:flex;align-items:center;gap:.4rem}.related-head strong{flex:1}.related-list{list-style:none;margin:.3rem 0 0;padding:0;display:flex;flex-direction:column;gap:.25rem}.related-list li{display:flex;flex-direction:column}.related-list li.is-past{opacity:.7}.related-meta,.related-note,.related-loading{color:var(--dm-text-muted);font-size:var(--dm-text-xs);margin:0}.login-challenge{display:flex;flex-direction:column;gap:.6rem;margin-bottom:.75rem}.login-challenge #challenge-code{font-size:1.25rem;letter-spacing:.2em;text-align:center}.login-trust{display:flex;align-items:center;gap:.45rem;font-size:.875rem}.login-enrol{display:flex;flex-direction:column;align-items:center;gap:.6rem;margin-bottom:1rem}.login-enrol img{background:#fff;padding:.5rem;border-radius:.5rem}.login-secret{display:flex;flex-direction:column;align-items:center;gap:.25rem;text-align:center}.login-secret code{font-size:.95rem;letter-spacing:.08em;word-break:break-all}.usx-secret{display:flex;flex-direction:column;gap:.75rem;padding:1rem}.usx-secret-link{display:block;padding:.6rem .75rem;border:1px solid var(--dm-border);border-radius:var(--dm-radius-sm);background:var(--dm-background-alt);word-break:break-all;font-size:.8rem;user-select:all}.usx-secret-actions{display:flex;align-items:center;justify-content:flex-end;gap:.5rem}.usx-secret-actions .btn{display:inline-flex;align-items:center;gap:.35rem}.usx-secret-note{margin-right:auto;color:var(--dm-success)}.usx-secret-foot{color:var(--dm-text-muted);font-size:.8rem;margin:0}.uex-reset{display:flex;flex-wrap:wrap;align-items:center;gap:.35rem .5rem;margin-top:.5rem}.uex-reset>small:first-child{flex-basis:100%;color:var(--dm-text-muted)}.uex-reset .btn{display:inline-flex;align-items:center;gap:.35rem}.uex-reset-open{flex-basis:100%;color:var(--dm-info)}
@@ -1 +1 @@
1
- export function isAuthScreen(e){return/^\/(login|reset-password)(\?|$)/.test(String(e??""))}const i=/^\/(login|reset-password)(\/|\?|$)/,a=/^\/[A-Za-z0-9\-._~%/?=&:@+,]*$/;export function safeReturnPath(e){const t=String(e??"").trim();return!t||t.length>500||t.startsWith("//")||!a.test(t)||t==="/"||i.test(t)?"":t}export function loginHashFor(e,{expired:t=!1}={}){const n=safeReturnPath(String(e??"").replace(/^#/,"")),r=[n&&`next=${encodeURIComponent(n)}`,t&&"why=expired"].filter(Boolean);return`#/login${r.length?`?${r.join("&")}`:""}`}export function readLoginHash(e){const t=String(e??"").split("?")[1]||"",n=new URLSearchParams(t);let r="";try{r=safeReturnPath(n.get("next")||"")}catch{r=""}return{next:r||"/",expired:n.get("why")==="expired"}}export function resetTokenFrom(e){const t=String(e??"").match(/[?&]token=([a-f0-9]{64})(?:&|$)/);return t?t[1]:""}export function passwordVerdict({password:e="",confirm:t="",problems:n=[]}){const r=[];e&&e.length<8&&r.push("At least 8 characters.");for(const o of n||[])o&&!/at least 8 characters/i.test(o)&&r.push(String(o));const s=!!t&&t!==e;return{ok:e.length>=8&&!r.length&&t===e,messages:r,mismatch:s}}export function minutesText(e){const t=Math.max(1,Math.round(Number(e)||0));if(t<55)return`${t} minute${t===1?"":"s"}`;const n=Math.round(t/60);return n<48?`${n} hour${n===1?"":"s"}`:`${Math.round(n/24)} days`}
1
+ export function isAuthScreen(e){return/^\/(login|reset-password)(\?|$)/.test(String(e??""))}const i=/^\/(login|reset-password)(\/|\?|$)/,c=/^\/[A-Za-z0-9\-._~%/?=&:@+,]*$/;export function safeReturnPath(e){const t=String(e??"").trim();return!t||t.length>500||t.startsWith("//")||!c.test(t)||t==="/"||i.test(t)?"":t}export function loginHashFor(e,{expired:t=!1}={}){const n=safeReturnPath(String(e??"").replace(/^#/,"")),r=[n&&`next=${encodeURIComponent(n)}`,t&&"why=expired"].filter(Boolean);return`#/login${r.length?`?${r.join("&")}`:""}`}export function readLoginHash(e){const t=String(e??"").split("?")[1]||"",n=new URLSearchParams(t);let r="";try{r=safeReturnPath(n.get("next")||"")}catch{r=""}return{next:r||"/",expired:n.get("why")==="expired"}}export function resetTokenFrom(e){const t=String(e??"").match(/[?&]token=([a-f0-9]{64})(?:&|$)/);return t?t[1]:""}export function passwordVerdict({password:e="",confirm:t="",problems:n=[]}){const r=[];e&&e.length<8&&r.push("At least 8 characters.");for(const o of n||[])o&&!/at least 8 characters/i.test(o)&&r.push(String(o));const s=!!t&&t!==e;return{ok:e.length>=8&&!r.length&&t===e,messages:r,mismatch:s}}export function minutesText(e){const t=Math.max(1,Math.round(Number(e)||0));if(t<55)return`${t} minute${t===1?"":"s"}`;const n=Math.round(t/60);return n<48?`${n} hour${n===1?"":"s"}`:`${Math.round(n/24)} days`}export function challengeText(e,t){const n=e||{};return t==="recovery"?{label:"Recovery code",help:"Type one of the recovery codes you saved when you set this up. Each works once.",inputMode:"text"}:{label:n.label||"Code",help:n.help||"Enter the code to finish signing in.",inputMode:n.inputMode==="text"?"text":"numeric"}}
@@ -1,134 +1,156 @@
1
+ <!-- Sign in, first-time setup, onboarding and password reset. Bound with M.applyBindings - see
2
+ admin/js/views/login.js: `step` says which panel shows, and nothing here is shown or hidden by hand.
3
+ The F.render forms (setup, login, forgot, reset) are drawn into their containers as before. -->
1
4
  <div class="login-wrap">
2
5
  <div class="login-card card">
3
6
  <div class="card-body">
4
7
  <div class="login-logo">
5
8
  <span data-icon="layout"></span>
6
- <h1 id="login-site-title">Domma CMS</h1>
9
+ <h1 data-bind-text="siteTitle.value">Domma CMS</h1>
7
10
  </div>
8
11
 
9
12
  <!-- ── Step 1: Setup (first-time - no users exist) ── -->
10
- <div id="setup-panel" style="display:none">
13
+ <div hidden data-bind-hidden="step.value !== 'setup'">
11
14
  <h2 class="login-heading">Welcome - create your admin account</h2>
12
15
  <p class="text-muted mb-4">This is a first-time setup. Set up your administrator account to get started.</p>
13
16
  <div id="setup-form-container"></div>
14
17
  </div>
15
18
 
16
19
  <!-- ── Step 2: Onboarding - Site Identity ── -->
17
- <div id="onboarding-site-panel" style="display:none">
20
+ <div hidden data-bind-hidden="step.value !== 'onboarding-site'">
18
21
  <h2 class="login-heading">Name your site</h2>
19
22
  <p class="text-muted mb-4">You can change these any time from Settings.</p>
20
23
  <div class="mb-3">
21
24
  <label class="form-label" for="ob-title">Site title</label>
22
- <input type="text" id="ob-title" class="form-input" placeholder="My Blog">
25
+ <input type="text" id="ob-title" class="form-input" placeholder="My Blog" data-model="obTitle.value">
23
26
  </div>
24
27
  <div class="mb-4">
25
28
  <label class="form-label" for="ob-tagline">Tagline <span class="text-muted">(optional)</span></label>
26
- <input type="text" id="ob-tagline" class="form-input" placeholder="A short description of your site">
29
+ <input type="text" id="ob-tagline" class="form-input" placeholder="A short description of your site" data-model="obTagline.value">
27
30
  </div>
28
- <div id="ob-site-error" class="alert alert-danger mb-3" style="display:none"></div>
29
- <button id="ob-site-btn" class="btn btn-primary btn-block">Continue</button>
30
- <a href="#" id="ob-site-skip" class="btn-skip">Skip this step</a>
31
+ <div class="alert alert-danger mb-3" hidden data-bind-hidden="!obSiteError.value" data-bind-text="obSiteError.value"></div>
32
+ <button class="btn btn-primary btn-block" data-on-click="saveSite" data-bind-disabled="obSiteSaving.value"
33
+ data-bind-text="obSiteSaving.value ? 'Saving…' : 'Continue'">Continue</button>
34
+ <a href="#" class="btn-skip" data-on-click="skipSite">Skip this step</a>
31
35
  </div>
32
36
 
33
37
  <!-- ── Step 3: Onboarding - Theme Picker ── -->
34
- <div id="onboarding-theme-panel" style="display:none">
38
+ <div hidden data-bind-hidden="step.value !== 'onboarding-theme'">
35
39
  <h2 class="login-heading">Choose a theme</h2>
36
40
  <p class="text-muted mb-3">Pick the colour scheme for your site. You can change it later.</p>
37
- <div class="theme-grid" id="theme-grid"></div>
38
- <div id="ob-theme-error" class="alert alert-danger mb-3" style="display:none"></div>
39
- <button id="ob-theme-btn" class="btn btn-primary btn-block mt-3">Apply theme</button>
40
- <a href="#" id="ob-theme-skip" class="btn-skip">Skip this step</a>
41
+ <div class="theme-grid" data-each="themes key=id">
42
+ <div class="theme-swatch" data-bind-title="id" data-on-click="$parent.pickTheme(id)"
43
+ data-bind-class="$parent.theme.value === id && 'selected'">
44
+ <div class="theme-swatch-preview" data-bind-style-background="bg">
45
+ <div class="theme-swatch-accent" data-bind-style-background="primary"></div>
46
+ </div>
47
+ <div class="theme-swatch-label">
48
+ <span data-bind-text="label"></span>
49
+ <span class="theme-swatch-mode" data-bind-text="dark ? 'Dark' : 'Light'"></span>
50
+ </div>
51
+ </div>
52
+ </div>
53
+ <div class="alert alert-danger mb-3" hidden data-bind-hidden="!obThemeError.value" data-bind-text="obThemeError.value"></div>
54
+ <button class="btn btn-primary btn-block mt-3" data-on-click="saveTheme" data-bind-disabled="obThemeSaving.value"
55
+ data-bind-text="obThemeSaving.value ? 'Applying…' : 'Apply theme'">Apply theme</button>
56
+ <a href="#" class="btn-skip" data-on-click="skipTheme">Skip this step</a>
41
57
  </div>
42
58
 
43
59
  <!-- ── Step 4: Onboarding - Done ── -->
44
- <div id="onboarding-done-panel" style="display:none">
60
+ <div hidden data-bind-hidden="step.value !== 'onboarding-done'">
45
61
  <div class="ob-done-icon"><span data-icon="check-circle"></span></div>
46
62
  <h2 class="login-heading">You're all set!</h2>
47
63
  <p class="text-muted mb-4">Your site is ready. Head to the dashboard to start creating content.</p>
48
- <a href="#/" id="ob-go-btn" class="btn btn-primary btn-block">Go to dashboard</a>
64
+ <a href="#/" class="btn btn-primary btn-block">Go to dashboard</a>
49
65
  </div>
50
66
 
51
67
  <!-- ── Login mode ── -->
52
- <div id="login-panel" style="display:none">
68
+ <div hidden data-bind-hidden="step.value !== 'login'">
53
69
  <h2 class="login-heading">Sign in</h2>
54
- <div id="login-notice" class="alert alert-info mb-3" style="display:none"></div>
70
+ <div class="alert alert-info mb-3" hidden data-bind-hidden="!notice.value" data-bind-text="notice.value"></div>
55
71
  <div id="login-form-container"></div>
56
- <a href="#" id="forgot-link" class="btn-skip">Forgot your password?</a>
72
+ <a href="#" class="btn-skip" data-on-click="forgot">Forgot your password?</a>
57
73
  </div>
58
74
 
59
- <!-- ── A sign-in check asks one more thing (two-factor) ── -->
60
- <div id="challenge-panel" style="display:none">
61
- <h2 class="login-heading" id="challenge-title">One more step</h2>
62
- <p class="text-muted mb-4" id="challenge-help"></p>
63
- <div class="login-enrol" id="challenge-enrol" style="display:none">
64
- <img id="challenge-image" alt="QR code to scan with your authenticator app" width="180" height="180">
75
+ <!-- ── A sign-in check asks one more thing (two-factor). Plugin text: bound as text, never markup. ── -->
76
+ <div hidden data-bind-hidden="step.value !== 'challenge'">
77
+ <h2 class="login-heading" data-bind-text="chTitle.value">One more step</h2>
78
+ <p class="text-muted mb-4" data-bind-text="chHelp.value"></p>
79
+ <div class="login-enrol" hidden data-bind-hidden="!(chSecret.value || chImage.value)">
80
+ <!-- Only a data: image reaches here (the server checks it). -->
81
+ <img alt="QR code to scan with your authenticator app" width="180" height="180"
82
+ hidden data-bind-hidden="!chImage.value" data-bind-src="chImage.value">
65
83
  <div class="login-secret">
66
84
  <small class="text-muted">Or type this key into the app:</small>
67
- <code id="challenge-secret"></code>
68
- <button type="button" class="btn btn-xs btn-ghost" id="challenge-copy">Copy</button>
85
+ <code data-bind-text="chSecret.value"></code>
86
+ <button type="button" class="btn btn-xs btn-ghost" data-on-click="copySecret">Copy</button>
69
87
  </div>
70
88
  </div>
71
- <form id="challenge-form" class="login-challenge" autocomplete="off">
72
- <label class="form-label" for="challenge-code" id="challenge-label">Code</label>
73
- <input id="challenge-code" class="form-input" autocomplete="one-time-code" inputmode="numeric" maxlength="64" required>
74
- <label class="login-trust" id="challenge-trust-wrap" style="display:none">
75
- <input type="checkbox" id="challenge-trust"> Trust this browser for 30 days</label>
76
- <p class="text-danger" id="challenge-error" style="display:none"></p>
77
- <button type="submit" class="btn btn-primary btn-block" id="challenge-submit">Verify</button>
89
+ <form class="login-challenge" autocomplete="off" data-on-submit="verify">
90
+ <label class="form-label" for="challenge-code" data-bind-text="chLabel.value">Code</label>
91
+ <input id="challenge-code" class="form-input" autocomplete="one-time-code" maxlength="64" required
92
+ data-model="chCode.value" data-bind-inputmode="chInputMode.value">
93
+ <label class="login-trust" hidden data-bind-hidden="!chCanTrust.value">
94
+ <input type="checkbox" data-model="chTrust.value"> Trust this browser for 30 days</label>
95
+ <p class="text-danger" hidden data-bind-hidden="!chError.value" data-bind-text="chError.value"></p>
96
+ <button type="submit" class="btn btn-primary btn-block" data-bind-disabled="chBusy.value">Verify</button>
78
97
  </form>
79
- <a href="#" id="challenge-recovery" class="btn-skip" style="display:none">Use a recovery code instead</a>
80
- <a href="#" id="challenge-back" class="btn-skip">Back to sign in</a>
98
+ <a href="#" class="btn-skip" hidden data-bind-hidden="!chCanRecover.value" data-on-click="toggleRecovery"
99
+ data-bind-text="chMode.value === 'recovery' ? 'Use the code instead' : 'Use a recovery code instead'">Use a recovery code instead</a>
100
+ <a href="#" class="btn-skip" data-on-click="backToLogin">Back to sign in</a>
81
101
  </div>
82
102
 
83
103
  <!-- ── Forgot password ── -->
84
- <div id="forgot-panel" style="display:none">
104
+ <div hidden data-bind-hidden="step.value !== 'forgot'">
85
105
  <h2 class="login-heading">Reset your password</h2>
86
- <p class="text-muted mb-4" id="forgot-intro">Enter your email and we'll send you a reset link.</p>
87
- <div id="forgot-form-container"></div>
88
- <div id="forgot-unavailable" class="alert alert-warning mb-3" style="display:none">
106
+ <p class="text-muted mb-4" data-bind-hidden="!byEmail.value">Enter your email and we'll send you a reset link.</p>
107
+ <div id="forgot-form-container" data-bind-hidden="!byEmail.value"></div>
108
+ <!-- No mail server: say so rather than promise an email that never comes. -->
109
+ <div class="alert alert-warning mb-3" hidden data-bind-hidden="byEmail.value">
89
110
  This site cannot send email, so it cannot send you a reset link. Ask an administrator to set a
90
111
  new password for you.
91
112
  </div>
92
- <a href="#" id="forgot-back-link" class="btn-skip">Back to sign in</a>
113
+ <a href="#" class="btn-skip" data-on-click="backToLogin">Back to sign in</a>
93
114
  </div>
94
115
 
95
116
  <!-- ── Forgot success ── -->
96
- <div id="forgot-success-panel" style="display:none">
117
+ <div hidden data-bind-hidden="step.value !== 'forgot-success'">
97
118
  <div class="ob-done-icon"><span data-icon="mail"></span></div>
98
119
  <h2 class="login-heading">Check your email</h2>
99
120
  <p class="text-muted mb-4">If an account exists for that address, we've sent a reset link. It expires in
100
- <span id="forgot-success-expiry">1 hour</span>. Nothing there? Check your spam folder.</p>
101
- <a href="#" id="forgot-success-back-link" class="btn btn-primary btn-block">Back to sign in</a>
121
+ <span data-bind-text="expiresIn.value || '1 hour'">1 hour</span>. Nothing there? Check your spam folder.</p>
122
+ <a href="#" class="btn btn-primary btn-block" data-on-click="backToLogin">Back to sign in</a>
102
123
  </div>
103
124
 
104
125
  <!-- ── Reset password (the link was checked on opening) ── -->
105
- <div id="reset-panel" style="display:none">
126
+ <div hidden data-bind-hidden="step.value !== 'reset'">
106
127
  <h2 class="login-heading">Set a new password</h2>
107
- <p class="text-muted mb-3">For <strong id="reset-email"></strong>. This link works once and
108
- expires in <span id="reset-expiry"></span>.</p>
128
+ <p class="text-muted mb-3">For <strong data-bind-text="resetEmail.value"></strong>. This link works once and
129
+ expires in <span data-bind-text="resetExpiry.value"></span>.</p>
109
130
  <div id="reset-form-container"></div>
110
- <ul id="reset-rules" class="login-rules" aria-live="polite" style="display:none"></ul>
131
+ <ul class="login-rules" aria-live="polite" hidden data-bind-hidden="!resetLines.value.length"
132
+ data-each="resetLines.value key=id"><li data-bind-text="text"></li></ul>
111
133
  <p class="text-muted login-small">Setting it signs this account out everywhere else, and we email you
112
134
  to say it changed.</p>
113
135
  </div>
114
136
 
115
137
  <!-- ── The reset link is no good ── -->
116
- <div id="reset-invalid-panel" style="display:none">
138
+ <div hidden data-bind-hidden="step.value !== 'reset-invalid'">
117
139
  <div class="ob-done-icon login-icon-warn"><span data-icon="clock"></span></div>
118
140
  <h2 class="login-heading">This link has expired or was already used</h2>
119
141
  <p class="text-muted mb-4">Reset links work once and only for a limited time. A newer link also
120
142
  replaces an older one - use the most recent email. You can ask for a new link now.</p>
121
- <a href="#" id="reset-invalid-new" class="btn btn-primary btn-block">Send me a new link</a>
122
- <a href="#" id="reset-invalid-back" class="btn-skip">Back to sign in</a>
143
+ <a href="#" class="btn btn-primary btn-block" data-on-click="newLink">Send me a new link</a>
144
+ <a href="#/login" class="btn-skip">Back to sign in</a>
123
145
  </div>
124
146
 
125
147
  <!-- ── Reset done ── -->
126
- <div id="reset-done-panel" style="display:none">
148
+ <div hidden data-bind-hidden="step.value !== 'reset-done'">
127
149
  <div class="ob-done-icon"><span data-icon="check-circle"></span></div>
128
150
  <h2 class="login-heading">Password changed</h2>
129
151
  <p class="text-muted mb-4">Sign in with your new password. Anywhere else you were signed in has been
130
152
  signed out.</p>
131
- <a href="#" id="reset-done-signin" class="btn btn-primary btn-block">Sign in</a>
153
+ <a href="#" class="btn btn-primary btn-block" data-on-click="signInAfterReset">Sign in</a>
132
154
  </div>
133
155
  </div>
134
156
  </div>
@@ -1,11 +1 @@
1
- import{api as l,isAuthenticated as N,setAuthData as b,takePendingLogout as U}from"../api.js";import{themesInDisplayOrder as V}from"../lib/themes.js";import{enhancePasswordFields as v}from"../lib/password-field.js";import{minutesText as W,passwordVerdict as Y,readLoginHash as j,resetTokenFrom as G}from"../lib/login-arrange.js";const H={name:{type:"string",required:!0,minLength:2,label:"Full Name",formConfig:{placeholder:"Your name",autocomplete:"name"}},email:{type:"email",required:!0,label:"Email Address",formConfig:{placeholder:"admin@example.com",autocomplete:"email"}},password:{type:"password",required:!0,minLength:8,label:"Password",formConfig:{placeholder:"\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",autocomplete:"new-password",tooltip:"Minimum 8 characters"}}},K={email:{type:"email",required:!0,label:"Email Address",formConfig:{placeholder:"you@example.com",autocomplete:"email"}},password:{type:"password",required:!0,label:"Password",formConfig:{placeholder:"\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",autocomplete:"current-password"}}};export const loginView={templateUrl:"/admin/js/templates/login.html",async onMount(e){Q();const t=U(),s=window.location.hash,{next:o,expired:r}=j(s);if(N()){k(s.startsWith("#/reset-password")?"/":o);return}if(s.startsWith("#/reset-password")){await ee(e,G(s));return}let c=!1;const p=E.loader(e.get(0),{type:"dots"});try{const u=await l.auth.setupStatus();c=u.needsSetup,x(e,u)}catch{E.toast("Could not reach the server.",{type:"error"})}finally{p.destroy()}c?(a(e,"setup"),Z(e)):(a(e,"login"),A(e,{next:o}),r&&!t&&e.find("#login-notice").text("Your session ended - sign in again to carry on where you were.").show()),Domma.icons.scan()}};const z={email:{type:"email",required:!0,label:"Email Address",formConfig:{placeholder:"you@example.com",autocomplete:"email"}}},J={password:{type:"password",required:!0,minLength:8,label:"New Password",formConfig:{placeholder:"\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",autocomplete:"new-password"}},confirmPassword:{type:"password",required:!0,label:"Confirm Password",formConfig:{placeholder:"\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",autocomplete:"new-password"}}};let P=Promise.resolve();function Q(){let e;P=new Promise(o=>{e=o});const t=M.subscribe("router:afterChange",()=>{s(),e()}),s=()=>{typeof t=="function"&&t()};setTimeout(()=>{s(),e()},1500)}function k(e){P.then(()=>R.navigate(e))}let g={byEmail:!0,expiresIn:""};function x(e,t={}){g={byEmail:t.resetByEmail!==!1,expiresIn:t.resetExpiresIn||""},t.siteTitle&&e.find("#login-site-title").text(t.siteTitle)}const X=["setup","onboarding-site","onboarding-theme","onboarding-done","login","forgot","forgot-success","reset","reset-invalid","reset-done","challenge"];function a(e,t){X.forEach(s=>e.find(`#${s}-panel`).hide()),e.find(`#${t}-panel`).show(),Domma.icons.scan()}function D(e,t,s){e.find(`#${t}`).text(s).show()}function Z(e){F.render("#setup-form-container",H,{},{layout:"stacked",submitText:"Create admin account",onSubmit:async t=>{try{const s=await l.auth.setup(t);b(s),a(e,"onboarding-site"),_(e)}catch(s){return E.toast(s.message||"Setup failed. Please try again.",{type:"error"}),!1}}}),v(e.find("#setup-form-container").get(0))}function _(e){e.find("#ob-site-skip").on("click",t=>{t.preventDefault(),a(e,"onboarding-theme"),I(e),L(e)}),e.find("#ob-site-btn").on("click",async()=>{e.find("#ob-site-error").hide();const t=e.find("#ob-title").val().trim(),s=e.find("#ob-tagline").val().trim(),o=e.find("#ob-site-btn").prop("disabled",!0).text("Saving\u2026");try{const r=await l.settings.get();await l.settings.save({...r,title:t||r.title,tagline:s||r.tagline,seo:{...r.seo||{},defaultTitle:t||r.seo&&r.seo.defaultTitle}});const c=await l.navigation.get();await l.navigation.save({...c,brand:{...c.brand||{},text:t||c.brand.text}}),a(e,"onboarding-theme"),I(e),L(e)}catch(r){D(e,"ob-site-error",r.message||"Could not save site details.")}finally{o.prop("disabled",!1).text("Continue")}})}function I(e){const t=e.find("#theme-grid").empty();V().forEach(s=>{const o=$(`
2
- <div class="theme-swatch" data-theme="${s.id}" title="${s.id}">
3
- <div class="theme-swatch-preview" style="background:${s.bg}">
4
- <div class="theme-swatch-accent" style="background:${s.primary}"></div>
5
- </div>
6
- <div class="theme-swatch-label">
7
- <span>${s.label}</span>
8
- <span class="theme-swatch-mode">${s.dark?"Dark":"Light"}</span>
9
- </div>
10
- </div>
11
- `);o.on("click",()=>{e.find(".theme-swatch").removeClass("selected"),o.addClass("selected")}),t.append(o)}),e.find('[data-theme="charcoal-dark"]').addClass("selected")}function L(e){e.find("#ob-theme-skip").on("click",t=>{t.preventDefault(),a(e,"onboarding-done")}),e.find("#ob-theme-btn").on("click",async()=>{e.find("#ob-theme-error").hide();const t=e.find(".theme-swatch.selected").attr("data-theme")||"charcoal-dark",s=e.find("#ob-theme-btn").prop("disabled",!0).text("Applying\u2026");try{const o=await l.settings.get();await l.settings.save({...o,theme:t}),a(e,"onboarding-done")}catch(o){D(e,"ob-theme-error",o.message||"Could not save theme.")}finally{s.prop("disabled",!1).text("Apply theme")}})}function A(e,{next:t="/",email:s=""}={}){F.render("#login-form-container",K,s?{email:s}:{},{layout:"stacked",submitText:"Sign in",onSubmit:async o=>{try{const r=await l.auth.login(o);if(r?.challenge&&r.ticket)return q(e,r,t),!1;b(r),k(t)}catch(r){return E.toast(r.message||"Invalid credentials. Please try again.",{type:"error"}),!1}}}),v(e.find("#login-form-container").get(0)),e.find("#forgot-link").off("click").on("click",o=>{o.preventDefault();const r=String(e.find('#login-form-container input[type="email"]').val()||"").trim();a(e,"forgot"),B(e,r)})}function q(e,{challenge:t,ticket:s},o="/"){let r="code";const c=e.find("#challenge-code"),p=e.find("#challenge-error"),u=e.find("#challenge-recovery"),m=f=>{r=f;const i=f==="recovery";e.find("#challenge-label").text(i?"Recovery code":t.label||"Code"),e.find("#challenge-help").text(i?"Type one of the recovery codes you saved when you set this up. Each works once.":t.help||"Enter the code to finish signing in."),c.val("").attr("inputmode",i||t.inputMode==="text"?"text":"numeric"),u.text(i?"Use the code instead":"Use a recovery code instead"),p.hide(),setTimeout(()=>c.trigger("focus"),0)};e.find("#challenge-title").text(t.title||"One more step");const y=!!(t.image||t.secret);e.find("#challenge-enrol").css("display",y?"":"none");const h=e.find("#challenge-image").get(0);h&&(h.src=t.image||"",h.style.display=t.image?"":"none"),e.find("#challenge-secret").text(t.secret||""),e.find("#challenge-copy").off("click").on("click",async()=>{try{await navigator.clipboard.writeText(String(t.secret||"").replace(/\s+/g,"")),E.toast("Key copied.",{type:"success"})}catch{E.toast("Could not copy - select the key and copy it.",{type:"warning"})}}),e.find("#challenge-trust-wrap").css("display",t.trust===!0?"":"none"),u.css("display",t.recovery===!0?"":"none"),m("code"),a(e,"challenge"),u.off("click").on("click",f=>{f.preventDefault(),m(r==="code"?"recovery":"code")}),e.find("#challenge-back").off("click").on("click",f=>{f.preventDefault(),a(e,"login")}),e.find("#challenge-form").off("submit").on("submit",async f=>{f.preventDefault();const i=e.find("#challenge-submit").prop("disabled",!0);try{const d=await l.auth.verifyLogin({ticket:s,response:String(c.val()||"").trim(),mode:r,trust:e.find("#challenge-trust").prop("checked")===!0});if(d?.challenge&&d.ticket){q(e,d,o);return}b(d),k(o)}catch(d){const n=d.message||"That did not match.";p.text(n).show(),/sign in again/i.test(n)&&setTimeout(()=>a(e,"login"),1800)}finally{i.prop("disabled",!1)}})}function B(e,t=""){const s=o=>{o.preventDefault(),a(e,"login")};if(e.find("#forgot-back-link").off("click").on("click",s),e.find("#forgot-success-back-link").off("click").on("click",s),!g.byEmail){e.find("#forgot-intro").hide(),e.find("#forgot-form-container").empty(),e.find("#forgot-unavailable").show();return}e.find("#forgot-intro").show(),e.find("#forgot-unavailable").hide(),F.render("#forgot-form-container",z,t?{email:t}:{},{layout:"stacked",submitText:"Send reset link",onSubmit:async o=>{try{const r=await l.auth.forgotPassword(o.email);r?.expiresIn&&(g.expiresIn=r.expiresIn)}catch{}g.expiresIn&&e.find("#forgot-success-expiry").text(g.expiresIn),a(e,"forgot-success"),Domma.icons.scan()}})}async function ee(e,t){if(!t){w(e);return}const s=E.loader(e.get(0),{type:"dots"});let o;l.auth.setupStatus().then(r=>x(e,r)).catch(()=>{});try{o=await l.auth.checkReset(t)}catch{w(e);return}finally{s.destroy()}e.find("#reset-email").text(o.email||""),e.find("#reset-expiry").text(W(o.minutesLeft)),a(e,"reset"),te(e,t,o.email||"")}function w(e){a(e,"reset-invalid"),e.find("#reset-invalid-back").off("click").on("click",t=>{t.preventDefault(),window.location.hash="#/login"}),e.find("#reset-invalid-new").off("click").on("click",async t=>{t.preventDefault();try{x(e,await l.auth.setupStatus())}catch{}a(e,"forgot"),B(e)})}function te(e,t,s){let o=[],r=0,c=null;const p=e.find("#reset-rules"),u=()=>({password:String(e.find('#reset-form-container input[name="password"]').val()||""),confirm:String(e.find('#reset-form-container input[name="confirmPassword"]').val()||"")}),m=()=>{const{password:i,confirm:d}=u(),n=Y({password:i,confirm:d,problems:o}),S=[...n.messages,...n.mismatch?["The two passwords do not match yet."]:[]],T=p.get(0);T.textContent="";for(const O of S){const C=document.createElement("li");C.textContent=O,T.appendChild(C)}return p.css("display",S.length&&i?"":"none"),n},y=async()=>{const{password:i}=u(),d=++r;if(i.length<8){o=[],m();return}try{const n=await l.auth.checkReset(t,i);d===r&&(o=n.problems||[])}catch(n){if(n.body?.expired){w(e);return}}d===r&&m()};F.render("#reset-form-container",J,{},{layout:"stacked",submitText:"Set new password",onSubmit:async i=>{await y();const d=m();if(!d.ok)return E.toast(d.mismatch?"The passwords do not match.":"That password does not meet the rules below.",{type:"error"}),!1;try{await l.auth.resetPassword(t,i.password),history.replaceState(null,"","#/reset-password"),a(e,"reset-done"),e.find("#reset-done-signin").off("click").on("click",n=>{n.preventDefault(),a(e,"login"),A(e,{email:s})})}catch(n){return n.body?.expired?(w(e),!1):(o=n.body?.problems||[],m(),E.toast(n.message||"The password was not changed.",{type:"error"}),!1)}}});const h=e.find("#reset-form-container form").get(0)||e.find("#reset-form-container").get(0);if(h&&s){const i=document.createElement("input");i.type="email",i.name="username",i.autocomplete="username",i.value=s,i.readOnly=!0,i.hidden=!0,h.prepend(i)}v(e.find("#reset-form-container").get(0));const f=i=>{i.target.matches?.('input[type="password"], input[name="password"], input[name="confirmPassword"]')&&(i.target.name==="password"&&(o=[]),m(),clearTimeout(c),i.target.name==="password"&&(c=setTimeout(y,400)))};e.find("#reset-form-container").get(0)?.addEventListener("input",f)}
1
+ import{api as i,isAuthenticated as B,setAuthData as f,takePendingLogout as A}from"../api.js";import{themesInDisplayOrder as D}from"../lib/themes.js";import{enhancePasswordFields as g}from"../lib/password-field.js";import{challengeText as b,minutesText as O,passwordVerdict as j,readLoginHash as H,resetTokenFrom as N}from"../lib/login-arrange.js";import{viewLifetime as V}from"../lib/tool-kit.js";const W={name:{type:"string",required:!0,minLength:2,label:"Full Name",formConfig:{placeholder:"Your name",autocomplete:"name"}},email:{type:"email",required:!0,label:"Email Address",formConfig:{placeholder:"admin@example.com",autocomplete:"email"}},password:{type:"password",required:!0,minLength:8,label:"Password",formConfig:{placeholder:"\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",autocomplete:"new-password",tooltip:"Minimum 8 characters"}}},Y={email:{type:"email",required:!0,label:"Email Address",formConfig:{placeholder:"you@example.com",autocomplete:"email"}},password:{type:"password",required:!0,label:"Password",formConfig:{placeholder:"\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",autocomplete:"current-password"}}},K={email:{type:"email",required:!0,label:"Email Address",formConfig:{placeholder:"you@example.com",autocomplete:"email"}}},U={password:{type:"password",required:!0,minLength:8,label:"New Password",formConfig:{placeholder:"\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",autocomplete:"new-password"}},confirmPassword:{type:"password",required:!0,label:"Confirm Password",formConfig:{placeholder:"\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",autocomplete:"new-password"}}};let S=Promise.resolve();function z(){let e;S=new Promise(o=>{e=o});const t=M.subscribe("router:afterChange",()=>{a(),e()}),a=()=>{typeof t=="function"&&t()};setTimeout(()=>{a(),e()},1500)}function y(e){S.then(()=>R.navigate(e))}const d=V(/^#\/(login|reset-password)(\?|$)/);function G(){const e=M.observable(null),t=M.observable("code");return{step:M.observable(""),siteTitle:M.observable("Domma CMS"),notice:M.observable(""),byEmail:M.observable(!0),expiresIn:M.observable(""),obTitle:M.observable(""),obTagline:M.observable(""),obSiteError:M.observable(""),obSiteSaving:M.observable(!1),themes:D(),theme:M.observable("charcoal-dark"),obThemeError:M.observable(""),obThemeSaving:M.observable(!1),chChallenge:e,chMode:t,chTitle:M.computed(()=>e.value?.title||"One more step"),chLabel:M.computed(()=>b(e.value,t.value).label),chHelp:M.computed(()=>b(e.value,t.value).help),chInputMode:M.computed(()=>b(e.value,t.value).inputMode),chImage:M.computed(()=>e.value?.image||""),chSecret:M.computed(()=>e.value?.secret||""),chCanTrust:M.computed(()=>e.value?.trust===!0),chCanRecover:M.computed(()=>e.value?.recovery===!0),chCode:M.observable(""),chTrust:M.observable(!1),chError:M.observable(""),chBusy:M.observable(!1),resetEmail:M.observable(""),resetExpiry:M.observable(""),resetLines:M.observable([])}}function w(e,t={}){e.byEmail.value=t.resetByEmail!==!1,e.expiresIn.value=t.resetExpiresIn||"",t.siteTitle&&(e.siteTitle.value=t.siteTitle)}export const loginView={templateUrl:"/admin/js/templates/login.html",async onMount(e){d.reset(),z();const t=A(),a=window.location.hash,{next:o,expired:s}=H(a);if(B()){y(a.startsWith("#/reset-password")?"/":o);return}const n=e.get(0).querySelector(".login-wrap")||e.get(0),c=G(),l={state:c,root:n,next:o,token:"",email:"",pw:{problems:[],asked:0,timer:null}},p=M.applyBindings(c,n,{methods:J(l)});if(d.add(()=>p.dispose()),d.add(()=>clearTimeout(l.pw.timer)),d.add(M.effect(()=>{c.step.value,requestAnimationFrame(()=>{n.isConnected&&I.scan(n)})})),a.startsWith("#/reset-password")){await $(l,N(a));return}let h=!1;const v=E.loader(e.get(0),{type:"dots"});try{const r=await i.auth.setupStatus();h=r.needsSetup,w(c,r)}catch{E.toast("Could not reach the server.",{type:"error"})}finally{v.destroy()}h?(c.step.value="setup",Q(l)):(c.step.value="login",T(l),s&&!t&&(c.notice.value="Your session ended - sign in again to carry on where you were."))}};function J(e){const{state:t}=e,a=o=>s=>{s?.preventDefault?.(),o()};return{skipSite:a(()=>{t.step.value="onboarding-theme"}),saveSite:()=>X(t),pickTheme:o=>{t.theme.value=o},skipTheme:a(()=>{t.step.value="onboarding-done"}),saveTheme:()=>Z(t),backToLogin:a(()=>{t.step.value="login"}),forgot:a(()=>{const o=String(e.root.querySelector('#login-form-container input[type="email"]')?.value||"").trim();t.step.value="forgot",C(e,o)}),toggleRecovery:a(()=>x(t,t.chMode.value==="code"?"recovery":"code")),copySecret:async()=>{try{await navigator.clipboard.writeText(String(t.chSecret.value).replace(/\s+/g,"")),E.toast("Key copied.",{type:"success"})}catch{E.toast("Could not copy - select the key and copy it.",{type:"warning"})}},verify:o=>{o?.preventDefault?.(),_(e)},newLink:a(async()=>{try{w(t,await i.auth.setupStatus())}catch{}t.step.value="forgot",C(e)}),signInAfterReset:a(()=>{t.step.value="login",T(e,{email:e.email})})}}function Q(e){F.render("#setup-form-container",W,{},{layout:"stacked",submitText:"Create admin account",onSubmit:async t=>{try{const a=await i.auth.setup(t);f(a),e.state.step.value="onboarding-site"}catch(a){return E.toast(a.message||"Setup failed. Please try again.",{type:"error"}),!1}}}),g(e.root.querySelector("#setup-form-container"))}async function X(e){e.obSiteError.value="";const t=e.obTitle.value.trim(),a=e.obTagline.value.trim();e.obSiteSaving.value=!0;try{const o=await i.settings.get();await i.settings.save({...o,title:t||o.title,tagline:a||o.tagline,brand:{...o.brand||{},text:t||o.brand?.text||o.title},seo:{...o.seo||{},defaultTitle:t||o.seo&&o.seo.defaultTitle}}),e.step.value="onboarding-theme"}catch(o){e.obSiteError.value=o.message||"Could not save site details."}finally{e.obSiteSaving.value=!1}}async function Z(e){e.obThemeError.value="",e.obThemeSaving.value=!0;try{await i.theme.save({theme:e.theme.value||"charcoal-dark"}),e.step.value="onboarding-done"}catch(t){e.obThemeError.value=t.message||"Could not save theme."}finally{e.obThemeSaving.value=!1}}function T(e,{email:t=""}={}){F.render("#login-form-container",Y,t?{email:t}:{},{layout:"stacked",submitText:"Sign in",onSubmit:async a=>{try{const o=await i.auth.login(a);if(o?.challenge&&o.ticket)return k(e,o),!1;f(o),y(e.next)}catch(o){return E.toast(o.message||"Invalid credentials. Please try again.",{type:"error"}),!1}}}),g(e.root.querySelector("#login-form-container"))}function k(e,{challenge:t,ticket:a}){const{state:o}=e;e.ticket=a,o.chChallenge.value=t||{},o.chTrust.value=!1,x(o,"code"),o.step.value="challenge"}function x(e,t){e.chMode.value=t,e.chCode.value="",e.chError.value="",setTimeout(()=>document.getElementById("challenge-code")?.focus(),0)}async function _(e){const{state:t}=e;t.chBusy.value=!0;try{const a=await i.auth.verifyLogin({ticket:e.ticket,response:String(t.chCode.value||"").trim(),mode:t.chMode.value,trust:t.chTrust.value===!0});if(a?.challenge&&a.ticket){k(e,a);return}f(a),y(e.next)}catch(a){const o=a.message||"That did not match.";t.chError.value=o,/sign in again/i.test(o)&&setTimeout(()=>{t.step.value="login"},1800)}finally{t.chBusy.value=!1}}function C(e,t=""){const{state:a}=e;a.byEmail.value&&F.render("#forgot-form-container",K,t?{email:t}:{},{layout:"stacked",submitText:"Send reset link",onSubmit:async o=>{try{const s=await i.auth.forgotPassword(o.email);s?.expiresIn&&(a.expiresIn.value=s.expiresIn)}catch{}a.step.value="forgot-success"}})}async function $(e,t){const{state:a}=e;if(!t){a.step.value="reset-invalid";return}const o=E.loader(e.root,{type:"dots"});let s;i.auth.setupStatus().then(n=>w(a,n)).catch(()=>{});try{s=await i.auth.checkReset(t)}catch{a.step.value="reset-invalid";return}finally{o.destroy()}e.token=t,e.email=s.email||"",a.resetEmail.value=e.email,a.resetExpiry.value=O(s.minutesLeft),a.step.value="reset",ee(e)}function ee(e){const{state:t,token:a,email:o,pw:s}=e,n=e.root.querySelector("#reset-form-container"),c=()=>({password:String(n.querySelector('input[name="password"]')?.value||""),confirm:String(n.querySelector('input[name="confirmPassword"]')?.value||"")}),l=()=>{const{password:r,confirm:m}=c(),u=j({password:r,confirm:m,problems:s.problems}),q=[...u.messages,...u.mismatch?["The two passwords do not match yet."]:[]];return t.resetLines.value=r?q.map((L,P)=>({id:P,text:L})):[],u},p=async()=>{const{password:r}=c(),m=++s.asked;if(r.length<8){s.problems=[],l();return}try{const u=await i.auth.checkReset(a,r);m===s.asked&&(s.problems=u.problems||[])}catch(u){if(u.body?.expired){t.step.value="reset-invalid";return}}m===s.asked&&l()};F.render("#reset-form-container",U,{},{layout:"stacked",submitText:"Set new password",onSubmit:async r=>{await p();const m=l();if(!m.ok)return E.toast(m.mismatch?"The passwords do not match.":"That password does not meet the rules below.",{type:"error"}),!1;try{await i.auth.resetPassword(a,r.password),history.replaceState(null,"","#/reset-password"),t.step.value="reset-done"}catch(u){return u.body?.expired?(t.step.value="reset-invalid",!1):(s.problems=u.body?.problems||[],l(),E.toast(u.message||"The password was not changed.",{type:"error"}),!1)}}});const h=n.querySelector("form")||n;if(o){const r=document.createElement("input");r.type="email",r.name="username",r.autocomplete="username",r.value=o,r.readOnly=!0,r.hidden=!0,h.prepend(r)}g(n);const v=r=>{r.target.matches?.('input[type="password"], input[name="password"], input[name="confirmPassword"]')&&(r.target.name==="password"&&(s.problems=[]),l(),clearTimeout(s.timer),r.target.name==="password"&&(s.timer=setTimeout(p,400)))};n.addEventListener("input",v),d.add(()=>n.removeEventListener("input",v))}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "0.88.0",
3
+ "version": "0.88.1",
4
4
  "description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
5
5
  "type": "module",
6
6
  "main": "server/server.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "note": "Written by `make sync-free-tier` in dcms-marketplace. Do not edit these plugins here - edit them there and sync.",
3
- "source": "dcms-marketplace@c07af11",
3
+ "source": "dcms-marketplace@6301533",
4
4
  "plugins": {
5
5
  "blog": {
6
6
  "version": "1.8.0",
@@ -82,27 +82,28 @@
82
82
  }
83
83
  },
84
84
  "security": {
85
- "version": "1.0.3",
85
+ "version": "1.1.0",
86
86
  "files": {
87
- "CLAUDE.md": "a9b7574236740de80513a535fb761127fa306f2177a5dda6e761213b1ad5b287",
87
+ "CLAUDE.md": "dc6f8755b8e6cdcac69cd928654395cde794e5b698032c132ce83dede24acead",
88
88
  "admin/lib/blocks.js": "5ef346b7de751518f8beb9212e3b89b270ae236560ae3d8a1f40d129c50918ff",
89
89
  "admin/lib/health.js": "3b233a3d29b2d69c333321602c13233b8221cfe1114aa03903739d9ae8ad65d5",
90
90
  "admin/lib/ip.js": "cf33be4fa2e2cad37bafd12ebec17f293891a88de402caad1bf9c9a57ce87680",
91
91
  "admin/lib/lockout.js": "c5e5e4b2934296f81ae8ed013725485bc269a0ef3cf3bdf3c16c53936635a1ee",
92
92
  "admin/lib/policy.js": "a4aedfd28f992e0e7759bbdac6344681eebe617c27849e91fa4405f6190f8462",
93
93
  "admin/lib/probes.js": "d56318903cb645a39f56e7762eddb7411941a72e1f446c1f9d79e35157f1d31b",
94
- "admin/lib/settings.js": "9b6c2fb3b54a1dcb642e3af6f584f93375f824fce67717a1ee56bebee9c35645",
95
- "admin/templates/security.html": "e96060b4849a1ef7691a918b9a77aff05ea3a2182a6093001454d04eb58c648d",
96
- "admin/views/security.js": "dcd38e07c5e83bbe013f5d764c52edef76d28ed4c3fe1cba8c5a59ce1b93e962",
94
+ "admin/lib/resets.js": "3724a2aec9f3c18ada96948aa6ce375ae3505bb7623a2c556e2e2670c5ca343c",
95
+ "admin/lib/settings.js": "b319953914f3a6f029ba991b36fe6fcd521956ea099578be42505f30e6c32faf",
96
+ "admin/templates/security.html": "df5798497ca1fd5ed200ce0ff921a055168ac7720f38b2db1bebf2c0efe71f04",
97
+ "admin/views/security.js": "280c1681f1aae6cf82a141e670d046c160c5f5fc3cb9de43f9d134a08b15a199",
97
98
  "bin/unlock.js": "5fcc3c0ef3ba3c424a366aa62896f51abad578183dfde283aa838a86549ca785",
98
99
  "config.js": "e6a00f7816637687d6a68ebbac65a71489246a926c28523c2a78d859e75b7ae0",
99
- "plugin.js": "3051c886f77b1b1e7f45873fbdcd78a5ba2d72caea97bde1235e84a298ab6114",
100
- "plugin.json": "bdcc88f0f5dec2d707806687ca80e1697255af49b4f30f43650e3824775deef8",
100
+ "plugin.js": "db5dc669a6a26bbe8756da59bdd04217e403be48012921b6d2b868cec57af489",
101
+ "plugin.json": "319d4333d03ed7d42ef9c2d0999befff7594343be963b6fee9c90bf7916ad142",
101
102
  "server/common-passwords.NOTICE": "3b148a49a569bfa24feaa7879db602da8f0607fa61ab6c52b7a62391fc7c009a",
102
103
  "server/common-passwords.txt": "68782d6a4a19a4768d5f15dd66bd534e7a33055cc755411e33f16d18c50fdcce",
103
104
  "server/facts.js": "25538a8d1cfe012688fceb8ec0d0dd7b43ac9e168430723d6fb731183701fb46",
104
105
  "server/state.js": "1319b322e94a4aec176948990b2d674b28dfece761eef4d5685df474037bfc4a",
105
- "tests/security.test.js": "951636a0e42ac8dbf78b69ca7e7e2adcd4e29d94a51998e738c3afa742c00866"
106
+ "tests/security.test.js": "f0cb70dcb28bc8b3de8d3b39064616296382c5b0f97bfd615ee8e24fe3b031bc"
106
107
  }
107
108
  },
108
109
  "shopping-cart": {
@@ -15,6 +15,13 @@ Ships with the engine (free tier, `make sync-free-tier`); switched ON when it fi
15
15
  MIT - see the NOTICE), not containing the name/email, not one or two characters repeated.
16
16
  - **Sign-in log**: `data/signins.jsonl`, kept `signinDays`; notifications (sources `security:lockouts`,
17
17
  `security:suspicious`, `security:probes` - the last off by default).
18
+ - **Reset requests** (1.1.0; core 0.88's `auth:resetRequested`): each "Forgot your password?" is a `kind: 'reset'`
19
+ line in the same log (`ok` = a link was sent, `reason` = the core's outcome - `admin/lib/resets.js` RESET_REASONS).
20
+ Sign-ins › Resets lists them; the overview tile counts 24 h. `security:resets` (setting `notify.resetProbing`, on) is
21
+ raised when one address asks about 3 different accounts within an hour - ALL the core allows it (forgot-password is
22
+ rate-limited to 3 an hour per address, so a higher threshold never trips) - or when 10 requests within an hour, from
23
+ any addresses, name accounts that do not exist (someone rotating addresses). Anything counting sign-ins must skip
24
+ `kind: 'reset'` lines - their `ok` is not a sign-in. Older engines never emit the event: nothing changes there.
18
25
  - **Health check**: `server/facts.js` gathers (users, engine vs npm latest, certificate via tls, security headers of the
19
26
  public home page, private paths fetched LOCALLY, TRUST_PROXY, bind, SMTP, licences); `admin/lib/health.js` judges.
20
27
  Unknown = 'info', never a pass. Cached 10 min; the sidebar badge (`/count`) = checks needing attention, red only
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Security - password reset requests (core's auth:resetRequested, 0.88). Pure.
3
+ *
4
+ * Each "Forgot your password?" goes into the sign-in log as `kind: 'reset'`.
5
+ * One address asking for resets of many different accounts is someone trying
6
+ * addresses, or trying to fill inboxes - that is what `recordReset` watches.
7
+ */
8
+
9
+ /** What became of a request, as the core reports it in `reason`. */
10
+ export const RESET_REASONS = {
11
+ sent: 'Link sent',
12
+ unknown: 'No such account',
13
+ inactive: 'Account switched off',
14
+ throttled: 'Held back - 3 an hour already',
15
+ 'no-smtp': 'Not sent - no mail server',
16
+ 'no-origin': 'Not sent - no Site URL',
17
+ failed: 'Email failed'
18
+ };
19
+
20
+ /**
21
+ * When it is worth telling someone. The core lets one address ask 3 times an hour
22
+ * (a route rate limit), and a person only ever asks about their own account - so
23
+ * one address using all 3 on different accounts is trying addresses. Someone
24
+ * rotating addresses gets past that, which is what the site-wide count of
25
+ * requests for accounts that do not exist is for.
26
+ */
27
+ export const RESET_PROBE = {threshold: 3, windowMin: 60};
28
+ export const RESET_UNKNOWN = {threshold: 10, windowMin: 60};
29
+
30
+ /**
31
+ * Note a reset request from an address and say whether it has just crossed the
32
+ * line: `threshold` different accounts within `windowMin`. Mutates `recent`
33
+ * (a Map of address -> [{at, email}]), dropping what is older than the window.
34
+ *
35
+ * @returns {{accounts: number, tripped: boolean}} tripped only on the request that reaches the threshold
36
+ */
37
+ export function recordReset(recent, ip, email, {now = Date.now(), threshold = RESET_PROBE.threshold, windowMin = RESET_PROBE.windowMin} = {}) {
38
+ if (!ip) return {accounts: 0, tripped: false};
39
+ const since = now - windowMin * 60_000;
40
+ const list = (recent.get(ip) || []).filter(r => r.at >= since);
41
+ const before = new Set(list.map(r => r.email)).size;
42
+ list.push({at: now, email: String(email || '').trim().toLowerCase()});
43
+ recent.set(ip, list);
44
+ const accounts = new Set(list.map(r => r.email)).size;
45
+ return {accounts, tripped: accounts >= threshold && before < threshold};
46
+ }
47
+
48
+ /**
49
+ * Note a request for an account that does not exist, from any address, and say
50
+ * whether the site has just reached `threshold` of them within `windowMin`.
51
+ * Mutates `stamps` (an array of times).
52
+ *
53
+ * @returns {{count: number, tripped: boolean}} tripped only on the request that reaches the threshold
54
+ */
55
+ export function recordUnknown(stamps, {now = Date.now(), threshold = RESET_UNKNOWN.threshold, windowMin = RESET_UNKNOWN.windowMin} = {}) {
56
+ const since = now - windowMin * 60_000;
57
+ const keep = stamps.filter(t => t >= since);
58
+ stamps.length = 0;
59
+ stamps.push(...keep, now);
60
+ return {count: stamps.length, tripped: stamps.length === threshold};
61
+ }
62
+
63
+ /** Forget site-wide stamps older than the window. Mutates `stamps`. */
64
+ export function pruneUnknown(stamps, {now = Date.now(), windowMin = RESET_UNKNOWN.windowMin} = {}) {
65
+ const keep = stamps.filter(t => t >= now - windowMin * 60_000);
66
+ stamps.length = 0;
67
+ stamps.push(...keep);
68
+ }
69
+
70
+ /** Forget addresses with nothing inside the window. */
71
+ export function pruneResets(recent, {now = Date.now(), windowMin = RESET_PROBE.windowMin} = {}) {
72
+ const since = now - windowMin * 60_000;
73
+ for (const [ip, list] of recent) {
74
+ const keep = list.filter(r => r.at >= since);
75
+ if (keep.length) recent.set(ip, keep); else recent.delete(ip);
76
+ }
77
+ }
78
+
79
+ /** One log entry as the Sign-ins list shows it. */
80
+ export function resetRowText(e) {
81
+ return {okText: 'Reset asked', tone: 'reset', note: RESET_REASONS[e.reason] || e.reason || (e.ok ? 'Link sent' : 'Not sent')};
82
+ }
@@ -11,7 +11,7 @@ export const DEFAULT_SETTINGS = {
11
11
  probes: {enabled: true, blockAfter: 5, withinMin: 10, blockForMin: 60, extra: [], allow: []},
12
12
  signinDays: 30,
13
13
  staleAdminDays: 90,
14
- notify: {lockouts: true, successAfterFails: true, probeBlocks: false}
14
+ notify: {lockouts: true, successAfterFails: true, probeBlocks: false, resetProbing: true}
15
15
  };
16
16
 
17
17
  const int = (v, min, max) => {
@@ -22,6 +22,7 @@
22
22
  <div class="scx-card scx-tile"><small>Failed sign-ins, 24h <span data-help="Failed sign-ins in the last 24 hours: a wrong password, an unknown email, a locked account."></span></small><strong data-bind-text="ov.value.failed">—</strong></div>
23
23
  <div class="scx-card scx-tile"><small>Blocked now <span data-help="Addresses blocked by a rule or for probing, and accounts or addresses locked after wrong passwords."></span></small><strong data-bind-text="ov.value.blocked">—</strong></div>
24
24
  <div class="scx-card scx-tile"><small>Probes refused today <span data-help="Requests for things no Domma site has - WordPress logins, .env files, database dumps. Scanners send them to every site they find; they are answered with an empty 'not found'."></span></small><strong data-bind-text="ov.value.probes">—</strong></div>
25
+ <div class="scx-card scx-tile"><small>Reset requests, 24h <span data-help="&quot;Forgot your password?&quot; requests in the last 24 hours, sent or not. Sign-ins › Resets lists them, with what happened and where from."></span></small><strong data-bind-text="ov.value.resets">—</strong></div>
25
26
  </div>
26
27
 
27
28
  <div class="scx-card">
@@ -41,6 +42,7 @@
41
42
  <button type="button" data-on-click="res_all" data-bind-class="result.value === 'all' && 'is-on'">All</button>
42
43
  <button type="button" data-on-click="res_failed" data-bind-class="result.value === 'failed' && 'is-on'">Failed</button>
43
44
  <button type="button" data-on-click="res_ok" data-bind-class="result.value === 'ok' && 'is-on'">Succeeded</button>
45
+ <button type="button" data-on-click="res_resets" data-bind-class="result.value === 'resets' && 'is-on'">Resets</button>
44
46
  </div>
45
47
  <select class="form-input scx-days" aria-label="Period" data-model="days.value">
46
48
  <option value="1">Last 24 hours</option><option value="7">Last 7 days</option><option value="30">Last 30 days</option><option value="365">All kept</option>
@@ -150,6 +152,7 @@
150
152
  .scx-cell { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
151
153
  .scx-pill { display: inline-flex; align-items: center; justify-self: start; font-size: var(--dm-text-xs); font-weight: 600; padding: .05rem .5rem; border-radius: 999px; }
152
154
  .scx-pill[data-ok="true"] { color: var(--dm-success); background: color-mix(in srgb, var(--dm-success) 14%, transparent); }
155
+ .scx-pill[data-ok="reset"] { color: var(--dm-info); background: color-mix(in srgb, var(--dm-info) 14%, transparent); }
153
156
  .scx-pill[data-ok="false"] { color: var(--dm-danger); background: color-mix(in srgb, var(--dm-danger) 12%, transparent); }
154
157
  .scx-kebab { border: 0; background: none; cursor: pointer; font-size: var(--dm-text-lg); line-height: 1; color: var(--dm-text-muted); border-radius: var(--dm-radius-sm); padding: 0 .25rem; }
155
158
  .scx-kebab:hover { color: var(--dm-primary); }
@@ -10,6 +10,7 @@
10
10
  * only, and rows are addressed by index.
11
11
  * `#/plugins/security/<tab>` opens a tab. Pure logic: ../lib/*.js.
12
12
  */
13
+ import {resetRowText} from '../lib/resets.js';
13
14
  import {checkSettings, DEFAULT_SETTINGS, mergeDefaults} from '../lib/settings.js';
14
15
  import {ipRuleValid} from '../lib/ip.js';
15
16
 
@@ -152,7 +153,8 @@ export const securityView = {
152
153
  const asOf = M.computed(() => (health.value?.at ? `Checked ${new Date(health.value.at).toLocaleTimeString('en-GB', {hour: '2-digit', minute: '2-digit'})}` : ''));
153
154
  const ov = M.computed(() => {
154
155
  const o = overview.value;
155
- return o ? {failed: String(o.failed24h), blocked: String(o.blocked + o.locks), probes: String(o.today?.probes || 0)} : {failed: '—', blocked: '—', probes: '—'};
156
+ return o ? {failed: String(o.failed24h), blocked: String(o.blocked + o.locks), probes: String(o.today?.probes || 0), resets: String(o.resets24h ?? 0)}
157
+ : {failed: '—', blocked: '—', probes: '—', resets: '—'};
156
158
  });
157
159
  const blockedCount = M.computed(() => { const b = blocks.value; const n = b.rules.length + b.temp.length + b.locks.length; return n ? String(n) : ''; });
158
160
 
@@ -185,18 +187,25 @@ export const securityView = {
185
187
  const q = query.value.trim().toLowerCase();
186
188
  const r = result.value;
187
189
  const only = ipFilter.value;
188
- signinList = signins.value.filter(e => (r === 'all' || (r === 'ok') === Boolean(e.ok))
190
+ // A reset request is in the log too (kind 'reset'): only under All and Resets.
191
+ const isReset = (e) => e.kind === 'reset';
192
+ signinList = signins.value.filter(e => (r === 'all' || (r === 'resets' ? isReset(e) : !isReset(e) && (r === 'ok') === Boolean(e.ok)))
189
193
  && (!only || e.ip === only)
190
194
  && (!q || [e.email, e.ip, e.ua, e.reason].some(v => String(v || '').toLowerCase().includes(q))));
191
- signinRows.value = signinList.slice(0, 1000).map((e, i) => ({
192
- id: `s${i}`, when: when(e.at), ok: String(Boolean(e.ok)), okText: e.ok ? 'Signed in' : 'Failed',
193
- email: e.email || '—', ip: e.ip || '—', ua: e.ua || '',
194
- note: e.ok ? browserOf(e.ua) : `${REASONS[e.reason] || e.reason || 'Failed'} · ${browserOf(e.ua)}`
195
- }));
195
+ signinRows.value = signinList.slice(0, 1000).map((e, i) => {
196
+ const row = {id: `s${i}`, when: when(e.at), email: e.email || '—', ip: e.ip || '—', ua: e.ua || ''};
197
+ if (isReset(e)) {
198
+ const t = resetRowText(e);
199
+ return {...row, ok: t.tone, okText: t.okText, note: `${t.note} · ${browserOf(e.ua)}`};
200
+ }
201
+ return {...row, ok: String(Boolean(e.ok)), okText: e.ok ? 'Signed in' : 'Failed',
202
+ note: e.ok ? browserOf(e.ua) : `${REASONS[e.reason] || e.reason || 'Failed'} · ${browserOf(e.ua)}`};
203
+ });
196
204
  }));
197
205
  const signinText = M.computed(() => {
198
206
  const n = signinList.length + 0 * signinRows.value.length;
199
207
  const f = ipFilter.value ? ` from ${ipFilter.value}` : '';
208
+ if (result.value === 'resets') return `${n.toLocaleString('en-GB')} reset request${n === 1 ? '' : 's'}${f}`;
200
209
  return `${n.toLocaleString('en-GB')} sign-in${n === 1 ? '' : 's'}${f}`;
201
210
  });
202
211
  const signinEmpty = M.computed(() => (query.value.trim() || ipFilter.value ? 'No sign-ins match.' : 'No sign-ins in this period.'));
@@ -280,7 +289,9 @@ export const securityView = {
280
289
  }
281
290
  function exportCsv() {
282
291
  if (!signinList.length) { E.toast('Nothing to export.', {type: 'info'}); return; }
283
- const rows = signinList.map(e => ({time: e.at, result: e.ok ? 'signed in' : 'failed', email: e.email, address: e.ip, reason: e.ok ? '' : (REASONS[e.reason] || e.reason || ''), browser: e.ua}));
292
+ const rows = signinList.map(e => (e.kind === 'reset'
293
+ ? {time: e.at, result: 'reset requested', email: e.email, address: e.ip, reason: resetRowText(e).note, browser: e.ua}
294
+ : {time: e.at, result: e.ok ? 'signed in' : 'failed', email: e.email, address: e.ip, reason: e.ok ? '' : (REASONS[e.reason] || e.reason || ''), browser: e.ua}));
284
295
  const url = URL.createObjectURL(new Blob([toCsv(rows)], {type: 'text/csv;charset=utf-8'}));
285
296
  const a = Object.assign(document.createElement('a'), {href: url, download: `sign-ins-${location.hostname}-${new Date().toISOString().slice(0, 10)}.csv`});
286
297
  document.body.appendChild(a); a.click(); a.remove();
@@ -385,14 +396,14 @@ export const securityView = {
385
396
  probeOn: M.observable(s.probes.enabled), after: M.observable(String(s.probes.blockAfter)), within: M.observable(String(s.probes.withinMin)), forMin: M.observable(String(s.probes.blockForMin)),
386
397
  extra: M.observable(s.probes.extra.join('\n')), allow: M.observable(s.probes.allow.join('\n')),
387
398
  signinDays: M.observable(String(s.signinDays)), staleDays: M.observable(String(s.staleAdminDays)),
388
- nLock: M.observable(s.notify.lockouts), nAfter: M.observable(s.notify.successAfterFails), nProbe: M.observable(s.notify.probeBlocks)
399
+ nLock: M.observable(s.notify.lockouts), nAfter: M.observable(s.notify.successAfterFails), nProbe: M.observable(s.notify.probeBlocks), nReset: M.observable(s.notify.resetProbing)
389
400
  };
390
401
  const body = () => ({
391
402
  lockout: {enabled: f.lockOn.value, accountThreshold: f.acc.value, addressThreshold: f.addr.value, windowMin: f.win.value, baseMin: f.base.value, maxMin: f.max.value},
392
403
  policy: {enabled: f.polOn.value, minLength: f.minLen.value, blockCommon: f.common.value, blockPersonal: f.personal.value},
393
404
  probes: {enabled: f.probeOn.value, blockAfter: f.after.value, withinMin: f.within.value, blockForMin: f.forMin.value, extra: f.extra.value, allow: f.allow.value},
394
405
  signinDays: f.signinDays.value, staleAdminDays: f.staleDays.value,
395
- notify: {lockouts: f.nLock.value, successAfterFails: f.nAfter.value, probeBlocks: f.nProbe.value}
406
+ notify: {lockouts: f.nLock.value, successAfterFails: f.nAfter.value, probeBlocks: f.nProbe.value, resetProbing: f.nReset.value}
396
407
  });
397
408
  const opened = JSON.stringify(body());
398
409
  const saving = M.observable(false);
@@ -436,6 +447,7 @@ export const securityView = {
436
447
  ${sw('nLock', 'Notify when an account or address is locked')}
437
448
  ${sw('nAfter', 'Notify when someone signs in after many wrong passwords')}
438
449
  ${sw('nProbe', 'Notify when a scanner is blocked', 'Off by default: on a public site this happens daily.')}
450
+ ${sw('nReset', 'Notify when password resets look like someone trying addresses', 'One address asking about 3 different accounts within an hour (a person only asks about their own), or 10 requests in an hour for accounts that do not exist, from anywhere. Every request is in the sign-in log either way.')}
439
451
  </div></section>
440
452
  <section><h4>Locked out yourself?</h4>
441
453
  <p class="scx-note">Run this in the site's folder on the server - it clears lockouts and blocks, and the site notices within seconds:</p>
@@ -499,6 +511,7 @@ export const securityView = {
499
511
  const model = {tab, loading, error, checksOk, canManage, scoreText, scoreWidth, scoreTone, attentionText, asOf, ov, blockedCount, checks,
500
512
  result, days, query, signinRows, signinText, signinEmpty, lockRows, tempRows, ruleRows, newRule, newNote, youText};
501
513
  const methods = {
514
+ res_resets: () => { result.value = 'resets'; },
502
515
  tab_overview: () => setTab('overview'), tab_signins: () => setTab('signins'), tab_blocked: () => setTab('blocked'),
503
516
  res_all: () => { result.value = 'all'; }, res_failed: () => { result.value = 'failed'; }, res_ok: () => { result.value = 'ok'; },
504
517
  exportCsv,
@@ -31,6 +31,7 @@ import {checkSettings, mergeDefaults} from './admin/lib/settings.js';
31
31
  import {accountKey, activeLocks, addressKey, lockedUntil, pruneLockouts, recordFailure, recordSuccess, retryText} from './admin/lib/lockout.js';
32
32
  import {passwordProblems} from './admin/lib/policy.js';
33
33
  import {isProbe} from './admin/lib/probes.js';
34
+ import {pruneResets, pruneUnknown, recordReset, recordUnknown} from './admin/lib/resets.js';
34
35
  import {blockedBy, isTrusted, pruneBlocks, recordProbe} from './admin/lib/blocks.js';
35
36
  import {ipRuleValid, isLoopbackIp, plainIp} from './admin/lib/ip.js';
36
37
  import {getConfig, saveConfig} from '../../server/config.js';
@@ -72,6 +73,7 @@ async function security(fastify, options = {}) {
72
73
  const notify = (p) => { try { hooks.notify?.(p); } catch { /* notifications are a courtesy */ } };
73
74
  hooks.registerNotificationSource?.({id: 'lockouts', label: 'Sign-in lockouts', description: 'An account or address was locked after repeated wrong passwords.', severity: 'warning', audience: 'admins'});
74
75
  hooks.registerNotificationSource?.({id: 'suspicious', label: 'Suspicious sign-ins', description: 'Someone signed in after many wrong passwords.', severity: 'warning', audience: 'admins'});
76
+ hooks.registerNotificationSource?.({id: 'resets', label: 'Password reset probing', description: 'One address asked for password resets of many different accounts.', severity: 'warning', audience: 'admins'});
75
77
  hooks.registerNotificationSource?.({id: 'probes', label: 'Scanners blocked', description: 'An address probing for weaknesses was blocked.', severity: 'info', audience: 'admins', defaultOn: false});
76
78
 
77
79
  // ---------------------------------------------------------------------
@@ -161,6 +163,38 @@ async function security(fastify, options = {}) {
161
163
  }
162
164
  });
163
165
 
166
+ // Password reset requests (core 0.88's auth:resetRequested): into the
167
+ // sign-in log, and a notice when one address asks about many accounts.
168
+ const recentResets = new Map();
169
+ const unknownResets = [];
170
+ hooks.on?.('auth:resetRequested', (e) => {
171
+ try {
172
+ const ip = plainIp(e.ip);
173
+ state.logSignin({at: new Date().toISOString(), kind: 'reset', ok: Boolean(e.sent), email: e.email || '', userId: e.userId || null,
174
+ ip, ua: e.ua || '', reason: e.reason || ''});
175
+ if (isLoopbackIp(ip)) return;
176
+ if (e.reason === 'unknown') {
177
+ const many = recordUnknown(unknownResets);
178
+ if (many.tripped && cfg.notify.resetProbing !== false) {
179
+ notify({source: 'resets', severity: 'warning', title: `${many.count} password resets for accounts that do not exist`,
180
+ dedupeKey: 'reset-unknown',
181
+ body: `Within the hour, ${many.count} "Forgot your password?" requests named accounts this site does not have, from ${new Set(state.readSignins(Date.now() - 3_600_000).filter(x => x.kind === 'reset' && x.reason === 'unknown').map(x => x.ip)).size || 'several'} addresses - someone trying email addresses. The sign-in log (Resets) shows which.`,
182
+ link: '#/plugins/security/signins'});
183
+ }
184
+ }
185
+ if (!ip) return;
186
+ const {accounts, tripped} = recordReset(recentResets, ip, e.email);
187
+ if (tripped && cfg.notify.resetProbing !== false) {
188
+ notify({source: 'resets', severity: 'warning', title: `Password resets asked for ${accounts} accounts from ${ip}`,
189
+ dedupeKey: `reset-probe:${ip}`,
190
+ body: `${ip} asked for password resets of ${accounts} different accounts within the hour - a person only ever asks about their own, so this is someone trying email addresses. The log shows which; block the address if it is not someone you know.`,
191
+ link: '#/plugins/security/signins'});
192
+ }
193
+ } catch (err) {
194
+ log.warn(`[security] reset request not recorded: ${err.message}`);
195
+ }
196
+ });
197
+
164
198
  hooks.on?.('auth:loginSucceeded', (e) => {
165
199
  try {
166
200
  const ip = plainIp(e.ip);
@@ -217,12 +251,14 @@ async function security(fastify, options = {}) {
217
251
  routes.get('/overview', may('read'), async () => {
218
252
  const s = state.get();
219
253
  const now = Date.now();
220
- const day = state.readSignins(now - 86_400_000);
254
+ const all = state.readSignins(now - 86_400_000);
255
+ const day = all.filter(e => e.kind !== 'reset');
221
256
  const today = s.counters.day === new Date().toISOString().slice(0, 10) ? s.counters : {probes: 0, blocks: 0, failed: 0};
222
257
  return {
223
258
  today,
224
259
  failed24h: day.filter(e => !e.ok).length,
225
260
  succeeded24h: day.filter(e => e.ok).length,
261
+ resets24h: all.length - day.length,
226
262
  locks: activeLocks(s.lockouts, now).length,
227
263
  blocked: Object.values(s.temp).filter(t => t.until > now).length + s.rules.length,
228
264
  checks: canCheck,
@@ -302,6 +338,8 @@ async function security(fastify, options = {}) {
302
338
  // ---------------------------------------------------------------------
303
339
  const tidy = () => {
304
340
  state.change((s) => { pruneLockouts(s.lockouts, {windowMin: cfg.lockout.windowMin}); pruneBlocks(s); });
341
+ pruneResets(recentResets);
342
+ pruneUnknown(unknownResets);
305
343
  state.pruneSignins(cfg.signinDays);
306
344
  };
307
345
  const daily = setInterval(tidy, 6 * 3_600_000);
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "security",
3
3
  "displayName": "Security",
4
- "version": "1.0.3",
4
+ "version": "1.1.0",
5
5
  "tier": "free",
6
6
  "description": "Keeps a Domma CMS site safe: a health check of what is weak and how to fix it, sign-in lockout after repeated wrong passwords, password rules, a log of every sign-in, and blocking of the scanners that probe every site for WordPress, .env files and the like.",
7
7
  "author": "Domma CMS",
8
- "date": "2026-09-26",
8
+ "date": "2026-09-27",
9
9
  "icon": "shield",
10
10
  "permissions": [
11
11
  {
@@ -132,3 +132,37 @@ test('health: facts become checks; unknown is info, never a pass; the score coun
132
132
  assert.equal(unknown.checks.find(c => c.id === 'certificate').status, 'info');
133
133
  assert.equal(unknown.checks.find(c => c.id === 'headers').status, 'info');
134
134
  });
135
+
136
+ test('resets: one address asking about 3 different accounts in an hour trips once', async () => {
137
+ const {recordReset, pruneResets, resetRowText} = await import('../admin/lib/resets.js');
138
+ const recent = new Map();
139
+ const now = Date.now();
140
+ const at = (m) => ({now: now + m * MIN});
141
+ assert.deepEqual(recordReset(recent, '1.2.3.4', 'a@x.test', at(0)), {accounts: 1, tripped: false});
142
+ // The same account again is not another account.
143
+ assert.equal(recordReset(recent, '1.2.3.4', 'A@x.test', at(1)).accounts, 1);
144
+ assert.equal(recordReset(recent, '1.2.3.4', 'b@x.test', at(2)).tripped, false);
145
+ assert.deepEqual(recordReset(recent, '1.2.3.4', 'c@x.test', at(3)), {accounts: 3, tripped: true});
146
+ assert.equal(recordReset(recent, '1.2.3.4', 'd@x.test', at(4)).tripped, false, 'only on crossing');
147
+ // Another address is counted on its own; an hour later the first starts again.
148
+ assert.equal(recordReset(recent, '5.6.7.8', 'a@x.test', at(5)).accounts, 1);
149
+ assert.equal(recordReset(recent, '1.2.3.4', 'z@x.test', at(70)).accounts, 1);
150
+ pruneResets(recent, {now: now + 200 * MIN});
151
+ assert.equal(recent.size, 0);
152
+ assert.equal(recordReset(recent, '', 'a@x.test').tripped, false);
153
+ assert.equal(resetRowText({reason: 'no-smtp'}).note, 'Not sent - no mail server');
154
+ assert.equal(resetRowText({reason: 'odd'}).note, 'odd');
155
+ });
156
+
157
+ test('resets: 10 requests for unknown accounts in an hour, from anywhere, trip once', async () => {
158
+ const {recordUnknown, pruneUnknown} = await import('../admin/lib/resets.js');
159
+ const stamps = [];
160
+ const now = Date.now();
161
+ for (let i = 0; i < 9; i++) assert.equal(recordUnknown(stamps, {now: now + i * MIN}).tripped, false);
162
+ assert.deepEqual(recordUnknown(stamps, {now: now + 9 * MIN}), {count: 10, tripped: true});
163
+ assert.equal(recordUnknown(stamps, {now: now + 10 * MIN}).tripped, false, 'only on reaching it');
164
+ // An hour on, the early ones have gone.
165
+ assert.equal(recordUnknown(stamps, {now: now + 65 * MIN}).count, 7, 'minutes 5-10 still inside the hour, and this one');
166
+ pruneUnknown(stamps, {now: now + 200 * MIN});
167
+ assert.equal(stamps.length, 0);
168
+ });
@@ -19,6 +19,7 @@
19
19
  * POST /forms/submit/:slug - public: accept submission
20
20
  * POST /forms/test-email - admin: send test email
21
21
  */
22
+ import {keepSubmissionsOut} from '../../services/siteGitignore.js';
22
23
  import {createRequire} from 'module';
23
24
  import {fileURLToPath} from 'url';
24
25
  import path from 'path';
@@ -893,6 +894,8 @@ export async function formsRoutes(fastify) {
893
894
  excused,
894
895
  outcome: ended ? {type: 'ended', trigger: ended.key, field: ended.field} : null
895
896
  });
897
+ // What a visitor sent is theirs, not the site's content: keep it out of the site repo.
898
+ try { keepSubmissionsOut(ROOT, targetSlug); } catch { /* never fail a submission over this */ }
896
899
  } else {
897
900
  // Could not resolve or provision a target - e.g. collection
898
901
  // storage is explicitly disabled with no slug. Fail loudly
@@ -84,9 +84,114 @@ export const PRIVATE_DATA = [
84
84
  'contacts', 'contacts-contacts', 'contacts-groups', 'roles', 'user-profiles',
85
85
  'user-messages', 'user-message-threads', 'analytics-events', 'enquiries',
86
86
  'blog-comments', 'invoices', 'invoice-receivers', 'invoice-issuers',
87
- 'surveys-contacts', 'surveys-invites'
87
+ 'surveys-contacts', 'surveys-invites',
88
+ // Paid plugins' visitor and customer records (0.88.1): Cart Pro's orders and
89
+ // discount codes, Calendar's RSVPs, the job board's people and applications.
90
+ 'shop-orders', 'shop-pro-codes', 'calendar-rsvps',
91
+ 'jb-applications', 'jb-candidate-profiles', 'jb-signups'
88
92
  ];
89
93
 
94
+ /**
95
+ * The collection a form stores its submissions in - the same choice as the
96
+ * submit route (routes/api/forms.js): the collection action's slug when it is
97
+ * on, otherwise the form's own slug. An action slug set but switched off is
98
+ * kept out too: ensureCollectionForForm provisions into it.
99
+ *
100
+ * @param {object} form - Parsed content/forms/<slug>.json
101
+ * @returns {string[]} collection slugs
102
+ */
103
+ export function formDataCollections(form) {
104
+ const action = form?.actions?.collection;
105
+ const safe = (v) => (typeof v === 'string' && /^[a-z0-9][a-z0-9_-]*$/i.test(v) ? v : '');
106
+ return [...new Set([safe(action?.slug), safe(form?.slug)].filter(Boolean))];
107
+ }
108
+
109
+ /** `[form name="x" /]` (or slug="x") anywhere in a text. */
110
+ function embeddedFormNames(text) {
111
+ const out = new Set();
112
+ for (const m of String(text).matchAll(/\[form\b([^\]]*)\]/gi)) {
113
+ const a = m[1].match(/\b(?:name|slug)\s*=\s*["']?([A-Za-z0-9_-]+)/);
114
+ if (a) out.add(a[1]);
115
+ }
116
+ return out;
117
+ }
118
+
119
+ function readTree(dir, exts, out = [], depth = 0) {
120
+ if (depth > 6) return out;
121
+ let entries = [];
122
+ try { entries = fs.readdirSync(dir, {withFileTypes: true}); } catch { return out; }
123
+ for (const e of entries) {
124
+ const full = path.join(dir, e.name);
125
+ if (e.isDirectory()) { if (e.name !== 'media' && e.name !== 'collections') readTree(full, exts, out, depth + 1); }
126
+ else if (exts.some(x => e.name.endsWith(x))) out.push(full);
127
+ }
128
+ return out;
129
+ }
130
+
131
+ /**
132
+ * The collections this site's VISITORS write to, from what is on disk - like
133
+ * PRIVATE_DATA, but only the site knows which they are. Before 0.88.1 nothing
134
+ * did this: a form storing in `find-a-tutor` had every enquiry - names, emails,
135
+ * phone numbers - committed to the site repo.
136
+ *
137
+ * "A form writes there" is not enough: every collection created in the admin
138
+ * gets a form of its own (ensureFormForCollection), and a content collection
139
+ * (FAQs, testimonials) must stay tracked. So a collection counts when
140
+ * - a form storing in it is EMBEDDED somewhere in content ([form name=...]), or
141
+ * - it was made by a form (schema meta.generatedFromForm).
142
+ * The submit route adds the rest the moment a visitor's entry is stored
143
+ * (keepFormDataOut) - the one signal that is never wrong.
144
+ *
145
+ * @param {string} siteRoot
146
+ * @returns {string[]} sorted, de-duplicated
147
+ */
148
+ export function siteFormDataCollections(siteRoot) {
149
+ const content = path.join(siteRoot, 'content');
150
+ const embedded = new Set();
151
+ for (const f of readTree(content, ['.md', '.html', '.json'])) {
152
+ if (f.startsWith(path.join(content, 'forms') + path.sep)) continue;
153
+ try { for (const n of embeddedFormNames(fs.readFileSync(f, 'utf8'))) embedded.add(n); } catch { /* skip */ }
154
+ }
155
+ const out = new Set();
156
+ const formsDir = path.join(content, 'forms');
157
+ let files = [];
158
+ try { files = fs.readdirSync(formsDir).filter(f => f.endsWith('.json')); } catch { /* no forms */ }
159
+ for (const f of files) {
160
+ try {
161
+ const form = JSON.parse(fs.readFileSync(path.join(formsDir, f), 'utf8'));
162
+ if (embedded.has(form.slug) || embedded.has(f.replace(/\.json$/, ''))) for (const c of formDataCollections(form)) out.add(c);
163
+ } catch { /* an unreadable form keeps nothing out - and breaks nothing */ }
164
+ }
165
+ const colDir = path.join(content, 'collections');
166
+ let cols = [];
167
+ try { cols = fs.readdirSync(colDir); } catch { /* none */ }
168
+ for (const c of cols) {
169
+ try {
170
+ const schema = JSON.parse(fs.readFileSync(path.join(colDir, c, 'schema.json'), 'utf8'));
171
+ if (schema?.meta?.generatedFromForm) out.add(c);
172
+ } catch { /* no schema */ }
173
+ }
174
+ return [...out].sort();
175
+ }
176
+
177
+ /** The rule that keeps one collection's entries out. */
178
+ export const collectionDataRule = (slug) => `/content/collections/${slug}/data.json`;
179
+
180
+ /**
181
+ * Keep a collection a visitor has just written to out of a managed .gitignore -
182
+ * called by the submit route once an entry is stored, so submissions never wait
183
+ * for the next regeneration to stop being committed. Same guard as
184
+ * allowInGitignore: a missing or hand-written file is left alone.
185
+ *
186
+ * @param {string} siteRoot
187
+ * @param {string} slug - The collection the entry went into
188
+ * @returns {string[]} The rules actually added
189
+ */
190
+ export function keepSubmissionsOut(siteRoot, slug) {
191
+ if (!/^[a-z0-9][a-z0-9_-]*$/i.test(String(slug || '')) || PRIVATE_DATA.includes(slug)) return [];
192
+ return allowInGitignore(siteRoot, [collectionDataRule(slug)]);
193
+ }
194
+
90
195
  /**
91
196
  * Lines bin/update.js appends when it cannot find them VERBATIM. They are
92
197
  * already ignored by `/*` and `/plugins/*`, but writing them out keeps the
@@ -151,10 +256,11 @@ export function pluginContentRules(name, rel) {
151
256
  * @param {Object<string,string[]>} [opts.pluginContent={}] - Extra content folders in distributed plugins
152
257
  * @param {string[]} [opts.sitePublic=[]] - Site-added entries directly under public/
153
258
  * @param {string[]} [opts.extras=[]] - Other site-owned top-level entries (docs, tools, README.md)
259
+ * @param {string[]} [opts.formData=[]] - Collections the site's forms store submissions in (siteFormDataCollections)
154
260
  * @param {string[]} [opts.siteRules=[]] - The site-specific section
155
261
  * @returns {string}
156
262
  */
157
- export function buildGitignore({sitePlugins = [], pluginContent = {}, sitePublic = [], extras = [], siteRules = []} = {}) {
263
+ export function buildGitignore({sitePlugins = [], pluginContent = {}, sitePublic = [], extras = [], formData = [], siteRules = []} = {}) {
158
264
  const L = [
159
265
  MANAGED_HEADER,
160
266
  '# This repo holds the site\'s CONTENT only. Everything is ignored, then what',
@@ -192,7 +298,12 @@ export function buildGitignore({sitePlugins = [], pluginContent = {}, sitePublic
192
298
  L.push('', '# Schemas the CMS rewrites on every boot - engine-owned, not authored');
193
299
  for (const c of ENGINE_SCHEMAS) L.push(`/content/collections/${c}/schema.json`);
194
300
  L.push('', '# Secrets, visitor-submitted and per-user data, runtime state');
195
- for (const c of PRIVATE_DATA) L.push(`/content/collections/${c}/data.json`);
301
+ for (const c of PRIVATE_DATA) L.push(collectionDataRule(c));
302
+ const submitted = [...new Set(formData)].filter(c => !PRIVATE_DATA.includes(c)).sort();
303
+ if (submitted.length) {
304
+ L.push('', '# Form submissions - what visitors sent (siteFormDataCollections)');
305
+ for (const c of submitted) L.push(collectionDataRule(c));
306
+ }
196
307
  L.push(
197
308
  '/content/users/',
198
309
  '/content/sessions/',