/* ═══════════════════════════════════════════════════════════════ IoT Web Configurator — Design System & Styles ═══════════════════════════════════════════════════════════════ */ /* * v1.5.1.1: 로컬 폰트 self-host (Inter 5 weight + JetBrains Mono 2 weight). * LAN 전용 IoT — 외부 CDN 의존 0. Latin subset .woff2 (각 ~20-40 KB). */ @font-face { font-family: 'Inter'; font-style: normal; font-weight: 300; font-display: swap; src: url('../fonts/Inter-Light.woff2') format('woff2'); } @font-face { font-family: 'Inter'; font-style: normal; font-weight: 400; font-display: swap; src: url('../fonts/Inter-Regular.woff2') format('woff2'); } @font-face { font-family: 'Inter'; font-style: normal; font-weight: 500; font-display: swap; src: url('../fonts/Inter-Medium.woff2') format('woff2'); } @font-face { font-family: 'Inter'; font-style: normal; font-weight: 600; font-display: swap; src: url('../fonts/Inter-SemiBold.woff2') format('woff2'); } @font-face { font-family: 'Inter'; font-style: normal; font-weight: 700; font-display: swap; src: url('../fonts/Inter-Bold.woff2') format('woff2'); } @font-face { font-family: 'JetBrains Mono'; font-style: normal; font-weight: 400; font-display: swap; src: url('../fonts/JetBrainsMono-Regular.woff2') format('woff2'); } @font-face { font-family: 'JetBrains Mono'; font-style: normal; font-weight: 500; font-display: swap; src: url('../fonts/JetBrainsMono-Medium.woff2') format('woff2'); } /* ─── CSS Custom Properties ─────────────────────────────────── */ :root { /* Colors — Dark Mode (Default) */ --bg-body: #0F172A; --bg-sidebar: rgba(15, 23, 42, 0.85); --bg-card: #1E293B; --bg-card-hover: #263548; --bg-input: #0F172A; --bg-input-focus: #1a2744; --bg-table-header: #162032; --bg-table-row-hover: rgba(99, 102, 241, 0.08); --border-default: #334155; --border-focus: #6366F1; --border-error: #EF4444; --border-card: rgba(99, 102, 241, 0.15); /* v1.7.1 Task 7: seal the legacy bare `--border` alias to the canonical `--border-default` token. Naming convention is `--{category}-{variant}`; bare aliases like this exist only for backward compatibility and must always point at a defined token (audited by scripts/audit_css_tokens.py). Because --border-default is redefined per theme, --border stays theme-aware. */ --border: var(--border-default); --text-primary: #F1F5F9; --text-secondary: #94A3B8; --text-muted: #64748B; --text-inverse: #0F172A; --accent: #6366F1; --accent-hover: #818CF8; --accent-subtle: rgba(99, 102, 241, 0.15); --accent-gradient: linear-gradient(135deg, #6366F1 0%, #818CF8 100%); --success: #10B981; --success-bg: rgba(16, 185, 129, 0.15); --error: #EF4444; --error-bg: rgba(239, 68, 68, 0.15); --warning: #F59E0B; --warning-bg: rgba(245, 158, 11, 0.15); --info: #3B82F6; --info-bg: rgba(59, 130, 246, 0.15); /* Spacing */ --space-xs: 4px; --space-sm: 8px; --space-md: 16px; --space-lg: 24px; --space-xl: 32px; --space-2xl: 48px; /* Typography */ --font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; --font-mono: 'JetBrains Mono', 'Fira Code', monospace; --font-size-xs: 11px; --font-size-sm: 12px; --font-size-base: 14px; --font-size-md: 16px; --font-size-lg: 20px; --font-size-xl: 24px; /* Border Radius */ --radius-sm: 6px; --radius-md: 8px; --radius-lg: 12px; --radius-xl: 16px; --radius-full: 50%; /* Shadows */ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.25); --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.3); --shadow-glow: 0 0 20px rgba(99, 102, 241, 0.15); /* Transitions */ --transition-fast: 0.15s ease; --transition-base: 0.25s ease; --transition-slow: 0.35s ease; /* Layout */ --sidebar-width: 240px; --sidebar-collapsed: 64px; --header-height: 60px; } /* ─── Light Mode ────────────────────────────────────────────── */ [data-theme="light"] { --bg-body: #F1F5F9; --bg-sidebar: rgba(255, 255, 255, 0.9); --bg-card: #FFFFFF; --bg-card-hover: #F8FAFC; --bg-input: #F8FAFC; --bg-input-focus: #FFFFFF; --bg-table-header: #F1F5F9; --bg-table-row-hover: rgba(99, 102, 241, 0.06); --border-default: #E2E8F0; --border-card: rgba(99, 102, 241, 0.1); --text-primary: #1E293B; --text-secondary: #64748B; --text-muted: #94A3B8; --accent: #4F46E5; --accent-hover: #6366F1; --accent-gradient: linear-gradient(135deg, #4F46E5 0%, #6366F1 100%); --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08); --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08); --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.1); --shadow-glow: 0 0 20px rgba(79, 70, 229, 0.08); } /* ─── Reset & Base ──────────────────────────────────────────── */ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } html { font-size: 14px; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } body { font-family: var(--font-family); background: var(--bg-body); color: var(--text-primary); min-height: 100vh; display: flex; overflow-x: hidden; transition: background var(--transition-base), color var(--transition-base); } a { color: var(--accent); text-decoration: none; } a:hover { color: var(--accent-hover); } /* ─── Scrollbar ─────────────────────────────────────────────── */ ::-webkit-scrollbar { width: 6px; height: 6px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: var(--border-default); border-radius: var(--radius-full); } ::-webkit-scrollbar-thumb:hover { background: var(--text-muted); } /* ═══════════════════════════════════════════════════════════════ SIDEBAR ═══════════════════════════════════════════════════════════════ */ .sidebar { position: fixed; left: 0; top: 0; bottom: 0; width: var(--sidebar-width); background: var(--bg-sidebar); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); border-right: 1px solid var(--border-card); display: flex; flex-direction: column; z-index: 100; transition: transform var(--transition-base); } .sidebar__header { padding: var(--space-lg); border-bottom: 1px solid var(--border-card); } .sidebar__logo { display: flex; align-items: center; gap: var(--space-sm); } .sidebar__logo-icon { font-size: 24px; } .sidebar__logo-text { font-weight: 700; font-size: var(--font-size-md); background: var(--accent-gradient); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } /* Nav */ .sidebar__nav { flex: 1; padding: var(--space-md) var(--space-sm); overflow-y: auto; } .nav-item { display: flex; align-items: center; gap: var(--space-md); padding: var(--space-md) var(--space-md); margin-bottom: 2px; border-radius: var(--radius-md); color: var(--text-secondary); font-size: var(--font-size-base); font-weight: 500; transition: all var(--transition-fast); position: relative; text-decoration: none; } .nav-item:hover { background: var(--accent-subtle); color: var(--text-primary); } .nav-item.active { background: var(--accent-subtle); color: var(--accent); } .nav-item.active::before { content: ''; position: absolute; left: 0; top: 8px; bottom: 8px; width: 3px; background: var(--accent-gradient); border-radius: 0 var(--radius-sm) var(--radius-sm) 0; } .nav-item.disabled { opacity: 0.35; pointer-events: none; } .nav-item--error::after { content: ''; position: absolute; right: 12px; top: 50%; transform: translateY(-50%); width: 8px; height: 8px; background: var(--error); border-radius: var(--radius-full); box-shadow: 0 0 6px var(--error-bg); } .nav-item__icon { font-size: 18px; width: 24px; text-align: center; } /* Footer */ .sidebar__footer { padding: var(--space-md) var(--space-md) var(--space-lg); border-top: 1px solid var(--border-card); } .sidebar__actions { display: flex; gap: var(--space-sm); margin-top: var(--space-sm); } .sidebar__version { text-align: center; margin-top: var(--space-md); font-size: var(--font-size-xs); color: var(--text-muted); } .sidebar__version-name { display: block; font-weight: 500; } .sidebar__version-num { display: block; opacity: 0.7; } /* ═══════════════════════════════════════════════════════════════ v1.8.0 — Two Faces (User vs Advanced/Debug view) ═══════════════════════════════════════════════════════════════ */ /* Advanced/Debug 보기 토글 (사이드바 footer). 보안 경계 아님 — 표시 스위치. */ .view-toggle { display: grid; grid-template-columns: auto 1fr; grid-template-rows: auto auto; column-gap: var(--space-sm); align-items: center; margin-top: var(--space-md); cursor: pointer; user-select: none; } .view-toggle__input { grid-row: 1 / span 2; margin: 0; cursor: pointer; } .view-toggle__label { font-size: var(--font-size-sm); color: var(--text-secondary, var(--text-muted)); font-weight: 500; } .view-toggle__hint { font-size: var(--font-size-xs); color: var(--text-muted); line-height: 1.3; } /* 모드 종속 표시 규칙 (User=기본 / body.advanced=고급). v1.8.1 (review): User 전용 콘텐츠는 전용 CSS 클래스가 아니라 JS isAdvanced() 분기로 게이팅한다(home.js networkApplyRows / pending.js 섹션). 따라서 advanced-only 만 CSS 로 구동한다(소비자 없던 user-only 규칙은 제거). JS 가 body.advanced 를 토글(app.js reflectAdvancedClass). */ .advanced-only { display: none !important; } body.advanced .advanced-only { display: revert !important; } /* ═══════════════════════════════════════════════════════════════ MOBILE HEADER ═══════════════════════════════════════════════════════════════ */ .mobile-header { display: none; position: fixed; top: 0; left: 0; right: 0; height: var(--header-height); background: var(--bg-sidebar); backdrop-filter: blur(20px); border-bottom: 1px solid var(--border-card); padding: 0 var(--space-md); align-items: center; justify-content: space-between; z-index: 90; } .mobile-header__menu { background: none; border: none; color: var(--text-primary); font-size: 24px; cursor: pointer; padding: var(--space-sm); } .mobile-header__title { font-weight: 600; font-size: var(--font-size-md); background: var(--accent-gradient); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } .sidebar-overlay { display: none; position: fixed; inset: 0; background: rgba(0, 0, 0, 0.5); z-index: 99; } .sidebar-overlay.show { display: block; } /* ═══════════════════════════════════════════════════════════════ MAIN CONTENT ═══════════════════════════════════════════════════════════════ */ .main { flex: 1; margin-left: var(--sidebar-width); padding: var(--space-xl) var(--space-2xl); min-height: 100vh; transition: margin-left var(--transition-base); } .page-container { max-width: 900px; margin: 0 auto; animation: fadeIn var(--transition-base) ease; } @keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } /* Page Header */ .page-header { margin-bottom: var(--space-xl); } .page-header__title { font-size: var(--font-size-xl); font-weight: 700; margin-bottom: var(--space-xs); } .page-header__desc { color: var(--text-secondary); font-size: var(--font-size-base); } /* ═══════════════════════════════════════════════════════════════ CARDS ═══════════════════════════════════════════════════════════════ */ .card { background: var(--bg-card); border: 1px solid var(--border-card); border-radius: var(--radius-lg); padding: var(--space-lg); margin-bottom: var(--space-lg); box-shadow: var(--shadow-sm); transition: box-shadow var(--transition-base), border-color var(--transition-base); } .card:hover { box-shadow: var(--shadow-md); border-color: rgba(99, 102, 241, 0.25); } .card__header { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--space-lg); padding-bottom: var(--space-md); border-bottom: 1px solid var(--border-card); } .card__title { font-size: var(--font-size-md); font-weight: 600; display: flex; align-items: center; gap: var(--space-sm); } .card__title-icon { font-size: 18px; } /* ═══════════════════════════════════════════════════════════════ FORM ELEMENTS ═══════════════════════════════════════════════════════════════ */ .form-group { margin-bottom: var(--space-md); } .form-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: var(--space-md); } .form-row--2col { grid-template-columns: 1fr 1fr; } .form-label { display: block; font-size: var(--font-size-sm); font-weight: 500; color: var(--text-secondary); margin-bottom: var(--space-xs); text-transform: uppercase; letter-spacing: 0.05em; } .form-input, .form-select { width: 100%; padding: 10px 14px; background: var(--bg-input); border: 1px solid var(--border-default); border-radius: var(--radius-md); color: var(--text-primary); font-family: var(--font-family); font-size: var(--font-size-base); transition: all var(--transition-fast); outline: none; } .form-input:focus, .form-select:focus { background: var(--bg-input-focus); border-color: var(--border-focus); box-shadow: 0 0 0 3px var(--accent-subtle); } .form-input:disabled, .form-select:disabled { opacity: 0.5; cursor: not-allowed; background: var(--bg-card); } .form-input.error { border-color: var(--border-error); box-shadow: 0 0 0 3px var(--error-bg); } .form-input--mono { font-family: var(--font-mono); font-size: 13px; } .form-hint { font-size: var(--font-size-xs); color: var(--text-muted); margin-top: var(--space-xs); } .form-error { font-size: var(--font-size-xs); color: var(--error); margin-top: var(--space-xs); } /* Inline field validation error (generated by validator.js) */ .field-error { display: block; font-size: var(--font-size-xs); color: var(--error); margin-top: var(--space-xs); font-weight: 500; letter-spacing: 0.02em; } /* IP Octet Input Group */ .ip-input-group { display: flex; align-items: center; gap: 2px; max-width: 280px; margin: 0 auto; } .ip-input-group .ip-octet { width: 56px; text-align: center; padding: 6px 4px; font-family: var(--font-mono); font-size: 13px; } .ip-input-group .ip-dot { color: var(--text-muted); font-weight: 700; font-size: 16px; user-select: none; flex-shrink: 0; } /* Checkbox Toggle (AI/DI ports) */ .checkbox-toggle { appearance: none; width: 36px; height: 20px; background: var(--border-default); border-radius: var(--radius-full); position: relative; cursor: pointer; transition: all var(--transition-fast); border: none; outline: none; flex-shrink: 0; } .checkbox-toggle::after { content: ''; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; background: white; border-radius: var(--radius-full); transition: transform var(--transition-fast); } .checkbox-toggle:checked { background: var(--accent); } .checkbox-toggle:checked::after { transform: translateX(16px); } /* v1.5.3.1: Sensor I/O port row + analog/digital input visual layout */ .form-row--ports { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 0.75rem; margin-top: 0.75rem; } .port-row { display: flex; align-items: center; justify-content: space-between; padding: 0.75rem 1rem; background: var(--bg-input, #0F172A); border: 1px solid var(--border-default); border-radius: 6px; } .port-row__label { display: flex; align-items: center; gap: 0.5rem; cursor: pointer; flex: 1; } .port-row__name { font-weight: 600; color: var(--text-primary); } .port-row__hint { font-size: 11px; color: var(--text-muted); margin-left: 0.25rem; } .card__desc { margin: 0.25rem 0 0.75rem; color: var(--text-secondary); font-size: 13px; } /* Error shake animation */ @keyframes shake { 0%, 100% { transform: translateX(0); } 20%, 60% { transform: translateX(-4px); } 40%, 80% { transform: translateX(4px); } } .form-input--error { border-color: var(--error) !important; box-shadow: 0 0 0 3px var(--error-bg) !important; animation: shake 0.4s ease; } /* Password wrapper */ .input-password { position: relative; } .input-password .form-input { padding-right: 40px; } .input-password__toggle { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); background: none; border: none; color: var(--text-muted); cursor: pointer; font-size: 16px; padding: 4px; } .input-password__toggle:hover { color: var(--text-primary); } /* Radio Group */ .radio-group { display: flex; gap: var(--space-lg); align-items: center; } .radio-label { display: flex; align-items: center; gap: var(--space-sm); cursor: pointer; font-size: var(--font-size-base); color: var(--text-primary); } .radio-label input[type="radio"] { appearance: none; width: 18px; height: 18px; border: 2px solid var(--border-default); border-radius: var(--radius-full); transition: all var(--transition-fast); position: relative; } .radio-label input[type="radio"]:checked { border-color: var(--accent); } .radio-label input[type="radio"]:checked::after { content: ''; position: absolute; top: 3px; left: 3px; width: 8px; height: 8px; background: var(--accent); border-radius: var(--radius-full); } /* Toggle */ .toggle-group { display: flex; align-items: center; justify-content: space-between; padding: var(--space-sm) 0; } .toggle-group__label { font-size: var(--font-size-base); color: var(--text-primary); } /* ═══════════════════════════════════════════════════════════════ BUTTONS ═══════════════════════════════════════════════════════════════ */ .btn { display: inline-flex; align-items: center; justify-content: center; gap: var(--space-sm); padding: 10px 20px; border: 1px solid transparent; border-radius: var(--radius-md); font-family: var(--font-family); font-size: var(--font-size-base); font-weight: 500; cursor: pointer; transition: all var(--transition-fast); outline: none; white-space: nowrap; } /* v1.11.2: explicit disabled styling — greys the button and suppresses hover lift / clicks (e.g. the upload-step NEXT button while a firmware upload is in flight). Don't rely on browser UA defaults for the grey-out. */ .btn:disabled, .btn[disabled] { opacity: 0.45; cursor: not-allowed; pointer-events: none; box-shadow: none; transform: none; } .btn--primary { background: var(--accent-gradient); color: white; border-color: transparent; box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3); } .btn--primary:hover { box-shadow: 0 4px 16px rgba(99, 102, 241, 0.4); transform: translateY(-1px); } .btn--primary:active { transform: translateY(0); } .btn--outline { background: transparent; color: var(--accent); border-color: var(--accent); } .btn--outline:hover { background: var(--accent-subtle); } .btn--danger { background: transparent; color: var(--error); border-color: var(--error); } .btn--danger:hover { background: var(--error-bg); } .btn--ghost { background: transparent; color: var(--text-secondary); border: none; padding: 6px 10px; } .btn--ghost:hover { color: var(--error); background: var(--error-bg); } .btn--sm { padding: 6px 14px; font-size: var(--font-size-sm); } .btn--full { width: 100%; } .btn--icon { width: 32px; height: 32px; padding: 0; border-radius: var(--radius-full); font-size: 16px; } /* ═══════════════════════════════════════════════════════════════ DATA TABLE (Protocol Mappings) ═══════════════════════════════════════════════════════════════ */ .data-table-wrapper { overflow-x: auto; border-radius: var(--radius-md); border: 1px solid var(--border-default); } .data-table { width: 100%; border-collapse: collapse; font-size: var(--font-size-sm); } .data-table th { background: var(--bg-table-header); padding: 10px 12px; text-align: left; font-weight: 600; font-size: var(--font-size-xs); text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-secondary); border-bottom: 1px solid var(--border-default); white-space: nowrap; } .data-table td { padding: 6px 8px; border-bottom: 1px solid rgba(51, 65, 85, 0.3); vertical-align: middle; } .data-table tr:hover td { background: var(--bg-table-row-hover); } .data-table tr:last-child td { border-bottom: none; } .data-table .form-input, .data-table .form-select { padding: 6px 10px; font-size: 13px; font-family: var(--font-mono); border-radius: var(--radius-sm); } .data-table .col-action { width: 40px; text-align: center; } /* ═══════════════════════════════════════════════════════════════ GRID LAYOUT ═══════════════════════════════════════════════════════════════ */ .grid-2col { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-lg); } .grid-4col { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-md); } /* ═══════════════════════════════════════════════════════════════ LOADING ═══════════════════════════════════════════════════════════════ */ .loading { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 60vh; gap: var(--space-md); color: var(--text-secondary); } .loading__spinner { width: 40px; height: 40px; border: 3px solid var(--border-default); border-top-color: var(--accent); border-radius: var(--radius-full); animation: spin 0.8s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } /* ═══════════════════════════════════════════════════════════════ TOAST ═══════════════════════════════════════════════════════════════ */ .toast-container { position: fixed; top: var(--space-lg); right: var(--space-lg); z-index: 1000; display: flex; flex-direction: column; gap: var(--space-sm); } .toast { display: flex; align-items: center; gap: var(--space-md); padding: var(--space-md) var(--space-lg); border-radius: var(--radius-md); background: var(--bg-card); border: 1px solid var(--border-card); box-shadow: var(--shadow-lg); backdrop-filter: blur(10px); min-width: 300px; animation: toastIn var(--transition-base) ease; font-size: var(--font-size-base); } .toast--success { border-left: 3px solid var(--success); } .toast--error { border-left: 3px solid var(--error); } .toast--warning { border-left: 3px solid var(--warning); } .toast--info { border-left: 3px solid var(--info); } .toast__icon { font-size: 18px; } .toast__message { flex: 1; } .toast__close { background: none; border: none; color: var(--text-muted); cursor: pointer; font-size: 16px; padding: 2px; } /* v1.7.1 Task 5: actionable toast CTA (Save → Apply needed). */ .toast__action { background: none; border: 1px solid currentColor; border-radius: var(--radius-sm, 4px); color: inherit; cursor: pointer; font: inherit; font-size: 12px; font-weight: 600; padding: 2px 8px; white-space: nowrap; } .toast__action:hover { text-decoration: underline; } @keyframes toastIn { from { opacity: 0; transform: translateX(40px); } to { opacity: 1; transform: translateX(0); } } .toast.removing { animation: toastOut var(--transition-fast) ease forwards; } @keyframes toastOut { to { opacity: 0; transform: translateX(40px); } } /* ═══════════════════════════════════════════════════════════════ THEME TOGGLE ═══════════════════════════════════════════════════════════════ */ .theme-toggle { position: fixed; bottom: var(--space-lg); right: var(--space-lg); width: 44px; height: 44px; border-radius: var(--radius-full); background: var(--bg-card); border: 1px solid var(--border-card); box-shadow: var(--shadow-md); cursor: pointer; font-size: 20px; display: flex; align-items: center; justify-content: center; z-index: 50; transition: all var(--transition-fast); } .theme-toggle:hover { box-shadow: var(--shadow-glow); transform: scale(1.1); } /* ═══════════════════════════════════════════════════════════════ INFO BANNER ═══════════════════════════════════════════════════════════════ */ .info-banner { display: flex; align-items: center; gap: var(--space-md); padding: var(--space-md) var(--space-lg); border-radius: var(--radius-md); background: var(--info-bg); border: 1px solid rgba(59, 130, 246, 0.2); margin-bottom: var(--space-lg); font-size: var(--font-size-sm); color: var(--info); } .info-banner--warning { background: var(--warning-bg); border-color: rgba(245, 158, 11, 0.2); color: var(--warning); } /* ═══════════════════════════════════════════════════════════════ RESPONSIVE ═══════════════════════════════════════════════════════════════ */ @media (max-width: 1024px) { .grid-2col { grid-template-columns: 1fr; } .grid-4col { grid-template-columns: repeat(2, 1fr); } .main { padding: var(--space-lg); } } @media (max-width: 768px) { .sidebar { transform: translateX(-100%); } .sidebar.open { transform: translateX(0); } .mobile-header { display: flex; } .main { margin-left: 0; padding: var(--space-md); padding-top: calc(var(--header-height) + var(--space-md)); } .grid-4col { grid-template-columns: 1fr 1fr; } .form-row { grid-template-columns: 1fr; } .form-row--2col { grid-template-columns: 1fr; } .card { padding: var(--space-md); } .toast-container { left: var(--space-md); right: var(--space-md); } .toast { min-width: auto; } .theme-toggle { bottom: var(--space-md); right: var(--space-md); } } /* ═══════════════════════════════════════════════════════════════ UTILITIES ═══════════════════════════════════════════════════════════════ */ .mt-sm { margin-top: var(--space-sm); } .mt-md { margin-top: var(--space-md); } .mt-lg { margin-top: var(--space-lg); } .mb-md { margin-bottom: var(--space-md); } .text-mono { font-family: var(--font-mono); } .text-muted { color: var(--text-muted); } .text-center { text-align: center; } .flex-between { display: flex; justify-content: space-between; align-items: center; } /* ═══════════════════════════════════════════════════════════════ INFO & COLLAPSIBLE CARDS ═══════════════════════════════════════════════════════════════ */ .card--info { background: var(--info-bg); border-color: rgba(59, 130, 246, 0.2); color: var(--info); } .card--info p { font-size: var(--font-size-sm); } .card--collapsible .card__header--clickable { cursor: pointer; user-select: none; } .card--collapsible .card__header--clickable:hover { opacity: 0.8; } .card__collapse-icon { font-size: var(--font-size-sm); color: var(--text-muted); transition: transform var(--transition-fast); } /* ═══════════════════════════════════════════════════════════════ LEGEND GRID ═══════════════════════════════════════════════════════════════ */ .legend-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: var(--space-sm) var(--space-lg); font-size: var(--font-size-sm); color: var(--text-secondary); } .legend-grid strong { font-family: var(--font-mono); color: var(--text-primary); } /* ═══════════════════════════════════════════════════════════════ SMALL SELECT (for tables) ═══════════════════════════════════════════════════════════════ */ .form-select--sm { padding: 6px 10px; font-size: 13px; font-family: var(--font-mono); border-radius: var(--radius-sm); } /* ═══════════════════════════════════════════════════════════════ FIELD VALIDATION ERROR (border/box-shadow + shake in base styles) ═══════════════════════════════════════════════════════════════ */ .field-error { display: block; font-size: var(--font-size-xs); color: var(--error); margin-top: var(--space-xs); } /* ═══════════════════════════════════════════════════════════════ LOG MANAGEMENT ═══════════════════════════════════════════════════════════════ */ /* Section divider between settings and file list */ .log-section-divider { border: none; border-top: 1px solid var(--border-default); margin: var(--space-lg) 0; } /* Action bar above log files table */ .log-action-bar { display: flex; align-items: center; justify-content: space-between; padding: var(--space-sm) var(--space-md); background: var(--bg-table-header); border: 1px solid var(--border-default); border-bottom: none; border-radius: var(--radius-md) var(--radius-md) 0 0; gap: var(--space-md); } .log-action-bar__left { display: flex; gap: var(--space-sm); align-items: center; } .log-action-bar__summary { font-size: var(--font-size-sm); white-space: nowrap; } /* Merge action bar with table below */ .log-action-bar + .data-table-wrapper { border-top-left-radius: 0; border-top-right-radius: 0; } /* Log type badges */ .log-type-badge { display: inline-block; padding: 2px 8px; border-radius: var(--radius-sm); font-size: var(--font-size-xs); font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; } .log-type-badge--log { background: var(--info-bg); color: var(--info); } .log-type-badge--archive { background: var(--success-bg); color: var(--success); } /* Placeholder states (loading, empty, error) */ .log-files-placeholder { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-sm); padding: var(--space-xl) var(--space-md); border: 1px dashed var(--border-default); border-radius: var(--radius-md); text-align: center; } .log-files-placeholder--error { border-color: var(--error); color: var(--error); } /* Log files table checkbox styling */ .log-files-table input[type="checkbox"] { width: 16px; height: 16px; cursor: pointer; accent-color: var(--accent); } /* ═══════════════════════════════════════════════════════════════ LOG STATS DASHBOARD ═══════════════════════════════════════════════════════════════ */ .log-stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-md); } .log-stat-card { background: var(--bg-input); border: 1px solid var(--border-default); border-radius: var(--radius-md); padding: var(--space-md); text-align: center; } .log-stat-card__value { font-size: var(--font-size-lg); font-weight: 700; color: var(--text-primary); margin-bottom: var(--space-xs); } .log-stat-card__label { font-size: var(--font-size-xs); color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.04em; } .log-stat-card--success { border-color: rgba(16, 185, 129, 0.3); } .log-stat-card--success .log-stat-card__value { color: var(--success); } .log-stat-card--info { border-color: rgba(59, 130, 246, 0.3); } .log-stat-card--info .log-stat-card__value { color: var(--info); } .log-stat-card--warning { border-color: rgba(245, 158, 11, 0.3); } .log-stat-card--warning .log-stat-card__value { color: var(--warning); } @media (max-width: 1024px) { .log-stats-grid { grid-template-columns: repeat(2, 1fr); } } @media (max-width: 768px) { .log-stats-grid { grid-template-columns: 1fr 1fr; } } /* ─── Landing Dashboard ─────────────────────────────────────── */ .sidebar__logo-img { height: 26px; width: auto; display: block; } .sidebar__logo-cap { /* v1.4.6.10 ②: --color-text-muted 미정의 → --text-muted (light/dark 양쪽 정의됨) */ font-size: 9px; letter-spacing: .16em; color: var(--text-muted, #7B8AA0); font-family: 'JetBrains Mono', monospace; margin-top: 3px; } [data-theme="dark"] .sidebar__logo-img { filter: brightness(0) invert(1); } /* v1.10: Dashboard V2 — 3-column grid. Column layout: [Network large | right compact col | right compact col] Row 0 (auto): dash-header-summary spans all 3 columns. Row 1 (large): dash-card--network spans rows 1-2 in column 1; dpworldapp / GNSS / Equipment stack in columns 2-3. Row 2 (bottom): Server Uplink / System / Current Issues span all 3 cols each. */ .page-container--dashboard { max-width: 1100px; } .dash-grid { display: grid; grid-template-columns: minmax(360px, 2fr) minmax(0, 1fr) minmax(0, 1fr); grid-template-rows: auto; gap: var(--space-md, 1rem); min-width: 0; margin-bottom: var(--space-md, 1rem); } @media (max-width: 720px) { .dash-grid { grid-template-columns: 1fr; } } /* v1.4.6.10 ②: --color-* 변수가 :root에 정의되지 않아 항상 하드코딩 다크 fallback이 적용, 라이트 모드에서도 카드만 검게 남던 버그. 실제 디자인 토큰(--bg-card / --text-primary / --text-secondary / --bg-input / --border-default)으로 통일하면 라이트/다크 양쪽 정상. */ .dash-card { min-width: 0; background: var(--bg-card, #1A2436); border: 1px solid var(--border-default, #29354A); border-radius: 8px; padding: 1rem 1.1rem; } .dash-card__title { font-size: .95rem; font-weight: 600; margin: 0 0 .8rem; color: var(--text-primary, #F1F5F9); } .dash-card__title--sr { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } .dash-card__sub { color: var(--text-muted, #64748B); font-size: 12px; margin: -.35rem 0 .85rem; } .dash-panel--wide { grid-column: 1 / -1; } /* v1.10: Health Overview / KPI / Operating Mode panel classes removed — renderHealthOverview + renderOperatingMode are not called by renderCards() in the V2 redesign. Dead CSS removed to keep the stylesheet lean. */ .dash-path__state, .dash-issue__detail { color: var(--text-muted, #64748B); font-size: 12px; } .dash-path { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: .7rem; } .dash-path__node { min-width: 0; border: 1px solid var(--border-default, #29354A); border-radius: 6px; padding: .75rem; background: var(--bg-input, #0E1726); } .dash-path__node--ok { border-color: rgba(16, 185, 129, .35); background: var(--success-bg); } .dash-path__node--warn { border-color: rgba(245, 158, 11, .4); background: var(--warning-bg); } .dash-path__node--error { border-color: rgba(239, 68, 68, .45); background: var(--error-bg); } .dash-path__state { display: flex; align-items: center; gap: .45rem; margin-bottom: .55rem; } .dash-path__label { color: var(--text-primary, #E2E8F0); font-size: 13px; font-weight: 600; } .dash-path__value { color: var(--text-secondary, #94A3B8); font-family: 'JetBrains Mono', monospace; font-size: 12px; margin-top: .2rem; overflow-wrap: anywhere; } .dash-issues__list { display: grid; gap: .4rem; } .dash-issues { padding-block: .8rem; } .dash-issues .dash-card__title { margin-bottom: .55rem; } .dash-issue { display: flex; align-items: flex-start; gap: .6rem; border: 1px solid var(--border-default, #29354A); border-radius: 6px; background: var(--bg-input, #0E1726); padding: .5rem .65rem; } .dash-issue--ok { border-color: rgba(16, 185, 129, .26); background: var(--bg-input, #0E1726); box-shadow: inset 3px 0 0 var(--success, #10B981); } .dash-issue--warn { border-color: rgba(245, 158, 11, .3); background: var(--bg-input, #0E1726); box-shadow: inset 3px 0 0 var(--warning, #F59E0B); } .dash-issue--error { border-color: rgba(239, 68, 68, .32); background: var(--bg-input, #0E1726); box-shadow: inset 3px 0 0 var(--error, #EF4444); } .dash-issue__body { min-width: 0; } .dash-issue__text { color: var(--text-primary, #E2E8F0); font-size: 12.5px; font-weight: 600; overflow-wrap: anywhere; } .dash-issue__detail { margin-top: .15rem; overflow-wrap: anywhere; } .dash-issue__action { color: var(--text-secondary, #94A3B8); font-size: 12px; margin-top: .25rem; overflow-wrap: anywhere; } /* v1.7.1 Task 2: issue action rendered as a clickable deep-link. */ button.dash-issue__action--link { display: inline-flex; align-items: center; gap: .25rem; width: 100%; text-align: left; background: none; border: none; color: var(--accent, #6366F1); cursor: pointer; font: inherit; font-size: 12px; } button.dash-issue__action--link:hover { text-decoration: underline; } /* v1.7.1 Task 1: stale / connection-lost banner (zombie dashboard guard). */ .dash-stale { display: flex; align-items: flex-start; gap: .6rem; border-color: var(--warning); background: var(--warning-bg, rgba(245, 158, 11, .12)); } .dash-stale__title { font-weight: 600; } .dash-stale__detail { margin-top: .15rem; color: var(--text-secondary, #94A3B8); font-size: 13px; overflow-wrap: anywhere; } /* When stale, dim the live cards so the operator does not trust them. */ .dash-grid[data-dash-stale="true"] .dash-card:not(.dash-stale) { opacity: .6; } /* ─── Dashboard V2 — Network card layout (Task 7) ─────────── */ /* dash-card--network: large left card spans rows 1..N of the right column (dpworldapp / GNSS / Equipment stack beside it). */ .dash-card--network { grid-column: 1; grid-row: span 2; } /* v1.10 V2 responsive: mobile collapses to single column. Port diagram collapses to vertical interface list (net-ports wraps to column). Order: header → Network → Current Issues → dpworldapp → GNSS → Equipment → Server Uplink → System (controlled by order cards appear in renderCards). */ @media (max-width: 720px) { .dash-panel--wide { grid-column: auto; } .dash-path { grid-template-columns: 1fr; } .dash-card--network { grid-column: auto; grid-row: auto; } .net-ports { grid-template-columns: 1fr; gap: .4rem; } .net-port { flex-direction: row; justify-content: flex-start; gap: .5rem; min-height: 52px; padding: .45rem .6rem; } .net-port__icon { width: 30px; height: 24px; } .net-port__signal { margin-left: auto; } .net-iface-row { grid-template-columns: 1fr; align-items: start; } .net-iface-row__item { justify-content: flex-start; flex-wrap: wrap; gap: .4rem; } .net-iface-row__value { white-space: normal; overflow: visible; overflow-wrap: anywhere; text-align: left; } } .dash-row { display: flex; align-items: baseline; gap: .5rem; margin-bottom: .5rem; font-size: 13px; } .dash-row__lbl { color: var(--text-secondary, #94A3B8); min-width: 96px; } .dash-row__val { color: var(--text-primary, #E2E8F0); font-family: 'JetBrains Mono', monospace; } .dash-row__val--dim { color: var(--text-muted, #64748B); font-size: 12px; } .dash-row--section { color: var(--text-muted, #888); font-size: 0.85em; border-top: 1px solid var(--border-default, #2a2a2a); padding-top: 6px; margin-top: 6px; display: block; } /* v1.5.5.2: per-interface 세부 row (IP/MAC 표시). header row 다음에 들여쓰기 + 작은 텍스트로 보조 정보 표시. ── eth0 Down IP 192.168.1.1 MAC 92:d4:78:cd:59:7b */ .dash-status-hero { display: flex; align-items: center; gap: .65rem; min-width: 0; margin-bottom: .65rem; padding: .55rem .65rem; border: 1px solid rgba(148, 163, 184, .16); border-radius: 6px; background: rgba(15, 23, 42, .18); } .dash-status-hero__icon { width: 34px; height: 34px; flex: 0 0 34px; display: inline-flex; align-items: center; justify-content: center; border-radius: 6px; color: var(--text-muted, #64748B); background: var(--bg-input, #0E1726); border: 1px solid var(--border-default, #29354A); } .dash-status-hero__copy { display: flex; flex-direction: column; min-width: 0; line-height: 1.25; } .dash-status-hero__eyebrow { color: var(--text-muted, #64748B); font-size: 11px; font-weight: 700; text-transform: uppercase; } .dash-status-hero__value { color: var(--text-primary, #E2E8F0); font-size: 14px; font-weight: 700; overflow-wrap: anywhere; } .dash-status-hero__detail { color: var(--text-secondary, #94A3B8); font-size: 12px; overflow-wrap: anywhere; } .dash-status-hero--ok { border-color: rgba(148, 163, 184, .18); box-shadow: inset 3px 0 0 var(--success, #10B981); } .dash-status-hero--warn { border-color: rgba(148, 163, 184, .18); box-shadow: inset 3px 0 0 var(--warning, #F59E0B); } .dash-status-hero--error { border-color: rgba(148, 163, 184, .18); box-shadow: inset 3px 0 0 var(--error, #EF4444); } .dash-status-hero__icon--ok { color: var(--success, #10B981); border-color: rgba(16, 185, 129, .32); } .dash-status-hero__icon--warn { color: var(--warning, #F59E0B); border-color: rgba(245, 158, 11, .35); } .dash-status-hero__icon--error { color: var(--error, #EF4444); border-color: rgba(239, 68, 68, .38); } .dash-row--header { margin-bottom: 2px; } .dash-row--sub { margin-bottom: 2px; padding-left: 20px; font-size: 12px; color: var(--text-muted); } .dash-row__lbl--sub { min-width: 36px; color: var(--text-muted); } /* v1.5.4.3 U11 fix: 9→10px (WCAG 1.4.11 non-text contrast 가시성 향상) + CSS 변수. 하드코딩 hex를 테마 토큰 --success/--warning/--error/--border-default로 교체. */ .dash-glyph { width: 10px; height: 10px; border-radius: 50%; display: inline-block; flex-shrink: 0; } .dash-glyph--ok { background: var(--success); box-shadow: 0 0 0 3px var(--success-bg); } .dash-glyph--warn { background: var(--warning); box-shadow: 0 0 0 3px var(--warning-bg); } .dash-glyph--error { background: var(--error); box-shadow: 0 0 0 3px var(--error-bg); } .dash-glyph--na { background: transparent; border: 1.5px solid var(--border-default); } /* v1.5.5.1: drift badge — configurator 측 view vs dpworldapp 측 실측값 mismatch 시 표시. inline 으로 .dash-row__val 안에 들어가는 작은 yellow warning icon + tooltip. click 안 함 — 단순 시각 신호 + title hover 로 양쪽 값 보임. */ .drift-warn { display: inline-flex; align-items: center; vertical-align: middle; margin-left: 0.4rem; color: var(--warning); cursor: help; } .drift-warn svg { stroke: var(--warning); } .dash-disk { margin-bottom: .7rem; } .dash-disk__head { display: flex; justify-content: space-between; font-size: 12px; margin-bottom: 3px; } .dash-disk__bar { /* v1.4.6.10 ②: --color-bg-secondary / --color-border 미정의 → 실제 토큰 사용 */ height: 11px; background: var(--bg-input, #0E1726); border: 1px solid var(--border-default, #2D3A52); border-radius: 4px; overflow: hidden; } .dash-disk__fill { height: 100%; transition: width .3s; background: var(--success); } /* v1.5.4.3 U4 fix: tier별 색상 — 인라인 hex 제거, 테마 토큰 적용. */ .dash-disk__fill--ok { background: var(--success); } .dash-disk__fill--warn { background: var(--warning); } .dash-disk__fill--error { background: var(--error); } /* ─── Dashboard V2 — Header summary bar (Task 7) ─────────── */ /* Spans full grid width; one-line equipment | iface | mode | uplink. Rendered by renderHeaderSummary() → .dash-header-summary element. */ .dash-header-summary { grid-column: 1 / -1; display: flex; align-items: center; flex-wrap: wrap; gap: .4rem .8rem; min-width: 0; background: var(--bg-input, #0E1726); border: 1px solid var(--border-default, #29354A); border-radius: 8px; padding: .6rem 1rem; font-size: 13px; } .dash-header__part { display: inline-flex; align-items: center; gap: .3rem; color: var(--text-secondary, #94A3B8); padding: .15rem .45rem; border-radius: 4px; background: var(--bg-card, #1A2436); border: 1px solid var(--border-default, #29354A); white-space: nowrap; } .dash-header__icon { display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; flex: 0 0 18px; color: var(--text-muted, #64748B); } .dash-header__icon--ok { color: var(--success, #10B981); } .dash-header__icon--warn { color: var(--warning, #F59E0B); } .dash-header__icon--error { color: var(--error, #EF4444); } .dash-header__label { color: var(--text-muted, #64748B); font-size: 11px; font-weight: 600; text-transform: uppercase; } .dash-header__value { min-width: 0; overflow-wrap: anywhere; } .dash-header__part--ok { border-color: rgba(16, 185, 129, .28); } .dash-header__part--warn { border-color: rgba(245, 158, 11, .34); } .dash-header__part--error { border-color: rgba(239, 68, 68, .38); } .dash-header__equipment { font-weight: 600; color: var(--text-primary, #E2E8F0); } .dash-header__iface { font-family: 'JetBrains Mono', monospace; } .dash-header__uplink { margin-left: auto; font-family: 'JetBrains Mono', monospace; font-size: 12px; } /* ─── Dashboard V2 — Overall status chip (v1.11.0) ────────── */ .dash-status-chip { display: inline-flex; align-items: center; gap: 6px; padding: 2px 9px; border-radius: var(--radius-sm); font-size: var(--font-size-sm); font-weight: 600; white-space: nowrap; } .dash-status-chip--ok { background: var(--success-bg); color: var(--success); } .dash-status-chip--warn { background: var(--warning-bg); color: var(--warning); } .dash-status-chip--error { background: var(--error-bg); color: var(--error); } /* ─── Dashboard V2 — Hardware Modules card (v1.11.0) ──────── */ .hw-module + .hw-module { margin-top: var(--space-md); padding-top: var(--space-md); border-top: 1px solid var(--border-default); } .hw-module__head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; } .hw-module__name { font-size: var(--font-size-sm); font-weight: 600; color: var(--text-primary); flex: 1; } .hw-module__state { font-size: 12px; color: var(--text-secondary); } /* ─── Dashboard V2 — In-card action row (v1.11.0) ─────────── */ .dash-card__action { margin-top: var(--space-md); padding-top: var(--space-md); border-top: 1px solid var(--border-default); } /* ─── Dashboard V2 — Port slots (Task 7) ──────────────────── */ /* Rendered by renderNetworkCard → net-port-panel / net-ports / net-port boxes. */ .net-port-panel { border: 1px solid var(--border-default, #29354A); border-radius: 8px; background: var(--bg-input, #0E1726); padding: .65rem; margin-bottom: .85rem; } .net-ports { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .5rem; } .net-port { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: .25rem; min-width: 0; min-height: 76px; padding: .55rem .45rem; border: 1.5px solid var(--border-default, #29354A); border-radius: 6px; background: var(--bg-card, #1A2436); text-align: center; } .net-port__icon { display: inline-flex; align-items: center; justify-content: center; width: 38px; height: 30px; color: var(--text-muted, #64748B); } .net-port__svg { width: 100%; height: 100%; } .net-port__signal { min-height: 14px; display: inline-flex; align-items: center; justify-content: center; } /* Tier colour modifiers — border + subtle bg tint */ .net-port--ok { border-color: rgba(16, 185, 129, .38); background: var(--bg-card, #1A2436); box-shadow: inset 0 -2px 0 rgba(16, 185, 129, .42); } .net-port--ok .net-port__icon { color: var(--success, #10B981); } .net-port--warn { border-color: rgba(245, 158, 11, .42); background: var(--bg-card, #1A2436); box-shadow: inset 0 -2px 0 rgba(245, 158, 11, .44); } .net-port--warn .net-port__icon { color: var(--warning, #F59E0B); } .net-port--error { border-color: rgba(239, 68, 68, .48); background: var(--bg-card, #1A2436); box-shadow: inset 0 -2px 0 rgba(239, 68, 68, .46); } .net-port--error .net-port__icon { color: var(--error, #EF4444); } .net-port--na { border-color: var(--border-default, #29354A); background: var(--bg-input, #0E1726); opacity: .65; } .net-port--wired .net-port__icon { transform: translateY(1px); } .net-port--wifi .net-port__icon { width: 34px; } .net-port__name { font-family: 'JetBrains Mono', monospace; font-size: 11px; font-weight: 600; color: var(--text-primary, #E2E8F0); letter-spacing: 0; } .net-port__label { font-size: 11px; color: var(--text-secondary, #94A3B8); line-height: 1.2; } .net-iface-list { display: grid; gap: .35rem; margin-bottom: .7rem; } .net-iface-row { display: grid; grid-template-columns: 28px minmax(78px, .7fr) minmax(0, 1.3fr); align-items: center; gap: .25rem .65rem; min-width: 0; border: 1px solid rgba(148, 163, 184, .14); border-radius: 6px; background: rgba(15, 23, 42, .18); padding: .42rem .55rem; font-size: 12px; } .net-iface-row__icon { width: 24px; height: 24px; display: inline-flex; align-items: center; justify-content: center; border-radius: 5px; color: var(--text-muted, #64748B); background: var(--bg-input, #0E1726); } .net-iface-row--advanced .net-iface-row__icon { grid-row: 1 / span 2; } .net-iface-row__icon--ok { color: var(--success, #10B981); } .net-iface-row__icon--warn { color: var(--warning, #F59E0B); } .net-iface-row__icon--error { color: var(--error, #EF4444); } .net-iface-row__state, .net-iface-row__item { display: inline-flex; align-items: center; min-width: 0; gap: .35rem; } .net-iface-row__state { grid-column: 2; } .net-iface-row--advanced .net-iface-row__state { grid-row: 1 / span 2; } .net-iface-row__item { grid-column: 3; justify-content: flex-start; } .net-iface-row__name { color: var(--text-primary, #E2E8F0); font-family: 'JetBrains Mono', monospace; font-weight: 600; } .net-iface-row__link, .net-iface-row__label { color: var(--text-muted, #64748B); font-size: 11px; } .net-iface-row__value { color: var(--text-secondary, #94A3B8); font-family: 'JetBrains Mono', monospace; overflow: visible; overflow-wrap: anywhere; text-overflow: clip; white-space: normal; } @media (max-width: 720px) { .page-container--dashboard { width: calc(100vw - 2rem); max-width: calc(100vw - 2rem); margin-inline: 0; } .dash-header-summary { align-items: stretch; } .dash-header__part { flex-wrap: wrap; max-width: 100%; min-width: 0; overflow-wrap: anywhere; white-space: normal; } .dash-header__uplink .dash-header__value { flex: 1 1 100%; } .dash-header__uplink { flex: 1 1 100%; margin-left: 0; } .net-ports { grid-template-columns: 1fr; gap: .4rem; } .net-port { flex-direction: row; justify-content: flex-start; gap: .5rem; min-height: 52px; padding: .45rem .6rem; } .net-port__icon { width: 30px; height: 24px; } .net-port__signal { margin-left: auto; } .net-iface-row { grid-template-columns: 28px 1fr; align-items: start; } .net-iface-row--user .net-iface-row__icon { grid-row: 1 / span 2; } .net-iface-row--advanced .net-iface-row__icon { grid-row: 1 / span 3; } .net-iface-row__state { grid-column: 2; grid-row: auto; } .net-iface-row__item { grid-column: 2; justify-content: flex-start; flex-wrap: wrap; gap: .4rem; } .net-iface-row__value { white-space: normal; overflow: visible; overflow-wrap: anywhere; text-align: left; } } /* ─── Dashboard V2 — RSSI bars (Task 7) ───────────────────── */ /* SVG wrapper + individual rect bars rendered by _rssiBarsSvg(). */ .rssi-bars { display: inline-block; vertical-align: middle; flex-shrink: 0; margin-left: .35rem; } .rssi-bar { /* base rect — individual tiers override fill */ } .rssi-bar--ok { fill: var(--success, #10B981); } .rssi-bar--warn { fill: var(--warning, #F59E0B); } .rssi-bar--error { fill: var(--error, #EF4444); } .rssi-bar--empty { fill: var(--border-default, #29354A); } /* ═══════════════════════════════════════════════════════════════ v1.4.6.10 — Accessibility (WCAG) ═══════════════════════════════════════════════════════════════ */ /* v1.4.6.10 ③: 키보드 포커스 표시 복원. 이전 .btn류에 outline:none만 적용되어 키보드 탭 이동 시 현재 위치 안 보이던 결함. :focus-visible은 키보드 trigger에만 작동 (마우스 click은 영향 없음 — 디자인 깨끗 유지). WCAG 2.1 SC 2.4.7 (Focus Visible). */ .btn:focus-visible, .nav-item:focus-visible, .theme-toggle:focus-visible, .input-password__toggle:focus-visible, .log-file-check:focus-visible, .log-action-bar__left button:focus-visible, .checkbox-toggle:focus-visible { /* v1.5.4.2 L2 — AI/DI toggle keyboard focus (WCAG 2.4.7) */ outline: 2px solid var(--accent, #6366F1); outline-offset: 2px; } /* v1.4.6.10 bonus: 모션 민감 사용자 보호 (WCAG 2.1 SC 2.3.3). prefers-reduced-motion 설정 시 모든 animation/transition을 사실상 즉시 완료 (0.01ms — 0이 아닌 이유는 일부 JS가 transitionend 이벤트에 의존). */ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } } /* ═══════════════════════════════════════════════════════════════ v1.5.0 Phase 1 — Lucide SVG icons + Sidebar nested + Accessibility ═══════════════════════════════════════════════════════════════ */ /* v1.5.0 P1: Lucide SVG inline icons. inline-block + currentColor stroke → theme 토글 시 자동 색 적용. vertical-align: middle → font-size 무관 robust 정렬. */ .icon { display: inline-block; vertical-align: middle; flex-shrink: 0; line-height: 0; } /* Pointer events: icon 안 button click을 button click과 같이 처리 */ button .icon, a .icon, label .icon { pointer-events: none; } /* v1.5.0 P1: Skip-to-content link (WCAG 2.4.1). 화면 밖 배치 + focus 시 노출. */ .skip-link { position: absolute; top: -40px; left: 0; background: var(--accent, #6366F1); color: var(--text-inverse, #fff); padding: 0.5rem 1rem; z-index: 9999; text-decoration: none; border-radius: 0 0 4px 0; } .skip-link:focus { top: 0; } /* v1.5.0 P1: Sidebar nested nav — group + leaf accordion */ .nav-group { display: block; margin: 0 0 0.25rem 0; } .nav-group__toggle { display: flex; align-items: center; gap: 0.5rem; width: 100%; padding: 0.5rem 0.75rem; background: transparent; color: var(--text-primary, #F1F5F9); border: none; cursor: pointer; font: inherit; font-weight: 500; text-align: left; border-radius: 4px; } .nav-group__toggle:hover { background: var(--bg-card-hover, #263548); } .nav-group__icon { display: inline-flex; } .nav-group__label { flex: 1; } .nav-group__chevron { display: inline-flex; transition: transform 0.2s ease; } .nav-group__toggle[aria-expanded="false"] .nav-group__chevron { transform: rotate(-90deg); } .nav-group__leaves { list-style: none; margin: 0; padding: 0 0 0 1.5rem; overflow: hidden; max-height: 1000px; transition: max-height 0.25s ease; } .nav-group__toggle[aria-expanded="false"] + .nav-group__leaves { max-height: 0; } .nav-item--leaf { padding-left: 0.5rem; } /* v1.5.0 P1: page-dirty dot. state.pageDirty matrix 값에 따라 표시. JS에서 .has-dirty 클래스 토글. */ .nav-item__dirty { display: none; width: 8px; height: 8px; border-radius: 50%; background: var(--warning, #F59E0B); box-shadow: 0 0 6px var(--warning-bg); margin-left: auto; } .nav-item.has-dirty .nav-item__dirty { display: inline-block; } .nav-item.nav-item--error .nav-item__dirty { display: none; } /* v1.5.0 P1: Disabled leaf (Firmware placeholder) */ .nav-item--disabled { opacity: 0.55; pointer-events: none; } .nav-item__badge { margin-left: auto; font-size: 9px; padding: 2px 6px; border-radius: 8px; background: var(--bg-input, #0F172A); color: var(--text-muted, #94A3B8); letter-spacing: 0.04em; } /* v1.5.0 P1: Sidebar overflow safeguard (workflow risk R8) */ .sidebar__nav { overflow-y: auto; max-height: calc(100vh - 120px); /* header + theme toggle 영역 제외 */ } /* v1.5.0 P1: prefers-reduced-motion — accordion + chevron 즉시 */ @media (prefers-reduced-motion: reduce) { .nav-group__chevron, .nav-group__leaves { transition: none !important; } } /* v1.5.0 P1: nav-guard modal */ .nav-guard__backdrop { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 9000; } .nav-guard__modal { background: var(--bg-card, #1E293B); color: var(--text-primary, #F1F5F9); padding: 1.5rem; border-radius: 8px; max-width: 480px; width: 90%; box-shadow: 0 10px 40px rgba(0,0,0,0.35); } .nav-guard__title { margin: 0 0 0.5rem; } .nav-guard__desc { margin: 0 0 1.25rem; } .nav-guard__actions { display: flex; gap: 0.5rem; justify-content: flex-end; }/* ═══════════════════════════════════════════════════════════════ FIRMWARE UPDATE PAGE — graphical status components Built on the existing design tokens (style.css). Merge block: append verbatim to src/static/css/style.css. ═══════════════════════════════════════════════════════════════ */ /* ═══════════════════════════════════════════════════════════════ COMPACT LAYOUT (scoped to .fw-page) — minimise scrolling ═══════════════════════════════════════════════════════════════ */ .fw-page .page-header { margin-bottom: var(--space-sm); } .fw-page .page-header__title { font-size: var(--font-size-lg); margin-bottom: 0; } .fw-page .page-header__desc { font-size: var(--font-size-xs); } .fw-page .card { padding: var(--space-md); margin-bottom: var(--space-sm); } .fw-page .card__header { margin-bottom: var(--space-sm); padding-bottom: var(--space-sm); } .fw-page .card__title { font-size: var(--font-size-base); } .fw-page .info-banner { padding: 6px var(--space-md); margin-bottom: var(--space-sm); font-size: var(--font-size-xs); } /* two-column row: upload + pre-flight sit side by side (pre-flash) */ .fw-grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-sm); align-items: start; margin-bottom: var(--space-sm); } .fw-grid2--wide-left { grid-template-columns: 1.4fr 1fr; } .fw-grid2 .card { margin-bottom: 0; } @media (max-width: 860px) { .fw-grid2, .fw-grid2--wide-left { grid-template-columns: 1fr; } } /* smaller stepper */ .fw-page .fw-stepper { margin: 2px 0 var(--space-sm); } .fw-page .fw-step { min-width: 58px; } .fw-page .fw-step__dot { width: 24px; height: 24px; font-size: 11px; } .fw-page .fw-step:not(:first-child)::before { top: 11px; } .fw-page .fw-step__label { font-size: 10px; } .fw-page .fw-step__detail { display: none; } /* drop sub-detail to save height */ /* smaller live bar + slot row */ .fw-page .fw-live { margin-top: 6px; } .fw-page .fw-live__row { margin-bottom: 4px; } .fw-page .fw-bar { height: 9px; } .fw-page .fw-live .flex-between { margin-top: 6px !important; } /* tighter drop zone */ .fw-page .fw-drop { padding: var(--space-md); } .fw-page .fw-drop__icon { font-size: 24px; margin-bottom: 4px; } .fw-page .fw-drop__main { font-size: var(--font-size-sm); } .fw-page .fw-drop__sub { font-size: 10px; margin-top: 2px; } /* tighter rows */ .fw-page .fw-comp { padding: 4px 0; } .fw-page .fw-check { padding: 5px 0; } .fw-page .fw-check__label { font-size: var(--font-size-xs); min-width: 140px; } .fw-page .fw-check__detail { font-size: 10px; } /* compact flash actions + overall */ .fw-page .fw-actions { margin-bottom: var(--space-sm); } .fw-page .fw-overall__pct { font-size: var(--font-size-md); } /* compact log */ .fw-page .fw-log { max-height: 140px; font-size: 11px; padding: var(--space-sm); } /* ─── Sticky progress header (always-visible) ───────────────── */ .fw-sticky-card { position: sticky; top: var(--space-md); z-index: 40; box-shadow: var(--shadow-md); } .fw-live { margin-top: var(--space-sm); } .fw-live__row { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 6px; gap: var(--space-md); } .fw-live__state { font-size: var(--font-size-sm); font-weight: 600; color: var(--text-primary); } .fw-live__state--active { color: var(--accent); } .fw-live__state--done { color: var(--success); } .fw-live__state--error { color: var(--error); } .fw-live__pct { font-family: var(--font-mono); font-size: var(--font-size-sm); color: var(--text-secondary); white-space: nowrap; } /* ─── Collapsible done sections ─────────────────────────────── */ .card--collapsed > :not(.card__header) { display: none; } .card--collapsed .card__header { margin-bottom: 0; padding-bottom: 0; border-bottom: none; } .card__summary { font-size: var(--font-size-xs); color: var(--success); font-weight: 500; margin-left: var(--space-sm); } .card__header--clickable { cursor: pointer; user-select: none; } .card__header--clickable:hover { opacity: 0.85; } @media (max-width: 768px) { .fw-sticky-card { top: calc(var(--header-height) + var(--space-sm)); } } /* ═══════════════════════════════════════════════════════════════ WIZARD (installer-style, one step at a time) ═══════════════════════════════════════════════════════════════ */ /* generic hide utility (not defined in the base style.css) */ .hidden { display: none !important; } .fw-page .fw-wizard-body { margin-bottom: var(--space-sm); } .fw-step-panel { min-height: 160px; } .fw-wizard-nav { display: flex; align-items: center; justify-content: space-between; gap: var(--space-md); padding: var(--space-sm) var(--space-md); background: var(--bg-card); border: 1px solid var(--border-card); border-radius: var(--radius-md); margin-bottom: var(--space-sm); position: sticky; bottom: var(--space-sm); box-shadow: var(--shadow-md); } .fw-wizard-nav .btn { min-width: 120px; } .fw-nav__hint { font-size: var(--font-size-sm); color: var(--text-muted); } .fw-nav__hint--error { color: var(--error); font-weight: 600; } @media (max-width: 768px) { .fw-wizard-nav .btn { min-width: 96px; } } /* ─── Accessibility helpers ─────────────────────────────────── */ /* v1.7.1 Task 4: generic screen-reader-only utility (mirrors .fw-sr-only). */ .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } .fw-sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } .fw-drop:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-color: var(--accent); } .fw-reboot__elapsed--overdue { color: var(--warning); font-weight: 600; } /* ─── Stepper (lifecycle phase tracker) ─────────────────────── */ .fw-stepper { display: flex; align-items: flex-start; gap: 0; margin: var(--space-sm) 0 var(--space-md); overflow-x: auto; padding-bottom: var(--space-xs); } .fw-step { display: flex; flex-direction: column; align-items: center; flex: 1 1 0; min-width: 76px; position: relative; } /* connector line between dots */ .fw-step:not(:first-child)::before { content: ''; position: absolute; top: 15px; right: 50%; width: 100%; height: 2px; background: var(--border-default); z-index: 0; } .fw-step--done:not(:first-child)::before, .fw-step--active:not(:first-child)::before { background: var(--accent); } .fw-step__dot { width: 32px; height: 32px; border-radius: var(--radius-full); display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 700; background: var(--bg-card); border: 2px solid var(--border-default); color: var(--text-muted); z-index: 1; transition: all var(--transition-base); } .fw-step__label { margin-top: var(--space-xs); font-size: var(--font-size-xs); color: var(--text-muted); text-align: center; letter-spacing: 0.02em; } .fw-step__detail { font-size: 10px; color: var(--text-muted); opacity: 0.7; text-align: center; margin-top: 1px; max-width: 92px; line-height: 1.2; } .fw-step--done .fw-step__dot { background: var(--success); border-color: var(--success); color: #fff; } .fw-step--done .fw-step__label { color: var(--text-secondary); } .fw-step--active .fw-step__dot { background: var(--accent); border-color: var(--accent); color: #fff; box-shadow: 0 0 0 4px var(--accent-subtle); animation: fwPulse 1.4s ease-in-out infinite; } .fw-step--active .fw-step__label { color: var(--accent); font-weight: 600; } .fw-step--error .fw-step__dot { background: var(--error); border-color: var(--error); color: #fff; } .fw-step--error .fw-step__label { color: var(--error); } @keyframes fwPulse { 0%, 100% { box-shadow: 0 0 0 4px var(--accent-subtle); } 50% { box-shadow: 0 0 0 7px var(--accent-subtle); } } /* ─── Drop zone ─────────────────────────────────────────────── */ .fw-drop { border: 2px dashed var(--border-default); border-radius: var(--radius-lg); padding: var(--space-xl) var(--space-lg); text-align: center; cursor: pointer; transition: all var(--transition-fast); background: var(--bg-input); } .fw-drop:hover { border-color: var(--accent); background: var(--bg-input-focus); } .fw-drop--over { border-color: var(--accent); background: var(--accent-subtle); transform: scale(1.005); } .fw-drop__icon { font-size: 32px; display: block; margin-bottom: var(--space-sm); } .fw-drop__main { font-size: var(--font-size-base); color: var(--text-primary); font-weight: 500; } .fw-drop__sub { font-size: var(--font-size-xs); color: var(--text-muted); margin-top: var(--space-xs); } .fw-drop__name { color: var(--accent); font-family: var(--font-mono); } /* ─── Overall progress ──────────────────────────────────────── */ .fw-overall { margin-bottom: var(--space-md); } .fw-overall__head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: var(--space-xs); } .fw-overall__pct { font-size: var(--font-size-lg); font-weight: 700; color: var(--text-primary); } .fw-overall__meta { font-size: var(--font-size-xs); color: var(--text-muted); font-family: var(--font-mono); } .fw-bar { height: 12px; background: var(--bg-input); border: 1px solid var(--border-default); border-radius: var(--radius-sm); overflow: hidden; } .fw-bar__fill { height: 100%; width: 0; background: var(--accent-gradient); transition: width var(--transition-base); } .fw-bar__fill--done { background: var(--success); } .fw-bar__fill--error { background: var(--error); } /* device-side commit/write in progress — animated stripes signal "still working" */ .fw-bar__fill--busy { background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.18) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.18) 50%, rgba(255, 255, 255, 0.18) 75%, transparent 75%); background-size: 28px 28px; animation: fwBusyStripes 0.8s linear infinite; } @keyframes fwBusyStripes { from { background-position: 0 0; } to { background-position: 28px 0; } } /* ─── Per-component rows ────────────────────────────────────── */ .fw-comp { display: grid; grid-template-columns: 28px 110px 1fr 64px; align-items: center; gap: var(--space-sm); padding: var(--space-sm) 0; border-bottom: 1px solid rgba(51, 65, 85, 0.3); } .fw-comp:last-child { border-bottom: none; } .fw-comp__sig { font-family: var(--font-mono); font-size: var(--font-size-xs); font-weight: 700; padding: 2px 6px; border-radius: var(--radius-sm); background: var(--accent-subtle); color: var(--accent); text-align: center; justify-self: start; } .fw-comp__name { display: flex; flex-direction: column; gap: 3px; min-width: 0; } .fw-comp__role { font-size: var(--font-size-sm); color: var(--text-primary); } .fw-comp__role small { color: var(--text-muted); font-family: var(--font-mono); } .fw-comp__track { height: 7px; background: var(--bg-input); border-radius: var(--radius-full); overflow: hidden; } .fw-comp__fill { height: 100%; width: 0; background: var(--accent); transition: width var(--transition-fast); } .fw-comp--done .fw-comp__fill { background: var(--success); width: 100%; } .fw-comp--active .fw-comp__fill { background: var(--accent); } .fw-comp--error .fw-comp__fill { background: var(--error); } .fw-comp__pct { font-family: var(--font-mono); font-size: var(--font-size-xs); color: var(--text-secondary); text-align: right; } .fw-comp__state-icon { font-size: 14px; text-align: center; } .fw-comp__hash { font-family: var(--font-mono); font-size: 10px; color: var(--text-muted); } /* ─── Inline component row (upload list — one file per row) ──── */ .fw-comp--inline { display: flex; align-items: center; gap: var(--space-sm); } .fw-comp--inline .fw-comp__state-icon { color: var(--success); } .fw-comp--inline .fw-comp__fname { font-family: var(--font-mono); font-size: var(--font-size-sm); color: var(--text-primary); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .fw-comp--inline .fw-comp__role2 { font-size: var(--font-size-xs); color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.04em; white-space: nowrap; } .fw-comp--inline .fw-comp__hash { font-size: 10px; white-space: nowrap; } .fw-comp--inline .fw-comp__size { font-family: var(--font-mono); font-size: var(--font-size-xs); color: var(--text-secondary); white-space: nowrap; min-width: 60px; text-align: right; } @media (max-width: 768px) { .fw-comp--inline .fw-comp__role2, .fw-comp--inline .fw-comp__hash { display: none; } } /* ─── Firmware build date line ──────────────────────────────── */ .fw-builddate { margin-top: var(--space-sm); font-size: var(--font-size-xs); color: var(--text-secondary); } .fw-builddate strong { color: var(--text-primary); font-family: var(--font-mono); } /* ─── Pre-flight checklist ──────────────────────────────────── */ .fw-check { display: flex; align-items: center; gap: var(--space-md); padding: var(--space-sm) 0; border-bottom: 1px solid rgba(51, 65, 85, 0.25); } .fw-check:last-child { border-bottom: none; } .fw-check__label { font-size: var(--font-size-sm); color: var(--text-primary); min-width: 200px; } .fw-check__detail { font-size: var(--font-size-xs); color: var(--text-muted); font-family: var(--font-mono); flex: 1; } .fw-check__opt { font-size: 10px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.06em; } /* ─── Slot before→after badge ───────────────────────────────── */ .fw-slot { display: inline-flex; align-items: center; gap: var(--space-xs); font-size: var(--font-size-sm); } .fw-slot__pill { font-family: var(--font-mono); font-weight: 700; padding: 2px 9px; border-radius: var(--radius-sm); background: var(--bg-input); border: 1px solid var(--border-default); color: var(--text-secondary); } .fw-slot__pill--next { background: var(--accent-subtle); border-color: var(--accent); color: var(--accent); } .fw-slot__arrow { color: var(--text-muted); } /* ─── Reboot watchdog ───────────────────────────────────────── */ .fw-reboot { display: flex; flex-direction: column; align-items: center; gap: var(--space-md); padding: var(--space-xl) var(--space-md); text-align: center; } .fw-reboot__title { font-size: var(--font-size-md); font-weight: 600; color: var(--warning); } .fw-reboot__sub { font-size: var(--font-size-sm); color: var(--text-secondary); max-width: 420px; } .fw-reboot__elapsed { font-family: var(--font-mono); font-size: var(--font-size-sm); color: var(--text-muted); } /* ─── Result banner (config preserved / restored) ───────────── */ .fw-result { display: flex; align-items: center; gap: var(--space-md); padding: var(--space-md) var(--space-lg); border-radius: var(--radius-md); font-size: var(--font-size-sm); } .fw-result--ok { background: var(--success-bg); color: var(--success); border: 1px solid rgba(16,185,129,0.25); } .fw-result--warn { background: var(--warning-bg); color: var(--warning); border: 1px solid rgba(245,158,11,0.25); } .fw-result__icon { font-size: 20px; } /* ─── Log console (collapsible) ─────────────────────────────── */ /* v1.5.4.3 U5 fix: 하드코딩 hex → 테마 토큰. 라이트 테마에서도 가독성 유지. 터미널 외관을 의도적으로 유지하려면 별도 클래스(.fw-log--terminal)로 분리할 것. */ .fw-log { background: var(--bg-input); color: var(--text-primary); font-family: var(--font-mono); font-size: var(--font-size-xs); padding: var(--space-md); border-radius: var(--radius-md); max-height: 220px; overflow-y: auto; white-space: pre-wrap; line-height: 1.6; border: 1px solid var(--border-default); } /* ─── Drop zone disabled (during flash) ─────────────────────── */ .fw-drop--disabled { opacity: 0.45; pointer-events: none; cursor: not-allowed; border-style: solid; } /* ─── Flash done / rebooted summary ─────────────────────────── */ .fw-done { display: flex; align-items: center; gap: var(--space-md); padding: var(--space-md) var(--space-sm); } .fw-done__icon { font-size: 28px; flex-shrink: 0; } .fw-done__title { font-size: var(--font-size-md); font-weight: 600; } .fw-done--ok .fw-done__title { color: var(--success); } .fw-done--reboot .fw-done__title { color: var(--accent); } .fw-done__sub { font-size: var(--font-size-sm); color: var(--text-secondary); margin-top: 2px; } /* ─── Power-loss danger banner (during flash/reboot) ────────── */ .fw-danger { display: flex; align-items: center; gap: var(--space-sm); padding: var(--space-sm) var(--space-md); border-radius: var(--radius-md); background: var(--error-bg); border: 1px solid rgba(239, 68, 68, 0.35); color: var(--error); font-size: var(--font-size-sm); font-weight: 600; margin-bottom: var(--space-sm); animation: fwDangerPulse 1.6s ease-in-out infinite; } @keyframes fwDangerPulse { 0%, 100% { border-color: rgba(239, 68, 68, 0.35); } 50% { border-color: rgba(239, 68, 68, 0.8); } } /* ─── Completion report ─────────────────────────────────────── */ .fw-report { font-size: var(--font-size-sm); } .fw-report__row { display: flex; justify-content: space-between; gap: var(--space-md); padding: 4px 0; border-bottom: 1px solid rgba(51, 65, 85, 0.22); } .fw-report__row:last-child { border-bottom: none; } .fw-report__row span:first-child { color: var(--text-secondary); white-space: nowrap; } .fw-report__row span:last-child { font-family: var(--font-mono); color: var(--text-primary); text-align: right; word-break: break-all; } /* ─── Confirm modal ─────────────────────────────────────────── */ .fw-modal-overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.55); display: flex; align-items: center; justify-content: center; z-index: 1100; padding: var(--space-md); backdrop-filter: blur(2px); animation: fadeIn var(--transition-fast) ease; } .fw-modal { background: var(--bg-card); border: 1px solid var(--border-card); border-radius: var(--radius-lg); box-shadow: var(--shadow-lg); max-width: 480px; width: 100%; padding: var(--space-lg); } .fw-modal__title { font-size: var(--font-size-md); font-weight: 600; margin-bottom: var(--space-sm); display: flex; align-items: center; gap: var(--space-sm); } .fw-modal__body { font-size: var(--font-size-sm); color: var(--text-secondary); } .fw-modal__list { margin: var(--space-md) 0; } .fw-modal__row { display: flex; justify-content: space-between; font-size: var(--font-size-sm); padding: 4px 0; border-bottom: 1px solid rgba(51, 65, 85, 0.25); } .fw-modal__row:last-child { border-bottom: none; } .fw-modal__row span:first-child { font-family: var(--font-mono); color: var(--text-primary); } .fw-modal__row span:last-child { font-family: var(--font-mono); color: var(--text-secondary); } .fw-modal__slot { margin: var(--space-md) 0; display: flex; align-items: center; gap: var(--space-sm); } .fw-modal__actions { display: flex; justify-content: flex-end; gap: var(--space-sm); margin-top: var(--space-lg); } /* ─── Action row ────────────────────────────────────────────── */ .fw-actions { display: flex; gap: var(--space-sm); flex-wrap: wrap; align-items: center; } .fw-actions__spacer { flex: 1; } /* ─── Responsive ────────────────────────────────────────────── */ @media (max-width: 768px) { .fw-comp { grid-template-columns: 24px 64px 1fr 48px; } .fw-check__label { min-width: 130px; } .fw-step { min-width: 64px; } } /* ─── v1.5.0 P4b: log.js 3 horizontal tabs ──────────────────── */ .log-tabs { display: flex; gap: 0.25rem; border-bottom: 1px solid var(--border-default); margin-bottom: 1rem; } .log-tab { padding: 0.5rem 1rem; background: transparent; border: none; color: var(--text-secondary); cursor: pointer; font: inherit; display: inline-flex; align-items: center; gap: 0.4rem; border-bottom: 2px solid transparent; transition: color 0.15s, border-color 0.15s; } .log-tab:hover { color: var(--text-primary); } .log-tab--active { color: var(--accent); border-bottom-color: var(--accent); } .log-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } .log-tab-panel[hidden] { display: none; } @media (prefers-reduced-motion: reduce) { .log-tab { transition: none; } } /* ─── v1.6.0 Network Apply badge ──────────────────────────── */ .net-badge { display: inline-block; padding: 1px 6px; border-radius: 4px; font-size: var(--font-size-xs); font-family: var(--font-mono); font-weight: 600; } .net-badge--ok { background: var(--success-bg); color: var(--success); } .net-badge--warn { background: var(--warning-bg); color: var(--warning); } .net-badge--fail { background: var(--error-bg); color: var(--error); } .net-badge--na { background: rgba(100,116,139,.15); color: var(--text-muted); } /* ═══════════════════════════════════════════════════════════════ v1.7.0 — Pending Changes (미저장/미적용 배지 + 패널) 기존 토큰(--warning/--info/--bg-card/--text-primary/--border) 재사용. ═══════════════════════════════════════════════════════════════ */ .pending-badge { display: flex; align-items: center; gap: 0.4rem; width: 100%; justify-content: center; margin-bottom: var(--space-sm, 8px); padding: 5px 8px; border: 1px solid var(--border, rgba(148,163,184,.3)); border-radius: 6px; background: var(--bg-card, #1E293B); color: var(--text-primary, #F1F5F9); font-size: var(--font-size-xs, 12px); cursor: pointer; } .pending-badge.hidden { display: none; } /* v1.7.1 Task 5: emphasis after a Save that leaves unapplied changes. */ .pending-badge--flash { animation: pendingFlash 0.6s ease-in-out 0s 3; border-color: var(--warning, #F59E0B); } @keyframes pendingFlash { 0%, 100% { box-shadow: 0 0 0 0 rgba(245, 158, 11, 0); } 50% { box-shadow: 0 0 0 3px rgba(245, 158, 11, .45); } } @media (prefers-reduced-motion: reduce) { .pending-badge--flash { animation: none; } } /* Mobile header badge: compact — no full-width stretch, no bottom margin */ .mobile-header .pending-badge { width: auto; margin-bottom: 0; } .pending-badge__count { display: inline-flex; align-items: center; gap: 0.3rem; white-space: nowrap; } .pending-badge__sep { opacity: 0.5; margin: 0 0.2rem; } .pending-badge__dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; } .pending-badge__count--unsaved .pending-badge__dot { background: var(--warning, #F59E0B); } .pending-badge__count--unapplied .pending-badge__dot { background: var(--info, #3B82F6); } /* Panel — nav-guard__backdrop/modal 스타일 흡수 + 자체 헤더/섹션 */ .pending-panel__backdrop { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 9100; } .pending-panel__modal { max-width: 640px; width: 92%; max-height: 82vh; overflow-y: auto; } .pending-panel__header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.75rem; } .pending-panel__title { margin: 0; font-size: var(--font-size-lg, 18px); } .pending-panel__close { background: none; border: none; cursor: pointer; font-size: 1.4rem; line-height: 1; color: var(--text-muted, #94A3B8); } .pending-section { margin-bottom: 1.25rem; } .pending-section__title { font-size: var(--font-size-base, 14px); margin: 0 0 0.5rem; } .pending-section--unapplied .pending-section__title { color: var(--info, #3B82F6); } .pending-section__actions { display: flex; gap: 0.5rem; justify-content: flex-end; margin-top: 0.5rem; } .pending-empty { color: var(--text-muted, #94A3B8); margin: 0; } .pending-block { border: 1px solid var(--border, rgba(148,163,184,.25)); border-radius: 6px; padding: 0.6rem 0.75rem; margin-bottom: 0.6rem; } .pending-block__head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.4rem; } .pending-block__title { margin: 0; font-size: var(--font-size-sm, 13px); } .pending-diff { width: 100%; border-collapse: collapse; font-size: var(--font-size-xs, 12px); } .pending-diff th, .pending-diff td { text-align: left; padding: 2px 6px; vertical-align: top; } .pending-diff th { color: var(--text-muted, #94A3B8); font-weight: 600; } .pending-diff__field { font-family: var(--font-mono); } .pending-diff__old { color: var(--text-muted, #94A3B8); text-decoration: line-through; } .pending-diff__arrow { color: var(--text-muted, #94A3B8); } .pending-diff__new { color: var(--warning, #F59E0B); font-weight: 600; } .pending-unapplied__desc { color: var(--text-muted, #94A3B8); font-size: var(--font-size-xs, 12px); margin: 0 0 0.4rem; } .pending-unapplied__list { margin: 0 0 0.5rem; padding-left: 1.1rem; } .pending-unapplied__item { font-family: var(--font-mono); font-size: var(--font-size-xs, 12px); }