/* 커스텀 스타일 — design-ref.md (Toss 스타일) 기반. Pico 뒤에 로드해 변수 오버라이드. 폰트는 폐쇄망 대비 로컬 동봉(static/fonts), CDN 의존 없음. [재디자인 노트] - 토큰(그림자/모션/포커스)을 추가해 라이트·다크가 함께 정돈되도록 함. - hover/press/정렬/진행/모달 진입에 절제된 마이크로 인터랙션 추가. - 확정 스타일: 요약카드=화이트+호버리프트, 표=제브라, 배포모달=중앙 경고형. - 모든 모션은 prefers-reduced-motion 에서 비활성화(파일 맨 끝). */ @font-face { font-family: "Pretendard Variable"; font-weight: 45 920; font-style: normal; font-display: swap; src: url("/static/fonts/PretendardVariable.woff2") format("woff2-variations"); } @font-face { font-family: "JetBrains Mono"; font-weight: 100 800; font-style: normal; font-display: swap; src: url("/static/fonts/JetBrainsMono.woff2") format("woff2-variations"); } :root { /* palette (design-ref §2) */ --bg: #ffffff; --bg-alt: #f9fafb; --surface: #f2f4f6; --surface-2: #e8f3ff; --text: #191f28; --text-secondary: #333d4b; --text-muted: #6b7684; --text-dim: #8b95a1; --border: #e5e8eb; --border-strong: #d1d6db; --accent: oklch(0.63 0.147 255); /* Toss Blue 계열이되 채도를 낮춰 눈부심(쨍) 완화 */ --accent-hover: oklch(0.55 0.16 255); --accent-deep: #1e40af; --accent-soft: #e8f3ff; --success-text: #00a37a; --success-soft: rgba(0, 200, 150, 0.12); --warning-text: #c5700a; --warning-soft: rgba(197, 112, 10, 0.12); --danger: #f04452; --danger-hover: #e0313f; --danger-soft: rgba(240, 68, 82, 0.1); --radius-card: 12px; /* 그림자 — 단계형. design-ref §6: 얇은 카드 그림자 + 무거운 모달 그림자 */ --shadow-sm: 0 1px 2px rgba(25, 31, 40, 0.04); --shadow-card: 0 1px 3px rgba(25, 31, 40, 0.05); --shadow-card-hover: 0 4px 16px rgba(25, 31, 40, 0.09); --shadow-modal: 0 12px 40px rgba(25, 31, 40, 0.18); /* 모션 토큰 — 도구답게 짧고 자연스럽게 */ --ease: cubic-bezier(0.32, 0.72, 0, 1); --dur-fast: 0.12s; --dur: 0.2s; --dur-slow: 0.32s; --focus-ring: 0 0 0 3px color-mix(in srgb, var(--accent) 30%, transparent); /* 영문/숫자(저장소명·버전·패키지수·시각)는 모노 — 도구다운 가독성 (design-ref §3). 혹시 섞이는 한글은 모노에 글리프가 없으니 한글 sans 로 폴백. */ --font-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, "Apple SD Gothic Neo", "Noto Sans KR", monospace; /* Pico 매핑 */ --pico-font-family-sans-serif: "Pretendard Variable", -apple-system, BlinkMacSystemFont, "Apple SD Gothic Neo", "Segoe UI", "Noto Sans KR", system-ui, sans-serif; --pico-font-family: var(--pico-font-family-sans-serif); --pico-line-height: 1.55; --pico-border-radius: 8px; --pico-background-color: var(--bg-alt); --pico-color: var(--text-secondary); --pico-h1-color: var(--text); --pico-h2-color: var(--text); --pico-h3-color: var(--text); --pico-muted-color: var(--text-muted); --pico-primary: #ffffff; --pico-primary-background: var(--accent); --pico-primary-hover-background: var(--accent-hover); --pico-primary-border: var(--accent); --pico-primary-hover-border: var(--accent-hover); --pico-card-background-color: var(--bg); --pico-card-border-color: var(--border); --pico-card-box-shadow: var(--shadow-card); color-scheme: light; } /* 다크 테마 — 토큰만 교체하면 컴포넌트는 자동으로 따라온다. design-ref 는 다크를 기본으로 쓰지 말라 하므로 토글 옵션으로만 제공. */ [data-theme="dark"] { color-scheme: dark; --bg: #1b1d23; --bg-alt: #131419; --surface: #262a33; --surface-2: #1e2b44; --text: #eceef2; --text-secondary: #c3c9d2; --text-muted: #8b95a1; --text-dim: #6b7480; --border: #2b2f39; --border-strong: #3a404c; --accent: #4d94ff; --accent-hover: #6aa6ff; --accent-deep: #2f6fd0; --accent-soft: #1e2b44; --success-text: #2ee0a8; --success-soft: rgba(46, 224, 168, 0.14); --warning-text: #ffb454; --warning-soft: rgba(255, 180, 84, 0.14); --danger: #ff6b78; --danger-hover: #ff8591; --danger-soft: rgba(255, 107, 120, 0.14); --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); --shadow-card: 0 1px 3px rgba(0, 0, 0, 0.3); --shadow-card-hover: 0 4px 18px rgba(0, 0, 0, 0.45); --shadow-modal: 0 12px 44px rgba(0, 0, 0, 0.6); --focus-ring: 0 0 0 3px color-mix(in srgb, var(--accent) 40%, transparent); --pico-background-color: var(--bg-alt); --pico-color: var(--text-secondary); --pico-h1-color: var(--text); --pico-h2-color: var(--text); --pico-h3-color: var(--text); --pico-muted-color: var(--text-muted); --pico-primary: #ffffff; --pico-primary-background: var(--accent); --pico-primary-hover-background: var(--accent-hover); --pico-primary-border: var(--accent); --pico-primary-hover-border: var(--accent-hover); --pico-card-background-color: var(--bg); --pico-card-border-color: var(--border); --pico-card-box-shadow: var(--shadow-card); --pico-form-element-background-color: var(--surface); --pico-form-element-border-color: var(--border); --pico-form-element-color: var(--text); --pico-progress-background-color: var(--surface); } body { background: var(--bg-alt); color: var(--text-secondary); -webkit-font-smoothing: antialiased; transition: background-color var(--dur) ease, color var(--dur) ease; } main.container { max-width: 1040px; padding-block: 40px; } h1, h2, h3 { color: var(--text); font-weight: 700; letter-spacing: -0.02em; text-wrap: balance; } h2 { font-size: 22px; margin: 0; } section { margin-bottom: 44px; } .mono { font-family: var(--font-mono); font-feature-settings: "tnum" 1; } /* 공통 포커스 링 — 키보드 접근성. 마우스 클릭엔 안 뜨게 focus-visible. */ :focus-visible { outline: none; box-shadow: var(--focus-ring); } /* 상단 내비 — 스크롤해도 상단 고정, 미세한 그림자로 레이어 분리 */ .app-nav { position: sticky; top: 0; z-index: 20; background: color-mix(in srgb, var(--bg) 88%, transparent); backdrop-filter: saturate(1.4) blur(10px); border-bottom: 1px solid var(--border); } .app-nav .container { max-width: 1040px; padding-block: 16px; display: flex; align-items: center; justify-content: space-between; } .app-nav .brand { margin: 0; font-size: 18px; font-weight: 700; color: var(--text); letter-spacing: -0.02em; display: inline-flex; align-items: center; gap: 9px; } .app-nav .brand .accent { color: var(--accent); } /* 브랜드 앞 로고 마크 — 차분한 단색(그라디언트 제거로 눈부심 완화) */ .app-nav .brand .brand-mark { width: 26px; height: 26px; border-radius: 7px; background: var(--accent); display: inline-grid; place-items: center; color: #fff; font-size: 13px; font-weight: 800; box-shadow: var(--shadow-sm); flex: none; } /* 테마 토글 (아이콘 버튼) */ .theme-toggle { --pico-background-color: transparent; --pico-border-color: var(--border-strong); --pico-color: var(--text-secondary); width: auto; margin: 0; padding: 8px 10px; line-height: 0; border-radius: 9px; transition: background-color var(--dur-fast) ease, border-color var(--dur-fast) ease, transform var(--dur-fast) var(--ease); } .theme-toggle:hover { --pico-background-color: var(--surface); --pico-border-color: var(--text-dim); --pico-color: var(--text); } .theme-toggle:active { transform: scale(0.94); } .theme-toggle .icon { width: 18px; height: 18px; transition: transform var(--dur) var(--ease); } .theme-toggle:hover .icon { transform: rotate(20deg); } .theme-toggle .sun { display: none; } .theme-toggle .moon { display: inline-block; } [data-theme="dark"] .theme-toggle .moon { display: none; } [data-theme="dark"] .theme-toggle .sun { display: inline-block; } .section-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 18px; } /* 상태: 라벨 + 배지 한 줄 */ .status-row { display: flex; align-items: center; gap: 12px; } .status-label { font-size: 22px; /* 저장소 h2 와 동일 */ font-weight: 700; letter-spacing: -0.02em; color: var(--text); } .status-row .status-line { font-size: 14px; } /* ── 버튼 — Toss: radius 7~8, weight 600, 누름 피드백 (design-ref §4·§6) ── */ button, [role="button"] { font-weight: 600; letter-spacing: -0.01em; border-radius: 8px; white-space: nowrap; transition: background-color var(--dur-fast) ease, border-color var(--dur-fast) ease, box-shadow var(--dur-fast) ease, opacity var(--dur) ease, transform var(--dur-fast) var(--ease); } button:not(:disabled):hover, [role="button"]:not(:disabled):hover { box-shadow: var(--shadow-sm); } button:not(:disabled):active, [role="button"]:not(:disabled):active { transform: scale(0.97); box-shadow: none; } /* primary 눌림 시 더 짙은 파랑 (design-ref: pressed=accent-deep) */ button[type="submit"]:not(.secondary):not(.ghost):not(.danger):active, button:not(.secondary):not(.ghost):not(.danger):not([role]):active { --pico-primary-background: var(--accent-deep); --pico-primary-border: var(--accent-deep); } /* 동기화 등 반복 노출 액션: 흰 배경 + 파란 글씨 + 옅은 파란 테두리. 표에 여러 개 떠도 번잡하지 않고, ghost(새로고침)와 확실히 구분됨. Pico v2 는 `:is(button,[type=submit],[type=button],[role=button]).secondary` (특정성 0,2,0)로 secondary 를 칠하므로, 같은 :is() 형태로 특정성을 맞춰 뒤에 로드되는 app.css 가 이기게 한다(단순 `button.secondary`=0,1,1 은 짐). */ :is(button, [role="button"], a).secondary { --pico-background-color: var(--bg); --pico-border-color: color-mix(in srgb, var(--accent) 30%, var(--border)); --pico-color: var(--accent); font-weight: 600; } /* Pico 의 secondary hover 는 `:is(button,…).secondary:is([aria-current]…,:hover,…)` = 특정성 (0,4,0) 이라 어두운 배경 + 흰 글씨로 덮어쓴다. 같은 (0,4,0) :is() 형태로 맞춰(뒤에 로드되어 우선) 밝은 파랑 hover 를 유지한다. 글씨색(--pico-color)도 함께 지정하지 않으면 Pico 가 흰색으로 만들어 옅은 배경 위에서 글씨가 사라진다. */ :is(button, [role="button"], a).secondary:hover:not(:disabled) { --pico-background-color: var(--accent-soft); --pico-border-color: var(--accent); --pico-color: var(--accent); } button.danger, [role="button"].danger { --pico-background-color: var(--danger); --pico-border-color: var(--danger); --pico-color: #ffffff; } button.danger:not(:disabled):hover, [role="button"].danger:not(:disabled):hover { --pico-background-color: var(--danger-hover); --pico-border-color: var(--danger-hover); } /* 유틸리티(새로고침 등): 중립 ghost — 동기화(틴티드 블루)와 구분 */ button.ghost, [role="button"].ghost { --pico-background-color: transparent; --pico-border-color: var(--border-strong); --pico-color: var(--text-secondary); } button.ghost:hover, [role="button"].ghost:hover { --pico-background-color: var(--surface); --pico-border-color: var(--text-dim); --pico-color: var(--text); } /* 아이콘 + 글자 버튼 */ .btn-icon { display: inline-flex; align-items: center; gap: 6px; } .btn-icon .icon { flex: none; } button:disabled, [role="button"]:disabled { opacity: 0.45; cursor: not-allowed; } /* 표 안 액션 버튼: 컴팩트하되 글자는 또렷하게 */ .btn-sm { padding: 7px 14px; font-size: 14px; margin: 0; width: auto; } /* 버튼 로딩 상태 — 텍스트를 스피너로 교체. .is-loading 토글로 사용. (HTMX hx-disabled-elt 로 비활성화될 때 함께 붙이면 자연스러움) */ button.is-loading, [role="button"].is-loading { color: transparent !important; position: relative; pointer-events: none; } button.is-loading::after, [role="button"].is-loading::after { content: ""; position: absolute; inset: 0; margin: auto; width: 15px; height: 15px; border: 2px solid color-mix(in srgb, currentColor 30%, transparent); border-top-color: var(--pico-color, #fff); border-radius: 50%; animation: spin 0.6s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } /* ── 요약 카드 ── */ .summary { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 24px; } .summary .card { position: relative; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-card); padding: 20px 22px; box-shadow: var(--shadow-card); transition: border-color var(--dur) ease, box-shadow var(--dur) ease, transform var(--dur) var(--ease); } .summary .card:hover { border-color: var(--border-strong); box-shadow: var(--shadow-card-hover); transform: translateY(-2px); } /* 카드 상단 라벨 + 아이콘 칩 한 줄 */ .summary .card-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; } .summary .card-ico { width: 34px; height: 34px; border-radius: 9px; display: grid; place-items: center; background: var(--surface); color: var(--text-muted); flex: none; } .summary .card-ico .icon { width: 18px; height: 18px; } .summary .card.is-accent .card-ico { background: var(--accent-soft); color: var(--accent); } .summary .card.is-success .card-ico { background: var(--success-soft); color: var(--success-text); } .summary .value { font-family: var(--font-mono); font-size: 34px; font-weight: 600; color: var(--text); line-height: 1.1; letter-spacing: -0.02em; font-feature-settings: "tnum" 1; } .summary .value.is-accent { color: var(--accent); } .summary .value.is-success { color: var(--success-text); } .summary .label { margin-top: 8px; font-size: 13px; font-weight: 500; color: var(--text-muted); } /* ── 표 (카드로 감싸기, hairline 행, hover) ── */ .table-card { background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-card); overflow: hidden; box-shadow: var(--shadow-card); } .table-card table { margin: 0; } .table-card thead th { font-size: 12px; font-weight: 600; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.05em; background: var(--bg-alt); border-bottom: 1px solid var(--border); padding-block: 13px; position: sticky; top: 0; } .table-card tbody td { height: 56px; font-size: 14px; border-bottom: 1px solid var(--border); padding-block: 12px; vertical-align: middle; /* Pico 의 `td,th{background-color:var(--pico-background-color)}`(흰색)이 td 자신의 배경으로 칠해져 tr 의 제브라/hover 배경을 덮어버린다(특정성이 아니라 페인트 순서 문제). td 배경을 투명으로 돌려 tr 배경이 보이게 한다. */ background: transparent; } .table-card tbody tr { transition: background-color var(--dur-fast) ease; } .table-card tbody tr:last-child td { border-bottom: none; } /* 2B안(확정): 제브라 스트라이프 + 틴티드 블루 hover. 짝수 행에 연한 회색(--bg-alt)을 깔아 넓은 표에서도 행이 눈으로 잘 따라감. (기존 color-mix 값은 거의 흰색이라 실제로 안 보였음 → bg-alt 로 또렷하되 은은하게) */ .table-card tbody tr:nth-child(even) { background: var(--bg-alt); } .table-card tbody tr:hover { background: var(--surface-2); } .repo-name a { font-family: var(--font-mono); font-weight: 600; color: var(--text); text-decoration: none; transition: color var(--dur-fast) ease; } .repo-name a:hover { color: var(--accent); } /* 숫자/버전/시각: 모노 + tabular — 본문 셀(td)과 .mono 에만 적용. 헤더(th)는 한글 라벨이라 모노 제외(Pretendard 유지). */ .mono, td.num { font-family: var(--font-mono); font-feature-settings: "tnum" 1; } td.num, th.num { text-align: right; } /* 액션 셀: 버튼↔진행률↔완료배지로 내용이 바뀌어도 행 높이·열 너비가 안 변하게 너비·높이를 고정한 박스로 만든다. 넘치는 내용은 클립. */ .row-action { display: flex; align-items: center; width: 172px; /* 버튼(테두리 포함 ~37.7px)↔진행률↔배지로 내용이 바뀌어도 높이가 안 흔들리도록 40px 로 고정하고 가운데 정렬(td 는 56px 고정이라 여유). 이전 36px 는 버튼보다 낮아 위아래 테두리가 잘리거나(overflow:hidden) 높이가 튀었다(min-height). */ height: 40px; } /* ── 검증 배지 칩 (dot + 라벨, 이모지 없음) ── */ .badge { display: inline-flex; align-items: center; gap: 6px; padding: 4px 11px; border-radius: 999px; font-size: 13px; font-weight: 600; white-space: nowrap; border: 1px solid transparent; } .badge::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; flex: none; } .badge-pass { color: var(--success-text); background: var(--success-soft); } /* PASS 배지의 dot 은 살짝 은은한 펄스 (통과했음을 안심시키는 신호) */ .badge-pass::before { box-shadow: 0 0 0 0 color-mix(in srgb, var(--success-text) 55%, transparent); animation: dot-pulse 2.4s var(--ease) infinite; } @keyframes dot-pulse { 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--success-text) 45%, transparent); } 60% { box-shadow: 0 0 0 5px transparent; } 100% { box-shadow: 0 0 0 0 transparent; } } .badge-warn { color: var(--warning-text); background: var(--warning-soft); } .badge-unset { color: var(--text-muted); background: var(--surface); } .badge-deployed { color: var(--accent-hover); background: var(--accent-soft); } /* 뒤로가기(대시보드) 버튼 — ghost, 제목과 간격 확보 */ .back-btn { margin-bottom: 20px; text-decoration: none; } /* 버전 검증 상세 */ .verify-detail { display: flex; align-items: center; gap: 12px; margin-top: 20px; margin-bottom: 20px; } .verify-detail small { color: var(--text-muted); font-family: var(--font-mono); } /* 버전 스냅샷 리스트 */ .version-item { display: flex; align-items: center; gap: 14px; padding: 16px 18px; border: 1px solid var(--border); border-radius: var(--radius-card); background: var(--bg); box-shadow: var(--shadow-card); margin-bottom: 12px; transition: border-color var(--dur) ease, box-shadow var(--dur) ease; } .version-item:hover { border-color: var(--border-strong); box-shadow: var(--shadow-card-hover); } .version-item .ver-no { font-family: var(--font-mono); font-weight: 700; font-size: 15px; color: var(--text); } .version-item .ver-meta { color: var(--text-muted); font-family: var(--font-mono); font-size: 13px; } .version-item .spacer { margin-left: auto; } /* 패키지 증감 */ .delta-add { color: var(--success-text); font-weight: 600; } .delta-del { color: var(--danger); font-weight: 600; } /* ── 데모(가짜 데이터) 모드 경고 배너 — 전 페이지 최상단. 실연동 오인 방지. ── */ .demo-banner { display: flex; align-items: center; gap: 10px; padding: 10px 20px; background: var(--warning-soft); color: var(--warning-text); font-size: 14px; font-weight: 500; border-bottom: 1px solid color-mix(in srgb, var(--warning-text) 28%, transparent); } .demo-banner strong { font-weight: 700; } .demo-banner .demo-dot { flex: none; width: 8px; height: 8px; border-radius: 50%; background: currentColor; animation: dot-pulse-warn 2s var(--ease) infinite; } @keyframes dot-pulse-warn { 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--warning-text) 45%, transparent); } 60% { box-shadow: 0 0 0 6px transparent; } 100% { box-shadow: 0 0 0 0 transparent; } } /* 연결 배지의 데모 변형 */ .status-demo { color: var(--warning-text); background: var(--warning-soft); } /* ── 배포 완료 배너 (#flash) ── */ .flash-banner { display: flex; align-items: center; gap: 10px; margin: 4px 0 22px; padding: 13px 16px; border-radius: 10px; background: var(--success-soft); color: var(--success-text); font-size: 15px; font-weight: 600; border: 1px solid color-mix(in srgb, var(--success-text) 24%, transparent); animation: flash-in var(--dur-slow) var(--ease); } @keyframes flash-in { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: translateY(0); } } /* 성공 체크 원 — dot 대신 그려지는 체크 애니메이션 */ .flash-banner .flash-check { flex: none; width: 20px; height: 20px; border-radius: 50%; background: var(--success-text); display: grid; place-items: center; } .flash-banner .flash-check svg { width: 12px; height: 12px; stroke: #fff; stroke-width: 3; fill: none; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 20; stroke-dashoffset: 20; animation: check-draw 0.4s var(--ease) 0.12s forwards; } @keyframes check-draw { to { stroke-dashoffset: 0; } } .flash-banner .flash-dot { flex: none; width: 8px; height: 8px; border-radius: 50%; background: currentColor; } .flash-banner .flash-sub { color: var(--text-muted); font-weight: 500; font-size: 13px; } .flash-banner .flash-warn { margin-left: auto; color: var(--warning-text); font-weight: 500; font-size: 13px; } /* ── 배포 확인 모달 ── */ #modal[open], dialog.deploy-dialog[open] { animation: backdrop-in var(--dur) ease; } @keyframes backdrop-in { from { opacity: 0; } to { opacity: 1; } } /* 모달이 열릴 때 배경 스크롤 잠금 + 다이얼로그 overflow 허용 → 순간 스크롤바 깜뱡임 제거 */ html:has(dialog[open]) { overflow: hidden; } dialog.deploy-dialog, dialog#modal { overflow: visible; } .deploy-modal { width: min(520px, 92vw); background: var(--bg); border: 1px solid var(--border); border-radius: 16px; box-shadow: var(--shadow-modal); padding: 0; overflow: hidden; animation: modal-in var(--dur-slow) var(--ease); } @keyframes modal-in { from { opacity: 0; transform: scale(0.96); } to { opacity: 1; transform: scale(1); } } /* 위험 확인 아이콘 — 중앙 정렬 원형(의식적·경고적). 모달 헤더. */ .deploy-modal .modal-head { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 10px; padding: 26px 24px 6px; } .deploy-modal .modal-head .warn-ico { flex: none; width: 52px; height: 52px; border-radius: 50%; display: grid; place-items: center; background: var(--danger-soft); color: var(--danger); } .deploy-modal .modal-head .warn-ico svg { width: 26px; height: 26px; } .deploy-modal .modal-head h3 { font-size: 21px; margin: 0; color: var(--text); } .deploy-modal .modal-head .head-sub { display: block; font-size: 13px; font-weight: 500; color: var(--danger); margin-top: 2px; } .deploy-modal .modal-body { padding: 20px 24px 24px; } .deploy-note { font-size: 15px; color: var(--text-secondary); margin-bottom: 16px; } .deploy-note .repo-hl { font-family: var(--font-mono); font-weight: 700; color: var(--text); } /* 현재 → 대상 프리뷰: 카드형 before/after */ .deploy-preview { display: grid; grid-template-columns: 1fr auto 1fr; align-items: stretch; gap: 10px; margin: 0 0 20px; } .deploy-preview .pv-box { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 12px 14px; } .deploy-preview .pv-box.pv-target { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 32%, transparent); } .deploy-preview .pv-label { font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-dim); margin-bottom: 6px; } .deploy-preview .pv-target .pv-label { color: var(--accent); } .deploy-preview .pv-ver { font-family: var(--font-mono); font-size: 20px; font-weight: 700; color: var(--text); line-height: 1.15; } .deploy-preview .pv-pkg { font-family: var(--font-mono); font-size: 12px; color: var(--text-muted); margin-top: 4px; } .deploy-preview .pv-arrow { align-self: center; color: var(--text-dim); display: grid; place-items: center; } .deploy-preview .pv-arrow svg { width: 22px; height: 22px; } /* 타입-투-컨펌 — 저장소 이름을 정확히 입력해야 확정 버튼이 열림 */ .confirm-label { display: block; font-size: 14px; font-weight: 500; color: var(--text-secondary); } .confirm-label .repo-hl { font-family: var(--font-mono); font-weight: 700; color: var(--text); background: var(--surface); padding: 1px 7px; border-radius: 5px; } .confirm-field { position: relative; margin-top: 8px; } .confirm-label input { font-family: var(--font-mono); font-size: 14px; --pico-form-element-spacing-vertical: 0.6rem; height: auto; padding-right: 40px; } /* 입력 상태 시각화: 비어있음(중립) → 불일치(빨강) → 일치(초록 체크) */ .confirm-field .confirm-state { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); width: 18px; height: 18px; opacity: 0; transition: opacity var(--dur) ease; } .confirm-field.is-valid input { border-color: var(--success-text); box-shadow: 0 0 0 3px var(--success-soft); } .confirm-field.is-valid .confirm-state.ok { opacity: 1; color: var(--success-text); } .confirm-field.is-invalid input { border-color: var(--danger); } .confirm-field.is-invalid .confirm-state.bad { opacity: 1; color: var(--danger); } .confirm-hint { font-size: 12px; margin-top: 6px; min-height: 1em; color: var(--text-dim); font-family: var(--font-mono); } .confirm-field.is-invalid ~ .confirm-hint { color: var(--danger); } .confirm-field.is-valid ~ .confirm-hint { color: var(--success-text); } /* 불일치로 확정 버튼 누르려 하면 흔들림 (JS 로 .shake 토글) */ .confirm-field.shake { animation: shake 0.32s var(--ease); } @keyframes shake { 0%, 100% { transform: translateX(0); } 20% { transform: translateX(-6px); } 40% { transform: translateX(6px); } 60% { transform: translateX(-4px); } 80% { transform: translateX(4px); } } .modal-actions { display: flex; justify-content: flex-end; align-items: center; gap: 10px; margin-top: 22px; } .modal-actions button { width: auto; margin: 0; padding: 11px 20px; font-size: 15px; } /* ── 동기화 진행률 바 ── */ .sync-progress { display: flex; flex-direction: column; gap: 5px; width: 150px; } .sync-progress progress { margin: 0; width: 100%; height: 6px; border-radius: 999px; } .sync-progress small { color: var(--text-muted); font-family: var(--font-mono); font-size: 12px; font-feature-settings: "tnum" 1; } /* 진행 중인 결정형 바 위에 흐르는 shimmer — 값(width)은 progress 가 담당, shimmer 는 "지금 살아있음"을 보여주는 오버레이 */ .sync-progress { position: relative; } .sync-progress.is-active progress::after { content: ""; } .sync-progress .sync-head { display: flex; align-items: baseline; justify-content: space-between; } .sync-progress .sync-pct { color: var(--accent); font-weight: 700; } /* 값을 모르는 초기 상태용 인디터미닛 바 */ .sync-progress.indeterminate progress { background: linear-gradient(90deg, transparent, var(--accent), transparent) no-repeat, var(--pico-progress-background-color, var(--surface)); background-size: 40% 100%; animation: indeterminate 1.2s var(--ease) infinite; } @keyframes indeterminate { 0% { background-position: -40% 0; } 100% { background-position: 140% 0; } } /* ── 연결 상태 칩 (dot + 텍스트) ── */ .status-line { display: inline-flex; align-items: center; gap: 8px; padding: 6px 12px; border-radius: 999px; font-weight: 600; font-size: 14px; background: var(--surface); } .status-line::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: currentColor; flex: none; } .status-ok { color: var(--success-text); background: var(--success-soft); } .status-ok::before { animation: dot-pulse 2.4s var(--ease) infinite; } .status-bad { color: var(--danger); background: var(--danger-soft); } .status-loading { color: var(--text-muted); } .status-loading::before { animation: blink 1s steps(2, start) infinite; } @keyframes blink { 50% { opacity: 0.3; } } /* ── 검색 툴바 / 정렬 헤더 / 페이저 ── */ .list-toolbar { margin-bottom: 14px; position: relative; } /* 선택자에 input[type=search] 를 포함해 특정성 (0,3,1) 확보. Pico v2 의 검색 아이콘/패딩 규칙(input:not(...)[type=search], 0,2,1)을 이겨야 기본 돋보기 배경이 사라지고 인라인 .search-ico 만 남는다(아이콘 중복 제거). */ .list-toolbar input[type="search"].repo-search { width: 100%; margin: 0; font-size: 14px; height: auto; /* 좌측 돋보기 아이콘 자리 확보 (아이콘 left:13 + 16px → 텍스트 40px 부터) */ padding: 0.62rem 14px 0.62rem 40px; /* Pico 기본 [type=search] 돋보기 배경 아이콘 제거 (인라인 .search-ico 사용) */ background-image: none; } .list-toolbar .search-ico { position: absolute; left: 13px; top: 50%; transform: translateY(-50%); width: 16px; height: 16px; color: var(--text-dim); pointer-events: none; } .table-card thead th a.th-sort { display: inline-flex; align-items: center; gap: 4px; color: inherit; text-decoration: none; cursor: pointer; transition: color var(--dur-fast) ease; } .table-card thead th a.th-sort:hover { color: var(--text-secondary); } .table-card thead th a.th-sort.is-active { color: var(--accent); } .sort-ind { font-size: 11px; transition: transform var(--dur) var(--ease); display: inline-block; } .th-sort.is-active.desc .sort-ind { transform: rotate(180deg); } .pager { display: flex; align-items: center; justify-content: flex-end; gap: 14px; padding: 12px 16px; border-top: 1px solid var(--border); color: var(--text-muted); font-size: 13px; } .pager button { padding: 6px 12px; font-size: 13px; } /* ── 빈 상태 / 에러 상태 ── */ .empty-note { padding: 48px 16px; color: var(--text-muted); text-align: center; } .empty-note .empty-ico { width: 40px; height: 40px; margin: 0 auto 12px; color: var(--text-dim); display: grid; place-items: center; } .empty-note .empty-title { font-weight: 600; color: var(--text-secondary); font-size: 15px; } .empty-note .empty-sub { font-size: 13px; margin-top: 4px; } .error-note { padding: 20px; border-radius: 10px; background: var(--danger-soft); color: var(--danger); border: 1px solid color-mix(in srgb, var(--danger) 22%, transparent); font-weight: 500; display: flex; align-items: center; gap: 10px; } /* ── 스켈레톤 로딩 (표 초기/재조회) ── */ .skeleton { background: linear-gradient( 90deg, var(--surface) 25%, color-mix(in srgb, var(--surface) 60%, var(--bg)) 37%, var(--surface) 63% ); background-size: 400% 100%; border-radius: 6px; animation: skeleton 1.4s ease infinite; } @keyframes skeleton { 0% { background-position: 100% 0; } 100% { background-position: 0 0; } } .skeleton-row { height: 16px; } /* HTMX 요청 중 페이드 (hx-swap 대상에 자동 적용) */ .htmx-swapping { opacity: 0.4; transition: opacity var(--dur) ease; } tr.htmx-request .row-action button { opacity: 0.6; } @media (max-width: 640px) { .summary { grid-template-columns: repeat(2, 1fr); } .deploy-preview { grid-template-columns: 1fr; } .deploy-preview .pv-arrow { transform: rotate(90deg); } } /* ───────────────────────────────────────────────────────────── 모션 최소화 선호 존중 (design-ref §6 취지) ───────────────────────────────────────────────────────────── */ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; scroll-behavior: auto !important; } .summary .card:hover { transform: none; } }