/* Tools tab — mirrors the Services tab visual structure (.task-item, .task-header, .task-info, .task-actions) plus a generic input modal for tools that need user inputs. */ .tools-section { padding: 0; } .tools-title { padding: 20px; background: transparent; border-bottom: 1px solid var(--border-color); margin-bottom: 0; } .tools-title h3 { margin: 0 0 8px 0; color: var(--text-primary, #fff); font-size: 18px; font-weight: 600; } .tools-title p { margin: 0; color: var(--text-secondary, #ccc); font-size: 13px; } .tools-list { display: flex; flex-direction: column; } .tools-rows { display: flex; flex-direction: column; gap: 0.6rem; padding: 1rem 1.25rem 2rem; } .tools-cat-pane { display: none; } .tools-cat-pane.active { display: flex; } .tools-tab-bar { display: flex; flex-wrap: wrap; gap: 0.4rem; padding: 0.75rem 1.25rem 0; border-bottom: 1px solid var(--border-color, rgba(255, 255, 255, 0.08)); margin-bottom: 0.25rem; } .tools-tab { display: inline-flex; align-items: center; gap: 0.4rem; background: transparent; border: 1px solid transparent; border-bottom: none; color: var(--text-secondary, #a0a0a0); padding: 0.45rem 0.85rem; font-size: 13px; font-weight: 500; border-radius: 6px 6px 0 0; cursor: pointer; transition: color 120ms ease, background 120ms ease, border-color 120ms ease; position: relative; bottom: -1px; } .tools-tab:hover { color: var(--text-primary, #fff); background: rgba(255, 255, 255, 0.04); } .tools-tab.active { color: var(--text-primary, #fff); background: var(--surface-color, rgba(255, 255, 255, 0.06)); border-color: var(--border-color, rgba(255, 255, 255, 0.08)); border-bottom-color: var(--surface-color, rgba(255, 255, 255, 0.06)); } .tools-tab-count { display: inline-flex; align-items: center; justify-content: center; min-width: 1.4em; padding: 0 0.4em; border-radius: 999px; background: rgba(255, 255, 255, 0.08); color: var(--text-secondary, #ccc); font-size: 11px; font-weight: 600; line-height: 1.5; } .tools-tab.active .tools-tab-count { background: var(--accent-color, #6c63ff); color: #fff; } .tools-loading { display: flex; align-items: center; justify-content: center; gap: 0.6rem; padding: 2rem; color: var(--text-secondary, var(--text-muted)); } .tools-spinner { width: 16px; height: 16px; border: 2px solid rgba(var(--text-rgb), 0.15); border-top-color: var(--accent-color, var(--accent)); border-radius: 50%; animation: tools-spin 0.7s linear infinite; } @keyframes tools-spin { to { transform: rotate(360deg); } } .tools-empty { text-align: center; padding: 2.5rem 1rem; color: var(--text-secondary, var(--text-muted)); } .tools-empty-icon { font-size: 2rem; display: block; margin-bottom: 0.5rem; } /* Tool row -------------------------------------------------------- */ /* Mirror .task-item shell from style.css so tool rows visually match task rows, but use a horizontal flex layout so the action button stays vertically centered across the whole row regardless of description length. */ .tool-item { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 14px 18px; background: rgba(var(--text-rgb), 0.03); border: 1px solid rgba(var(--text-rgb), 0.08); border-radius: 8px; } .tool-text { flex: 1 1 auto; min-width: 0; } .tool-head { display: flex; align-items: center; gap: 8px; } .tool-icon { font-size: 18px; line-height: 1; } .tool-title { color: var(--text-primary, #fff); font-size: 14px; font-weight: 600; } .tool-desc { margin: 4px 0 0 0; color: var(--text-secondary, var(--text-muted)); font-size: 12px; line-height: 1.4; } .tool-action { flex: 0 0 auto; display: flex; align-items: center; } /* Matches the .task-btn.delete look (translucent fill + colored border) but bigger and green. The delete button uses bootstrap red var(--status-danger); we use bootstrap green var(--status-success) for parity. */ .tool-run-btn { background: rgba(var(--status-success-rgb), 0.12); border: 1px solid rgba(var(--status-success-rgb), 0.3); color: var(--status-success); padding: 10px 22px; border-radius: 6px; font-size: 13px; font-weight: 600; letter-spacing: 0.2px; min-width: 96px; cursor: pointer; transition: all 0.2s ease; } .tool-run-btn:hover { background: rgba(var(--status-success-rgb), 0.22); border-color: rgba(var(--status-success-rgb), 0.45); transform: translateY(-1px); } .tool-run-btn:active { transform: translateY(0); } .tool-run-btn.destructive { background: rgba(var(--status-danger-rgb), 0.1); border-color: rgba(var(--status-danger-rgb), 0.3); color: var(--status-danger); } .tool-run-btn.destructive:hover { background: rgba(var(--status-danger-rgb), 0.22); border-color: rgba(var(--status-danger-rgb), 0.45); } /* Tool modal ------------------------------------------------------ */ /* Center the modal vertically + horizontally. Mirrors the gluetun-modal trick: the inline `style="display: block"` set in JS triggers this selector, which overrides to flexbox so the content sits in the middle of the viewport regardless of its height. */ .tool-modal { position: fixed; inset: 0; z-index: 1100; } .tool-modal[style*="display: block"] { display: flex !important; align-items: center; justify-content: center; } /* Global .modal-body sets padding: 0 (it's used for full-bleed content like the readme iframe). The tool modal's form needs real breathing room around it — match the gluetun modal's padding so the picker cards don't sit flush against the edges. overflow:hidden so any inner scrollable region (e.g. the URL list in app_urls_multi) is the *only* thing that scrolls, not the whole modal body. */ .tool-modal .modal-body { padding: 20px; overflow: hidden; display: flex; flex-direction: column; min-height: 0; } .tool-modal .tool-form { display: flex; flex-direction: column; min-height: 0; flex: 1; } .tool-modal .tool-form .form-group-app-urls, .tool-modal .tool-form .form-group:has(.app-urls-multi) { display: flex; flex-direction: column; min-height: 0; flex: 1; margin-bottom: 0; gap: 0; } .tool-modal .app-urls-multi { display: flex; flex-direction: column; min-height: 0; flex: 1; gap: 0; } /* Single bordered shell that holds the search row + URL list as one visual unit, mirroring the framing the rest of the WebUI uses. */ .app-urls-container { display: flex; flex-direction: column; min-height: 0; flex: 1; background: rgba(var(--text-rgb), 0.02); border: 1px solid rgba(var(--text-rgb), 0.08); border-radius: 10px; overflow: hidden; } /* Title bar at the top of the picker container — replaces the old floating .form-label so the field reads as one cohesive unit. */ .app-urls-title { padding: 10px 14px; font-size: 13px; font-weight: 600; color: var(--text-primary); background: rgba(var(--accent-rgb), 0.08); border-bottom: 1px solid rgba(var(--accent-rgb), 0.20); letter-spacing: 0.2px; flex-shrink: 0; } .app-urls-title .required-mark { color: var(--status-danger); margin-left: 2px; } .app-urls-header { display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; border-bottom: 1px solid rgba(var(--text-rgb), 0.06); background: rgba(var(--bg-rgb), 0.12); flex-shrink: 0; } .tool-modal .modal-footer { padding: 16px 20px; border-top: 1px solid var(--border-color); display: flex; justify-content: stretch; gap: 12px; } .tool-modal .modal-footer .btn { flex: 1 1 0; } .tool-modal-confirm { background: rgba(var(--status-warning-rgb), 0.12); border: 1px solid rgba(var(--status-warning-rgb), 0.4); color: var(--status-warning); padding: 10px 12px; border-radius: 6px; font-size: 13px; margin-bottom: 14px; } .tool-form .form-group { margin-bottom: 14px; } .tool-form .form-group:last-child { margin-bottom: 0; } .tool-form .form-label { display: block; margin-bottom: 6px; color: var(--text-primary, #fff); font-size: 13px; font-weight: 500; } .tool-form .required-mark { color: var(--status-danger); } /* installed_apps_multi — visually mirrors gluetun country picker. */ .installed-apps-multi { display: flex; flex-direction: column; gap: 12px; } /* Framed card holding the search input + bulk-action buttons, same treatment as .gluetun-search-card. */ .installed-apps-search-card { display: flex; flex-direction: column; gap: 10px; padding: 10px 12px; background: rgba(var(--text-rgb), 0.03); border: 1px solid rgba(var(--text-rgb), 0.08); border-radius: 10px; } .installed-apps-search-row { display: flex; align-items: center; gap: 8px; padding: 6px 10px; background: rgba(var(--text-rgb), 0.04); border: 1px solid rgba(var(--text-rgb), 0.10); border-radius: 8px; transition: border-color 0.15s ease, box-shadow 0.15s ease; } .installed-apps-search-row:focus-within { border-color: rgba(var(--accent-rgb), 0.55); box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.12); } .installed-apps-search-icon { color: rgba(var(--text-rgb), 0.55); flex-shrink: 0; } .installed-apps-search { flex: 1; background: transparent; border: none; outline: none; color: var(--text-primary); font-size: 14px; padding: 2px 0; } .installed-apps-actions { display: flex; gap: 8px; } .installed-apps-actions .btn { flex: 1 1 0; } /* Grid of selectable apps, matching .gluetun-country-list. */ .installed-apps-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 6px 14px; max-height: 45vh; overflow-y: auto; padding: 4px 2px; background: transparent; border: none; } .installed-apps-item { display: flex; align-items: center; gap: 10px; padding: 8px 10px; background: rgba(var(--text-rgb), 0.03); border: 1px solid rgba(var(--text-rgb), 0.08); border-radius: 10px; cursor: pointer; transition: background 0.15s ease, border-color 0.15s ease; } .installed-apps-item:hover { background: rgba(var(--text-rgb), 0.06); border-color: rgba(var(--accent-rgb), 0.25); } .installed-apps-item:has(input:checked) { background: rgba(var(--accent-rgb), 0.10); border-color: rgba(var(--accent-rgb), 0.45); } .installed-apps-item input[type="checkbox"] { -webkit-appearance: none; appearance: none; width: 18px; height: 18px; flex-shrink: 0; cursor: pointer; border-radius: 5px; background: rgba(var(--text-rgb), 0.04); border: 1.5px solid rgba(var(--text-rgb), 0.18); box-shadow: inset 0 0 0 1px rgba(var(--text-rgb), 0.02); position: relative; transition: background 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease, transform 0.12s ease; margin: 0; } .installed-apps-item:hover input[type="checkbox"] { border-color: rgba(var(--accent-rgb), 0.55); box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.08); } .installed-apps-item input[type="checkbox"]:focus-visible { outline: none; border-color: rgba(var(--accent-rgb), 0.85); box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.20); } .installed-apps-item input[type="checkbox"]:checked { background: linear-gradient(135deg, var(--accent), var(--accent)); border-color: rgba(var(--accent-rgb), 0.9); box-shadow: 0 0 0 1px rgba(var(--accent-rgb), 0.35); } .installed-apps-item input[type="checkbox"]:checked::after { content: ''; position: absolute; inset: 0; background-image: url("data:image/svg+xml;utf8,"); background-repeat: no-repeat; background-position: center; background-size: 13px 13px; animation: installedAppsCheckPop 0.22s cubic-bezier(0.34, 1.56, 0.64, 1); } @keyframes installedAppsCheckPop { 0% { transform: scale(0.4); opacity: 0; } 100% { transform: scale(1); opacity: 1; } } .installed-apps-icon { width: 22px; height: 22px; border-radius: 5px; object-fit: contain; flex-shrink: 0; } .installed-apps-name { font-size: 14px; color: var(--text-primary); font-weight: 500; } .installed-apps-multi-empty { padding: 24px; text-align: center; color: rgba(var(--text-rgb), 0.6); font-size: 13px; } /* app_urls_multi — flat task-style list. One URL per row, no per-app grouping. Each row is slim (icon + label + URL inline + checkbox) and visually echoes the .task-item shell from style.css. */ .app-urls-list { display: flex !important; flex-direction: column; gap: 4px; grid-template-columns: none !important; padding: 8px 10px; background: transparent; border: none; /* Fill available space inside the container and scroll only this region — overrides the inherited .installed-apps-list max-height which was sized for the wide-grid layout. */ flex: 1; min-height: 0; max-height: none; overflow-y: auto; } .app-urls-loading { padding: 18px; text-align: center; color: rgba(var(--text-rgb), 0.55); font-size: 13px; font-style: italic; } .app-url-row.installed-apps-item { display: flex; align-items: center; gap: 10px; padding: 6px 12px; min-height: 34px; background: rgba(var(--text-rgb), 0.03); border: 1px solid rgba(var(--text-rgb), 0.06); border-radius: 6px; cursor: pointer; transition: background 0.12s ease, border-color 0.12s ease; line-height: 1; } .app-url-row:hover { background: rgba(var(--text-rgb), 0.06); border-color: rgba(var(--accent-rgb), 0.25); } .app-url-row:has(input:checked) { background: rgba(var(--accent-rgb), 0.10); border-color: rgba(var(--accent-rgb), 0.40); } /* Slim flat row checkbox — ~⅓ smaller than the wide-grid gluetun style, no glow. */ .app-url-row input[type="checkbox"] { width: 10px; height: 10px; border-radius: 2px; flex-shrink: 0; align-self: center; } .app-url-row input[type="checkbox"]:checked { box-shadow: none; } .app-url-row input[type="checkbox"]:checked::after { background-size: 8px 8px; } .app-url-icon { width: 28px; height: 28px; border-radius: 6px; object-fit: contain; flex-shrink: 0; align-self: center; } .app-url-label { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; color: var(--text-primary); font-weight: 500; white-space: nowrap; line-height: 1; overflow: hidden; text-overflow: ellipsis; min-width: 0; flex: 1; } .app-url-sep { color: rgba(var(--text-rgb), 0.35); font-weight: 400; margin: 0 2px; } /* User list modal — opens after a list_users tool task completes. */ .user-list { display: flex; flex-direction: column; gap: 6px; } .user-row { display: flex; align-items: center; gap: 10px; padding: 10px 12px; background: rgba(var(--text-rgb), 0.03); border: 1px solid rgba(var(--text-rgb), 0.08); border-radius: 8px; } .user-row:hover { background: rgba(var(--text-rgb), 0.05); } .user-row-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; } .user-row-primary { font-size: 14px; font-weight: 500; color: var(--text-primary); } .user-row-secondary { font-size: 12px; color: rgba(var(--text-rgb), 0.55); font-family: ui-monospace, "SF Mono", Menlo, monospace; } .user-row-roles { display: inline-flex; align-self: flex-start; margin-top: 2px; font-size: 10px; text-transform: uppercase; letter-spacing: 0.4px; padding: 2px 6px; border-radius: 3px; background: rgba(var(--accent-rgb), 0.10); border: 1px solid rgba(var(--accent-rgb), 0.25); color: var(--accent); } .user-row-actions { display: flex; gap: 4px; flex-shrink: 0; } .user-row-btn { width: 30px; height: 30px; display: inline-flex; align-items: center; justify-content: center; font-size: 14px; background: transparent; border: 1px solid rgba(var(--text-rgb), 0.12); border-radius: 5px; cursor: pointer; transition: background 0.12s, border-color 0.12s, color 0.12s; color: rgba(var(--text-rgb), 0.85); } .user-row-btn:hover { background: rgba(var(--accent-rgb), 0.10); border-color: rgba(var(--accent-rgb), 0.40); } .user-row-btn.danger:hover { background: rgba(var(--status-danger-rgb), 0.12); border-color: rgba(var(--status-danger-rgb), 0.45); } .user-row-roles.is-admin { background: rgba(var(--status-warning-rgb), 0.12); border-color: rgba(var(--status-warning-rgb), 0.30); color: var(--status-warning); } /* Toggle inside a tool form — match form-group spacing so it sits flush with siblings. .form-group:last-child rule already handles the bottom. */ .tool-form > .tool-form-toggle { margin-bottom: 14px; } .tool-form > .tool-form-toggle:last-child { margin-bottom: 0; }