/* ============================================================================= * dash-v2.css — dashboard.php 전용 오버레이 (app.css 뒤에 로드) * 목업 review/ui_mockup_v2.html 반영: 글래스 툴바 · 스프링 모션 · 세그먼트 탭 · * 결합 센서 카드(누수+온습도) · 카운트다운 링 · 카드형 테이블 헤더. * app.css 는 7페이지 공용이므로 건드리지 않는다. 인쇄 규칙은 app.css 것을 계승. * 폐쇄망: 외부 리소스 0. * ============================================================================= */ :root { --spring: cubic-bezier(.32,.72,.28,1); /* 감쇠 스프링 근사 */ } /* ── 글래스 툴바: 반투명 재질 + sticky, 콘텐츠가 아래로 스크롤 ─────────────── */ header { background: rgba(20,26,61,.78); backdrop-filter: blur(18px) saturate(160%); -webkit-backdrop-filter: blur(18px) saturate(160%); } .top-actions a { transition: background .18s var(--spring), color .15s, border-color .15s, transform .15s var(--spring); } .top-actions a:active { transform: scale(.96); } /* ── 새로고침 카운트다운 링 ─────────────────────────────────────────────────── */ .refresh { display: inline-flex; align-items: center; gap: 8px; } .refresh-ring { width: 15px; height: 15px; flex: none; } .refresh-ring circle { fill: none; stroke: var(--primary-600); stroke-width: 2.5; stroke-dasharray: 38; stroke-linecap: round; transform: rotate(-90deg); transform-origin: center; animation: dash-countdown 30s linear infinite; } @keyframes dash-countdown { from { stroke-dashoffset: 0; } to { stroke-dashoffset: 38; } } .refresh.paused .refresh-ring circle { animation-play-state: paused; opacity: .35; } .refresh-toggle { transition: transform .15s var(--spring), background .15s; } .refresh-toggle:active { transform: scale(.93); } /* ── 세그먼트 탭바(iOS segmented control) ───────────────────────────────────── */ .tabbar { border-radius: var(--r-pill); padding: 4px; gap: 3px; } .tabbar .tab-link { border-radius: var(--r-pill); padding: 8px 18px; transition: background .22s var(--spring), color .15s, transform .15s var(--spring); } .tabbar .tab-link:active { transform: scale(.96); } .tabbar .tab-link.active { color: #fff; background: linear-gradient(135deg, var(--primary), #8f6bff); box-shadow: 0 4px 14px rgba(108,123,255,.35); } .tabbar .tab-link.active::after { content: none; } .tabbar .tab-link.active .tab-badge.crit { background: rgba(255,255,255,.22); color: #fff; } .tabbar .tab-print { border-radius: var(--r-pill); } /* ── 인터랙션 공통: 즉각 반응(:active) + 부상(hover) 스프링 ─────────────────── */ .kpi, .sensor-card, .leak-status-card, .alert-strip, .quick-tile { transition: transform .2s var(--spring), border-color .2s, box-shadow .2s, background .2s; } .kpi:hover, .sensor-card:hover, .leak-status-card:hover { transform: translateY(-3px); box-shadow: var(--shadow-lg); } .kpi:active, .sensor-card:active, .alert-strip:active { transform: translateY(-1px) scale(.985); } .alert-strip:hover { transform: translateX(3px); } /* ── 결합 센서 카드 (누수+온습도) ──────────────────────────────────────────── 상태 우선순위: 오프라인 > 누수 확정 > 확인중 > 온습도 경보 > 정상 */ .sensor-card { position: relative; overflow: hidden; } .sensor-card::before { content: ""; position: absolute; inset: 0 0 auto 0; height: 3px; background: var(--normal); opacity: .85; } .sensor-card.alert::before { background: var(--leak); } .sensor-card.leak::before { background: var(--chart-hum); } .sensor-card.pending::before { background: var(--warn); } .sensor-card.offline::before { background: var(--offline); } .sensor-card.offline { opacity: .78; } .sensor-card.leak { border-color: rgba(63,199,230,.4); background: linear-gradient(180deg, rgba(63,199,230,.10), transparent 60%), var(--surface); animation: dash-leak-glow 1.8s ease-in-out infinite; } @keyframes dash-leak-glow { 50% { box-shadow: 0 0 26px rgba(63,199,230,.30); } } /* 유형 칩 2개(결합) 나란히 */ .sc-types { display: flex; gap: 4px; flex: none; } /* 상태 라인: 점 + 텍스트 (badge-row 대체 — 기존 배지도 유지 호환) */ .status-line { display: flex; align-items: center; gap: 7px; font-size: .8125rem; font-weight: 700; margin-top: var(--sp-2); } .status-line .sdot { width: 8px; height: 8px; border-radius: 50%; background: var(--normal); box-shadow: 0 0 8px var(--normal); flex: none; } .status-line.ok { color: var(--normal-ink); } .status-line.err { color: var(--leak-ink); } .status-line.err .sdot { background: var(--leak); box-shadow: 0 0 8px var(--leak); animation: dash-pulse 1.2s ease-in-out infinite; } .status-line.leakst { color: var(--cold-ink); } .status-line.leakst .sdot { background: var(--chart-hum); box-shadow: 0 0 8px var(--chart-hum); animation: dash-pulse 1.2s ease-in-out infinite; } .status-line.pend { color: var(--warn-ink); } .status-line.pend .sdot { background: var(--warn); box-shadow: 0 0 8px var(--warn); } .status-line.off { color: var(--offline-ink); } .status-line.off .sdot { background: var(--offline); box-shadow: none; } @keyframes dash-pulse { 50% { opacity: .4; } } /* 온습도 큰 수치 2열 */ .vals { display: flex; gap: var(--sp-5); margin-top: var(--sp-3); } .vals .v { flex: 1; min-width: 0; } .vals .v .n { font-size: 1.7rem; font-weight: 800; letter-spacing: -.03em; line-height: 1.08; font-variant-numeric: tabular-nums; color: var(--ink); } .vals .v .n small { font-size: .85rem; color: var(--ink-2); font-weight: 600; letter-spacing: 0; } .vals .v .l { font-size: .7rem; color: var(--ink-3); margin-top: 2px; } .vals .v.temp .n { color: var(--chart-temp); } .vals .v.hum .n { color: var(--chart-hum); } .vals .v .n.hot { text-shadow: 0 0 14px rgba(255,138,92,.5); } .vals .v .n.cold { color: var(--cold-ink); } .vals .v .n.humid { text-shadow: 0 0 14px rgba(255,178,62,.5); } .vals .v .n.dry { color: var(--dry-ink); } /* 24h 스파크라인 */ .spark24 { width: 100%; height: 34px; display: block; margin-top: var(--sp-2); } .spark24 polyline { fill: none; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; } .spark24 .sp-t { stroke: var(--chart-temp); opacity: .85; } .spark24 .sp-h { stroke: var(--chart-hum); opacity: .85; stroke-dasharray: 5 3; } /* 누수 행(결합 카드 내부): 경과·SMS·사진 */ .leak-row { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; margin-top: var(--sp-3); padding-top: var(--sp-3); border-top: 1px solid var(--line-2); } .leak-row .leak-fact { font-size: .8125rem; color: var(--ink-2); font-variant-numeric: tabular-nums; } .leak-row .leak-fact b { color: var(--ink); font-weight: 800; } .leak-row .leak-thumbs { margin-left: auto; } .leak-row .leak-thumbs img { width: 46px; height: 36px; object-fit: cover; transition: transform .18s var(--spring); } .leak-row .leak-thumbs a:hover img { transform: scale(1.5); } /* 누수 카드 하단 물결(장식) */ .leak-wave { position: absolute; left: 0; right: 0; bottom: 0; height: 42px; opacity: .14; pointer-events: none; } .leak-wave path { fill: var(--chart-hum); } /* 카드 푸터: 최종확인 + 보고 주기 */ .sc-foot { display: flex; justify-content: space-between; gap: var(--sp-2); font-size: .72rem; color: var(--ink-3); margin-top: var(--sp-3); font-variant-numeric: tabular-nums; } .sc-foot .stale { color: var(--leak-ink); font-weight: 700; } /* ── 이력 테이블: 카드형 헤더(테이블 위 캡션 스트립) ────────────────────────── */ h2.table-title { margin-bottom: 0; font-size: .9375rem; } .table-title { display: flex; align-items: center; gap: var(--sp-2); background: var(--surface-2); border: 1px solid var(--line); border-bottom: none; border-radius: var(--r-lg) var(--r-lg) 0 0; padding: 12px 16px; font-size: .9375rem; font-weight: 700; } .table-title .count-pill { margin-left: auto; font-size: .7rem; font-weight: 700; color: var(--ink-2); background: var(--surface); border: 1px solid var(--line); padding: 2px 10px; border-radius: var(--r-pill); font-variant-numeric: tabular-nums; } .table-title + .table-wrap, .table-title + details.mobile-detail .table-wrap { border-radius: 0 0 var(--r-lg) var(--r-lg); } .table-title + .table-wrap table, .table-title + details.mobile-detail table { border-radius: 0 0 var(--r-lg) var(--r-lg); border-top: none; } .table-title .h2-sub { margin-left: auto; font-size: .72rem; font-weight: 600; color: var(--ink-3); font-variant-numeric: tabular-nums; } /* ── 히어로 미세 조정: 큰 제목 음수 트래킹 ──────────────────────────────────── */ .status-hero h2 { letter-spacing: -.02em; } .kpi .kpi-val, .card .val, .gauge-num b { letter-spacing: -.03em; } /* ── 미세먼지(PM) 계통 색상·요소 (온습도 orange 와 구분되는 magenta/violet 계열) ────── PMS7003 미세먼지 경보가 온습도 경보로 오독되지 않도록 별도 색 계통을 부여한다. PM 토큰은 app.css 에 없으므로 여기서 정의(dash-v2.css 는 app.css 뒤 로드 — 재정의 아님, additive). */ :root { --pm: #d24fd6; /* 미세먼지 대표 액센트(도넛·칩·라인) */ --pm-ink: #eca3ea; /* 어두운 배경 위 텍스트용 밝은 변형 */ --pm-bg: rgba(210,79,214,.15); --pm25: #e85fbf; /* PM2.5 계열(magenta-pink) */ --pm10: #a06bff; /* PM10 계열(violet) */ } /* 유형 칩(미세먼지) — sc-type.metric(청록)·sc-type.leak(회색)과 구분 */ .sc-type.pm { background: var(--pm-bg); color: var(--pm-ink); } /* 카드 상태(미세먼지 경보) — 상단 바 + 배경 그라디언트(온습도 alert 와 별개 색) */ .sensor-card.pm { border-color: rgba(210,79,214,.4); background: linear-gradient(180deg, var(--pm-bg), transparent 60%), var(--surface); } .sensor-card.pm::before { background: var(--pm); } /* 상태 라인(미세먼지 경보) */ .status-line.pmst { color: var(--pm-ink); } .status-line.pmst .sdot { background: var(--pm); box-shadow: 0 0 8px var(--pm); animation: dash-pulse 1.2s ease-in-out infinite; } /* 미세먼지 큰 수치 2열 — 온습도 vals 재사용, 색만 PM 계열. 위반 시 hot 글로우(magenta) */ .vals.pm-vals { margin-top: var(--sp-2); } .vals .v.pm25 .n { color: var(--pm25); } .vals .v.pm10 .n { color: var(--pm10); } .pm-vals .v .n.hot { text-shadow: 0 0 14px rgba(210,79,214,.55); } /* 상태 pill(미세먼지/결로) — 이력·측정 표의 상태 배지 */ .pill.high_pm25 { background: var(--pm-bg); color: var(--pm-ink); } .pill.high_pm10 { background: var(--pm-bg); color: var(--pm-ink); } .pill.condensation_risk { background: var(--warn-bg); color: var(--warn-ink); } /* 추이 차트(미세먼지) — dashboard.php 인라인