/* ============================================================================= * app.css — 통합 모니터링 관리자 디자인 시스템 "Slate NOC Console" * * 단일 진실원(single source of truth). 7개 페이지가 이 파일 하나만 링크한다. * * * 설계 원칙: * - 모든 색/여백/반경/타이포는 :root CSS 변수. 페이지에 hex 리터럴 금지. * - 한글 시스템 폰트 우선(폐쇄망 — 웹폰트 로드 불가). 외부 리소스 0. * - 상태는 3중 인코딩: 색 + 도형(점/좌측레일/글리프) + 텍스트 라벨. * (색맹·흑백 인쇄에서도 정상/누수/경보/오프라인 구분됨 — WCAG 1.4.1) * - 명도 대비 WCAG AA(본문 4.5:1). 본문/캡션 하한 .8125rem, 라벨/배지/타임스탬프는 .75rem 허용. * - 라이트 우선 + prefers-color-scheme 다크 자동 + 인쇄는 강제 라이트(리포트/증적). * ============================================================================= */ /* ── 디자인 토큰 (라이트) ─────────────────────────────────────────────────── */ :root { /* 표면·잉크 */ --bg: #eef1f5; --surface: #ffffff; --surface-2: #f5f7fa; --raise: #fbfcfe; --ink: #1b2430; /* 본문 (on surface: ~13:1) */ --ink-2: #54606f; /* 보조 텍스트 (on white: ~5.2:1 AA) */ --ink-3: #6b7687; /* 캡션/축라벨 (on white: ~4.5:1 AA, 최소선) */ --line: #dce1e8; --line-2: #eaeef3; /* 브랜드 (절제된 스틸블루 — 저채도라 저온 시맨틱/링크와 분리) */ --primary: #2b5bab; --primary-600: #234f8f; --on-primary: #ffffff; --primary-tint: #e9eff8; /* 헤더(앱바) — 라이트: 브랜드 primary 배경 + 흰 잉크 */ --header-bg: var(--primary); --header-ink: var(--on-primary); /* 상태 시맨틱 — rail/dot 용(진한 색) + ink(텍스트) + bg(옅은 배경) */ --normal: #1f8a4c; --normal-ink: #136c39; --normal-bg: #e6f4ec; --leak: #d12b2b; --leak-ink: #b11f1f; --leak-bg: #fbe8e8; --warn: #b26a00; --warn-ink: #935600; --warn-bg: #f8edda; --offline:#6b7683; --offline-ink:#4b5560; --offline-bg:#ebeef2; /* 지표 칩 — 서로/브랜드와 뚜렷이 구분 */ --hot-ink: var(--leak-ink); --hot-bg: var(--leak-bg); --cold-ink: #0b5a6e; --cold-bg: #e1f1f4; /* 티일(저온) — 스틸블루와 다름 */ --humid-ink: var(--warn-ink); --humid-bg: var(--warn-bg); --dry-ink: #4a4fa0; --dry-bg: #ecedf8; /* 절제된 인디고(저습, 희소) */ /* 차트 라인 */ --chart-temp: #c2410c; /* 온도=따뜻 */ --chart-hum: #0e7490; /* 습도=차가움(티일) */ --chart-grid: var(--line); /* 타이포 — 한글 시스템 폰트 우선(오프라인 일관 렌더). 웹폰트 미로드. */ --font: "Malgun Gothic","Apple SD Gothic Neo",system-ui,-apple-system,"Segoe UI",Roboto,"Noto Sans KR",sans-serif; --mono: ui-monospace,SFMono-Regular,Consolas,"D2Coding",monospace; /* 4/8 간격 리듬 */ --sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px; --sp-5: 20px; --sp-6: 24px; --sp-8: 32px; /* 반경 스케일 */ --r-sm: 4px; --r: 8px; --r-lg: 12px; --r-pill: 999px; /* 그림자 (절제) */ --shadow: 0 1px 3px rgba(16,24,40,.08), 0 1px 2px rgba(16,24,40,.04); --shadow-lg: 0 6px 20px rgba(16,24,40,.12); --ring: 0 0 0 3px rgba(43,91,171,.28); } /* ── 다크 (관제 야간/개인 선호) — 같은 토큰을 명도만 조정 ──────────────────── */ @media (prefers-color-scheme: dark) { :root { --bg: #10151c; --surface: #1a2129; --surface-2: #212a34; --raise: #232d38; --ink: #e6ecf3; --ink-2: #a3adba; --ink-3: #8a95a2; --line: #2b3540; --line-2: #232c36; --primary: #4c8dff; --primary-600: #6ba1ff; --on-primary: #0d1219; --primary-tint: #182740; /* 헤더 — 다크: surface-2 배경 + 본문 잉크(하단 헤어라인은 header 규칙) */ --header-bg: var(--surface-2); --header-ink: var(--ink); /* 다크 필드에서 4.5:1+ 유지 위해 명도 상향 */ --normal: #35c775; --normal-ink: #7fe0a8; --normal-bg: #12281d; --leak: #ff5a52; --leak-ink: #ff958e; --leak-bg: #301617; --warn: #ffb020; --warn-ink: #ffcb6b; --warn-bg: #2d2410; --offline:#8a94a1; --offline-ink:#aab3bf; --offline-bg:#20272f; --cold-ink: #5fd0e6; --cold-bg: #10262c; --dry-ink: #a7abf0; --dry-bg: #1c1e33; --hot-ink: var(--leak-ink); --hot-bg: var(--leak-bg); --humid-ink: var(--warn-ink); --humid-bg: var(--warn-bg); --chart-temp: #ff8a5c; --chart-hum: #43c4dc; --shadow: 0 1px 3px rgba(0,0,0,.5); --shadow-lg: 0 8px 24px rgba(0,0,0,.55); --ring: 0 0 0 3px rgba(76,141,255,.4); } /* 다크 헤더: primary 배너가 아닌 surface-2이므로 본문과 분리할 1px 하단 헤어라인 */ header { border-bottom: 1px solid var(--line); } } /* ── 리셋·베이스 ──────────────────────────────────────────────────────────── */ * { box-sizing: border-box; margin: 0; padding: 0; } html { -webkit-text-size-adjust: 100%; } body { font-family: var(--font); background: var(--bg); color: var(--ink); line-height: 1.55; font-size: 1rem; -webkit-font-smoothing: antialiased; } /* 수치는 자릿수 고정폭(표·지표 정렬) */ .val, .metric-chip, .report-stat strong, td.num, .num, .status-clock { font-variant-numeric: tabular-nums; } a { color: var(--primary); } :focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; border-radius: var(--r-sm); } img { max-width: 100%; } /* 타입 스케일 (4단) */ h1 { font-size: 1.375rem; font-weight: 700; line-height: 1.3; } h2 { font-size: 1.125rem; font-weight: 700; color: var(--ink); margin: var(--sp-6) 0 var(--sp-3); } .panel h2, .status-hero h2 { margin-top: 0; } label, .label { font-size: .875rem; font-weight: 600; } .caption, small { font-size: .8125rem; color: var(--ink-3); } /* ── 헤더(앱바) ───────────────────────────────────────────────────────────── */ header { background: var(--header-bg); color: var(--header-ink); padding: var(--sp-4) var(--sp-6); display: flex; align-items: center; justify-content: space-between; gap: var(--sp-4); } header h1 { font-size: 1.3rem; color: var(--header-ink); } .top-actions { display: flex; gap: var(--sp-2); align-items: center; flex-wrap: wrap; } .top-actions a { color: var(--header-ink); font-size: .8125rem; font-weight: 600; text-decoration: none; background: rgba(255,255,255,.14); border: 1px solid rgba(255,255,255,.22); padding: 6px 14px; border-radius: var(--r-sm); } .top-actions a:hover { background: rgba(255,255,255,.24); } /* ── 레이아웃 ─────────────────────────────────────────────────────────────── */ .container { max-width: 1320px; margin: var(--sp-6) auto; padding: 0 var(--sp-4); } .ops-grid { display: grid; grid-template-columns: minmax(0,1.5fr) minmax(280px,.9fr); gap: var(--sp-4); align-items: start; margin-bottom: var(--sp-6); } .cards { display: flex; gap: var(--sp-4); flex-wrap: wrap; margin-bottom: var(--sp-6); } .sensor-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px,1fr)); gap: var(--sp-4); margin-bottom: var(--sp-6); } /* ── 패널·카드 ───────────────────────────────────────────────────────────── */ .panel, .card, .chart-panel, .leak-status-card, .empty-note { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r); } .panel { padding: var(--sp-5); } .chart-panel { padding: var(--sp-5); margin-bottom: var(--sp-6); } .card { padding: var(--sp-5) var(--sp-6); flex: 1; min-width: 180px; } .card span { color: var(--ink-2); font-size: .8125rem; } .card .val { font-size: 2rem; font-weight: 700; margin-top: var(--sp-1); color: var(--ink); } /* ── 시스템 상태 배너(hero) ──────────────────────────────────────────────── */ .status-hero { border-radius: var(--r); padding: var(--sp-5); margin-bottom: var(--sp-5); display: grid; grid-template-columns: minmax(0,1fr) auto; gap: var(--sp-4); align-items: center; color: var(--ink); background: var(--surface); border: 1px solid var(--line); border-left: 6px solid var(--offline); } .status-hero h2 { font-size: 1.25rem; display: flex; align-items: center; gap: var(--sp-2); } .status-hero h2::before { content: ""; width: .7em; height: .7em; border-radius: 50%; background: var(--offline); flex: none; } .status-hero p { color: var(--ink-2); font-size: .9rem; margin-top: 2px; } .status-hero.normal { border-left-color: var(--normal); } .status-hero.warning { border-left-color: var(--warn); } .status-hero.danger { border-left-color: var(--leak); background: var(--leak-bg); } .status-hero.normal h2::before { background: var(--normal); } .status-hero.warning h2::before { background: var(--warn); } .status-hero.danger h2::before { background: var(--leak); } .status-clock { text-align: right; font-size: .8125rem; color: var(--ink-3); white-space: nowrap; } /* ── 센서 상태 카드 (좌측레일 + 점 + 배지 = 3중) ─────────────────────────── */ .sensor-card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r); border-left: 1px solid var(--line); padding: var(--sp-5); } .sensor-card.alert { border-left: 6px solid var(--leak); background: var(--leak-bg); } .sensor-card.offline { border-left: 6px solid var(--offline); background: var(--offline-bg); } /* 배지/알약 공통 — 앞에 점 글리프(도형 중복), 텍스트 라벨 필수 */ .badge, .pill { display: inline-flex; align-items: center; gap: .4em; border-radius: var(--r-pill); padding: 3px 10px; font-size: .75rem; font-weight: 700; line-height: 1.4; background: var(--normal-bg); color: var(--normal-ink); } .badge::before, .pill::before { content: ""; width: .5em; height: .5em; border-radius: 50%; background: currentColor; flex: none; } .badge.ok, .pill.ok { background: var(--normal-bg); color: var(--normal-ink); } .badge.alert,.pill.alert { background: var(--leak-bg); color: var(--leak-ink); } .badge.offline,.pill.offline{ background: var(--offline-bg); color: var(--offline-ink); } .pill.high_temp { background: var(--hot-bg); color: var(--hot-ink); } .pill.low_temp { background: var(--cold-bg); color: var(--cold-ink); } .pill.high_humidity{ background: var(--humid-bg); color: var(--humid-ink); } .pill.low_humidity { background: var(--dry-bg); color: var(--dry-ink); } .pill.leak, .pill.leak_alert { background: var(--leak-bg); color: var(--leak-ink); } .pill.leak_recovery { background: var(--normal-bg); color: var(--normal-ink); } /* 지표 칩 */ .metric-line { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-3); } .metric-chip { display: inline-flex; align-items: center; border-radius: var(--r-pill); background: var(--surface-2); color: var(--ink-2); padding: 4px 10px; font-size: .78rem; font-weight: 700; } .metric-chip.hot { background: var(--hot-bg); color: var(--hot-ink); } .metric-chip.cold { background: var(--cold-bg); color: var(--cold-ink); } .metric-chip.humid { background: var(--humid-bg); color: var(--humid-ink); } .metric-chip.dry { background: var(--dry-bg); color: var(--dry-ink); } .metric-time { color: var(--ink-3); font-size: .75rem; margin-top: var(--sp-2); } /* ── 운영 자가진단 ───────────────────────────────────────────────────────── */ .diag-list { display: grid; gap: var(--sp-2); } .diag-item { border: 1px solid var(--line); border-radius: var(--r); padding: 9px 10px; display: grid; grid-template-columns: auto minmax(0,1fr); gap: var(--sp-2); align-items: start; } .diag-dot { width: 10px; height: 10px; border-radius: 50%; margin-top: 5px; background: var(--normal); flex: none; } .diag-item.warn .diag-dot { background: var(--warn); } .diag-item.critical .diag-dot { background: var(--leak); } .diag-item strong { display: block; font-size: .8125rem; } .diag-item span { color: var(--ink-2); font-size: .78rem; line-height: 1.4; } /* ── 30일 운영 요약 ─────────────────────────────────────────────────────── */ .report-grid { display: grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: var(--sp-3); } .report-stat { border: 1px solid var(--line); border-radius: var(--r); padding: var(--sp-3); background: var(--surface); } .report-stat span { display: block; color: var(--ink-2); font-size: .75rem; margin-bottom: 4px; } .report-stat strong { font-size: 1.1rem; } /* ── 표 ───────────────────────────────────────────────────────────────────── */ .table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; margin-bottom: var(--sp-6); border-radius: var(--r); } table { width: 100%; border-collapse: collapse; background: var(--surface); border: 1px solid var(--line); border-radius: var(--r); overflow: hidden; } .table-wrap table { margin-bottom: 0; } th { background: var(--surface-2); font-size: .8125rem; font-weight: 700; color: var(--ink-2); padding: 10px 14px; text-align: left; border-bottom: 1px solid var(--line); white-space: nowrap; } td { padding: 9px 14px; border-bottom: 1px solid var(--line-2); font-size: .875rem; color: var(--ink); } tbody tr:hover td { background: var(--surface-2); } .alert-row td { background: var(--leak-bg); } .alert-row:hover td { background: var(--leak-bg); } /* 상태 텍스트(인라인) — 색 + 앞 글리프 */ .ok-color, .err-color, .warn-color { font-weight: 700; } .ok-color { color: var(--normal-ink); } .err-color { color: var(--leak-ink); } .warn-color{ color: var(--warn-ink); } .ok-color::before { content: "\2713\00a0"; } /* ✓ */ .err-color::before { content: "\2715\00a0"; } /* ✕ */ .warn-color::before{ content: "\26a0\00a0"; } /* ⚠ */ /* 점검 통과/실패(설치 마법사·보안 증적) — 색만 아님, 글리프+텍스트 */ .check.ok, .check-ok { color: var(--normal-ink); font-weight: 700; } .check.fail, .check-fail { color: var(--leak-ink); font-weight: 700; } .check.ok::before, .check-ok::before { content: "\2713\00a0"; } .check.fail::before, .check-fail::before { content: "\2715\00a0"; } /* ── 차트 ─────────────────────────────────────────────────────────────────── */ .chart-legend { display: flex; gap: var(--sp-4); flex-wrap: wrap; margin: var(--sp-2) 0 var(--sp-3); font-size: .8125rem; color: var(--ink-2); } .chart-legend span { display: inline-flex; align-items: center; gap: 6px; } .legend-swatch { width: 14px; height: 4px; border-radius: 2px; display: inline-block; } .legend-swatch.temp { background: var(--chart-temp); } .legend-swatch.hum { background: var(--chart-hum); } .chart-svg-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; } .chart-empty, .empty-note { color: var(--ink-3); padding: var(--sp-6); text-align: center; } .empty-note { padding: var(--sp-5); } /* ── 누수 패널 ───────────────────────────────────────────────────────────── */ .leak-status-card { border-left: 1px solid var(--line); padding: var(--sp-5); } .leak-status-card.leak { border-left: 6px solid var(--leak); background: var(--leak-bg); } .leak-status-card.offline { border-left: 6px solid var(--offline); background: var(--offline-bg); } .leak-thumbs { display: flex; flex-wrap: wrap; gap: var(--sp-2); } .leak-thumbs a { display: inline-block; line-height: 0; } .leak-thumbs img { width: 64px; height: 64px; object-fit: cover; border-radius: var(--r-sm); border: 1px solid var(--line); background: var(--surface-2); } /* ── 모바일 요약 타일/링크 ──────────────────────────────────────────────── */ .mobile-quick { display: none; } .quick-grid { display: grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: var(--sp-2); margin-bottom: var(--sp-3); } .quick-tile { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r); padding: 10px; text-decoration: none; color: var(--ink); min-height: 70px; } .quick-tile span { display: block; color: var(--ink-2); font-size: .75rem; margin-bottom: 5px; } .quick-tile strong { font-size: 1.25rem; line-height: 1; } .quick-tile.danger { border-color: var(--leak); background: var(--leak-bg); } .quick-tile.warn { border-color: var(--warn); background: var(--warn-bg); } .quick-tile.ok { border-color: var(--normal); background: var(--normal-bg); } .quick-links { display: flex; gap: var(--sp-2); overflow-x: auto; -webkit-overflow-scrolling: touch; padding-bottom: 6px; margin-bottom: var(--sp-4); } .quick-links a { flex: 0 0 auto; color: var(--primary-600); background: var(--primary-tint); border: 1px solid var(--line); text-decoration: none; border-radius: var(--r-pill); padding: 8px 12px; font-size: .8125rem; font-weight: 700; } /* ── 모바일 접힘(disclosure) ────────────────────────────────────────────── */ .mobile-detail { margin-bottom: var(--sp-6); } .mobile-detail summary { display: none; } .refresh { float: right; font-size: .8125rem; color: var(--ink-2); } /* ── 폼 / 로그인 / 카드형 페이지 ────────────────────────────────────────── */ .login-box { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg); padding: var(--sp-8) var(--sp-6); box-shadow: var(--shadow-lg); width: 100%; max-width: 400px; } .login-box h1 { color: var(--primary); margin-bottom: var(--sp-2); text-align: center; } .login-box p.sub { font-size: .875rem; color: var(--ink-2); text-align: center; margin-bottom: var(--sp-6); } .form-group { margin-bottom: var(--sp-4); } .form-group label { display: block; color: var(--ink-2); margin-bottom: 6px; } .form-group input, input[type=text], input[type=password], textarea { width: 100%; padding: 10px 14px; border: 1px solid var(--line); border-radius: var(--r); font-size: .95rem; font-family: inherit; color: var(--ink); background: var(--surface); outline: none; } .form-group input:focus, input:focus, textarea:focus { border-color: var(--primary); box-shadow: var(--ring); } /* 강제 색상(고대비/Windows High Contrast)에서는 border-color·box-shadow가 무시되어 입력 포커스가 사라진다 → 시스템 아웃라인으로 포커스 링 복원 */ @media (forced-colors: active) { input:focus, textarea:focus { outline: 2px solid; outline-offset: 1px; } } .btn { display: inline-block; width: 100%; padding: 12px; background: var(--primary); color: var(--on-primary); border: none; border-radius: var(--r); font-size: 1rem; font-weight: 700; cursor: pointer; text-align: center; } .btn:hover { background: var(--primary-600); } .btn:disabled { background: var(--offline); cursor: not-allowed; } .btn.secondary { background: var(--surface); color: var(--primary); border: 1px solid var(--line); } /* 배너 — 성공 배너는 .banner-ok. (생 .ok 는 .badge.ok/.pill.ok/.quick-tile.ok 와 특이도 동률로 충돌해 정상 배지를 사각형으로 깨뜨리므로 네임스페이스한다.) */ .error, .expired, .banner-ok { padding: 10px 14px; border-radius: var(--r); font-size: .875rem; margin-bottom: var(--sp-4); border: 1px solid var(--line); border-left-width: 4px; } .error { background: var(--leak-bg); color: var(--leak-ink); border-left-color: var(--leak); } .expired { background: var(--warn-bg); color: var(--warn-ink); border-left-color: var(--warn); } .banner-ok { background: var(--normal-bg); color: var(--normal-ink); border-left-color: var(--normal); } /* MFA 시크릿/코드 블록 등 */ .secret, code, .mono { font-family: var(--mono); background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r-sm); padding: 2px 6px; word-break: break-all; color: var(--ink); } .secret { display: block; padding: 12px; font-size: 1.05rem; letter-spacing: .06em; } /* ── 반응형 ──────────────────────────────────────────────────────────────── */ @media (max-width: 768px) { header { flex-direction: column; align-items: flex-start; } .top-actions { margin-top: var(--sp-3); overflow-x: auto; flex-wrap: nowrap; -webkit-overflow-scrolling: touch; } .top-actions a { flex: 0 0 auto; } .status-hero { grid-template-columns: 1fr; } .status-clock { text-align: left; } .mobile-quick { display: block; } .ops-grid { grid-template-columns: 1fr; } .report-grid { grid-template-columns: repeat(2, minmax(0,1fr)); } .cards { flex-direction: column; } .card, .sensor-card { min-width: 0; } header h1 { font-size: 1.1rem; } .card .val { font-size: 1.6rem; } th, td { padding: 8px 10px; } td { font-size: .8125rem; } .mobile-detail { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r); overflow: hidden; } .mobile-detail summary { display: flex; justify-content: space-between; align-items: center; gap: 10px; padding: 13px 14px; color: var(--ink); font-weight: 700; cursor: pointer; list-style: none; } .mobile-detail summary::-webkit-details-marker { display: none; } .mobile-detail summary span { color: var(--ink-2); font-size: .8125rem; font-weight: 600; } .mobile-detail .table-wrap { margin-bottom: 0; border-top: 1px solid var(--line); border-radius: 0; } } @media (max-width: 480px) { .container { padding: 0 var(--sp-3); } header { padding: var(--sp-3) var(--sp-4); } .quick-grid { grid-template-columns: repeat(2, minmax(0,1fr)); } .sensor-grid { grid-template-columns: 1fr; } .card { padding: var(--sp-4); } .col-hide-xs { display: none; } .report-grid { grid-template-columns: 1fr; } .login-box { padding: var(--sp-6) var(--sp-5); margin: 0 var(--sp-3); } /* 캡션/본문 하한 유지 — 이 밑으로 축소 금지 */ th, td { font-size: .8125rem; } } /* ── 인쇄 — 강제 라이트(월간 리포트·보안 증적은 종이 가독) ────────────────── */ @media print { :root { --bg: #fff; --surface: #fff; --surface-2: #fff; --raise: #fff; --ink: #000; --ink-2: #333; --ink-3: #444; --line: #999; --line-2: #ccc; --primary: #14315e; --on-primary: #fff; --primary-tint: #fff; --normal-bg: #fff; --leak-bg: #fff; --warn-bg: #fff; --offline-bg: #fff; --cold-bg: #fff; --dry-bg: #fff; --shadow: none; --shadow-lg: none; } body { background: #fff; color: #000; } header, .top-actions, .refresh, .mobile-quick, .btn { display: none !important; } .panel, .card, table, .chart-panel, .status-hero { box-shadow: none; border: 1px solid #999; page-break-inside: avoid; break-inside: avoid; } .badge, .pill { border: 1px solid currentColor; } /* 흑백에서도 테두리로 구분 */ a { color: #000; text-decoration: underline; } }