/* ========================================================================== Design System: Primitives & Tokens ========================================================================== */ :root { /* --- Primitives: Slate (Neutral) --- */ --slate-50: #f8fafc; --slate-100: #f1f5f9; --slate-200: #e2e8f0; --slate-300: #cbd5e1; --slate-400: #94a3b8; --slate-500: #64748b; --slate-600: #475569; --slate-700: #334155; --slate-800: #1e293b; --slate-900: #0f172a; --slate-950: #020617; /* --- Primitives: Colors --- */ --blue-500: #3b82f6; --blue-600: #2563eb; --blue-700: #1d4ed8; --green-500: #22c55e; --green-600: #16a34a; --amber-500: #f59e0b; --amber-600: #d97706; --red-500: #ef4444; --red-600: #dc2626; /* --- Semantic Tokens: Light Mode (Default) --- */ --bg: var(--slate-50); --surface: #ffffff; --fg: var(--slate-900); --fg-muted: var(--slate-500); --border: var(--slate-200); --border-subtle: var(--slate-100); --primary: var(--blue-500); --primary-hover: var(--blue-600); --success: var(--green-500); --warning: var(--amber-500); --danger: var(--red-500); --warning-bg: #fffbeb; --warning-fg: #78350f; --warning-border: #fcd34d; /* Sidebar — Light mode */ --sidebar-bg: var(--slate-100); --sidebar-fg: var(--slate-700); --sidebar-hover: rgba(0, 0, 0, 0.07); --sidebar-hover-fg: var(--primary); --sidebar-border: rgba(0, 0, 0, 0.08); --sidebar-width: 240px; /* Login page */ --dot-color: rgba(0, 0, 0, 0.055); /* Layout */ --topbar-height: 48px; --radius: 8px; --radius-sm: 4px; --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); --shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; --font-mono: "SF Mono", "Fira Code", "Fira Mono", Menlo, Consolas, monospace; --surface-alt: var(--slate-100); /* Legacy variable mapping */ --card-bg: var(--surface); } /* --- Dark Mode Overrides --- */ @media (prefers-color-scheme: dark) { :root { --bg: var(--slate-900); --surface: var(--slate-800); --fg: var(--slate-100); --fg-muted: var(--slate-400); --border: var(--slate-700); --border-subtle: var(--slate-800); --surface-alt: var(--slate-900); --sidebar-bg: var(--slate-800); --sidebar-fg: var(--slate-200); --sidebar-hover: rgba(255, 255, 255, 0.1); --sidebar-hover-fg: #fff; --sidebar-border: rgba(255, 255, 255, 0.1); --dot-color: rgba(255, 255, 255, 0.045); --warning-bg: #2d2007; --warning-fg: #fde68a; --warning-border: #92400e; } } /* Manual Dark Mode Toggle */ .dark { color-scheme: dark; --bg: var(--slate-900); --surface: var(--slate-800); --fg: var(--slate-100); --fg-muted: var(--slate-400); --border: var(--slate-700); --border-subtle: var(--slate-800); --surface-alt: var(--slate-900); --zebra-stripe: rgba(255,255,255,0.03); --sidebar-bg: var(--slate-800); --sidebar-fg: var(--slate-200); --sidebar-hover: rgba(255, 255, 255, 0.1); --sidebar-hover-fg: #fff; --sidebar-border: rgba(255, 255, 255, 0.1); --dot-color: rgba(255, 255, 255, 0.045); --warning-bg: #2d2007; --warning-fg: #fde68a; --warning-border: #92400e; } /* Force Light Mode (override system dark preference) */ .light { color-scheme: light; --bg: var(--slate-50); --surface: #ffffff; --fg: var(--slate-900); --fg-muted: var(--slate-500); --border: var(--slate-200); --border-subtle: var(--slate-100); --surface-alt: var(--slate-100); --sidebar-bg: var(--slate-100); --sidebar-fg: var(--slate-700); --sidebar-hover: rgba(0, 0, 0, 0.07); --sidebar-hover-fg: var(--primary); --sidebar-border: rgba(0, 0, 0, 0.08); --dot-color: rgba(0, 0, 0, 0.055); --warning-bg: #fffbeb; --warning-fg: #78350f; --warning-border: #fcd34d; } .light .alert-error { background: #fef2f2; color: #991b1b; border-color: #fecaca; } .light .alert-info { background: #eff6ff; color: #1e40af; border-color: #bfdbfe; } .light .health-bar-track { background-color: var(--slate-200); } .light .zone-badge { background-color: hsl(var(--zone-hue), 80%, 90%); color: hsl(var(--zone-hue), 80%, 30%); border-color: hsl(var(--zone-hue), 60%, 80%); } .light .matrix-allow { background: #dcfce7; color: #166534; } .light .matrix-deny { background: #fef2f2; color: #991b1b; } .light .matrix-self { background: var(--slate-100); color: var(--slate-400); } .light .badge-accept { background: #dcfce7; color: #166534; } .light .badge-reject { background: #fef2f2; color: #991b1b; } .light .badge-drop { background: var(--slate-100); color: var(--slate-600); } .light .badge-continue { background: #dbeafe; color: #1e40af; } /* ========================================================================== Reset & Base ========================================================================== */ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } html { height: 100%; overflow: hidden; } body { height: 100%; font-family: var(--font-sans); font-size: 15px; line-height: 1.5; color: var(--fg); background: var(--bg); -webkit-font-smoothing: antialiased; } a { color: var(--primary); text-decoration: none; transition: color 0.15s ease; } a:hover { text-decoration: underline; } /* ========================================================================== Layout ========================================================================== */ .layout { display: flex; height: calc(100vh - var(--topbar-height)); margin-top: var(--topbar-height); overflow: hidden; } #sidebar { position: relative; z-index: 150; width: var(--sidebar-width); background: var(--sidebar-bg); color: var(--sidebar-fg); display: flex; flex-direction: column; flex-shrink: 0; border-right: 1px solid var(--sidebar-border); overflow-y: auto; } .main-column { flex: 1; display: flex; flex-direction: column; overflow: hidden; } #content { flex: 1; width: 100%; padding: 2rem; overflow-y: auto; max-width: 1200px; margin: 0 auto; box-sizing: border-box; } /* ========================================================================== Sidebar Components ========================================================================== */ .topbar-logo { height: calc(var(--topbar-height) - 14px); width: auto; display: block; } .sidebar-nav { flex: 1; padding: 0.5rem 0; overflow-y: auto; } /* Standalone nav link (Dashboard) */ .nav-standalone { margin: 0.25rem 0.5rem 0.5rem; } /* Top-level accordion groups (Status / Configure / Maintenance) */ details.nav-group-top { } details.nav-group-top > summary.nav-group-summary-top { list-style: none; cursor: pointer; font-size: 0.7rem; font-weight: 700; letter-spacing: 0.07em; text-transform: uppercase; color: var(--sidebar-fg); opacity: 0.7; padding: 0.75rem 1rem 0.375rem; display: flex; align-items: center; gap: 0.35rem; user-select: none; transition: opacity 0.15s; } details.nav-group-top > summary.nav-group-summary-top:hover { opacity: 1; } details.nav-group-top > summary.nav-group-summary-top::-webkit-details-marker { display: none; } details.nav-group-top > summary.nav-group-summary-top::marker { display: none; } details.nav-group-top > summary.nav-group-summary-top::before { content: '▸'; font-size: 0.7rem; } details[open].nav-group-top > summary.nav-group-summary-top::before { content: '▾'; } /* Static sub-section labels within top-level accordion */ .nav-section-label { font-size: 0.6rem; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--sidebar-fg); opacity: 0.45; padding: 0.75rem 1rem 0.2rem; user-select: none; } .nav-group-items { list-style: none; padding-bottom: 0.25rem; } .nav-icon { display: inline-block; width: 1rem; height: 1rem; background-color: currentColor; -webkit-mask-image: var(--icon); mask-image: var(--icon); -webkit-mask-size: contain; mask-size: contain; -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat; -webkit-mask-position: center; mask-position: center; flex-shrink: 0; } .nav-link { display: flex; align-items: center; gap: 0.625rem; padding: 0.5rem 1rem; color: var(--sidebar-fg); text-decoration: none; font-size: 0.875rem; border-radius: 0.375rem; margin: 0 0.5rem; transition: background-color 0.15s, color 0.15s; font-weight: 500; } .nav-link:hover { background-color: var(--sidebar-hover); color: var(--sidebar-hover-fg); text-decoration: none; } .nav-link.active { background-color: var(--primary); color: #fff; text-decoration: none; } /* Button styled as a nav-link (for actions like Reboot) */ .nav-link-btn { width: 100%; background: none; border: none; cursor: pointer; text-align: left; font-family: inherit; font-size: inherit; } .nav-link-danger:hover { color: var(--danger); background-color: var(--sidebar-hover); } .sidebar-footer { padding: 1.25rem; border-top: 1px solid var(--sidebar-border); } /* ========================================================================== Buttons ========================================================================== */ .btn { display: inline-flex; align-items: center; justify-content: center; padding: 0.5rem 1rem; border: 1px solid transparent; border-radius: var(--radius); font-size: 0.9rem; font-weight: 500; cursor: pointer; text-decoration: none; transition: all 0.15s ease; line-height: 1.25; } .btn-primary { background: var(--primary); color: #fff; } .btn-primary:hover { background: var(--primary-hover); text-decoration: none; } .btn-secondary { background: transparent; color: var(--text-muted); border-color: var(--border); } .btn-secondary:hover { background: var(--border); color: var(--text); text-decoration: none; } .btn-block { display: flex; width: 100%; } /* Sidebar Actions */ .btn-sidebar-action, .btn-logout { display: flex; width: 100%; align-items: center; justify-content: center; background: transparent; color: var(--sidebar-fg); border: 1px solid rgba(255,255,255,0.2); padding: 0.5rem 0.75rem; border-radius: var(--radius); cursor: pointer; font-size: 0.85rem; text-decoration: none; margin-bottom: 0.5rem; transition: all 0.15s ease; } .btn-sidebar-action:hover, .btn-logout:hover { background: var(--sidebar-hover); border-color: rgba(255,255,255,0.3); text-decoration: none; } .btn-danger-hover:hover { border-color: var(--danger); color: #fca5a5; background: rgba(220, 38, 38, 0.1); } /* ========================================================================== Cards & Content Containers ========================================================================== */ .info-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem; margin-bottom: 1.5rem; align-items: stretch; } /* Cards inside a grid don't need their own bottom margin — the grid gap handles spacing */ .info-grid > .info-card { margin-bottom: 0; } /* Full-width card spanning all grid columns */ .info-grid-full { grid-column: 1 / -1; } /* Two-column card for wide tables — only kicks in once the grid has ≥2 columns */ @media (min-width: 680px) { .info-grid-span-2 { grid-column: span 2; } } .info-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow-sm); margin-bottom: 1.5rem; display: flex; flex-direction: column; } /* Legacy support: info-card often contains direct h2 */ .info-card h2 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; color: var(--fg-muted); padding: 1rem 1.25rem; border-bottom: 1px solid var(--border); margin: 0; background: var(--bg); border-radius: var(--radius) var(--radius) 0 0; } .card-header { padding: 0.75rem 1.25rem; background: var(--bg); border-bottom: 1px solid var(--border); font-size: 0.85rem; text-transform: uppercase; border-radius: var(--radius) var(--radius) 0 0; letter-spacing: 0.05em; font-weight: 600; color: var(--fg-muted); } .card-body { padding: 1.25rem; } /* ========================================================================== Tables ========================================================================== */ .info-table, .disk-table, .counters-table { width: 100%; border-collapse: collapse; font-size: 0.9rem; } .info-table th, .info-table td, .disk-table th, .disk-table td, .counters-table th, .counters-table td { padding: 0.75rem 1.25rem; border-bottom: 1px solid var(--border); } .info-table th { width: 1%; white-space: nowrap; color: var(--fg-muted); font-weight: 500; text-align: left; } /* Disk usage widget (one entry per mount point) */ .disk-item { padding: 0.7rem 1.25rem; border-bottom: 1px solid var(--border); } .disk-item:last-child { border-bottom: none; } .disk-item-header { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 0.4rem; } .disk-mount { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 500; color: var(--fg); } .disk-pct { font-size: 0.8rem; color: var(--fg-muted); } .disk-tag { font-family: var(--font-body); font-size: 0.65rem; font-weight: 500; text-transform: uppercase; letter-spacing: 0.04em; color: var(--fg-muted); border: 1px solid var(--border); border-radius: 3px; padding: 0 0.3rem; margin-left: 0.4rem; vertical-align: middle; } .disk-stats { font-size: 0.78rem; color: var(--fg-muted); margin-top: 0.3rem; font-variant-numeric: tabular-nums; } .sensor-group-hdr th { padding: 0.35rem 1.25rem 0.2rem; font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: var(--fg-muted); background: var(--bg); border-bottom: none; } .sensor-child th { padding-left: 2rem; } .data-table th.num, .data-table td.num { text-align: right; font-variant-numeric: tabular-nums; } .info-table tr:last-child th, .info-table tr:last-child td, .disk-table tbody tr:last-child td, .counters-table tbody tr:last-child td { border-bottom: none; } .info-table tr:nth-child(even), .disk-table tbody tr:nth-child(even), .counters-table tbody tr:nth-child(even) { background-color: var(--zebra-stripe, rgba(0,0,0,0.025)); } .info-table th { text-align: left; font-weight: 500; color: var(--fg-muted); width: 40%; background: transparent; } .disk-table thead th, .counters-table thead th { background: var(--bg); font-weight: 600; color: var(--fg-muted); text-align: left; font-size: 0.8rem; text-transform: uppercase; } .counters-table td { font-family: var(--font-mono); } /* New Data Table Styles */ .data-table-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: var(--radius); /* Scroll-shadow: a right-edge shadow appears automatically when the table overflows, fading away once scrolled to the end. Pure CSS, no JS needed. The `local` gradients act as white masks pinned to the scroll content; the `scroll` gradients are the actual shadows fixed to the viewport edge. */ background: linear-gradient(to right, var(--surface), var(--surface)) left / 24px 100% no-repeat local, linear-gradient(to left, var(--surface), var(--surface)) right / 24px 100% no-repeat local, linear-gradient(to right, rgba(0,0,0,0.07), transparent) left / 8px 100% no-repeat scroll, linear-gradient(to left, rgba(0,0,0,0.07), transparent) right / 8px 100% no-repeat scroll; background-color: var(--surface); } /* Remove the nested border when a table lives inside a card — the card already provides the frame */ .info-card .data-table-wrap { border: none; border-radius: 0; } .data-table { width: 100%; border-collapse: collapse; font-size: 0.9rem; white-space: nowrap; } .data-table th, .data-table td { padding: 0.75rem 1rem; border-bottom: 1px solid var(--border); text-align: left; vertical-align: top; } .data-table th { background: var(--bg); color: var(--fg-muted); font-weight: 600; font-size: 0.8rem; text-transform: uppercase; } .data-table tbody tr:nth-child(even) { background-color: var(--zebra-stripe, rgba(0,0,0,0.025)); } .data-table tbody tr:hover { background-color: var(--slate-100); } @media (prefers-color-scheme: dark) { .data-table tbody tr:hover { background-color: rgba(255,255,255,0.06); } } .dark .data-table tbody tr:hover { background-color: rgba(255,255,255,0.06); } /* ========================================================================== Forms & Inputs ========================================================================== */ .form-group { margin-bottom: 1.25rem; } .form-group label { display: block; font-size: 0.85rem; font-weight: 600; margin-bottom: 0.4rem; color: var(--fg); } .form-group input { display: block; width: 100%; padding: 0.6rem 0.8rem; background: var(--surface); color: var(--fg); border: 1px solid var(--border); border-radius: var(--radius); font-size: 0.9rem; transition: all 0.15s ease; } .form-group input:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15); } /* ========================================================================== Authentication ========================================================================== */ @keyframes login-rise { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: translateY(0); } } .login-wrapper { display: flex; align-items: center; justify-content: center; min-height: 100vh; background-color: var(--bg); background-image: radial-gradient(var(--dot-color) 1.5px, transparent 1.5px); background-size: 22px 22px; padding: 1rem; } .login-card { background: var(--surface); padding: 2.5rem; border-radius: var(--radius); box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.07), 0 2px 4px -2px rgb(0 0 0 / 0.07), 0 0 0 1px var(--border); width: 100%; max-width: 400px; border-top: 2px solid var(--primary); animation: login-rise 0.4s cubic-bezier(0.16, 1, 0.3, 1) both; } .login-logo { display: block; max-width: 180px; height: auto; margin: 0 auto 2rem; } /* ========================================================================== Alerts & Banners ========================================================================== */ /* Top progress bar — shown during htmx page navigations */ .page-progress { position: fixed; top: 0; left: 0; height: 2px; width: 0%; background: var(--primary); z-index: 9999; opacity: 0; pointer-events: none; } .conn-banner { background: var(--danger); color: #fff; text-align: center; padding: 0.5rem; font-size: 0.9rem; font-weight: 600; position: fixed; top: var(--topbar-height); /* #sidebar sits at z-index 150 (above this banner's 100), so a left:0 banner gets its left edge clipped behind the sidebar and the text drifts visually off-centre. Inset the banner past the sidebar on desktop; on ≤1024px the sidebar slides off-screen and the banner reclaims full width. */ left: var(--sidebar-width); right: 0; z-index: 100; } @media (max-width: 1024px) { .conn-banner { left: 0; } } .alert { padding: 1rem; border-radius: var(--radius); margin-bottom: 1.5rem; font-size: 0.9rem; border: 1px solid transparent; } .alert-error { background: #fef2f2; color: var(--red-600); border-color: #fecaca; } .alert-info { background: #eff6ff; color: var(--blue-700); border-color: #bfdbfe; } /* Dark mode adjustments for alerts */ @media (prefers-color-scheme: dark) { .alert-error { background: rgba(239, 68, 68, 0.1); color: #fca5a5; border-color: rgba(239, 68, 68, 0.2); } .alert-info { background: rgba(59, 130, 246, 0.1); color: #93c5fd; border-color: rgba(59, 130, 246, 0.2); } } .dark .alert-error { background: rgba(239, 68, 68, 0.1); color: #fca5a5; border-color: rgba(239, 68, 68, 0.2); } .dark .alert-info { background: rgba(59, 130, 246, 0.1); color: #93c5fd; border-color: rgba(59, 130, 246, 0.2); } /* ========================================================================== Status Indicators ========================================================================== */ /* Legacy Status Dots */ .status-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 0.5rem; vertical-align: middle; } .status-up { background: var(--success); } .status-down { background: var(--danger); } /* New Interface Status Indicators */ .iface-status { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 0.5rem; } .iface-up { background-color: var(--success); box-shadow: 0 0 0 rgba(34, 197, 94, 0.4); animation: pulse-green 2s infinite; } .iface-down { background-color: var(--danger); } .iface-lower-down { background-color: var(--warning); } @keyframes pulse-green { 0% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.7); } 70% { box-shadow: 0 0 0 6px rgba(34, 197, 94, 0); } 100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); } } /* ========================================================================== Components: Health Bar ========================================================================== */ .health-bar-track { width: 100%; height: 6px; background-color: var(--slate-200); border-radius: 99px; overflow: hidden; } .health-bar-fill { height: 100%; background-color: var(--success); border-radius: 99px; transition: width 0.5s ease; } .health-bar-fill.is-warn { background-color: var(--warning); } .health-bar-fill.is-crit { background-color: var(--danger); } /* Dark mode track */ @media (prefers-color-scheme: dark) { .health-bar-track { background-color: var(--slate-700); } } .dark .health-bar-track { background-color: var(--slate-700); } /* ========================================================================== Components: Zone Badge ========================================================================== */ .zone-badge { /* Requires --zone-hue to be set inline or via utility */ --zone-hue: 200; /* Default */ display: inline-flex; align-items: center; padding: 0.25rem 0.6rem; border-radius: var(--radius-sm); font-size: 0.75rem; font-weight: 600; background-color: hsl(var(--zone-hue), 80%, 90%); color: hsl(var(--zone-hue), 80%, 30%); border: 1px solid hsl(var(--zone-hue), 60%, 80%); } @media (prefers-color-scheme: dark) { .zone-badge { background-color: hsl(var(--zone-hue), 60%, 20%); color: hsl(var(--zone-hue), 80%, 85%); border-color: hsl(var(--zone-hue), 60%, 30%); } } .dark .zone-badge { background-color: hsl(var(--zone-hue), 60%, 20%); color: hsl(var(--zone-hue), 80%, 85%); border-color: hsl(var(--zone-hue), 60%, 30%); } /* ========================================================================== Utilities ========================================================================== */ .num { font-variant-numeric: tabular-nums; font-family: var(--font-mono); } .text-muted { color: var(--fg-muted); } /* Page title — used at the top of each content page */ .page-title { font-size: 1.375rem; font-weight: 700; color: var(--fg); letter-spacing: -0.025em; margin-bottom: 1.5rem; } /* Removes the redundant wrapping div some pages use */ .page-content { display: contents; } /* ========================================================================== Domain Specific: Firewall, Keystore, Software ========================================================================== */ /* Zone Matrix */ .matrix-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: var(--radius); } .zone-matrix { border-collapse: collapse; font-size: 0.9rem; width: 100%; } .zone-matrix th, .zone-matrix td { padding: 0.75rem; text-align: center; border: 1px solid var(--border); min-width: 90px; } .zone-matrix thead th { font-weight: 600; color: var(--fg-muted); background: var(--bg); } .zone-matrix tbody th { font-weight: 600; text-align: right; background: var(--bg); color: var(--fg-muted); } .matrix-corner { color: var(--fg-muted); } .matrix-allow { background: #dcfce7; color: #166534; font-weight: 700; } .matrix-deny { background: #fef2f2; color: #991b1b; font-weight: 700; } .matrix-cond { background: #fef9c3; color: #854d0e; font-weight: 700; } .matrix-self { background: var(--slate-100); color: var(--slate-400); } /* Clickable cells */ .matrix-cell[data-verdict] { cursor: pointer; } .matrix-cell[data-verdict]:hover { filter: brightness(0.93); } @media (prefers-color-scheme: dark) { .matrix-allow { background: rgba(22, 163, 74, 0.2); color: #86efac; } .matrix-deny { background: rgba(220, 38, 38, 0.2); color: #fca5a5; } .matrix-cond { background: rgba(234, 179, 8, 0.2); color: #fde047; } .matrix-self { background: var(--slate-800); color: var(--slate-600); } } .dark .matrix-allow { background: rgba(22, 163, 74, 0.2); color: #86efac; } .dark .matrix-deny { background: rgba(220, 38, 38, 0.2); color: #fca5a5; } .dark .matrix-cond { background: rgba(234, 179, 8, 0.2); color: #fde047; } .dark .matrix-self { background: var(--slate-800); color: var(--slate-600); } .light .matrix-cond { background: #fef9c3; color: #854d0e; } /* Flow detail panel — shown below matrix when a cell is clicked. display:flex overrides HTML [hidden], so we restore that here. */ .fw-flow-detail { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 1rem; background: var(--surface-alt); border-top: 1px solid var(--border); font-size: 0.875rem; } .fw-flow-detail[hidden] { display: none !important; } .fw-detail-flow { font-weight: 600; } .fw-detail-sep { color: var(--fg-muted); } .fw-detail-text { color: var(--fg-muted); } /* Policy immutable lock icon */ .policy-lock-col { width: 1.5rem; min-width: 1.5rem; padding: 0 0.25rem !important; text-align: center; } .policy-lock { color: var(--fg-muted); font-size: 0.85rem; user-select: none; } .data-table td.policy-lock-col, .data-table th.policy-lock-col { vertical-align: middle; } .matrix-legend { display: flex; gap: 1.5rem; font-size: 0.85rem; color: var(--fg-muted); padding: 0.75rem 1rem; } .legend-item { display: flex; align-items: center; gap: 0.5rem; } .legend-swatch { display: inline-flex; align-items: center; justify-content: center; width: 1.5rem; height: 1.25rem; border-radius: 4px; border: 1px solid var(--border); font-weight: 700; font-size: 0.75rem; } /* Badges */ .badge { display: inline-block; padding: 0.25rem 0.6rem; border-radius: var(--radius-sm); font-size: 0.75rem; font-weight: 600; text-transform: capitalize; } .badge-accept { background: #dcfce7; color: #166534; } .badge-reject { background: #fef2f2; color: #991b1b; } .badge-drop { background: var(--slate-100); color: var(--slate-600); } .badge-continue { background: #dbeafe; color: #1e40af; } @media (prefers-color-scheme: dark) { .badge-accept { background: rgba(22, 163, 74, 0.2); color: #86efac; } .badge-reject { background: rgba(220, 38, 38, 0.2); color: #fca5a5; } .badge-drop { background: var(--slate-800); color: var(--slate-400); } .badge-continue { background: rgba(59, 130, 246, 0.2); color: #93c5fd; } } .dark .badge-accept { background: rgba(22, 163, 74, 0.2); color: #86efac; } .dark .badge-reject { background: rgba(220, 38, 38, 0.2); color: #fca5a5; } .dark .badge-drop { background: var(--slate-800); color: var(--slate-400); } .dark .badge-continue { background: rgba(59, 130, 246, 0.2); color: #93c5fd; } /* General-purpose status badges */ .badge-up { background: #dcfce7; color: #166534; } .badge-down { background: #fef2f2; color: #991b1b; } .badge-warning { background: #fef9c3; color: #854d0e; } .badge-info { background: #dbeafe; color: #1e40af; } .badge-neutral { background: var(--slate-100); color: var(--slate-600); } @media (prefers-color-scheme: dark) { .badge-up { background: rgba(22, 163, 74, 0.2); color: #86efac; } .badge-down { background: rgba(220, 38, 38, 0.2); color: #fca5a5; } .badge-warning { background: rgba(245, 158, 11, 0.2); color: #fcd34d; } .badge-info { background: rgba(59, 130, 246, 0.2); color: #93c5fd; } .badge-neutral { background: var(--slate-800); color: var(--slate-400); } } .dark .badge-up { background: rgba(22, 163, 74, 0.2); color: #86efac; } .dark .badge-down { background: rgba(220, 38, 38, 0.2); color: #fca5a5; } .dark .badge-warning { background: rgba(245, 158, 11, 0.2); color: #fcd34d; } .dark .badge-info { background: rgba(59, 130, 246, 0.2); color: #93c5fd; } .dark .badge-neutral { background: var(--slate-800); color: var(--slate-400); } .light .badge-up { background: #dcfce7; color: #166534; } .light .badge-down { background: #fef2f2; color: #991b1b; } .light .badge-warning { background: #fef9c3; color: #854d0e; } .light .badge-info { background: #dbeafe; color: #1e40af; } .light .badge-neutral { background: var(--slate-100); color: var(--slate-600); } /* mDNS service chips — self-contained, no .badge inheritance */ .mdns-svc { display: inline-block; font-size: 0.75rem; font-weight: 500; padding: 0.15rem 0.45rem; border-radius: var(--radius-sm); white-space: nowrap; background: var(--slate-100); color: var(--slate-600); } .mdns-svc-link { background: #2563eb; color: #ffffff; text-decoration: none; } .mdns-svc-link:hover { background: #1d4ed8; color: #ffffff; } @media (prefers-color-scheme: dark) { .mdns-svc { background: var(--slate-800); color: var(--slate-400); } .mdns-svc-link { background: rgba(59, 130, 246, 0.2); color: #93c5fd; } .mdns-svc-link:hover { background: rgba(59, 130, 246, 0.35); color: #93c5fd; } } .dark .mdns-svc { background: var(--slate-800); color: var(--slate-400); } .dark .mdns-svc-link { background: rgba(59, 130, 246, 0.2); color: #93c5fd; } .dark .mdns-svc-link:hover { background: rgba(59, 130, 246, 0.35); color: #93c5fd; } .light .mdns-svc { background: var(--slate-100); color: var(--slate-600); } .light .mdns-svc-link { background: #2563eb; color: #ffffff; } .light .mdns-svc-link:hover { background: #1d4ed8; color: #ffffff; } /* mDNS extra-address rows — hidden until toggle */ .mdns-extra-row { display: none; } .mdns-extra-addr { color: var(--fg-muted); padding-top: 0.1rem; padding-bottom: 0.1rem; } /* mDNS toggle column and chevron button */ .mdns-toggle-col { width: 1.5rem; min-width: 1.5rem; padding: 0 0.25rem !important; text-align: center; } .data-table td.mdns-toggle-col, .data-table th.mdns-toggle-col { vertical-align: middle; } .mdns-addr-toggle { background: none; border: none; cursor: pointer; padding: 0; color: var(--fg-muted); font-size: 0.75rem; line-height: 1; } .mdns-addr-toggle[aria-expanded="true"] .mdns-addr-arrow { display: inline-block; transform: rotate(90deg); } .mdns-lastseen { white-space: nowrap; } /* Services table */ .svc-name { font-family: var(--font-mono); font-size: 0.85rem; white-space: nowrap; } .svc-num { white-space: nowrap; color: var(--fg-muted); font-size: 0.85rem; } .svc-desc { color: var(--fg-muted); font-size: 0.875rem; } .svc-status { font-size: 0.75rem; font-weight: 600; text-transform: lowercase; white-space: nowrap; } .svc-running { color: #16a34a; } .svc-stopped { color: #ca8a04; } .svc-error { color: #dc2626; } @media (prefers-color-scheme: dark) { .svc-running { color: #86efac; } .svc-stopped { color: #fde047; } .svc-error { color: #fca5a5; } } .dark .svc-running { color: #86efac; } .dark .svc-stopped { color: #fde047; } .dark .svc-error { color: #fca5a5; } .light .svc-running { color: #16a34a; } .light .svc-stopped { color: #ca8a04; } .light .svc-error { color: #dc2626; } /* ─── NACM matrix ─────────────────────────────────────────────────────────── */ .nacm-matrix .nacm-col { text-align: center; width: 5rem; } .nacm-group { font-family: var(--font-mono); font-size: 0.85rem; } .nacm-cell { display: inline-flex; align-items: center; justify-content: center; width: 2rem; height: 2rem; border-radius: 4px; font-size: 1rem; font-weight: 700; } .nacm-full { background: rgba(34, 197, 94, 0.2); color: #16a34a; } .nacm-restricted { background: rgba(234, 179, 8, 0.2); color: #854d0e; } .nacm-denied { background: rgba(239, 68, 68, 0.2); color: #dc2626; } .nacm-restrictions-row td { padding-top: 0; } .nacm-restrictions { font-size: 0.75rem; color: var(--fg-muted); font-style: italic; } .nacm-legend { padding: 0.5rem 1rem 0.75rem; font-size: 0.8rem; color: var(--fg-muted); display: flex; align-items: center; gap: 0.35rem; } .nacm-legend .nacm-cell { width: 1.4rem; height: 1.4rem; font-size: 0.8rem; } @media (prefers-color-scheme: dark) { .nacm-full { background: rgba(34, 197, 94, 0.2); color: #86efac; } .nacm-restricted { background: rgba(234, 179, 8, 0.2); color: #fde047; } .nacm-denied { background: rgba(239, 68, 68, 0.2); color: #fca5a5; } } .dark .nacm-full { background: rgba(34, 197, 94, 0.2); color: #86efac; } .dark .nacm-restricted { background: rgba(234, 179, 8, 0.2); color: #fde047; } .dark .nacm-denied { background: rgba(239, 68, 68, 0.2); color: #fca5a5; } .light .nacm-full { background: rgba(34, 197, 94, 0.15); color: #16a34a; } .light .nacm-restricted { background: rgba(234, 179, 8, 0.15); color: #854d0e; } .light .nacm-denied { background: rgba(239, 68, 68, 0.15); color: #dc2626; } /* Sub-heading within a card body (e.g. "Active Leases" in DHCP) */ .section-subtitle { font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: var(--fg-muted); padding: 1rem 1.25rem 0.5rem; margin: 0; border-top: 1px solid var(--border); } /* Row of stat chips (e.g. DHCP counters) */ .stats-row { display: flex; flex-wrap: wrap; gap: 0.5rem; padding: 0.75rem 1.25rem; border-bottom: 1px solid var(--border); } /* Stat chip: label + value pair */ .stat-chip { display: inline-flex; align-items: center; gap: 0.375rem; padding: 0.3rem 0.65rem; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-sm); font-size: 0.8rem; white-space: nowrap; } .stat-chip-label { color: var(--fg-muted); font-weight: 500; } .stat-chip-value { color: var(--fg); font-weight: 700; font-variant-numeric: tabular-nums; } /* Interfaces */ .iface-name { white-space: nowrap; font-weight: 500; } .iface-name a { color: var(--primary); text-decoration: none; } .iface-name a:hover { text-decoration: underline; } /* Forwarding flag column */ .iface-fwd-col { width: 1.5rem; min-width: 1.5rem; padding: 0 0.25rem !important; text-align: center; } .iface-fwd-flag { font-size: 0.8rem; color: var(--primary); user-select: none; } /* Tree connector column — pseudo-elements draw the continuous vertical/horizontal lines. Wide enough to be a comfortable click target for the collapse button on parent rows. */ .iface-tree-col { width: 1.75rem; min-width: 1.75rem; padding: 0 !important; position: relative; overflow: visible; text-align: center; } /* These two narrow icon columns must stay middle-aligned even though the rest of the table uses vertical-align: top. Use td.class for higher specificity (0-1-1 beats 0-1-0). */ .data-table td.iface-tree-col, .data-table td.iface-fwd-col, .data-table th.iface-fwd-col { vertical-align: middle; } /* Parent row: draw a vertical line from row center down to the bottom edge */ .iface-tree-col.tree-parent::after { content: ''; position: absolute; left: 50%; top: 50%; bottom: 0; width: 2px; margin-left: -1px; background: var(--border); } /* Middle member: full-height vertical line + horizontal arm right */ .iface-tree-col.tree-mid::before { content: ''; position: absolute; left: 50%; top: 0; bottom: 0; width: 2px; margin-left: -1px; background: var(--border); } .iface-tree-col.tree-mid::after { content: ''; position: absolute; left: 50%; top: 50%; right: 0; height: 2px; margin-top: -1px; background: var(--border); } /* Last member: vertical line top-to-center + horizontal arm right */ .iface-tree-col.tree-last::before { content: ''; position: absolute; left: 50%; top: 0; bottom: 50%; width: 2px; margin-left: -1px; background: var(--border); } .iface-tree-col.tree-last::after { content: ''; position: absolute; left: 50%; top: 50%; right: 0; height: 2px; margin-top: -1px; background: var(--border); } /* Member rows: subtle background tint; name indented to clear the tree arm */ .iface-member { background: var(--surface-alt); } /* Tighter left padding on the name cell so connector arms read as attached to the text */ .iface-name { padding-left: 0.4rem !important; } .iface-member-name { padding-left: 1rem !important; } /* Bridge/LAG collapse toggle — sits inside .iface-tree-col, centred over the connector line. z-index: 1 ensures it is clickable above the ::after pseudo-element. */ .bridge-toggle { background: none; border: none; cursor: pointer; padding: 0; color: var(--fg-muted); font-size: 0.65rem; line-height: 1; display: block; width: 100%; position: relative; z-index: 1; } .bridge-toggle-arrow { display: inline-block; transition: transform 0.15s ease; } /* When the parent row is collapsed: rotate arrow and hide the connector line below */ .bridge-collapsed .bridge-toggle-arrow { transform: rotate(-90deg); } .bridge-collapsed .iface-tree-col.tree-parent::after { display: none; } .data-detail { color: var(--fg-muted); font-size: 0.85em; } .addr-origin { color: var(--fg-muted); font-size: 0.75rem; margin-left: 0.5rem; } .wg-peer { padding: 1rem 0; border-bottom: 1px solid var(--border); } .wg-peer:last-child { border-bottom: none; } .eth-stats { display: grid; grid-template-columns: 1fr 1fr; font-size: 0.85rem; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; } .eth-stats-row { display: flex; justify-content: space-between; padding: 0.5rem 1rem; border-bottom: 1px solid var(--border); background: var(--surface); } .eth-stats dt { color: var(--fg-muted); font-family: var(--font-mono); } .eth-stats dd { text-align: right; font-family: var(--font-mono); font-weight: 600; } /* Side-by-side comparison of supported vs advertised PMDs on iface-detail. Equal-width columns keep the lists visually parallel even when one side is empty (no auto-negotiation, or no module data yet). */ .pmd-compare { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; } .pmd-col-head { font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: var(--fg-muted); margin-bottom: 0.3rem; } .pmd-list { list-style: none; padding: 0; margin: 0; font-family: var(--font-mono); font-size: 0.85rem; } .pmd-list li { padding: 0.1rem 0; } .iface-detail-header { margin-bottom: 1.5rem; border-bottom: 1px solid var(--border); padding-bottom: 1rem; } .iface-detail-header h2 { font-size: 1.5rem; font-weight: 600; color: var(--fg); } .back-link { display: inline-flex; align-items: center; margin-bottom: 0.5rem; font-size: 0.9rem; } .info-card h3 { font-size: 0.85rem; font-weight: 600; text-transform: uppercase; color: var(--fg-muted); margin: 1.5rem 0 0.75rem; padding-left: 0.5rem; border-left: 3px solid var(--primary); } /* Logs viewer. Tab strip selects the source file, content area is a monospaced log buffer with client-side text/regex filter and best-effort severity colouring (heuristic, since on-disk sysklogd doesn't store ). Live tail, rotations and download wire up in the next step. */ /* ========================================================================== Maintenance > Diagnostics ========================================================================== */ .diag-card { display: flex; flex-direction: column; } .diag-tabs { display: flex; flex-wrap: wrap; gap: 0.35rem; padding: 0.5rem 1rem; border-bottom: 1px solid var(--border); background: var(--surface); } .diag-tab { padding: 0.3rem 0.75rem; border: 1px solid var(--border); border-radius: 4px; background: transparent; color: var(--fg-muted); font-size: 0.85rem; cursor: pointer; transition: background 0.1s, color 0.1s, border-color 0.1s; } .diag-tab:hover { color: var(--fg); border-color: var(--fg-muted); } .diag-tab.active { background: var(--primary); color: #fff; border-color: var(--primary); } .diag-form { padding: 0.9rem 1rem 0.4rem; } /* The [hidden] attribute is how JS shows per-tool option fields, but the author `display:flex` on .diag-field outranks the UA [hidden] rule, so hidden fields would still render. Restore it for everything inside the form (specificity class+attr beats the bare class). */ .diag-form [hidden] { display: none; } .diag-row { display: flex; flex-wrap: wrap; gap: 0.75rem; margin-bottom: 0.75rem; } .diag-row:empty { margin: 0; } .diag-field { display: flex; flex-direction: column; gap: 0.25rem; } .diag-field-grow { flex: 1 1 16rem; } .diag-label { font-size: 0.78rem; color: var(--fg-muted); } .diag-hint { font-size: 0.8rem; color: var(--fg-muted); align-self: center; } .diag-opts:empty { display: none; } .diag-count, .diag-size, .diag-maxhops, .diag-mtr-count, .diag-mtr-size { width: 7rem; } .diag-check { display: inline-flex; align-items: center; gap: 0.35rem; font-size: 0.85rem; white-space: nowrap; padding: 0.15rem 0; } .diag-check input { margin: 0; } .diag-actions { display: flex; align-items: center; gap: 0.5rem; padding: 0.25rem 0 0.5rem; } .diag-status { font-size: 0.78rem; color: var(--fg-muted); margin-left: 0.4rem; letter-spacing: 0.02em; } .diag-status:not(:empty)::before { content: "●"; margin-right: 0.25rem; font-size: 0.85em; } .diag-status.pending::before { color: var(--warning, #c97a00); animation: logs-tail-pulse 1s ease-in-out infinite; } .diag-status.err::before { color: var(--danger, #b03030); } .diag-output { padding: 0.25rem 1rem 1rem; } .diag-placeholder { color: var(--fg-muted); font-size: 0.85rem; padding: 1rem 0; } .diag-text { font-family: var(--font-mono); font-size: 0.78rem; line-height: 1.4; background: var(--surface-deep, var(--surface)); border: 1px solid var(--border); border-radius: 4px; padding: 0.5rem 0.75rem; margin: 0; max-height: 55vh; overflow: auto; white-space: pre-wrap; word-break: break-all; } .diag-mtr { width: 100%; border-collapse: collapse; font-size: 0.82rem; } .diag-mtr th, .diag-mtr td { text-align: right; padding: 0.3rem 0.6rem; border-bottom: 1px solid var(--border); font-variant-numeric: tabular-nums; } .diag-mtr th { color: var(--fg-muted); font-weight: 600; font-size: 0.76rem; } .diag-mtr-hop { text-align: right; width: 3rem; } .diag-mtr-host { text-align: left; font-family: var(--font-mono); } .diag-mtr tbody tr:hover { background: var(--surface); } .diag-loss-bad { color: var(--danger); font-weight: 600; } .diag-dns-head { font-size: 0.9rem; margin: 0.5rem 0; } .diag-dns-list { list-style: none; padding: 0; margin: 0; } .diag-dns-list li { padding: 0.2rem 0; font-family: var(--font-mono); font-size: 0.85rem; } .diag-dns-err { color: var(--danger); font-size: 0.9rem; } /* On the logs page, #content holds only the logs card — turn it into a flex column and clip its own overflow so the card fills the available height and the *buffer* scrolls, not the page. :has scopes this to the logs page without touching any other view. */ #content:has(.logs-card) { display: flex; flex-direction: column; overflow: hidden; } .logs-card { display: flex; flex-direction: column; flex: 1; min-height: 0; margin-bottom: 0; /* no trailing gap when the card fills #content */ } .logs-tabs { display: flex; flex-wrap: wrap; gap: 0.35rem; padding: 0.5rem 1rem; border-bottom: 1px solid var(--border); background: var(--surface); } .logs-tab { display: inline-flex; align-items: center; gap: 0.4rem; padding: 0.3rem 0.75rem; border: 1px solid var(--border); border-radius: 4px; background: transparent; color: var(--fg-muted); font-size: 0.85rem; cursor: pointer; transition: background 0.1s, color 0.1s, border-color 0.1s; } .logs-tab:hover { color: var(--fg); border-color: var(--fg-muted); } .logs-tab.active { background: var(--primary); color: white; border-color: var(--primary); } .logs-tab-empty { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.6; } .logs-body { display: flex; flex-direction: column; flex: 1; /* fill the card below the tabs so .logs-content can grow */ min-height: 0; padding: 0.75rem 1rem; gap: 0.6rem; } .logs-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; } .logs-toolbar .cfg-input { flex: 1; min-width: 14rem; } .logs-toggle { display: inline-flex; align-items: center; gap: 0.3rem; font-size: 0.85rem; color: var(--fg-muted); cursor: pointer; user-select: none; } .logs-toggle input[type="checkbox"]:disabled + * { opacity: 0.5; } .logs-disabled-link { pointer-events: none; opacity: 0.5; } .logs-content { font-family: var(--font-mono); font-size: 0.78rem; line-height: 1.35; background: var(--surface-deep, var(--surface)); border: 1px solid var(--border); border-radius: 4px; padding: 0.5rem 0.75rem; /* Height is governed by the flex chain below (#content:has(.logs-card) → .logs-card → .logs-body → here), not a viewport calc — so the buffer fills exactly the space left by the card chrome and #content never grows its own scrollbar. min-height:0 lets this flex item shrink below its content size so overflow stays internal. */ flex: 1; min-height: 0; overflow: auto; /* No `white-space: pre` here — the rule belongs on `.logs-line` only, because the parent's trailing newline text node (left over from the range template) would otherwise render as a blank line above any JS-appended live-tail content. */ } .logs-load-earlier { display: block; margin: 0 auto 0.5rem; } .logs-download { display: inline-flex; align-items: center; gap: 0.35rem; text-decoration: none; } .logs-download .nav-icon { width: 0.9rem; height: 0.9rem; } .logs-no-more { display: block; text-align: center; font-size: 0.78rem; color: var(--fg-muted); font-family: var(--font-body); padding: 0.25rem 0 0.5rem; } .logs-tail-status { font-size: 0.72rem; font-family: var(--font-body); color: var(--fg-muted); margin-left: 0.4rem; letter-spacing: 0.02em; } .logs-tail-status:not(:empty)::before { content: "●"; margin-right: 0.25rem; font-size: 0.85em; } .logs-tail-status.pending::before { color: var(--warning, #c97a00); animation: logs-tail-pulse 1s ease-in-out infinite; } .logs-tail-status.live::before { color: var(--success, #2c8a4a); animation: logs-tail-pulse 1.6s ease-in-out infinite; } .logs-tail-status.err::before { color: var(--danger, #b03030); } @keyframes logs-tail-pulse { 0%, 100% { opacity: 0.45; } 50% { opacity: 1; } } .logs-line { white-space: pre; border-left: 3px solid transparent; padding: 0 0 0 0.4rem; } .logs-line.logs-err { color: var(--danger); border-left-color: var(--danger); background: color-mix(in srgb, var(--danger) 7%, transparent); } .logs-line.logs-warn { color: var(--warning); border-left-color: var(--warning); background: color-mix(in srgb, var(--warning) 6%, transparent); } .logs-status { font-size: 0.8rem; color: var(--fg-muted); } .logs-status code { font-family: var(--font-mono); font-size: 0.78rem; color: var(--fg); } /* Software & Reboot */ .sw-install-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 1.5rem; margin-top: 1.5rem; } .sw-install-grid > .info-card { margin: 0; } .sw-card-muted { opacity: 0.6; pointer-events: none; } .sw-help-text { font-size: 0.875rem; color: var(--fg-muted); margin-bottom: 1rem; line-height: 1.6; } .sw-help-text a { color: var(--primary); } .sw-help-text code, .sw-hint-body code { font-family: var(--font-mono); font-size: 0.8em; } .sw-hint { background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-sm); margin-bottom: 1.25rem; font-size: 0.875rem; } .sw-hint summary { cursor: pointer; padding: 0.55rem 0.8rem; color: var(--fg-muted); user-select: none; list-style: none; display: flex; align-items: center; gap: 0.4rem; } .sw-hint summary::-webkit-details-marker { display: none; } .sw-hint summary::before { content: '›'; display: inline-block; transition: transform 0.15s; } details.sw-hint[open] summary::before { transform: rotate(90deg); } .sw-hint-body { padding: 0.75rem 0.8rem; border-top: 1px solid var(--border); } .sw-hint-body p { margin: 0 0 0.5rem; color: var(--fg-muted); font-size: 0.85rem; } .sw-hint-body p:last-child { margin-bottom: 0; } .sw-hint-code { display: block; font-family: var(--font-mono); font-size: 0.8rem; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 0.6rem 0.75rem; white-space: pre; overflow-x: auto; color: var(--fg); margin-bottom: 0.5rem; } .sw-boot-order-row { display: flex; align-items: center; gap: 0.4rem; padding: 0.55rem 1.25rem; border-bottom: 1px solid var(--border); font-size: 0.8rem; } .sw-boot-order-label { color: var(--fg-muted); font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.06em; font-weight: 600; margin-right: 0.2rem; } .sw-boot-slots { display: flex; gap: 0.3rem; align-items: center; flex: 1; } .sw-boot-badge { cursor: grab; user-select: none; } .sw-boot-badge.sw-boot-dragging { opacity: 0.35; } .sw-boot-badge.sw-boot-drop-before { box-shadow: -3px 0 0 var(--primary); } .sw-slot-list { display: flex; flex-direction: column; } .sw-slot-item { padding: 0.75rem 1.25rem; border-bottom: 1px solid var(--border); } .sw-slot-item:last-child { border-bottom: none; } .sw-slot-primary { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.2rem; } .sw-slot-name { font-weight: 600; font-size: 0.9rem; } .sw-slot-version { font-family: var(--font-mono); font-size: 0.8rem; color: var(--fg-muted); } .sw-slot-date { font-size: 0.75rem; color: var(--fg-muted); opacity: 0.75; margin-top: 0.1rem; } .sw-upload-placeholder { border: 2px dashed var(--border); border-radius: var(--radius); padding: 2rem 1rem; text-align: center; color: var(--fg-muted); margin-bottom: 1rem; display: flex; flex-direction: column; align-items: center; gap: 0.6rem; font-size: 0.875rem; } .software-form .form-group { margin-bottom: 0.75rem; } .software-form .sw-checkbox-row { margin-bottom: 0.75rem; } .sw-checkbox-row { display: flex; align-items: center; gap: 0.5rem; font-size: 0.875rem; color: var(--fg-muted); cursor: pointer; user-select: none; } .sw-checkbox-row input[type="checkbox"] { accent-color: var(--primary); cursor: pointer; } /* Multi-select dropdown built on
+ checkboxes so the summary row looks like a native never renders as a dropdown, hence the substitute. */ .cfg-multi { position: relative; display: inline-block; min-width: 14rem; } .cfg-multi-summary { display: flex; align-items: center; justify-content: space-between; padding: 0.4rem 0.6rem; background: var(--surface); color: var(--fg); border: 1px solid var(--border); border-radius: var(--radius); font-size: 0.875rem; cursor: pointer; list-style: none; user-select: none; } .cfg-multi-summary::-webkit-details-marker { display: none; } .cfg-multi-summary::after { content: '\25BE'; margin-left: 0.5rem; color: var(--fg-muted); transition: transform 0.15s; } details[open] > .cfg-multi-summary::after { transform: rotate(180deg); } details[open] > .cfg-multi-summary { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15); } .cfg-multi-body { position: absolute; top: 100%; left: 0; right: 0; margin-top: 0.25rem; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 0.5rem 0.75rem; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); z-index: 10; max-height: 20rem; overflow-y: auto; display: flex; flex-direction: column; gap: 0.35rem; } .cfg-multi-item { display: flex; align-items: center; gap: 0.5rem; font-size: 0.875rem; white-space: nowrap; cursor: pointer; } .cfg-multi-item input[type="checkbox"] { accent-color: var(--primary); cursor: pointer; } .sw-result { display: flex; align-items: center; gap: 0.75rem; font-size: 0.9rem; font-weight: 500; } .sw-result svg { flex-shrink: 0; } .sw-result-ok { color: var(--success); } .sw-result-err { color: var(--danger); } .sw-result-body { display: flex; flex-direction: column; } .sw-result-actions { margin-top: 1rem; } /* Compact pill badges used in the software slot list and boot-order row. */ .sw-install-grid .badge-neutral { background: var(--slate-200); font-size: 0.65rem; font-weight: 600; padding: 0.15em 0.5em; border-radius: 999px; text-transform: uppercase; letter-spacing: 0.04em; vertical-align: middle; } @media (prefers-color-scheme: dark) { .sw-install-grid .badge-neutral { background: var(--slate-700); color: var(--slate-300); } } .dark .sw-install-grid .badge-neutral { background: var(--slate-700); color: var(--slate-300); } .progress-bar-wrap--flush { margin-top: 0; } .progress-bar-wrap { background: var(--slate-200); border-radius: var(--radius); height: 1.25rem; overflow: hidden; margin-top: 0.5rem; } .progress-bar { background: var(--primary); height: 100%; width: 0%; transition: width 0.3s ease; } .progress-text { font-size: 0.85rem; color: var(--fg-muted); margin-top: 0.5rem; text-align: center; } .reboot-overlay { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 60vh; } .reboot-spinner { width: 48px; height: 48px; border: 4px solid var(--slate-200); border-top-color: var(--primary); border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 1.5rem; } @keyframes spin { to { transform: rotate(360deg); } } .reboot-message { font-size: 1.25rem; font-weight: 600; color: var(--fg); } .reboot-status { font-size: 1rem; color: var(--fg-muted); margin-top: 0.5rem; } .reboot-status.is-error { color: var(--danger); } /* Keystore */ .empty-message { color: var(--fg-muted); font-style: italic; padding: 1rem; text-align: center; } .key-row-toggle { background: none; border: none; padding: 0; cursor: pointer; color: var(--fg); font: inherit; display: inline-flex; align-items: center; gap: 0.4rem; text-align: left; } .key-row-arrow { font-size: 0.6rem; color: var(--fg-muted); display: inline-block; transition: transform 0.15s ease; flex-shrink: 0; } .key-row-toggle[aria-expanded="true"] .key-row-arrow { transform: rotate(90deg); } .key-detail-row { display: none; } .key-detail-row.is-open { display: table-row; } .key-detail-cell { background: var(--surface-alt); padding: 0.75rem 1rem 0.75rem 1.5rem !important; border-top: none !important; text-align: left !important; } .key-detail-body { display: flex; flex-direction: column; gap: 0.6rem; } .key-field { display: flex; flex-direction: column; gap: 0.2rem; } .key-field-label { font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--fg-muted); } .key-field-value { font-family: var(--font-mono); font-size: 0.8rem; line-height: 1.6; word-break: break-all; white-space: pre-wrap; color: var(--fg); background: var(--surface); padding: 0.4rem 0.6rem; border-radius: 4px; max-width: 72ch; display: block; } /* Survey */ .survey-chart { max-width: 100%; height: auto; display: block; } .survey-hint { font-size: 0.8rem; color: var(--fg-muted); margin-bottom: 1rem; } /* WiFi */ .wifi-radio-header { display: flex; justify-content: space-between; align-items: center; width: 100%; } .wifi-caps { display: flex; gap: 0.375rem; align-items: center; } .wifi-survey-body { padding: 1rem; display: flex; align-items: center; justify-content: center; } .wifi-survey-body .survey-chart { max-height: 220px; width: auto; } .wifi-ssid { font-weight: 400; color: var(--fg-muted); font-size: 0.85rem; margin-left: 0.5rem; font-family: var(--font-mono); } .wifi-band-list { display: flex; flex-direction: column; gap: 0.375rem; } .wifi-band-item { display: flex; align-items: center; gap: 0.5rem; } .wifi-band-caps { display: inline-flex; gap: 0.25rem; } /* Signal Strength */ .signal-excellent { color: var(--success); font-weight: 600; } .signal-good { color: var(--green-600); font-weight: 600; } .signal-ok { color: var(--warning); font-weight: 600; } .signal-poor { color: var(--danger); font-weight: 600; } /* ========================================================================== Topbar & User Menu ========================================================================== */ .topbar { position: fixed; top: 0; left: 0; right: 0; display: flex; align-items: center; gap: 0.75rem; padding: 0 1rem; height: var(--topbar-height); background: var(--sidebar-bg); border-bottom: 1px solid var(--sidebar-border); z-index: 200; } .topbar-right { display: flex; align-items: center; gap: 0.5rem; margin-left: auto; } .hamburger-btn { background: none; border: 1px solid var(--border); border-radius: 0.375rem; padding: 0.375rem; cursor: pointer; color: var(--fg-muted); display: none; align-items: center; } .hamburger-btn:hover { color: var(--fg); border-color: var(--fg-muted); } /* User menu button */ .user-menu { position: relative; } .user-menu-btn { display: flex; align-items: center; gap: 0.4rem; background: none; border: none; border-radius: var(--radius); padding: 0.35rem 0.5rem; font-size: 0.875rem; font-weight: 500; color: var(--fg-muted); cursor: pointer; transition: color 0.15s; } .user-menu-btn:hover { color: var(--fg); } .user-menu-btn .chevron { color: var(--fg-muted); transition: transform 0.2s; } .user-menu-btn[aria-expanded="true"] .chevron { transform: rotate(180deg); } /* Dropdown panel — opens on hover */ .user-dropdown { position: absolute; top: 100%; right: 0; min-width: 190px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow); padding: 0.375rem 0; z-index: 200; /* Hidden by default; shown on hover or when button is aria-expanded */ opacity: 0; pointer-events: none; transform: translateY(-4px); transition: opacity 0.15s, transform 0.15s; } .user-menu:hover .user-dropdown, .user-menu-btn[aria-expanded="true"] + .user-dropdown { opacity: 1; pointer-events: auto; transform: translateY(0); } .dropdown-section-label { font-size: 0.7rem; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--fg-muted); padding: 0.25rem 0.875rem 0.125rem; } .dropdown-divider { border: none; border-top: 1px solid var(--border); margin: 0.375rem 0; } .dropdown-item { display: flex; align-items: center; gap: 0.5rem; width: 100%; padding: 0.45rem 0.875rem; font-size: 0.875rem; color: var(--fg-muted); background: none; border: none; cursor: pointer; text-decoration: none; text-align: left; transition: background 0.1s, color 0.1s; box-sizing: border-box; } .dropdown-item:hover, .dropdown-item:focus { background: var(--border); color: var(--fg); text-decoration: none; } .dropdown-item svg { flex-shrink: 0; color: var(--fg-muted); transition: color 0.1s; } .dropdown-item:hover svg, .dropdown-item:focus svg { color: var(--fg); } .dropdown-item-danger:hover, .dropdown-item-danger:focus { color: var(--danger); } .dropdown-item-danger:hover svg, .dropdown-item-danger:focus svg { color: var(--danger); } /* Theme checkmark — hidden by default, shown on active option */ .theme-check { margin-left: auto; flex-shrink: 0; opacity: 0; transition: opacity 0.1s; } .theme-opt.is-active .theme-check, .timeout-opt.is-active .theme-check { opacity: 1; } /* Login page floating theme button */ .login-theme-btn { position: fixed; bottom: 1.25rem; right: 1.25rem; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 0.45rem; cursor: pointer; color: var(--fg-muted); display: flex; align-items: center; justify-content: center; box-shadow: var(--shadow-sm); transition: all 0.15s; } .login-theme-btn:hover { color: var(--fg); border-color: var(--fg-muted); } .login-header { display: flex; align-items: center; justify-content: center; gap: 0.5rem; margin-bottom: 1.75rem; color: var(--fg-muted); } .login-header h1 { font-size: 1.25rem; font-weight: 600; color: var(--fg); line-height: 1; } /* ========================================================================== Responsive Layout ========================================================================== */ /* Narrow (≤1024px): Sidebar hidden behind hamburger overlay */ @media (max-width: 1024px) { #sidebar { position: fixed; top: var(--topbar-height); left: 0; height: calc(100vh - var(--topbar-height)); z-index: 150; transform: translateX(-100%); transition: transform 0.25s ease; width: var(--sidebar-width); } body.sidebar-open #sidebar { transform: translateX(0); } body.sidebar-open::after { content: ''; position: fixed; top: var(--topbar-height); left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.4); z-index: 140; } #content { height: auto; min-height: 100%; } .hamburger-btn { display: flex; } } /* Desktop (>1024px): hamburger hidden, sidebar always visible */ @media (min-width: 1025px) { .hamburger-btn { display: none; } } /* Column priority hiding for data tables */ @media (max-width: 600px) { .col-priority-2 { display: none; } } @media (max-width: 480px) { .col-priority-3 { display: none; } } /* ─── Confirm dialog ──────────────────────────────────────────────────────── */ #confirm-dialog { border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); color: var(--fg); padding: 1.5rem 2rem 1.25rem; min-width: 20rem; max-width: 90vw; box-shadow: var(--shadow); /* Centre in the viewport regardless of scroll position */ position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); margin: 0; } #confirm-dialog::backdrop { background: rgba(0, 0, 0, 0.45); } .dialog-message { font-size: 0.95rem; margin: 0 0 1.25rem; line-height: 1.5; } .dialog-actions { display: flex; gap: 0.75rem; justify-content: flex-end; } .btn-outline { background: transparent; color: var(--fg); border: 1px solid var(--border); } .btn-outline:hover { background: var(--border); } .btn-danger-outline { background: transparent; color: var(--danger); border: 1px solid var(--danger); } .btn-danger-outline:hover { background: rgba(220, 38, 38, 0.08); } .btn-accept { background: var(--success); color: #fff; border: 1px solid transparent; } .btn-accept:hover { background: var(--green-600); } /* ─── Configure toolbar ───────────────────────────────────────────────────── */ .configure-toolbar { position: fixed; bottom: 0; left: var(--sidebar-width); right: 0; z-index: 130; background: var(--surface); border-top: 1px solid var(--border); padding: 0.75rem 1.5rem; box-shadow: 0 -2px 8px rgba(0,0,0,0.08); } @media (max-width: 1024px) { .configure-toolbar { left: 0; } } .configure-toolbar-inner { display: flex; gap: 0.75rem; } /* Push page content above the toolbar */ .configure-page { padding-bottom: 4rem; } /* ─── Unsaved-changes banner ──────────────────────────────────────────────── */ .cfg-unsaved-banner { display: flex; align-items: center; gap: 0.6rem; padding: 0.5rem 1.25rem; background: var(--warning-bg); border-bottom: 1px solid var(--warning-border); color: var(--warning-fg); font-size: 0.875rem; flex-shrink: 0; } /* cfgError inline feedback */ .cfg-save-status.error { color: var(--danger); cursor: pointer; } /* ─── Configure activity log ─────────────────────────────────────────────── */ .cfg-log-wrap { position: relative; margin-left: auto; } .cfg-log-btn { position: relative; } .cfg-log-badge { position: absolute; top: -6px; right: -6px; min-width: 16px; height: 16px; padding: 0 3px; background: var(--danger); color: #fff; border-radius: 8px; font-size: 0.65rem; line-height: 16px; text-align: center; } .cfg-log-panel { display: none; position: absolute; bottom: calc(100% + 8px); right: 0; width: 420px; max-height: 300px; background: var(--card-bg); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: 0 4px 16px rgba(0,0,0,0.15); flex-direction: column; z-index: 300; font-size: 0.8rem; } .cfg-log-panel:not([hidden]) { display: flex; } .cfg-log-header { display: flex; justify-content: space-between; align-items: center; padding: 0.4rem 0.75rem; border-bottom: 1px solid var(--border); font-weight: 600; font-size: 0.8rem; } .cfg-log-close { background: none; border: none; cursor: pointer; color: var(--fg-muted); font-size: 0.9rem; } .cfg-log-list { list-style: none; margin: 0; padding: 0; overflow-y: auto; flex: 1; } .cfg-log-entry { padding: 0.3rem 0.75rem; border-bottom: 1px solid var(--border-subtle, var(--border)); } .cfg-log-empty { padding: 0.5rem 0.75rem; color: var(--fg-muted); } .cfg-log-ts { color: var(--fg-muted); margin-right: 0.4rem; } .cfg-log-ok { color: var(--fg); } .cfg-log-error { color: var(--danger); background: rgba(var(--danger-rgb, 220,38,38), 0.05); } /* ─── Editable form inputs inside cards ───────────────────────────────────── */ .cfg-input { width: 100%; padding: 0.4rem 0.6rem; background: var(--surface); color: var(--fg); border: 1px solid var(--border); border-radius: var(--radius); font-size: 0.875rem; font-family: inherit; transition: border-color 0.15s, box-shadow 0.15s; box-sizing: border-box; } .cfg-input:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15); } select.cfg-input { appearance: none; -webkit-appearance: none; padding-right: 2rem; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 0.6rem center; cursor: pointer; } .cfg-input-sm { max-width: 8rem; } /* Inputs in info-table cells must shrink to the column; without this the intrinsic min-width of a datetime-local (wide in Firefox) forces the table past the card edge. min-width:0 isn't enough on its own for datetime-local, so cards with one use .info-table-fixed (table-layout: fixed + ) to cap the columns and make the input shrink. */ .info-table .cfg-input { min-width: 0; } .info-table-fixed { table-layout: fixed; } .cfg-input-mono { font-family: var(--font-mono); font-size: 0.85rem; } .cfg-reset-col { width: 1%; white-space: nowrap; padding-left: 0.25rem; } .cfg-section-head { font-size: 0.85rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); margin: 0 0 0.4rem; padding-bottom: 0.25rem; border-bottom: 1px solid var(--border); } .cfg-subsection-head { font-size: 0.8rem; font-weight: 600; color: var(--text-muted); margin: 0.5rem 0 0.25rem; } .cfg-fold { margin: 0.5rem 0 0; } .cfg-fold > summary { font-size: 0.85rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); padding-bottom: 0.25rem; border-bottom: 1px solid var(--border); cursor: pointer; list-style: none; display: flex; align-items: center; gap: 0.4rem; user-select: none; } .cfg-fold > summary::-webkit-details-marker { display: none; } .cfg-fold > summary::before { content: '›'; display: inline-block; transition: transform 0.15s; font-size: 1rem; } details.cfg-fold[open] > summary::before { transform: rotate(90deg); } .cfg-fold-body { padding-top: 0.5rem; } .fw-check-scroll { max-height: 12rem; overflow-y: auto; border: 1px solid var(--border); border-radius: 4px; padding: 0.4rem 0.6rem; background: var(--input-bg, var(--surface)); } .fw-check-grid { display: flex; flex-wrap: wrap; gap: 0.35rem 1.25rem; } .fw-check-grid label { display: flex; align-items: center; gap: 0.35rem; font-weight: normal; cursor: pointer; } /* Editable table rows */ .cfg-table td { padding: 0.45rem 0.75rem; vertical-align: middle; } .cfg-table > tbody > tr > td:last-child { width: 3rem; text-align: center; } /* Leading nav-chevron column for complex (drill-down) list rows */ .yt-nav-col { width: 1.5rem; padding: 0.25rem !important; text-align: center; color: var(--fg-muted); } .btn-icon { display: inline-flex; align-items: center; justify-content: center; width: 1.75rem; height: 1.75rem; border: none; border-radius: var(--radius); background: transparent; color: var(--fg-muted); cursor: pointer; transition: background 0.1s, color 0.1s; padding: 0; } .btn-icon:hover { background: var(--border); color: var(--fg); } .btn-icon-danger:hover { color: var(--danger); background: rgba(220,38,38,0.08); } .btn-add-row { display: flex; align-items: center; gap: 0.4rem; font-size: 0.8rem; color: var(--primary); background: none; border: none; cursor: pointer; padding: 0.5rem 0.75rem; } .btn-add-row:hover { text-decoration: underline; } /* Inline forms inside users table */ .user-shell-form, .user-add-inline { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; } .user-shell-form .cfg-input { flex: 1; min-width: 0; } .user-add-inline .cfg-input { width: auto; flex: 1; min-width: 8rem; } /* Add Interface modal dialog */ .iface-modal { border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); color: var(--fg); padding: 0; min-width: 42rem; max-width: 90vw; box-shadow: var(--shadow); /* Native .showModal() centers by default, but some browsers drift on long content — pin the position to be safe. */ position: fixed; inset: 0; margin: auto; } .iface-modal::backdrop { background: rgba(0, 0, 0, 0.45); } .iface-modal-form { display: flex; flex-direction: column; } .iface-modal-header { display: flex; align-items: center; justify-content: space-between; padding: 0.75rem 1rem; border-bottom: 1px solid var(--border); } .iface-modal-header h3 { margin: 0; font-size: 1rem; } .iface-modal-header .btn-close { background: none; border: none; font-size: 1.4rem; line-height: 1; cursor: pointer; color: var(--fg-muted); padding: 0 0.25rem; } .iface-modal-header .btn-close:hover { color: var(--fg); } .iface-modal-body { padding: 0.75rem 1rem; } .iface-modal-footer { display: flex; align-items: center; gap: 0.5rem; padding: 0.6rem 1rem; border-top: 1px solid var(--border); } .iface-fields { border: none; padding: 0; margin: 0.5rem 0 0; } .iface-fields-unsupported { padding: 0.5rem 0; } .cfg-hint { font-size: 0.8rem; color: var(--fg-muted); margin-top: 0.25rem; } /* Inline keystore "+ New" create-key form, used by the Add Interface modal's WiFi PSK and WireGuard private-key rows. */ .ks-picker-row { display: flex; align-items: center; gap: 0.5rem; } .ks-picker-row > select { flex: 1; min-width: 0; } .ks-create-form { margin-top: 0.5rem; padding: 0.5rem; border: 1px dashed var(--border); border-radius: 4px; display: flex; flex-direction: column; gap: 0.35rem; background: var(--card-bg); } /* `display: flex` above overrides the `[hidden]` attribute's UA-stylesheet `display: none`, so the fold-out form was always shown. Restore the collapsed-by-default behaviour. */ .ks-create-form[hidden] { display: none; } .ks-create-form > input, .ks-create-form > textarea { width: 100%; } .ks-create-actions { display: flex; gap: 0.5rem; } /* WiFi PSK passphrase — masked text input, NOT type="password". A real password input pulls in browser password managers; the WiFi key is a device-config value, not a per-site credential, so we mask with CSS instead and let the user toggle visibility via the eye button. The webkit property covers Chrome/Edge/Safari (>90% of browsers); Firefox shows the value in plain text. */ .cfg-input-mask { -webkit-text-security: disc; font-family: text-security-disc, inherit; letter-spacing: 0.08em; } .cfg-input-mask.cfg-secret-shown { -webkit-text-security: none; font-family: inherit; letter-spacing: normal; } .cfg-secret-wrap { display: flex; align-items: center; gap: 0.35rem; } .cfg-secret-wrap > input { flex: 1; min-width: 0; } .cfg-secret-toggle { background: none; border: 1px solid var(--border); border-radius: 4px; padding: 0.25rem 0.4rem; cursor: pointer; font-size: 0.9rem; line-height: 1; } /* Sub-table inside an inline create form. Same row layout as the rest of the wizard (label on the left, value on the right) but indented a touch so the user reads it as "fields belonging to the picker above". */ .info-table-sub { margin: 0; } .info-table-sub th { padding-left: 0.75rem; font-weight: normal; font-size: 0.85rem; color: var(--fg-muted); } /* Card-level save feedback */ .cfg-card-footer { display: flex; align-items: center; gap: 0.75rem; padding: 0.6rem 1rem; border-top: 1px solid var(--border); } .cfg-save-status { font-size: 0.8rem; color: var(--fg-muted); min-height: 1.2em; } .cfg-save-status.saved { color: #16a34a; } .cfg-save-error { font-size: 0.8rem; color: var(--danger); } @media (prefers-color-scheme: dark) { .cfg-save-status.saved { color: #86efac; } } .dark .cfg-save-status.saved { color: #86efac; } .light .cfg-save-status.saved { color: #16a34a; } /* ========================================================================== System Control page ========================================================================== */ .sc-action-body { display: flex; flex-direction: column; justify-content: space-between; flex: 1; gap: 1rem; } .sc-desc { color: var(--fg-muted); font-size: 0.875rem; line-height: 1.5; margin: 0; } .sc-danger-card { border-color: var(--warning-border); } .sc-danger-body { display: flex; flex-direction: column; } .sc-danger-item { display: flex; align-items: center; justify-content: space-between; gap: 1.5rem; padding: 1.25rem; flex-wrap: wrap; } .sc-danger-text { flex: 1; min-width: 0; } .sc-danger-text strong { display: block; margin-bottom: 0.35rem; } .sc-danger-sep { height: 1px; background: var(--border); margin: 0 1.25rem; } .sc-fd-ok { color: var(--success); font-size: 0.875rem; } .sc-fd-err { color: var(--danger); font-size: 0.875rem; } .sc-dt-row { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; margin-bottom: 0.5rem; } .sc-restore-label { display: flex; align-items: center; gap: 0.4rem; margin-top: 0.75rem; cursor: pointer; } .sc-restore-warn-text { color: var(--warning-fg); font-size: 0.875rem; } /* ========================================================================== YANG Tree Navigation ========================================================================== */ /* Two-column layout: tree pane + detail pane */ .yang-layout { display: flex; gap: 1.5rem; align-items: flex-start; } .yang-tree-pane { width: 260px; flex-shrink: 0; overflow-y: auto; max-height: calc(100vh - 14rem); margin-bottom: 0; } .yang-detail-pane { flex: 1; min-width: 0; } /* Tree list */ .yang-tree, .yt-children { list-style: none; padding: 0; margin: 0; } .yang-tree { padding: 0.25rem 0; } .yt-children { padding-left: 0; margin-left: 1.1rem; } .yt-children > li { position: relative; padding-left: 1rem; } /* Horizontal arm */ .yt-children > li::before { content: ''; position: absolute; left: 0; top: 0.85rem; width: 0.75rem; height: 1px; background: var(--border); } /* Vertical connector — runs full height for non-last, stops at arm for last */ .yt-children > li::after { content: ''; position: absolute; left: 0; top: 0; width: 1px; background: var(--border); } .yt-children > li:not(:last-child)::after { bottom: 0; } .yt-children > li:last-child::after { height: calc(0.85rem + 1px); } /* Style for the root "/" node */ .yt-root { font-family: var(--font-mono); font-weight: 600; color: var(--fg-muted); } /* Expandable node (container / list) */ .yt-node { position: relative; } /* Suppress the browser's default triangle on */ .yt-node > summary { list-style: none; } .yt-node > summary::-webkit-details-marker { display: none; } /* Shared label style for both and