/* ===================================
   폰트 설정 (Pretendard + Inter)
   =================================== */

/* Pretendard - 한글 최적화 폰트 */
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css');

/* Inter - 영문/숫자 최적화 폰트 */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');

/* ===================================
   Colorful & Dynamic Design System
   Monday.com + ClickUp Style
   =================================== */

/* ═══════════════════════════════════════════════════════════════════
   라이트(칼라) 테마 팔레트 — 중성 슬레이트
   ───────────────────────────────────────────────────────────────────
   [중성 슬레이트 전환 2026-09-19 / A1 · 설계 #1280(A2) ②구현]
     오너 지시: "A 중성 슬레이트 #f1f5f9 로 해줘" (2026-09-16)
                "칼라모드가 눈이 피곤한 상황이야" (2026-09-19)

     ■ 무엇이 문제였나 — 8개월간 방치된 **임시 테스트값**이었다
       유입 커밋 fdae6cb8 (2026-01-24):
         "UI: Increase saturation of Color theme background (mint)
          to make it visually distinct"
       → 테마가 구분되는지 **눈으로 확인하려고** 채도를 올린 값이고,
         주석에도 "(테스트용)" 이라고 적혀 있었다. 그대로 운영에 남았다.
         원래값은 --bg-main #f0fdf4 / --border-main #d1fae5 였다.

     ■ 왜 눈이 피로했나 — 배경이 전경을 이기고 있었다
       지각 채도(LCh C*) 실측:
         구 --bg-main #d1fae5 → C* 18.1   ← 화면에서 가장 넓은 면
         진척 신호색          → C* 10~16   ← 정작 봐야 할 정보
       **정보가 없는 면이 정보가 있는 면보다 진했다.** 눈이 쉴 곳이 없고
       지연 행이 바탕에 묻혔다. 신규값은 C* 2.5 로 신호색 아래에 둔다.
       (참고: GitHub light 1.2 / Monday 2.1 / Linear 0.0)

     ⛔ 민트(#d1fae5)·에메랄드(#34d399)로 되돌리지 말 것. 그게 이 결함 자체다.
        회귀 방지: tests/light-slate-status-parity.test.ts LS-1
     ⚠️ 같은 hex 가 **다른 의미**로 쓰이는 곳이 있다 — 함께 바꾸지 말 것.
        (전역 치환 금지. 이 블록만 편집한다 — #1280 §5.1)
        [2026-09-19 · A1 갱신] 완료 계열이 틸로 이동하면서(#1328) 이 목록의
        --color-done 항목들은 해소됐다. 남은 동명 hex 충돌:
        #d1fae5 → 크로스하이라이트 ON 배경(.btn-ghost.list-cross-highlight-on)
        #34d399 → app-api-monitor.js (완료색 토큰과 무관한 자체 값)
     ⛔ 다크 값은 이 블록과 무관하다. 다크는 scripts/gen-dark-palette.mjs 가
        바탕 L* 1값에서 파생한다 — 손으로 고치지 말 것.
   ═══════════════════════════════════════════════════════════════════ */
:root, :root[data-theme="color"] {
  /* Primary Colors */
  --bg-main: #f1f5f9;       /* L*96.3 C*2.5 — 중성 슬레이트(오너 지정). 카드 대비 1.10:1 */
  --bg-card: #ffffff;       /* 카드 배경은 흰색 (기준면) */
  --border-main: #d0d4d9;   /* L*84.7 — 카드 대비 1.49:1. 다크 --border-main(1.48) 미러 */
  --text-main: #41464c;     /* L*29.5 — 카드 대비 9.52:1 ✅AAA. 다크 --text-main(9.48) 미러 */
  /* [신설 2026-09-19 · #1280 §3.1] 라이트 전용 격자/구분선 토큰.
     다크에는 --dk-line 쌍이 있는데 라이트에는 대응물이 없어, 각 모듈이
     #e5e7eb 같은 값을 인라인으로 흩뿌려 놓았다(app-resource-leveling.js 폴백 17곳).
     토큰을 먼저 세운다 — 인라인 정리는 후속 건(#1280 §6).
     ⚠️ line 이 line-soft 보다 **진해야** 한다(격자 > 행 구분선). LS-1 이 잠근다. */
  --lt-line: #c8cdd2;       /* L*82.2 — 격자/테두리. 카드 대비 1.60:1 (다크 --dk-line 미러) */
  --lt-line-soft: #dce1e5;  /* L*89.3 — 행 구분선. 카드 대비 1.32:1 (다크 --dk-line-soft 미러) */
  --color-primary-50: #eff6ff;
  --color-primary-100: #dbeafe;
  --color-primary-500: #3b82f6;
  --color-primary-600: #2563eb;
  --color-primary-700: #1d4ed8;
}

/* ⛔ [화이트 테마 제거 2026-09-09 / A3 · 오너 지시 "다크에 진척색을 넣고 화이트는 없애자"]
   `:root[data-theme="white"]` 토큰 블록을 제거했다.
   이유: 화이트 테마는 진척 신호색(행 배경)을 흰색으로 덮어 **핵심 기능을 지웠다**.
        투자도 비대칭이었다(다크 88블록/68커밋 vs 화이트 5블록/2커밋·50분 후 중단).
   유효 테마는 **칼라(:root 기본) · 다크** 2종이다.
   설계: docs/proposals/theme-consolidation.md §4 (전수 11곳)
   ⚠️ 'white'/'light' 가 저장된 기존 사용자는 app-state.js normalizeTheme() 이
      'color' 로 승격한다(§4.2). 이 블록만 지우고 정규화를 빼면 토큰이 전부
      폴백돼 화면이 깨지므로 반드시 함께 유지할 것. */

/* ═══════════════════════════════════════════════════════════════════
   다크 테마 팔레트
   ───────────────────────────────────────────────────────────────────
   [다크모드 밝기 상향 2026-08-19 / A2 · 오너 지시 "L2"]
     증상: 다크모드가 "너무 어둡다".
     원인: 면적이 가장 넓은 배경이 L*(지각명도) 8~10 으로 업계 최암부
           수준이었다 (GitHub dark 5 / Notion dark 8.8 대비).
           게다가 주배경(L*8)이 카드(L*16)보다 8단계 어두워 화면
           대부분이 최암부로 채워졌다.
     수정: 색조·채도는 보존하고 지각명도 L* 만 균일하게 +7 상향.
           (선형 Y 스케일링 → "네이비 톤" 유지, 밝기만 변경)
           목표 수준 = GitHub dark dimmed (배경 L*15).
     주의: ⛔ 이 블록의 값을 개별로 손대지 말 것. 아래 --dk-* 토큰이
           tailwind.config(src/index.tsx, gantt.html) 의 다크 팔레트와
           gantt_v2.js 색상표와 "같은 밝기 단계"를 공유한다.
           한 곳만 바꾸면 화면별 밝기가 어긋난다.
   ═══════════════════════════════════════════════════════════════════ */
:root[data-theme="dark"] {
  /* ══ 다크 전용 표면 토큰 ══════════════════════════════════════════
     [A안 단일 바탕 통일 2026-08-22 / A2 · 오너 지시 "A안으로 해줘"]

     배경: L2 상향 후에도 화면이 3개 밝기로 쪼개져 보였다.
           ① 툴바 --bg-card L*23.3  ② 목록 --dk-canvas L*17.0
           ③ 하단 빈 영역 --bg-main L*15.0  → ①③ 격차 8.3단계.
           아래쪽이 "푹 꺼진" 느낌의 원인.
     조치: 바탕 3개를 ①번 값(#2a384f L*23.3) 하나로 통일한다.
           또한 목록/간트만 중성 회색(#262a33)이라 톤이 겉돌던 문제도
           같은 네이비 계열로 흡수한다.

     ⚠️ 통일의 범위 — "바탕"만 통일한다. 그 위에 얹히는 면은 통일 대상이 아니다.
        바탕 = --bg-main / --bg-card / --dk-canvas 세 개뿐.
        아래 --dk-surface-0 / --dk-canvas-hd 는 바탕 위에 얹히는 "구분면"이라
        일부러 바탕보다 2.6단계 낮게 유지한다. 이것까지 같은 값으로 올리면
        간트 줄무늬 행과 타임라인 헤더가 통째로 사라진다(실측 Δ+0.0).

     ⚠️ 통일의 대가와 그 보정(반드시 함께 유지할 것):
        (1) 행 배경 == 바탕 이 되어 표 경계가 사라진다
            → --dk-line / --dk-line-soft 를 바탕 대비 1.3:1 이상으로 보강.
        (2) 바탕이 밝아지면 그 위 텍스트 대비가 떨어진다
            → 보조/3차 텍스트를 밝은 쪽으로 재보정 (본 파일 하단 및 gantt_v2.js).
     ⛔ 값 변경 시 src/index.tsx · gantt.html · gantt_v2.js 와 동시 수정. */
  --dk-base:      #16181d;  /* L*8.2 — ★ 단일 바탕 (툴바=목록=빈영역) */
  --dk-surface-0: #101218;  /* L*5.5 — 간트 줄무늬 행 / 타임라인 헤더. ★바탕이 아니다.
                               바탕보다 2.3단계 낮게 두어야 줄무늬가 보인다.
                               같은 값으로 올리면 Δ+0.0 → 줄무늬 소멸. */
  --dk-surface-1: #16181d;  /* L*8.2 — 카드/패널 (바탕과 동일: Δ0.0) */
  --dk-surface-2: #2c2e34;  /* L*19 — 상승면/호버 */
  --dk-surface-3: #3f4148;  /* L*27.6 — 보조 */
  --dk-canvas:    #16181d;  /* = --dk-base — 목록/간트 본문 */
  --dk-canvas-hd: #101218;  /* L*5.5 — 간트 헤더만 2.3단계 낮춰 열 구분 유지 */
  --dk-line:      #414449;  /* L*34.0 — 격자/테두리. 바탕 대비 1.59:1 */
  --dk-line-soft: #33363b;  /* L*28.9 — 행 구분선. 바탕 대비 1.32:1 */

  --bg-main: #16181d;       /* = --dk-base */
  --bg-card: #16181d;       /* = --dk-base */
  --border-main: #3b3d43;   /* L*31.8 — 바탕 대비 1.47:1 */
  --text-main: #d7d8db;     /* L*87.7 — 최악(최명) 행 대비 7.00:1 ✅AAA */
  
  /* [다크모드 밝기 상향 2026-08-19 / A2] primary-50/100 도 함께 상향.
     이유: 이 두 값은 각각 구 --bg-card(#1e293b) / 구 --border-main(#334155) 와
           "같은 값"이었다. 표면만 밝히고 이들을 그대로 두면 원래 동일 톤이던
           면끼리 6.9단계 차이가 생겨 화면에 어두운 "구멍"처럼 보인다.
           → 같은 ΔL* +7 을 적용해 원래의 동일 관계를 복원한다. */
  --color-primary-50: #16181d;  /* = --bg-card 와 동일 유지 (구 #1e293b) */
  --color-primary-100: #3b3d43; /* = --border-main 과 동일 유지 (구 #334155) */
  --color-primary-500: #60a5fa; /* 차분한 파스텔 블루 — 강조색은 무변경 */
  --color-primary-600: #3b82f6;
  --color-primary-700: #2563eb;
  
  /* Status Colors for Dark Mode (Pastel/Muted tones)
     ── 배지 "배경"만 표면 밝기를 따라간다 (전경 글자색은 무변경).
        검토/완료/막힘/진행중 배경(#78350f·#064e3b·#7f1d1d·#1e3a8a)은 이미
        새 카드(L*23.3)보다 밝아 그대로 두는 것이 대비상 유리하다 —
        올리면 전경 대비가 오히려 떨어진다(실측: 검토 5.43→4.18:1).
        반면 todo-light 만 구 카드색과 동일값이라 반드시 함께 올려야 한다. */
  --color-todo: #94a3b8;
  --color-todo-light: #16181d; /* = --bg-card 와 동일 유지 (구 #1e293b) */
  --color-in-progress: #60a5fa;
  --color-in-progress-light: #1e3a8a;
  --color-review: #fbbf24;
  --color-review-light: #78350f;
  /* [브랜드 우선 색 재배치 2026-09-19 · A1] 완료는 초록 -> 틸.
     정통 녹색(h146°)을 브랜드가 가져가면서 완료가 비켜났다. 설계 #1328 §3.2.
     ⛔ 에메랄드(#34d399/#064e3b)로 되돌리지 말 것 — 브랜드와 ΔE00 15.7 로 붙는다.
     대비: #2dd4bf on #134e4a = 5.09:1 (구 5.06:1 동등). 회귀 방지: BC-3/BC-4 */
  --color-done: #2dd4bf;     /* 틸 — h181.7° C*45.5 */
  --color-done-light: #134e4a;
  --color-blocked: #f87171;  /* 코랄 레드 */
  --color-blocked-light: #7f1d1d;
}

/* [다크모드 밝기 상향 2026-08-19 / A2] --dk-* 토큰을 `.dark` 클래스에도 이중 정의.
   이유: 위 블록은 `:root[data-theme="dark"]` 에만 걸리는데, 스타일 규칙 다수가
   `.dark xxx` 형태로도 선언되어 있다(간트 별도창 등 data-theme 이 늦게 붙는 경로 대비).
   토큰이 정의되지 않으면 var() 가 fallback 값으로 떨어져 밝기 단계가 어긋난다.
   ⛔ 위 :root[data-theme="dark"] 블록의 값과 반드시 동일하게 유지할 것. */
.dark {
  /* [A안 단일 바탕 통일 2026-08-22] 위 :root[data-theme="dark"] 와 동일값 */
  --dk-base:      #16181d;
  --dk-surface-0: #101218;
  --dk-surface-1: #16181d;
  --dk-surface-2: #2c2e34;
  --dk-surface-3: #3f4148;
  --dk-canvas:    #16181d;
  --dk-canvas-hd: #101218;
  --dk-line:      #414449;
  --dk-line-soft: #33363b;
}

/* Status Colors (Common) */
:root {
  /* Status Colors */
  --color-todo: #94a3b8;
  --color-todo-light: #f1f5f9;
  --color-in-progress: #3b82f6;
  --color-in-progress-light: #dbeafe;
  --color-review: #f59e0b;
  --color-review-light: #fef3c7;
  /* [브랜드 우선 색 재배치 2026-09-19 · A1] 완료는 초록 -> 틸. 설계 #1328 §3.2.
     ⛔ 에메랄드(#10b981/#d1fae5)로 되돌리지 말 것 — 브랜드 #16a34a 와 ΔE00 10.1 로 붙는다.
     ⚠️ 위 :root[data-theme="dark"] 블록에도 같은 이름이 있다. 전역 치환 금지 —
        블록을 한정해 고칠 것(#1280 §5.1 / 2026-09-18 실사고).
     대비: #0d9488 on #ccfbf1 = 3.32:1 (구 2.24:1 대비 개선). 회귀 방지: BC-3/BC-4 */
  --color-done: #0d9488;
  --color-done-light: #ccfbf1;
  --color-blocked: #ef4444;
  --color-blocked-light: #fee2e2;
  
  /* Shadows */
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
  --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
  --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
  --shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.15);
  
  /* Border Radius */
  --radius-sm: 4px;
  --radius-md: 6px;
  --radius-lg: 8px;
  --radius-xl: 12px;
}

/* 전역 폰트 설정 */
* {
  font-family: 'Pretendard', 'Inter', -apple-system, BlinkMacSystemFont, 
               'Segoe UI', 'Apple SD Gothic Neo', 'Roboto', 'Helvetica Neue', 
               Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* 숫자/영문 전용 (작업ID, 진행율, 날짜 등) */
.task-id,
.progress-text,
.task-number,
input[type="number"],
.font-mono {
  font-family: 'Inter', 'Pretendard', 'Courier New', monospace;
  font-feature-settings: 'tnum' 1; /* 숫자 고정폭 */
}

/* 제목 폰트 */
h1, h2, h3, h4, h5, h6 { 
  font-family: 'Pretendard', 'Inter', -apple-system, BlinkMacSystemFont, sans-serif !important;
}

h1 {
  font-weight: 700;
}

h2 {
  font-weight: 700;
}

h3 {
  font-weight: 600;
}

h4 {
  font-weight: 600;
}

/* ===================================
   폰트 크기 & 굵기 최적화
   =================================== */

/* 기본 본문 크기 */
body {
  font-size: 0.8125rem; /* 13px */
  font-weight: 400;
  line-height: 1.5;
  background-color: var(--bg-main);
  color: var(--text-main);
  transition: background-color 0.3s ease, color 0.3s ease;
}

/* 헤더 텍스트 */
.header-text {
  font-size: 0.8125rem; /* 13px */
  font-weight: 500; /* Medium */
}

/* 테이블 셀 기본 */
.table-cell-text {
  font-size: 0.8125rem; /* 13px */
  font-weight: 400; /* Regular */
}

/* 작업 제목 */
.task-title {
  font-size: 0.8125rem; /* 13px */
  font-weight: 500; /* Medium */
}

/* 작업 ID (숫자 강조) */
.task-id-text {
  font-size: 0.75rem; /* 12px */
  font-weight: 600; /* Semi-bold */
}

/* 상태 뱃지 */
.badge-text {
  font-size: 0.6875rem; /* 11px */
  font-weight: 600; /* Semi-bold */
}

/* 진행율 텍스트 */
.percentage-text {
  font-size: 0.75rem; /* 12px */
  font-weight: 600; /* Semi-bold */
}

/* 버튼 텍스트 */
button {
  font-weight: 500; /* Medium */
  font-family: 'Pretendard', 'Inter', sans-serif;
}

/* ===================================
   모달 & 폼 요소 폰트
   =================================== */

/* 모달 제목 */
.modal-title,
h3.text-xl,
h4.text-sm {
  font-family: 'Pretendard', 'Inter', sans-serif !important;
  font-weight: 600;
}

/* 모달 레이블 */
label {
  font-family: 'Pretendard', 'Inter', sans-serif !important;
  font-weight: 500;
  font-size: 0.8125rem;
}

/* 입력 필드 */
input,
textarea,
select {
  font-family: 'Pretendard', 'Inter', sans-serif !important;
  font-size: 0.8125rem;
  font-weight: 400;
}

/* placeholder 텍스트 */
input::placeholder,
textarea::placeholder {
  font-family: 'Pretendard', 'Inter', sans-serif !important;
  font-weight: 400;
}

/* 버튼 내부 아이콘 + 텍스트 */
button i {
  font-family: 'Font Awesome 5 Free', 'Font Awesome 5 Brands' !important;
}

/* FontAwesome 아이콘 글리프 폰트 보호.
   전역 .text-sm / .text-xs / .text-lg 규칙이 font-family: 'Pretendard' !important 를
   강제하는데, 클래스 선택자(0,1,0)가 button i(0,0,2)보다 특정도가 높아
   아이콘 <i class="... text-sm"> 에서 아이콘 폰트가 Pretendard 로 덮어써져
   글리프(\f142 등)가 깨져 '가로선'처럼 보였음(#946 재발 원인).
   아래 규칙은 .fa* 아이콘 클래스와 크기 유틸을 함께 지정해 특정도(0,2,x)를
   확보하고, CDN(FA6)이 실제 로드하는 5/6 Free·Brands 패밀리를 모두 나열해
   어떤 스타일이든 아이콘 폰트가 유지되도록 한다. */
i.fa,
i.fas,
i.far,
i.fab,
i.fal,
i.fa-solid,
i.fa-regular,
i.fa-brands,
button i.fas,
button i.far,
button i.fab {
  font-family: 'Font Awesome 6 Free', 'Font Awesome 6 Brands',
               'Font Awesome 5 Free', 'Font Awesome 5 Brands', 'FontAwesome' !important;
}

/* 옵션 텍스트 */
option {
  font-family: 'Pretendard', 'Inter', sans-serif !important;
  font-size: 0.8125rem;
}

/* Tailwind CSS 클래스 오버라이드 */
.text-xl {
  font-family: 'Pretendard', 'Inter', sans-serif !important;
}

.text-2xl {
  font-family: 'Pretendard', 'Inter', sans-serif !important;
}

.text-lg {
  font-family: 'Pretendard', 'Inter', sans-serif !important;
}

.text-sm {
  font-family: 'Pretendard', 'Inter', sans-serif !important;
  font-size: 0.8125rem !important; /* 13px */
}

.text-xs {
  font-family: 'Pretendard', 'Inter', sans-serif !important;
  font-size: 0.75rem !important; /* 12px */
}

/* 폰트 굵기 조정 */
.font-bold {
  font-weight: 600 !important; /* Bold → Semi-bold */
}

.font-semibold {
  font-weight: 600 !important;
}

.font-medium {
  font-weight: 500 !important;
}

/* 모든 텍스트 요소 */
p, span, div, a {
  font-family: 'Pretendard', 'Inter', sans-serif;
}

/* ===================================
   간트 차트 스타일 (frappe-gantt v0.10.0)
   =================================== */

/* ===================================
   작업 목록(Task List) 레이아웃 복구
   =================================== */

/* 작업 목록 컨테이너 (PC 기본) */
#task-list-container {
  width: 30%;
  min-width: 300px;
  max-width: 500px;
  background: white;
  border-right: 2px solid #dee2e6;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* 모바일 전용 스타일 격리 */
@media (max-width: 1024px) {
  #task-list-container {
    width: 40%; /* 모바일에서는 조금 더 넓게 */
    max-width: none;
  }
}

/* 간트 차트 배경 */
.gantt-container {
  font-family: 'Pretendard', 'Inter', sans-serif !important;
}

/* 작업 바 스타일 */
.gantt .bar-todo {
  fill: #cbd5e1 !important; /* 회색 - 할일 */
}

.gantt .bar-progress {
  fill: #3b82f6 !important; /* 파랑 - 진행중 */
}

.gantt .bar-done {
  fill: #0d9488 !important; /* [2026-09-19 · A1] 틸 - 완료. 구 #10b981 은 브랜드와 ΔE00 10.1 로 붙었다 */
}

/* 프로젝트 타임라인 바 (범위 표시용) */
.gantt .bar-project-timeline {
  fill: #f59e0b !important; /* 주황 - 프로젝트 전체 기간 */
  opacity: 0.3 !important;
  stroke: #f59e0b !important;
  stroke-width: 2 !important;
  stroke-dasharray: 5, 5 !important; /* 점선 */
}

.gantt .bar-project-timeline .bar-label {
  font-weight: 700 !important;
  fill: #d97706 !important;
  font-size: 13px !important;
}

/* 진행율 표시 */
.gantt .bar-progress-wrapper {
  fill: rgba(59, 130, 246, 0.2) !important;
}

/* 호버 효과 */
.gantt .bar-wrapper:hover .bar {
  opacity: 0.85;
  filter: brightness(0.95);
}

/* 작업명 텍스트 */
.gantt .bar-label {
  font-family: 'Pretendard', 'Inter', sans-serif !important;
  font-size: 12px !important;
  font-weight: 500 !important;
  fill: #333 !important;
}

/* 날짜 헤더 */
.gantt .upper-text,
.gantt .lower-text {
  font-family: 'Pretendard', 'Inter', sans-serif !important;
  font-size: 12px !important;
  fill: #666 !important;
}

/* 그리드 라인 */
.gantt .grid-row {
  fill: #fafafa;
}

.gantt .grid-row:nth-child(even) {
  fill: #f5f5f5;
}

/* 오늘 날짜 하이라이트 */
.gantt .today-highlight {
  fill: rgba(59, 130, 246, 0.1) !important;
}

/* 팝업 스타일 */
.gantt-popup {
  font-family: 'Pretendard', 'Inter', sans-serif !important;
  background: white;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  padding: 12px;
  box-shadow: var(--shadow-md);
}

/* 카드 배경 변수 적용 */
.task-card {
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  cursor: grab;
  border-radius: var(--radius-lg);
  background: var(--bg-card);
  border: 1px solid var(--border-main);
  position: relative;
  overflow: hidden;
}

.task-card::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: 4px;
  height: 100%;
  background: transparent;
  transition: all 0.3s;
}

.task-card:hover {
  transform: translateY(-4px);
  box-shadow: var(--shadow-xl);
  border-color: var(--color-primary-500);
}

.task-card:hover::before {
  background: linear-gradient(180deg, var(--color-primary-500), var(--color-primary-600));
}

.task-card:active {
  cursor: grabbing;
  transform: translateY(-2px) scale(0.98);
  box-shadow: var(--shadow-lg);
}

/* 드래그 중인 카드 */
.task-card.opacity-50 {
  opacity: 0.5;
}

/* 드롭 존 하이라이트 */
[data-status].bg-blue-50 {
  background-color: #eff6ff !important;
  transition: background-color 0.2s ease;
}

/* 프로젝트 아이템 */
.project-item {
  transition: background-color 0.2s ease;
}

/* 모달 애니메이션 */
@keyframes fadeIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes slideUp {
  from {
    transform: translateY(20px);
    opacity: 0;
  }
  to {
    transform: translateY(0);
    opacity: 1;
  }
}

.fixed.bg-black {
  animation: fadeIn 0.2s ease;
}

.bg-white.rounded-lg.shadow-xl {
  animation: slideUp 0.3s ease;
}

/* ===================================
   Phase 1: ClickUp 스타일 헤더 + 행
   =================================== */

/* 테이블 헤더 스타일 - 제거 (이제 인라인으로 처리) */

/* 작업 행 스타일
   ── [리스트뷰 노션형 리디자인 2026-07-15 · A1] ───────────────────────
   목표: "엑셀도 카드도 아닌 애매함" → 노션풍 담백한 표.
   - 카드 요소(그림자·둥근모서리·행간격 2px)는 JS 인라인에서 제거함(app-task-render.js).
   - 여기서 행 높이를 단일 관리(과거 CSS 32px vs 인라인 38px 불일치 해소).
   - 구분선은 노션톤의 아주 옅은 라인으로 낮춰 격자 무게를 줄인다.
   - 진척 상태 배경색(getTaskRowBackgroundColor, !important)은 그대로 유지(기능). */
.task-row {
  border-bottom: 1px solid rgba(15, 23, 42, 0.06); /* 노션톤 옅은 구분선 */
  transition: background-color 0.12s ease;
  font-size: 0.8125rem; /* 13px */
  font-weight: 400;
  min-height: 36px; /* 행 높이 단일 관리 (노션풍 여백) */
  height: 36px;
}

/* [노션형 2026-07-15 · A1] 라이트모드 호버 — 상태 배경색(!important) 위에
   얹히도록 배경 대신 옅은 오버레이(inset box-shadow)를 사용.
   (일반 background 는 상태색 !important 에 가려져 안 먹음) */
.task-row:hover {
  box-shadow: inset 0 0 0 9999px rgba(15, 23, 42, 0.035);
}

/* [P1] 작업 완료 체크박스 (작업번호 앞)
   설계: docs/proposals/gantt-task-completion.md (§5.1) */
.task-complete-check {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  min-width: 18px;
  height: 18px;
  margin-right: 4px;
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
  user-select: none;
  color: #9ca3af;
  transition: color 0.12s ease, transform 0.08s ease;
}
/* [브랜드 우선 색 재배치 2026-09-19 · A1] #16a34a -> 틸. 설계 #1328 §3.2.
   구 값은 브랜드 기준색과 **완전히 같은 값**이었다(ΔE00 0.0) — 완료 체크가
   브랜드 버튼과 구분되지 않는다. 완료 의미는 틸로 비켜난다. */
.task-complete-check:hover { color: #0d9488; transform: scale(1.1); }
.task-complete-check.checked { color: #0d9488; }
.task-complete-check.disabled {
  cursor: not-allowed;
  opacity: 0.35;
}
.task-complete-check.disabled:hover { color: #9ca3af; transform: none; }
:root[data-theme="dark"] .task-complete-check,
.dark .task-complete-check { color: #6b7280; }
:root[data-theme="dark"] .task-complete-check.checked,
.dark .task-complete-check.checked { color: #2dd4bf; } /* [2026-09-19 · A1] 라이트와 같은 이유 — 틸. 바탕 대비 6.41:1 */

/* [P0] 트리 Drag & Drop — 드래그 중 / 드롭 대상 강조
   설계: docs/proposals/gantt-tree-drag-drop.md (§5.1~5.2) */
.task-row[draggable="true"] {
  cursor: grab;
}
.task-row.dnd-dragging {
  opacity: 0.45;
}
.task-row.dnd-over {
  box-shadow: inset 0 0 0 9999px rgba(59, 130, 246, 0.12),
              inset 3px 0 0 0 #3b82f6 !important;
}
:root[data-theme="dark"] .task-row.dnd-over,
.dark .task-row.dnd-over {
  box-shadow: inset 0 0 0 9999px rgba(59, 130, 246, 0.22),
              inset 3px 0 0 0 #60a5fa !important;
}
/* 임계경로 행은 이미 붉은 inset 을 쓰므로 호버 오버레이가 그걸 덮지 않게 제외
   (임계 표시 우선). 대신 살짝 더 진한 테두리 힌트로 호버감만 준다. */
.task-row.is-critical-path:hover {
  box-shadow: inset 0 0 0 9999px rgba(229, 72, 77, 0.10);
}

/* 🔴 임계경로(Critical Path) 행 — 좌측 빨강 막대.
   border-left(레이아웃 밀림)/inset box-shadow(자식 배경에 가려짐) 대신
   절대배치 ::before 로 그려, 칸을 밀지 않으면서 자식 배경 위에 항상 보이게 한다. */
.task-row.is-critical-path::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 3px;
  background-color: #e5484d;
  border-top-left-radius: 4px;
  border-bottom-left-radius: 4px;
  z-index: 5;
  pointer-events: none;
}

/* ⛔ [지연 좌측 막대 제거 2026-09-09 / A3 · 오너 지시 "빨간바는 제거해줘"]
   `.task-row.is-delayed-row` / `::after` / 다크 오버라이드를 제거했다.

   왜 넣었다가 뺐나 — 설계 의도가 실제로 성립하지 않았다.
     지연 배경(#4a3a4d)은 다크 바탕 대비 1.13:1 로 다른 4색(1.31~1.34:1)보다
     낮아, "색이 부족하니 형태로 보완한다"는 취지로 좌측 막대를 넣었다
     (설계서 §3.4). 그런데 **임계경로 막대와 구별되지 않았다.**

       | 항목      | 임계경로   | 지연(제거됨) |
       |-----------|-----------|-------------|
       | 가상요소  | ::before  | ::after     |
       | 위치      | left: 0   | left: 0  ← 완전히 같은 자리 |
       | 폭        | 3px       | 4px         |
       | z-index   | 5 (위)    | 4 (아래)    |
       | 색(라이트)| #e5484d   | #dc2626  ← 두 색 대비 1.23:1 |

     즉 ① 같은 자리에 ② 거의 같은 빨강으로 겹쳤고, 둘 다 해당하는 행에서는
     임계경로가 위에 덮여 지연은 **1px 만 삐져나왔다**. 사용자는 "지연"인지
     "임계경로"인지 알 수 없다 — 형태로 구분한다는 전제가 무너진 것이다.

   ⛔ 되살리지 말 것. 좌측 0 위치는 이미 3개가 경쟁한다:
      임계경로(::before) · 고정 마일스톤(::after) · 선택/호버 오버레이.
      지연을 형태로 표현하려면 **좌측이 아닌 다른 채널**(예: 상태 배지의
      아이콘 🐢 을 행에 병기)을 써야 한다. → gaps.md M-20 에서 별건 추적.

   ℹ️ 지연 **배경색**(#4a3a4d)은 그대로 유지된다. 제거된 것은 막대뿐이다. */

/* ══════════════════════════════════════════════════════════════════
   🟢🔴 선후행 크로스하이라이트 (app-cross-highlight.js)
   ──────────────────────────────────────────────────────────────────
   - 작업 선택 시 선행=녹색 / 후행=빨강 테두리(outline).
   - 색상 충돌 회피:
     · 임계경로는 좌측 3px 세로 막대(::before) → 여기선 행 전체 outline → 위치가 달라 공존.
     · 본인(선택)=파랑 outline(.task-row-selected) → 선행/후행에는 적용 안 하므로 충돌 없음.
     · 보이는 선후행 = 진한 실선 outline 2px / 접힌 그룹에 숨은 선후행 = 조상 행에 연한 점선 1.5px.
   - outline 은 레이아웃을 밀지 않아 행 높이/칸 정렬에 영향 없음.
   ══════════════════════════════════════════════════════════════════ */
/* 선행(화면에 보임) — 진한 틸 실선
   [브랜드 우선 색 재배치 2026-09-19 · A1] 구 #16a34a 는 브랜드 기준색과 같은 값이라
   "선행 관계"인지 "브랜드 강조"인지 구분되지 않았다. 관계선은 틸로 비켜난다.
   ⚠️ 후행(#dc2626 빨강)과의 대비 관계는 그대로다 — 틸 vs 빨강 ΔE00 60.1. 설계 #1328 §3.2 */
.task-row.xh-pred {
  outline: 2px solid #0d9488 !important;
  outline-offset: -2px;
}
/* 후행(화면에 보임) — 진한 빨강 실선 (임계경로 좌측 막대와 위치가 달라 겹치지 않음) */
.task-row.xh-succ {
  outline: 2px solid #dc2626 !important;
  outline-offset: -2px;
}
/* 선행이 접힌 그룹 안에 숨음 → 그 조상 행에 연한 틸 점선 (힌트) */
.task-row.xh-pred-hidden {
  outline: 1.5px dashed #5eead4 !important; /* [2026-09-19 · A1] 위 실선과 같은 계열(틸)로 맞춘다 */
  outline-offset: -2px;
}
/* 후행이 접힌 그룹 안에 숨음 → 그 조상 행에 연한 빨강 점선 (힌트) */
.task-row.xh-succ-hidden {
  outline: 1.5px dashed #fca5a5 !important;
  outline-offset: -2px;
}
/* 다크 모드: 채도를 약간 낮춘 톤 */
:root[data-theme="dark"] .task-row.xh-pred { outline-color: #2dd4bf !important; } /* [2026-09-19 · A1] 틸 */
:root[data-theme="dark"] .task-row.xh-succ { outline-color: #ef4444 !important; }
:root[data-theme="dark"] .task-row.xh-pred-hidden { outline-color: rgba(94, 234, 212, 0.7) !important; } /* [2026-09-19 · A1] 틸 */
:root[data-theme="dark"] .task-row.xh-succ-hidden { outline-color: rgba(252, 165, 165, 0.7) !important; }

/* 🦓 Zebra Stripe: 짝수 행을 약간 어둡게 */
.task-hierarchical:nth-child(even) .task-row {
  filter: brightness(0.98);
}

/* 일반 행의 z-index를 낮게 설정 */
.task-hierarchical {
  position: relative;
  z-index: 1;
}

/* ──────────────────────────────────────────────────────────────
   [마일스톤 행고정 2026-06-17 / 2026-06-18 / 2026-06-19 개선] 엑셀 '틀고정'처럼
   마일스톤 그룹 + 개별 마일스톤 행을 세로 스크롤 시 스크롤 컨테이너 상단에 고정.
   - 마일스톤과 작업 간 선후행(의존성) 연결 시 마일스톤이 스크롤로
     사라지지 않게 하여 작업 편의를 높인다.
   - position: sticky 는 가장 가까운 스크롤 조상(=세로 스크롤 컨테이너)
     기준으로 동작한다.
   - [2026-06-18] 여러 행이 모두 top:0 이면 겹쳐서 1줄만 보이던 버그 수정:
     JS(renderFlatTask)가 각 고정 행에 계단식 top( index * rowHeight )을
     인라인으로 부여해 차곡차곡 쌓이게 한다. → CSS 에서 top 을 고정하지 않는다.
   - [2026-06-19] 그룹 행(is_milestone_group)도 고정한다(markStickyMilestones 가
     펼쳐진 그룹에 _msStickyIndex 를 부여 → .milestone-sticky 클래스가 붙음).
   - [2026-06-19] '틈새로 비쳐 보임' 수정: 행 래퍼(.task-hierarchical) 높이는
     rowHeight(40px)인데 안쪽 .task-row 는 38px + margin-bottom 2px 였다.
     그 2px 틈으로 아래 스크롤 작업이 비쳤다. → 래퍼 자체에 불투명 배경을 깔아
     틈새까지 가린다(아래 .milestone-sticky 배경 규칙).
   - z-index 를 올려 항상 위에 보이도록 한다.
   ────────────────────────────────────────────────────────────── */
.task-hierarchical.milestone-sticky {
  position: -webkit-sticky;
  position: sticky;
  /* top 은 JS 가 인라인으로 부여(계단식 누적). CSS 에서 고정하지 않는다. */
  z-index: 20; /* 일반 행(z-index:1)·선택 행(z-index:1) 위에 항상 표시 */
  /* [2026-06-19] 래퍼 자체를 불투명하게 — 행 사이 margin(2px) 틈으로 아래
     스크롤 작업이 비쳐 보이지 않게 한다. .task-row 배경과 동일 톤. */
  background-color: #f5f3ff !important; /* purple-50 */
}

/* 고정된 마일스톤 행의 배경: 아래 행이 비쳐 보이지 않도록 불투명 처리 +
   마일스톤임을 알리는 옅은 보라 강조(브랜드 마일스톤 색과 통일). */
.task-hierarchical.milestone-sticky .task-row {
  background-color: #f5f3ff !important; /* purple-50 */
  background-image: none !important;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08) !important;
  border-bottom: 1px solid rgba(124, 58, 237, 0.25); /* purple-600 alpha */
}

/* 고정 행 좌측에 마일스톤 강조 막대(보라) — 일반/임계경로 막대와 구분 */
.task-hierarchical.milestone-sticky .task-row::after {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 3px;
  background-color: #7c3aed; /* purple-600 */
  border-top-left-radius: 4px;
  border-bottom-left-radius: 4px;
  z-index: 6;
  pointer-events: none;
}

/* 다크 모드: 고정 마일스톤 행 배경을 어둡게(보라 계열) */
:root[data-theme="dark"] .task-hierarchical.milestone-sticky .task-row,
.dark .task-hierarchical.milestone-sticky .task-row {
  background-color: #2e1065 !important; /* purple-950 */
  border-bottom-color: rgba(167, 139, 250, 0.35);
}

/* 다크 모드: 래퍼 자체 배경도 어둡게 — 행 사이 틈새 비침 방지 */
:root[data-theme="dark"] .task-hierarchical.milestone-sticky,
.dark .task-hierarchical.milestone-sticky {
  background-color: #2e1065 !important; /* purple-950 */
}

/* 고정 행에서는 zebra-stripe(밝기 보정)가 배경을 흐리지 않도록 무효화 */
.task-hierarchical.milestone-sticky:nth-child(even) .task-row {
  filter: none;
}

/* 🎨 행 배경색이 보이도록 모든 하위 요소를 투명하게 */
.task-row > div,
.task-row > div > div,
.task-row .resizable-column,
.task-row .editable-field {
  background-color: transparent !important;
}

/* hover 효과만 유지 (약한 강조) */
.task-row .editable-field:hover {
  background-color: rgba(0, 0, 0, 0.03) !important;
  outline: 1px solid rgba(0, 0, 0, 0.05);
}

/* 선택된 행 강조 - outline + 그림자 조합 */
.task-row-selected {
  outline: 2px solid #1976d2 !important;
  outline-offset: -2px;
  box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.2) inset !important;
  position: relative;
  z-index: 1;
}

/* 라디오 버튼 커스텀 */
.task-radio {
  width: 18px;
  height: 18px;
  cursor: pointer;
  accent-color: #228be6;
}

/* 체크박스 커스텀 (향후 사용) */
.task-checkbox {
  width: 18px;
  height: 18px;
  cursor: pointer;
  accent-color: #228be6;
  border-radius: 4px;
}

/* 셀 패딩 */
.task-cell {
  padding: 4px 10px; /* Compact Mode - 상하 패딩 축소 */
  font-size: 0.8125rem; /* 13px */
  font-family: 'Pretendard', 'Inter', sans-serif;
  color: #212529;
  vertical-align: middle;
  letter-spacing: -0.01em; /* 한글 자간 조정 */
}

/* 계층 들여쓰기 */
.task-indent {
  display: inline-block;
}

/* 접기/펼치기 아이콘 */
.toggle-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  cursor: pointer;
  color: #5c7cfa; /* 더 밝고 예쁜 블루 */
  transition: all 0.2s ease;
  border-radius: 4px;
  background-color: rgba(92, 124, 250, 0.08); /* 연한 블루 배경 */
}

.toggle-icon:hover {
  background-color: rgba(92, 124, 250, 0.15); /* 호버 시 더 진한 블루 */
  color: #4263eb; /* 더 진한 블루 텍스트 */
  transform: scale(1.1); /* 살짝 확대 효과 */
}

.toggle-icon.expanded i::before {
  content: "\f068"; /* fa-minus */
}

.toggle-icon:not(.expanded) i::before {
  content: "\f067"; /* fa-plus */
}

/* [세부칼럼(시각) 토글 2026-07-20 · A3] 일자 헤더에 붙는 시각 펼침/접힘 아이콘.
   Tree(.toggle-icon)와 동일한 박스 모양을 재사용하되, 헤더 크기에 맞게 살짝 축소.
   .expanded 여부에 따라 fa-plus / fa-minus 는 위의 공용 규칙이 그대로 적용된다. */
.time-col-toggle.toggle-icon {
  width: 16px;
  height: 16px;
  vertical-align: middle;
}
.time-col-toggle.toggle-icon i {
  font-size: 9px;
}
/* 접혀 있는데 해당 시각 값이 존재하면 '값 있음'을 진한 파랑으로 알림 */
.time-col-toggle.toggle-icon.time-toggle-has-value {
  color: #4263eb;
  background-color: rgba(66, 99, 235, 0.16);
}

/* 작업 ID/이름 링크 스타일 */
.task-link {
  color: #1976d2; /* 기본: 더 진한 파랑 */
  text-decoration: none;
  font-weight: 500;
  font-family: 'Pretendard', 'Inter', sans-serif;
  font-size: 0.8125rem; /* 13px */
  letter-spacing: -0.01em;
  transition: color 0.15s ease;
  /* 말줄임표 처리 */
  display: inline-block;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: middle;
}

/* 다크 모드 전용 링크 색상 (밝은 회색/흰색)
   [화이트 제거 2026-09-09 / A3] 짝이었던 `[data-theme="white"] .task-link` 제거 (§4 #2) */
[data-theme="dark"] .task-link {
  color: #e5e7eb; /* gray-200 */
}

.task-link:hover {
  color: #1565c0; /* 호버 시 더 어두운 파랑 */
  text-decoration: none;
}

/* 다크 모드 호버 색상 ([화이트 제거 2026-09-09 / A3] white 분기 제거 — §4 #3) */
[data-theme="dark"] .task-link:hover {
  color: #ffffff; /* white */
}

/* 편집 가능 필드 호버 */
.editable-field {
  cursor: pointer;
  padding: 6px 8px; /* 패딩 증가 */
  border-radius: 0; /* 4px -> 0 */
  transition: background-color 0.15s ease, outline 0.15s ease;
  /* 말줄임표 처리 */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.editable-field:hover {
  background-color: #f8f9fa;
  outline: 1px solid #e9ecef;
}

/* ===================================
   Phase 2: 상태/진행율 시각화
   =================================== */

/* ========================================
   Status Badges — Subtle (2026-06-07)
   리스트뷰 '상태' 컬럼이 버튼/알약처럼 너무 튀어 주변(평문 셀)과
   이질감이 컸다. 색상으로 상태 구분은 유지하되, 버튼처럼 보이게 하던
   요소(컬러 테두리·그림자·그라데이션·대문자·자간·pulse 애니메이션·hover
   부양)를 걷어내 "은은한 색 태그"로 낮춘다.
   ======================================== */
.status-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 8px;
  border-radius: 6px;
  font-size: 0.6875rem; /* 11px */
  font-weight: 600;
  font-family: 'Pretendard', 'Inter', sans-serif;
  line-height: 1.4;
  /* 대문자/넓은 자간/그림자/테두리 제거 — 평문 셀들과 톤을 맞춤 */
  border: 1px solid transparent;
  box-shadow: none;
  transition: background-color 0.15s, color 0.15s;
}

/* 편집 가능한 셀에 마우스를 올렸을 때만 아주 옅게 반응 (부양/그림자 없음) */
.editable-field:hover .status-badge {
  filter: brightness(0.97);
}

/* 상태별 — 옅은 단색 배경 + 진한 글자색 (그라데이션/컬러 테두리 제거) */
.status-not-started, .status-todo {
  background: var(--color-todo-light);
  color: var(--color-todo);
}

.status-in-progress {
  background: var(--color-in-progress-light);
  color: var(--color-in-progress);
  /* pulse 애니메이션 제거 — 시선을 과도하게 끌던 주범 */
}

.status-completed, .status-done {
  background: var(--color-done-light);
  color: var(--color-done);
}

.status-on-hold {
  background: var(--color-review-light);
  color: var(--color-review);
}

.status-blocked {
  background: var(--color-blocked-light);
  color: var(--color-blocked);
}

/* ========================================
   Progress Bar - ClickUp Style
   ======================================== */
.progress-container {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 4px 0;
}

.progress-bar-wrapper {
  flex: 1;
  height: 12px;
  background: linear-gradient(to right, #f3f4f6, #e5e7eb);
  border-radius: 6px;
  overflow: hidden;
  box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.06);
  position: relative;
}

.progress-bar {
  height: 100%;
  border-radius: 6px;
  transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
  position: relative;
  overflow: hidden;
}

/* 반짝임 효과 */
.progress-bar::after {
  content: '';
  position: absolute;
  top: 0;
  left: -100%;
  width: 100%;
  height: 100%;
  background: linear-gradient(
    90deg,
    transparent,
    rgba(255, 255, 255, 0.5),
    transparent
  );
  animation: shimmer 2s infinite;
}

@keyframes shimmer {
  0% { left: -100%; }
  100% { left: 100%; }
}

/* 진행률별 그라데이션 색상 */
.progress-low {
  background: linear-gradient(90deg, #ef4444, #dc2626);
}

.progress-medium {
  background: linear-gradient(90deg, #f59e0b, #d97706);
}

/* [브랜드 우선 색 재배치 2026-09-19 · A1] 진척 막대의 "높음/완료"도 완료 의미다 -> 틸.
   구 #10b981/#059669 는 브랜드와 각각 ΔE00 10.1 / 9.5 로 붙었다. */
.progress-high {
  background: linear-gradient(90deg, #0d9488, #0f766e);
}

.progress-complete {
  background: linear-gradient(90deg, #0d9488, #0f766e);
}

.progress-medium {
  background-color: #ffa726; /* 따뜻한 주황 */
}

.progress-high {
  background-color: #66bb6a; /* 신선한 초록 */
}

.progress-complete {
  background-color: #43a047; /* 진한 초록 */
}

.progress-text {
  font-size: 0.75rem;
  font-weight: 600;
  font-family: 'Inter', 'Pretendard', sans-serif;
  min-width: 36px;
  text-align: right;
  font-feature-settings: 'tnum' 1; /* 숫자 고정폭 */
}

/* ===================================
   칼럼 설정 - 드래그앤드롭 개선
   =================================== */

/* 컬럼 아이템 기본 스타일 */
.column-item {
  transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
  user-select: none;
  position: relative;
}

/* 드래그 핸들 강조 */
.column-item .drag-handle {
  cursor: grab;
  transition: all 0.2s ease;
}

.column-item .drag-handle:hover {
  color: #2563eb !important;
  transform: scale(1.1);
}

.column-item .drag-handle:active {
  cursor: grabbing;
  transform: scale(1.05);
}

/* 드래그 중 스타일 */
.column-item.dragging {
  opacity: 0.5 !important;
  transform: scale(0.95);
  box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.2);
  border-color: #3b82f6;
  background-color: #eff6ff;
}

/* 드롭 타겟 스타일 */
.column-item.drag-over {
  border-color: #3b82f6 !important;
  background-color: #dbeafe !important;
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
}

.column-item.drag-over::before {
  content: '';
  position: absolute;
  top: -2px;
  left: 0;
  right: 0;
  height: 2px;
  background: linear-gradient(90deg, #3b82f6, #60a5fa);
  animation: slideIn 0.2s ease;
}

@keyframes slideIn {
  from {
    width: 0;
    opacity: 0;
  }
  to {
    width: 100%;
    opacity: 1;
  }
}

/* ===================================
   프로젝트 리스트 드래그앤드롭
   =================================== */

/* 프로젝트 아이템 기본 스타일 */
.project-item {
  transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
  user-select: none;
  position: relative;
}

/* 드래그 핸들 스타일 */
.project-item .drag-handle {
  cursor: grab;
  transition: all 0.2s ease;
  opacity: 0.6;
}

.project-item:hover .drag-handle {
  opacity: 1;
}

.project-item .drag-handle:hover {
  color: #2563eb !important;
  transform: scale(1.15);
}

.project-item .drag-handle:active {
  cursor: grabbing;
  transform: scale(1.1);
}

/* 드래그 중 스타일 */
.project-item.dragging {
  opacity: 0.5;
  transform: scale(0.98);
  box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.2);
  border-left: 4px solid #3b82f6 !important;
}

/* 드롭 타겟 스타일 */
.project-item.drag-over {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
  background-color: rgba(59, 130, 246, 0.05) !important;
}

.project-item.drag-over::before {
  content: '';
  position: absolute;
  top: -2px;
  left: 0;
  right: 0;
  height: 3px;
  background: linear-gradient(90deg, #3b82f6, #60a5fa);
  animation: slideIn 0.2s ease;
  border-radius: 2px;
}

/* 다크 모드 프로젝트 드래그앤드롭 */
.dark .project-item.drag-over {
  background-color: rgba(59, 130, 246, 0.1) !important;
}

/* 포/백워드 아이콘 */
.base-date-icon {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 0.75rem;
  color: #495057;
}

.base-date-icon i {
  color: #868e96;
}

/* 담당자/생성자 아바타 스타일 (향후 구현) */
.user-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background-color: #228be6;
  color: white;
  font-size: 0.625rem;
  font-weight: 600;
  text-transform: uppercase;
}

/* ===================================
   아이콘 스타일
   =================================== */

/* 필드 내부 아이콘 */
.fa-user,
.fa-user-circle,
.fa-clock,
.fa-calendar-day,
.fa-calendar-check,
.fa-exclamation-circle {
  font-size: 0.625rem; /* 10px */
  opacity: 0.7;
  transition: opacity 0.15s ease;
}

/* 호버 시 아이콘 강조 */
.editable-field:hover .fa-user,
.editable-field:hover .fa-clock,
.editable-field:hover .fa-calendar-day,
.editable-field:hover .fa-calendar-check {
  opacity: 1;
}

/* 경고 아이콘 (마감일 지남) */
.fa-exclamation-circle {
  opacity: 1;
  animation: pulse 2s infinite;
}

@keyframes pulse {
  0%, 100% {
    opacity: 1;
  }
  50% {
    opacity: 0.6;
  }
}

/* ===================================
   Phase 3: 계층 표시 개선
   =================================== */

/* 🎨 계층별 배경색 제거 (실적 칼라링 우선) */
/* 실적 상태에 따른 배경색이 JavaScript에서 inline style로 적용됨 */

/* 드래그 핸들 개선 */
.drag-handle {
  cursor: grab;
  transition: opacity 0.2s ease;
}

.task-row:hover .drag-handle {
  opacity: 1;
}

/* 오른쪽 고정 선택 칼럼 hover 배경 */
.task-row:hover .select-column-sticky {
  background-color: #f9fafb !important;
}

/* 작업 ID 스타일 개선 */
.task-id {
  font-family: 'Inter', 'Pretendard', monospace;
  font-weight: 600;
  color: #495057;
  font-size: 0.75rem;
  font-feature-settings: 'tnum' 1; /* 숫자 고정폭 */
  letter-spacing: 0.01em;
}

/* ===================================
   Resizable Columns (Excel-like)
   =================================== */

.resizable-column {
  position: relative;
  box-sizing: border-box;
  /* 말줄임표 처리 */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  /* 세로 중앙 정렬 */
  display: flex;
  align-items: center;
  min-height: 32px;
}

/* ===================================
   LIST VIEW 칼럼 경계선 (C안 · 2026-08-11 · A1)
   ===================================

   ❑ 무엇을 고쳤나
     헤더 경계선과 본문 경계선이 최대 4px 어긋나던 문제.
     대상: schedule_forecast(일정예보) / assignee_load / checklist 3개 칼럼.

   ❑ 왜 어긋났나 — 기존 구조
       외곽 div : width=칼럼폭(고정) + px-N 패딩        ← 칸의 진짜 경계
         └ 내부 div : w-full + border-r                 ← 선은 여기 있었다
     내부 div 는 `w-full` 이므로 폭이 "칼럼폭 − 좌우패딩" 이다.
     따라서 선의 x 좌표가
         칼럼 왼쪽 + 칼럼폭 − 우측패딩
     이 되어 **패딩값에 따라 선 위치가 달라졌다.**
     헤더는 전부 px-2(8px) 인데 위 3개 칼럼의 본문만 px-1(4px) →  8−4 = 4px 어긋남.
     (px-1 을 쓰는 이유는 배지·스파크라인·세그먼트바의 표시 폭 확보다. 그래서
      단순히 px-2 로 바꾸는 처방은 그 칼럼들의 도입 취지에 역행한다.)

   ❑ 어떻게 고쳤나 — 선을 "칸의 진짜 오른쪽 끝" 에 절대배치한다
     `.resizable-column`(외곽 div)은 이미 `position: relative` 다(위 규칙 참조).
     내부 div 는 `position: static` 이므로, 내부 div 의 ::after 가 기준으로 삼는
     containing block 은 **외곽 div** 가 된다.
     ⇒ `right: 0` 이 곧 "칼럼 칸의 오른쪽 끝" 이고, **패딩과 완전히 무관**해진다.

   ❑ 이 방식이 border-r 이동보다 나은 점 (전부 Playwright 실측 확인)
     · 어긋남            : 4.0px → 0.0px
     · 내용 가용 폭      : 잠식 0 (absolute 라 레이아웃 폭을 먹지 않는다.
                           외곽 div 로 border 를 옮기는 방식은 1px 잠식했다)
     · 칼럼 폭           : 유지 (폭 예산 1920px 무스크롤 불변식 안전)
     · 헤더 액센트 선    : 16px 유지 가능 (--accent 변형).
                           border 를 외곽 div 로 옮기면 헤더 선이 32px 로 늘어나
                           헤더 행의 시각 디자인이 바뀌어 버린다.
     · .column-resizer   : resizer 는 칸 우측기준 **−5px~+5px** (2026-08-12 변경,
                           경계선을 정중앙에 두는 좌우 대칭), 경계선은 −1px~0px
                           → **의도적으로 겹친다**.
                           경계선이 `pointer-events: none` 이므로 히트테스트는
                           항상 resizer 가 이긴다. 상세는 .column-resizer 주석 참조.
     · hover 배경/!important 행배경과 같은 요소에 border 를 얹지 않으므로
       상태 스타일(hover)과 구조 스타일(경계선)이 섞이지 않는다

   ❑ 주의
     · `pointer-events: none` 필수 — 셀 클릭/인라인편집/체크박스를 가로막지 않는다.
     · 마지막 칼럼은 이 클래스를 붙이지 않는다(기존 isLast 분기 그대로).
     · `data-column` 등 DOM 계약은 외곽 div 에 그대로 남는다 → 인라인 편집기
       (row.querySelector('[data-column=...]'))·리사이저 모두 영향 없음.
*/

.cell-divider::after {
  content: '';
  position: absolute;
  /* 기준 = 외곽 .resizable-column (position: relative). 내부 div 는 static 이다. */
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  width: 1px;
  height: 100%;
  background: rgb(229 231 235);   /* = tailwind gray-200 (기존 border-gray-200 과 동일) */
  pointer-events: none;           /* 🔴 클릭/편집 방해 금지 */
}

/* 헤더 및 선택칼럼 — 16px 짧은 액센트 선 (기존 h-4 디자인 보존) */
.cell-divider--accent::after {
  height: 16px;
}

.dark .cell-divider::after {
  background: rgb(55 65 81);      /* = tailwind gray-700 (기존 dark:border-gray-700 과 동일) */
}

/* ⭐⭐ [드래그 위치 = 칼럼 경계 "정중앙" 2026-08-12 · A1]

   ❑ 문제
     최초 `right: 5px; width: 6px` → 잡히는 구간이 칸 우측기준 **−11px ~ −5px**.
     실제 칼럼 경계선(−1px~0px)에서 **5px 안쪽에서 끊긴다** → 눈에 보이는
     경계선을 정확히 겨냥해 누르면 아무 일도 일어나지 않아 직관적이지 않았다.
     (Excel·Google Sheets 는 모두 경계선을 **가운데 두고** 좌우 양쪽에서 잡힌다)

   ❑ 1차 조치(폐기) — `right: 0; width: 9px` → 구간 −9px ~ 0px
     경계선을 "닿기"는 했지만 여전히 **칸 안쪽에서만** 잡혔다. 즉 경계선 오른쪽
     절반(+1px~+5px)에서는 안 잡힌다 → 여전히 비대칭이라 손이 익지 않는다.

   ❑ ⭐ 최종 조치 — `right: -5px; width: 10px` → 잡는 구간 **−5px ~ +5px**
     경계선을 **정중앙**에 두고 좌우 대칭으로 잡는다 (실측 −5 ~ +4px 전 지점 OK).
     · 경계선 픽셀(−1px)과 그 오른쪽(+1~+4px) 모두 잡힌다 → 경계 "관통" 확인.
     · Excel 과 동일한 감각. 어느 쪽에서 접근해도 같은 거리에서 잡힌다.

   ❑ 음수 offset 이 클리핑되지 않게 하는 방법 (이게 핵심)
     `.resizable-column` 은 `overflow: hidden` 이다. 그래서 예전에는
     "음수 right → 우측 절반이 클리핑되어 죽는다"고 판단해 정중앙안을 기각했다.
     그 판단은 **틀렸다**. overflow 의 목적은 두 가지인데 분리가 가능하다.
       (a) 긴 칼럼 라벨의 말줄임(ellipsis)
       (b) 음수 offset 자식의 클리핑
     (a)는 라벨을 담는 **내부 래퍼**가 대신 수행할 수 있다. 따라서
       · resizer 를 가진 칸(= 헤더)만 `overflow: visible` 로 풀고,
       · 말줄임 책임을 내부 래퍼로 내리면
     (b)의 클리핑만 사라지고 (a)는 그대로 유지된다.
     `:has(> .column-resizer)` 로 스코프를 한정했으므로 **본문 셀은 무영향**
     (본문 셀에는 resizer 가 없다 → 선택자 미매칭 → hidden 유지).

   ❑ 실측 (Playwright + 실제 setupColumnResizers 이식 + 실제 마우스 드래그,
            Tailwind 로드 + 실제 style.css + 실제 헤더 마크업)
       경계기준 offset   1차안(right:0,w:9)   최종안(right:-5,w:10)
         −11 / −9 / −6px        OK                  X   (칸 안쪽 깊은 곳은 이제 제외)
         −5 ~ −0.5px            OK                  OK
         +0.5 ~ +4px            X                   OK  ← 경계 오른쪽도 잡힌다
         +5px 이상              X                   X   (다음 칼럼 침범 없음)
       대칭(−4/+4) : no → **YES**      경계관통(−1/+1) : no → **YES**
       라벨 유출   : 0px → **0px** (내부 래퍼가 계속 말줄임)
       경계선 생존 : 16px → **16px** (::after 의 기준은 여전히 외곽 칸)
       드래그 결과 : 경계(0px)를 정확히 잡고 +60px → 폭 210px, 저장값 210 정상

   ❑ 부수효과 (오히려 개선)
     클리핑 주체가 외곽(패딩 포함)→내부(패딩 제외)로 바뀌므로, 긴 라벨이
     경계선에 닿기 전 `px-2`(8px) 패딩을 존중하며 잘린다.
       실측: 라벨 우측끝 ~ 경계 여백 1px → **8px** (라벨이 경계선에 붙지 않는다)

   ❑ 경계선(.cell-divider::after)과의 겹침은 안전하다
     경계선은 `pointer-events: none` 이므로 히트테스트 대상이 아니다.
     −1px 지점의 elementFromPoint = `.column-resizer` (실측 확인).

   ❑ 헤더 드래그앤드롭(칼럼 순서 변경)과의 충돌 없음
     app-column-header-ux.js 의 dragstart/contextmenu 핸들러가
     `closest('.column-resizer')` 가드를 갖고 있고, resizer 의 mousedown 이
     stopPropagation 한다. 구간이 넓어져도 가드 로직은 그대로 유효하다.

   ⚠️ 유지보수 주의
     · `.column-resizer` 의 `right` 를 음수로 두는 것은 아래
       `:has(> .column-resizer) { overflow: visible }` 규칙과 **한 쌍**이다.
       한쪽만 되돌리면 즉시 퇴행한다(클리핑 부활 또는 라벨 유출).
     · 전역 `.resizable-column { overflow: hidden }` 은 **절대 지우지 말 것**.
       본문 셀(1단 구조)은 내부 래퍼가 없어서 전역 visible 이면 글자가 샌다.
*/
.column-resizer {
  position: absolute;
  top: 0;
  /* 칸의 오른쪽 끝(= 경계선 위치)을 **정중앙**에 두는 좌우 대칭 배치: −5px ~ +5px.
     ⚠️ 이 주석에 `right` + 콜론 + 숫자 형태를 쓰지 말 것 — 회귀 테스트가 선언값을
        정규식으로 파싱하는데 주석 쪽을 먼저 잡아 오탐이 난 적이 있다. */
  right: -5px;
  width: 10px;
  height: 100%;
  cursor: col-resize;
  background: transparent;
  z-index: 20;                     /* 경계선(::after, z-index 없음)보다 위 */
  user-select: none;
}

/* ⭐ 위 음수 offset 이 클리핑되지 않게 하는 한 쌍의 규칙.
      resizer 를 가진 칸(= 헤더)에만 적용된다 → 본문 셀은 hidden 유지. */
.resizable-column:has(> .column-resizer) {
  overflow: visible;               /* resizer 가 칸 밖으로 나갈 수 있게 한다 */
}
/* 말줄임 책임을 내부 래퍼로 이전 — 라벨은 계속 칸 안에서 잘린다(유출 0). */
.resizable-column:has(> .column-resizer) > div:not(.column-resizer) {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;                    /* flex 자식이 줄어들 수 있게 (없으면 안 잘린다) */
}

.column-resizer:hover {
  background: rgba(59, 130, 246, 0.3);
}

.column-resizer:active,
.column-resizer.resizing {
  background: rgba(59, 130, 246, 0.6);
}

/* ===================================
   칼럼 설정 드래그 앤 드롭
   =================================== */

.column-item {
  transition: all 0.2s ease;
}

.column-item:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}

.column-item.opacity-50 {
  opacity: 0.5;
  transform: scale(0.95);
}

.column-item.border-blue-500 {
  border-color: #3b82f6 !important;
  background-color: #eff6ff !important;
}

.drag-handle {
  cursor: grab;
  user-select: none;
}

.drag-handle:active {
  cursor: grabbing;
}

/* ===================================
   버튼 통일 스타일 (Flat & Simple)
   =================================== */

/* ===================================================================
   버튼 사이즈 토큰 (단일 진실 소스 / Single Source of Truth)
   -------------------------------------------------------------------
   모든 툴바 버튼(.btn, .btn-primary, .btn-secondary, .btn-ghost ...)은
   아래 변수 한 세트로 크기가 통일된다. 색상(variant)만 다르고
   높이/패딩/폰트/라운드는 절대 개별 지정하지 않는다.
   (Asana·Linear·shadcn/ui 등과 동일한 "size 토큰 + variant 색상" 패턴)
   =================================================================== */
:root {
  --btn-pad-y: 0.375rem;     /* 6px */
  --btn-pad-x: 0.625rem;     /* 10px */
  --btn-font: 0.8125rem;     /* 13px */
  --btn-radius: 0.375rem;    /* 6px */
  --btn-min-h: 30px;         /* 모든 버튼 동일 높이 */
  --btn-line-h: 1.25;
  /* [버튼 너비 통일 2026-06-07 → 축소 2026-07-21 · A1]
     모든 툴바 버튼의 최소 너비를 통일해 떨림을 막되, 폭을 과하게 잡지 않는다.
     · 기존 96px(한글 4글자)는 간트/뷰/설정 등 짧은 버튼까지 96px로 부풀려
       버튼이 많아진 뒤 상단 툴바가 2줄로 줄바꿈(부하율·더보기가 아랫줄로 밀림)되었다.
     · 사용자 요청("다른 버튼 폭을 줄여 색칠한 2개 버튼도 윗줄로"): 최소 너비를
       한글 3글자 기준으로 축소 → 버튼당 ~20px 절약(×14개 ≈ 280px)해 한 줄에 수용.
     계산: 아이콘 ~16px + gap 6px + 한글 3자(≈39px) + 좌우 패딩 16px ≈ 76px.
     여전히 min-width 라 3자보다 긴 버튼(체크리스트 삭제 등)은 내용에 맞게 확장된다. */
  --btn-min-w: 76px;         /* 모든 버튼 최소 너비(한글 3글자 기준, 떨림 방지) */
}

/* 공통 사이즈 베이스: .btn 단독 / .btn-* variant 모두 이 토큰을 상속 */
.btn,
.btn-primary,
.btn-secondary,
.btn-warning,
.btn-danger,
.btn-info,
.btn-light,
.btn-success,
.btn-indigo,
.btn-purple,
.btn-ghost,
.btn-ai {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: var(--btn-min-h);
  padding: var(--btn-pad-y) var(--btn-pad-x);
  font-size: var(--btn-font);
  font-weight: 500;
  line-height: var(--btn-line-h);
  border-radius: var(--btn-radius);
  border: 1px solid transparent;
  white-space: nowrap;
  vertical-align: middle;
  transition: all 0.2s;
}

/* [버튼 너비 통일 2026-06-07] 데스크탑 툴바(기본 행 + 선택 액션 띠) 안의 모든 버튼은
   너비를 한글 4글자 기준(--btn-min-w)으로 통일한다.
   - 글자가 짧은 버튼은 이 너비까지 늘어나 시각적으로 동일하게 보임.
   - 글자가 긴 버튼(체크리스트 삭제 등)은 min-width 이므로 내용에 맞게 자연 확장.
   - 아이콘 전용 버튼(.btn-icon-only)은 정사각형 유지를 위해 통일 너비에서 제외. */
#desktop-main-row .btn,
#desktop-main-row .btn-primary,
#desktop-main-row .btn-secondary,
#desktop-main-row .btn-warning,
#desktop-main-row .btn-danger,
#desktop-main-row .btn-info,
#desktop-main-row .btn-light,
#desktop-main-row .btn-success,
#desktop-main-row .btn-indigo,
#desktop-main-row .btn-purple,
#desktop-main-row .btn-ghost,
#desktop-main-row .btn-ai,
#desktop-selection-bar .btn,
#desktop-selection-bar .btn-primary,
#desktop-selection-bar .btn-secondary,
#desktop-selection-bar .btn-warning,
#desktop-selection-bar .btn-danger,
#desktop-selection-bar .btn-info,
#desktop-selection-bar .btn-light,
#desktop-selection-bar .btn-success,
#desktop-selection-bar .btn-indigo,
#desktop-selection-bar .btn-purple,
#desktop-selection-bar .btn-ghost,
#desktop-selection-bar .btn-ai {
  min-width: var(--btn-min-w);
}

/* 아이콘 전용 버튼: 통일 너비 제외 + 정사각형(높이=너비) 유지 */
#desktop-main-row .btn-icon-only,
#desktop-selection-bar .btn-icon-only {
  min-width: var(--btn-min-h);
  width: var(--btn-min-h);
  padding-left: 0;
  padding-right: 0;
}

/* [상단 툴바 한 줄 수용 2026-07-21 · A1] 사용자 요청("다른 버튼 폭을 줄여 색칠한 2개 버튼도
   윗줄로"): 상단 기본 툴바(#desktop-main-row) 안 버튼만 좌우 패딩·아이콘 간격을 소폭 줄여
   가로 공간을 추가 확보한다(부하율 컨트롤바 + 설정 더보기(⋯)가 아랫줄로 밀리지 않도록).
   · 모바일 시트/선택 액션 띠에는 영향 없음(스코프 한정).
   · 라벨 폰트/높이는 유지 → 가독성/정렬 불변, 오직 여백만 축소. */
#desktop-main-row .btn,
#desktop-main-row .btn-primary,
#desktop-main-row .btn-secondary,
#desktop-main-row .btn-ghost,
#desktop-main-row .btn-ai,
#desktop-main-row .btn-danger,
#desktop-main-row .btn-info,
#desktop-main-row .btn-success {
  padding-left: 0.5rem;    /* 10px → 8px */
  padding-right: 0.5rem;
  gap: 4px;                /* 아이콘-라벨 간격 6px → 4px */
}
/* 아이콘 전용 버튼은 위 패딩 축소 대상에서 제외(정사각형 유지) — 재적용 */
#desktop-main-row .btn-icon-only {
  padding-left: 0;
  padding-right: 0;
}

/* [버튼 너비 통일 2026-06-07] 비활성(disabled) 시각 상태 모디파이어.
   ----------------------------------------------------------------
   기존에는 비활성 버튼의 className 을 raw Tailwind 유틸(bg-gray-300 px-3 py-1.5 ...)로
   "교체"해서 .btn-* 베이스(사이즈 토큰)가 사라졌고, 그 결과 너비/높이가 제각각이 되었다.
   이제는 .btn-* 클래스를 "유지"한 채 이 모디파이어만 추가로 얹어
   - 사이즈(너비/높이/패딩/폰트)는 토큰 그대로 통일 유지
   - 색상/커서/투명도만 비활성 모양으로 바꾼다. */
.btn-disabled-soft {
  background-color: #d1d5db !important;   /* gray-300 */
  color: #6b7280 !important;              /* gray-500 */
  border-color: transparent !important;
  opacity: 0.5;
  cursor: not-allowed !important;
}
.dark .btn-disabled-soft,
:root[data-theme="dark"] .btn-disabled-soft {
  background-color: var(--dk-surface-3, #3f4148) !important;   /* gray-600 */
}
.btn-disabled-soft:hover {
  /* 비활성 상태에서는 hover 변화 없음 */
  background-color: #d1d5db !important;
}
.dark .btn-disabled-soft:hover,
:root[data-theme="dark"] .btn-disabled-soft:hover {
  background-color: var(--dk-surface-3, #3f4148) !important;
}

/* ===== 선택 액션 띠: 흔들림/색 강조 최소화(2026-06-15) =====
   선택 띠(#desktop-selection-bar)를 항상 같은 자리에 두고,
   파란색 활성화 효과는 제거해 차분한 무채색 띠로 유지한다.
   - 표시/숨김 토글이 아니라 '흐림(opacity)' 전환이므로 레이아웃 시프트(목록 밀림)가 없다.
   - 배경/테두리 색은 선택 여부와 무관하게 동일(무채색) → 색이 튀지 않음.
   - 미선택 상태(.sel-bar-dim)에서는 opacity 만 낮춰 시각적 무게를 줄인다. */
#desktop-selection-bar {
  /* dim ↔ active 전환을 부드럽게 (opacity 만 변하므로 색 깜빡임 없음) */
  transition: opacity 0.18s ease;
  /* nowrap + 가로 스크롤 시 스크롤바가 레이아웃 높이를 흔들지 않도록 */
  scrollbar-width: thin;
  /* [팝오버 뒤겹침 근본 수정 2026-07-20 · A1 / side-effect 재수정 2026-07-21 · A1]
     이 띠는 .sel-bar-dim 에서 opacity:0.5 → 새 stacking context 를 만든다.
     그러면 내부 팝오버(#ai-decompose-popover, #sel-more-dropdown)의 z-index:9999 는
     "이 띠 안에서만" 유효해지고, 바깥(목록 마일스톤·컬럼헤더 z-index:20~50)과는
     '띠 자체의 스택 순위'로 경쟁한다. 기존 z-20 이면 목록에 밀려 팝오버가 뒤로 깔린다.

     [side-effect 주의] 무조건 z-index:60 으로 올리면 이번엔 이 띠가
     모달(#edit-modal 등 z-50)까지 덮어버린다(새 작업 생성창 위로 툴바가 뜸).
     → 그래서 '팝오버가 열려 있는 동안에만' .sel-bar-popover-open 클래스로
       z-index 를 끌어올리고, 평상시에는 낮게 유지해 모달(z-50)이 항상 위에 오게 한다.
       (열림/닫힘 클래스 토글은 app-toolbar-sel-more.js / app-toolbar-events.js 가 담당.)
     (position 은 마크업의 sticky 유지 — sticky 도 z-index 가 먹는다.) */
  z-index: 20;
}
/* 팝오버/드롭다운이 열려 있는 동안에만 목록 위로 끌어올린다.
   (모달 z-50 보다는 낮게 유지 — 모달이 열리면 아래 클래스는 붙지 않는다) */
#desktop-selection-bar.sel-bar-popover-open {
  z-index: 40 !important;
}
#desktop-selection-bar.sel-bar-dim {
  opacity: 0.5;
}
/* 미선택(흐림) 상태에서는 호버해도 색 변화 없이 차분하게 유지 */
#desktop-selection-bar.sel-bar-dim:hover {
  opacity: 0.65;
}

/* 모바일 전용 버튼 스타일 (격리) */
@media (max-width: 1024px) {
  :root {
    --btn-pad-x: 0.5rem;     /* 8px : 모바일은 조금 더 좁게 */
    --btn-font: 0.75rem;     /* 12px */
    --btn-min-h: 28px;
  }
}

.btn-primary {
  /* 색상(variant)만 지정 — 크기는 공통 사이즈 베이스에서 상속 */
  background-color: var(--color-primary-600);
  color: white;
  position: relative;
  overflow: hidden;
}

/* [화이트 제거 2026-09-09 / A3] `:root[data-theme="white"] .btn-primary` 및
   `:hover` 제거 (§4 #4·#5). 칼라는 --color-primary-* 토큰을 사용하므로
   아래 기본 색상이 그대로 적용된다. */
.btn-primary:hover {
  background-color: var(--color-primary-700);
}

.btn-primary:active {
  transform: translateY(0);
}

.btn-primary:disabled {
  background-color: #d1d5db;
  cursor: not-allowed;
  transform: none;
}

.btn-warning {
  background-color: var(--color-review);
  color: white;
}

.btn-warning:hover {
  background-color: #d97706;
}

.btn-warning:disabled {
  background-color: #d1d5db;
  cursor: not-allowed;
  transform: none;
}

.btn-danger {
  background-color: var(--color-blocked);
  color: white;
}

.btn-danger:hover {
  background-color: #dc2626;
}

.btn-danger:disabled {
  background-color: #d1d5db;
  cursor: not-allowed;
  transform: none;
}

.btn-secondary {
  background-color: #6b7280;
  color: white;
}

.btn-secondary:hover {
  background-color: #4b5563;
}

.btn-secondary:disabled {
  background-color: #d1d5db;
  cursor: not-allowed;
}

.btn-light {
  background-color: #f3f4f6;
  color: #374151;
}

.btn-light:hover {
  background-color: #e5e7eb;
}

.btn-light:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* [브랜드 우선 색 재배치 2026-09-19 · A1] #16a34a/#15803d -> 틸. 설계 #1328 §3.2.
   구 값은 브랜드 기준색·강조색과 **완전히 같았다**(ΔE00 0.0). "성공" 버튼과
   "브랜드 주 버튼"이 같은 색이면 어느 쪽이 제품 색인지 알 수 없다.
   성공/완료 의미는 틸로 비켜난다 — 브랜드와 ΔE00 19.4. */
.btn-success {
  background-color: #0d9488;
  color: white;
}

.btn-success:hover {
  background-color: #0f766e;
}

.btn-success:disabled {
  background-color: #d1d5db;
  cursor: not-allowed;
}

/* 특수 버튼 스타일 (색상만 지정) */
.btn-indigo {
  background-color: #4f46e5;
  color: white;
}

.btn-indigo:hover {
  background-color: #4338ca;
}

.btn-purple {
  background-color: #9333ea;
  color: white;
}

.btn-purple:hover {
  background-color: #7e22ce;
}

/* ── 누락 variant 보강 + 신규 유틸 variant ───────────────────────── */

/* info: 메일발송 등 (기존 HTML 에서 btn-info 사용했으나 CSS 정의 누락) */
.btn-info {
  background-color: #0ea5e9;
  color: white;
}

.btn-info:hover {
  background-color: #0284c7;
}

.btn-info:disabled {
  background-color: #d1d5db;
  color: #fff;
  cursor: not-allowed;
}

/* ghost: 업데이트·설정·다운로드·휴지통 등 회색 유틸 버튼 통일용.
   기존에 px-2 py-1 text-xs 인라인으로 제각각이던 버튼을 이 클래스로 대체. */
.btn-ghost {
  background-color: #f3f4f6;
  color: #374151;
}

.btn-ghost:hover {
  background-color: #e5e7eb;
}

.btn-ghost:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

:root[data-theme="dark"] .btn-ghost {
  background-color: var(--dk-surface-2, #2c2e34);
  color: #e5e7eb;
}

:root[data-theme="dark"] .btn-ghost:hover {
  background-color: var(--dk-surface-3, #3f4148);
}

/* ghost 위에 의미색만 입히는 modifier (휴지통=빨강, 마일스톤=보라 등) */
.btn-ghost.btn-ghost-danger { color: #dc2626; }
:root[data-theme="dark"] .btn-ghost.btn-ghost-danger { color: #f87171; }
.btn-ghost.btn-ghost-purple {
  background-color: #f3e8ff;
  color: #7e22ce;
}
:root[data-theme="dark"] .btn-ghost.btn-ghost-purple {
  background-color: rgba(147, 51, 234, 0.18);
  color: #d8b4fe;
}

/* 툴팁 토글 on/off 상태 색상 (사이즈는 .btn-ghost 토큰 그대로) */
.btn-ghost.tooltip-toggle-on {
  background-color: #dbeafe;
  color: #1d4ed8;
}
.btn-ghost.tooltip-toggle-on:hover { background-color: #bfdbfe; }
:root[data-theme="dark"] .btn-ghost.tooltip-toggle-on {
  background-color: rgba(59, 130, 246, 0.32);
  color: #93c5fd;
}
.btn-ghost.tooltip-toggle-off { color: #9ca3af; }
:root[data-theme="dark"] .btn-ghost.tooltip-toggle-off { color: #6b7280; }

/* 임계경로(Critical Path) 토글 ON 상태 — 빨강 강조 (간트 막대 색상과 통일) */
.btn-ghost.list-critical-path-on {
  background-color: #fde2e2;
  color: #c81e1e;
}
.btn-ghost.list-critical-path-on:hover { background-color: #fbcaca; }
:root[data-theme="dark"] .btn-ghost.list-critical-path-on {
  background-color: rgba(229, 72, 77, 0.32);
  color: #fca5a5;
}

/* 선후행 크로스하이라이트 토글 ON 상태 — 청록(teal) 강조 (선행 녹/후행 빨과 혼동 없는 중립색) */
.btn-ghost.list-cross-highlight-on {
  background-color: #d1fae5;
  color: #0f766e;
}
.btn-ghost.list-cross-highlight-on:hover { background-color: #a7f3d0; }
:root[data-theme="dark"] .btn-ghost.list-cross-highlight-on {
  background-color: rgba(20, 184, 166, 0.28);
  color: #5eead4;
}

/* 영향확인(일정예보 후행 전파) 토글 ON 상태 — 인디고 강조 (CP 빨강/CrossHL 청록과 혼동 없는 중립색) */
.btn-ghost.list-schedule-impact-on {
  background-color: #e0e7ff;
  color: #4338ca;
}
.btn-ghost.list-schedule-impact-on:hover { background-color: #c7d2fe; }
:root[data-theme="dark"] .btn-ghost.list-schedule-impact-on {
  background-color: rgba(99, 102, 241, 0.28);
  color: #a5b4fc;
}

/* ai: AI 분해 전용 (보라 계열) — disabled 시 회색 통일 */
.btn-ai {
  background-color: #9333ea;
  color: white;
}

.btn-ai:hover {
  background-color: #7e22ce;
}

/* [색감 조화 2026-06-07 옵션A] 다크모드에서는 AI(보라) 액센트 채도를 한 단계 낮춰
   눈부심을 줄인다. 라이트용 #9333ea 를 그대로 쓰면 어두운 배경에서 과하게 튄다. */
:root[data-theme="dark"] .btn-ai {
  background-color: #8b5cf6;   /* violet-500: 차분한 파스텔 보라 */
}
:root[data-theme="dark"] .btn-ai:hover {
  background-color: #7c3aed;   /* violet-600 */
}

.btn-ai:disabled {
  background-color: #d1d5db;
  color: #6b7280;
  cursor: not-allowed;
  opacity: 0.7;
}

/* ===================================
   모달 통일 스타일
   =================================== */

.modal-overlay {
  position: fixed;
  inset: 0;
  background-color: rgba(0, 0, 0, 0.6);
  backdrop-filter: blur(2px);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 50;
  animation: fadeIn 0.2s ease;
  backdrop-filter: blur(4px);
}

/* [필터 모달화 2026-06-15] #filter-modal 처럼 DOM 에 상주하는 모달은
   .hidden 토글로 표시/숨김을 제어한다. .modal-overlay 의 display:flex 와
   Tailwind .hidden(display:none) 은 동일 특정성이라 로드 순서에 따라
   결과가 흔들릴 수 있으므로, 여기서 !important 로 명시해 항상 숨김이 이기게 한다. */
.modal-overlay.hidden {
  display: none !important;
}

.modal-content {
  background: linear-gradient(135deg, #ffffff, #f9fafb);
  border-radius: 1rem;
  box-shadow: 
    0 20px 50px rgba(0, 0, 0, 0.15),
    0 0 0 1px rgba(0, 0, 0, 0.05);
  padding: 1.5rem;
  width: 100%;
  max-width: 48rem;
  animation: slideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

.modal-content-large {
  max-width: 56rem;
}

.modal-content-small {
  max-width: 28rem;
}

.modal-header {
  border-bottom: 1px solid #e5e7eb;
  padding-bottom: 1rem;
  margin-bottom: 1rem;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.modal-title {
  font-size: 1.125rem;
  font-weight: 600;
  color: #111827;
}

.modal-body {
  max-height: calc(90vh - 150px);
  overflow-y: auto;
}

.modal-footer {
  border-top: 1px solid #e5e7eb;
  padding-top: 1rem;
  margin-top: 1rem;
  display: flex;
  justify-content: flex-end;
  gap: 0.75rem;
}
/* ===================================
   [Cell-Level Inline Edit] Row Edit Mode 폐기 (2026-06)
   - 셀단위 편집으로 전면 교체됨.
   - 이전 row-edit 관련 CSS (.edit-row-wrapper, .edit-inline-action-bar,
     .row-save-btn, .row-cancel-btn, .edit-field-input, .edit-field-select,
     .edit-content-row, .modified-badge, .task-hierarchical.editing-mode 등)
     은 전체 제거됨.
   - 셀단위 편집 인풋 스타일은 Tailwind 유틸리티 클래스 (field-edit-input)
     로 인라인 처리 — 별도 CSS 필요 없음.
   =================================== */

/* ===================================
   멀티 셀렉트 드롭다운 스타일
   =================================== */
.multiselect-container {
  position: relative;
  display: inline-block;
  min-width: 160px;
}

.multiselect-btn {
  width: 100%;
  text-align: left;
  background-color: white;
  border: 1px solid #d1d5db;
  border-radius: 0.375rem;
  padding: 0.375rem 0.75rem;
  font-size: 0.875rem;
  cursor: pointer;
  display: flex;
  justify-content: space-between;
  align-items: center;
  color: #374151;
}

.multiselect-btn:hover {
  border-color: #3b82f6;
}

.multiselect-btn::after {
  content: '\f078';
  font-family: 'Font Awesome 5 Free';
  font-weight: 900;
  font-size: 0.75rem;
  margin-left: 0.5rem;
  color: #9ca3af;
}

.multiselect-dropdown {
  position: absolute;
  top: 100%;
  left: 0;
  z-index: 50;
  width: 100%;
  min-width: 200px;
  margin-top: 0.25rem;
  background-color: white;
  border: 1px solid #e5e7eb;
  border-radius: 0.375rem;
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
  display: none;
  max-height: 300px;
  overflow-y: auto;
}

.multiselect-dropdown.show {
  display: block;
}

.multiselect-option {
  display: flex;
  align-items: center;
  padding: 0.5rem 0.75rem;
  cursor: pointer;
  transition: background-color 0.15s;
}

.multiselect-option:hover {
  background-color: #f3f4f6;
}

.multiselect-option input[type="checkbox"] {
  margin-right: 0.5rem;
  width: 1rem;
  height: 1rem;
  cursor: pointer;
}

.multiselect-option span {
  font-size: 0.875rem;
  color: #374151;
}

.multiselect-count {
  background-color: #3b82f6;
  color: white;
  font-size: 0.75rem;
  padding: 0.125rem 0.375rem;
  border-radius: 9999px;
  margin-left: 0.5rem;
}

/* ===================================
   Compact Modal Style (90% Scale)
   =================================== */
.modal-compact {
  max-width: 36rem !important; /* 약 576px (기존 48rem 대비 75%) */
  max-height: 90vh !important; /* 기존 90vh 대비 약 90% */
  font-size: 0.8125rem; /* 13px */
}

/* 내부 입력 필드 컴팩트 스타일 */
.modal-compact input,
.modal-compact select,
.modal-compact textarea {
  font-size: 0.75rem !important; /* 12px */
  padding: 0.375rem 0.5rem !important; /* px-2 py-1.5 */
}

.modal-compact label {
  font-size: 0.75rem !important; /* 12px */
  margin-bottom: 0.125rem !important;
}

.modal-compact .modal-body {
  padding: 1rem 1.25rem !important; /* p-4 */
}

.modal-compact .grid {
  gap: 0.75rem !important; /* gap-3 */
}

.modal-compact .space-y-3 > :not([hidden]) ~ :not([hidden]) {
  margin-top: 0.5rem !important; /* space-y-2 */
}

/* ===================================
   [Hotfix] 다크 모드 입력창 가시성 긴급 패치
   =================================== */
/* Tailwind 설정 실패 시에도 강제로 적용되도록 CSS 변수 및 !important 사용 */
:root[data-theme="dark"] input,
:root[data-theme="dark"] textarea,
:root[data-theme="dark"] select,
.dark input,
.dark textarea,
.dark select {
  color: #dadcdf !important;            /* 눈이 편안한 오프화이트 */
  background-color: var(--dk-surface-1, #16181d) !important; /* 패널과 동일한 배경 */
  border-color: var(--dk-surface-2, #2c2e34) !important;     /* 은은한 테두리 */
}

/* 입력 전 안내 문구(placeholder)도 밝게 처리 */
:root[data-theme="dark"] ::placeholder,
.dark ::placeholder {
  color: #9ca3af !important; /* gray-400 */
  opacity: 1 !important;     /* 브라우저 투명도 리셋 */
}

/* 읽기 전용 필드는 약간 더 어둡게 처리하여 구분 */
:root[data-theme="dark"] input:read-only,
:root[data-theme="dark"] textarea:read-only,
:root[data-theme="dark"] select:disabled {
  background-color: var(--dk-surface-1, #16181d) !important; /* bg-slate-800 */ /* gray-800 */
  color: #cbd5e1 !important;            /* gray-300 */
  border-color: var(--dk-surface-2, #2c2e34) !important;     /* gray-700 */
}

/* ===================================
   간트 차트 다크 모드 (Gantt Dark Mode)
   =================================== */

/* 다크 모드 활성화 시 적용 */
:root[data-theme="dark"] .gantt-container,
.dark .gantt-container {
  background-color: var(--dk-surface-1, #16181d) !important; /* bg-slate-800 */ /* bg-gray-800 */
}

/* 그리드 배경 */
:root[data-theme="dark"] .gantt .grid-header,
.dark .gantt .grid-header {
  fill: var(--dk-surface-2, #2c2e34) !important; /* bg-gray-700 */
  stroke: var(--dk-surface-3, #3f4148) !important; /* border-gray-600 */
}

:root[data-theme="dark"] .gantt .grid-row,
.dark .gantt .grid-row {
  fill: var(--dk-surface-1, #16181d) !important; /* bg-gray-800 */
}

:root[data-theme="dark"] .gantt .grid-row:nth-child(even),
.dark .gantt .grid-row:nth-child(even) {
  fill: var(--dk-surface-0, #101218) !important; /* 간트 줄무늬 행 (구 fallback #1d263b) */
}

/* 그리드 라인 */
:root[data-theme="dark"] .gantt .grid line,
.dark .gantt .grid line {
  stroke: var(--dk-surface-2, #2c2e34) !important; /* border-gray-700 */
}

/* 텍스트 색상 반전 */
:root[data-theme="dark"] .gantt .bar-label,
.dark .gantt .bar-label {
  fill: #e2e8f0 !important; /* text-gray-200 */
  text-shadow: 0 1px 2px rgba(0,0,0,0.8); /* 가독성 확보 */
}

:root[data-theme="dark"] .gantt .upper-text,
:root[data-theme="dark"] .gantt .lower-text,
.dark .gantt .upper-text,
.dark .gantt .lower-text {
  fill: #9ca3af !important; /* text-gray-400 */
}

/* 오늘 날짜 하이라이트 */
:root[data-theme="dark"] .gantt .today-highlight,
.dark .gantt .today-highlight {
  fill: rgba(59, 130, 246, 0.2) !important; /* 파란색 반투명 */
}

/* 주말 하이라이트 (만약 있다면) */
:root[data-theme="dark"] .gantt .holiday-highlight,
.dark .gantt .holiday-highlight {
  fill: rgba(239, 68, 68, 0.1) !important; /* 붉은색 반투명 */
}

/* 팝업 다크 모드 */
:root[data-theme="dark"] .gantt-popup,
.dark .gantt-popup {
  background: var(--dk-surface-2, #2c2e34) !important;
  color: #f3f4f6 !important;
  border: 1px solid var(--dk-surface-3, #3f4148) !important;
  box-shadow: 0 4px 6px rgba(0,0,0,0.3) !important;
}

:root[data-theme="dark"] .gantt-popup .title,
.dark .gantt-popup .title {
  /* [다크모드 밝기 상향 2026-08-19 / A2] 구 #60a5fa(blue-400) → #93c5fd(blue-300).
     이유: 배경이 #374151 → #455165 로 밝아지면서 동일 제목색의 대비가
           4.05:1 → 3.16:1 로 떨어진다(AA 반상 3:1 거의 밑바닥). 한 단계
           밝게 올려 4.45:1 로 회복시킴. 색조(파랑)는 동일하게 유지. */
  color: #93c5fd !important; /* 파란색 제목 — 새 배경 대비 4.45:1 */
  border-bottom: 1px solid var(--dk-surface-3, #3f4148) !important;
}

/* 작업 목록(왼쪽) 다크 모드
   [노션형 다크 2026-07-15 · A1] 노션 다크 톤(#191c22 짙은 차콜 · 부드러운 회색 텍스트).
   완전 검정(#000) 대신 차콜을 써서 장시간 봐도 눈이 편하게. */
:root[data-theme="dark"] #task-list-container,
.dark #task-list-container {
  background-color: var(--dk-canvas, #16181d) !important; /* = 단일 바탕 --dk-base */
  border-right: 1px solid var(--dk-line, #414449) !important;
}

:root[data-theme="dark"] #task-list-header,
.dark #task-list-header {
  background-color: var(--dk-canvas, #16181d) !important; /* 헤더도 동일 톤 (노션은 헤더 대비 약함) */
  border-bottom: 1px solid var(--dk-line, #414449) !important;
  /* [A안 단일 바탕 통일 2026-08-22 / A2] 구 #9499a3 → #9ba1ab (ΔL* +3).
     이유: 바탕이 #262a33(L*17) → #2a384f(L*23.3) 로 또 밝아지면서 같은
           라벨색의 대비가 4.13:1 로 AA(4.5:1) 미달로 떨어졌다. 한 단계
           밝게 올려 4.55:1 로 회복. 노션풍 "옅은 라벨" 인상은 유지.
     ⛔ gantt_v2.js 의 textSecondary 와 동일 값이어야 한다. */
  color: #9ba1ab !important; /* 옅은 헤더 라벨 — 새 바탕 대비 4.55:1 ✅AA */
}

:root[data-theme="dark"] .task-row,
.dark .task-row {
  border-bottom: 1px solid var(--dk-line-soft, #33363b) !important;
  /* [A안 2026-08-22] 행 배경 == 바탕 이 되어 표 경계가 오직 이 선에만 의존한다.
     그래서 --dk-line-soft 를 바탕 대비 1.32:1 로 보강해 두었다(구 1.02:1 = 안 보임).
     본문 글자는 새 바탕 대비 7.96:1 — 여유 충분해 상향 보정 불필요 (검증값). */
  color: #d4d4d2 !important; /* 부드러운 밝은 회색 */
}

/* 다크 호버 — 상태 배경색(!important) 위에 얹히도록 오버레이 사용 */
:root[data-theme="dark"] .task-row:hover,
.dark .task-row:hover {
  box-shadow: inset 0 0 0 9999px rgba(255, 255, 255, 0.045);
}
:root[data-theme="dark"] .task-row.is-critical-path:hover,
.dark .task-row.is-critical-path:hover {
  box-shadow: inset 0 0 0 9999px rgba(255, 107, 107, 0.14);
}

/* 타임라인 헤더 다크 모드 */
:root[data-theme="dark"] #timeline-header,
.dark #timeline-header {
  background-color: var(--dk-surface-0, #101218) !important; /* 타임라인 헤더 (구 fallback #1d263b) */
  border-bottom: 1px solid var(--dk-surface-2, #2c2e34) !important;
}

/* 캔버스 컨테이너 배경 */
:root[data-theme="dark"] #timeline-container,
.dark #timeline-container {
  background-color: var(--dk-surface-1, #16181d) !important; /* bg-slate-800 */
}

/* 헤더 캔버스 배경 */
:root[data-theme="dark"] #timeline-header-canvas,
.dark #timeline-header-canvas {
  background-color: var(--dk-surface-0, #101218) !important; /* 헤더 캔버스 (구 fallback #1d263b) */
}

/* ===================================
   모바일 전용 스타일 격리
   =================================== */

/* 모바일 타임스케일 버튼 (컴팩트) */
@media (max-width: 1024px) {
  .mobile-scale-btn-compact.active {
    background-color: #2563eb;
    color: white;
    border-color: #2563eb;
    font-weight: 600;
  }
}

/* 스크롤바 숨김 (가로 스크롤 시) - 모바일 전용 */
@media (max-width: 1024px) {
  .no-scrollbar::-webkit-scrollbar {
    display: none;
  }
  .no-scrollbar {
    -ms-overflow-style: none;
    scrollbar-width: none;
  }
}

/* ===================================
   [PR-C 2026-05-28] 칼럼 헤더 인라인 UX
   드래그앤드롭 순서 변경 + 우클릭 컨텍스트 메뉴
   =================================== */

/* 드래그 가능한 헤더 셀 — 기본 cursor */
#header-scroll-container .resizable-column[draggable="true"] {
  cursor: grab;
}
#header-scroll-container .resizable-column[draggable="true"]:active {
  cursor: grabbing;
}

/* 드래그 중인 자기 자신 */
#header-scroll-container .resizable-column.col-header-dragging {
  opacity: 0.45;
}

/* 드롭 타겟 후보 — 좌측 가이드 라인 */
#header-scroll-container .resizable-column.col-header-drag-over {
  box-shadow: inset 3px 0 0 0 #3b82f6;
  background-color: rgba(59, 130, 246, 0.08);
}

/* 다크모드 보정 */
:root[data-theme="dark"] #header-scroll-container .resizable-column.col-header-drag-over,
.dark #header-scroll-container .resizable-column.col-header-drag-over {
  background-color: rgba(59, 130, 246, 0.18);
}

/* 컨텍스트 메뉴 다크모드 보정 (인라인 스타일 우선이지만 폰트색 보강) */
/*  #col-header-context-menu: 칼럼 헤더 메뉴 / #task-row-context-menu: 작업 행 메뉴(A+B) */
:root[data-theme="dark"] #col-header-context-menu,
.dark #col-header-context-menu,
:root[data-theme="dark"] #task-row-context-menu,
.dark #task-row-context-menu {
  background: var(--dk-surface-1, #16181d) !important;
  color: #f3f4f6 !important;
  border-color: rgba(255, 255, 255, 0.12) !important;
}
:root[data-theme="dark"] #col-header-context-menu button,
.dark #col-header-context-menu button,
:root[data-theme="dark"] #task-row-context-menu button,
.dark #task-row-context-menu button {
  color: #f3f4f6 !important;
}
:root[data-theme="dark"] #col-header-context-menu button:disabled,
.dark #col-header-context-menu button:disabled,
:root[data-theme="dark"] #task-row-context-menu button:disabled,
.dark #task-row-context-menu button:disabled {
  color: #9ca3af !important;
}

/* ============================================================ */
/* [Mobile Bottom ActionBar] feat/mobile-bottom-actionbar         */
/*  2번 모바일AI / 2026-06-04                                     */
/*  사용자 피드백: "버튼이 위에 있어 UX 나쁨, 아래로 배치"          */
/*                                                                 */
/*  목적:                                                          */
/*  1. Thumb-zone 최적화 — 한 손 조작 가능                         */
/*  2. iOS Safe Area inset 처리                                    */
/*  3. 하단 액션바와 메인 컨텐츠 겹침 방지                           */
/*  4. 다크 모드 호환                                              */
/*  5. PC(lg:) 영역 영향 없음 (모든 셀렉터에 lg: 미만 조건 적용)    */
/* ============================================================ */

/* 모바일 (< lg = 1024px)에서만 적용 */
/* [더보기 고장 2026-09-11] 폭 미디어쿼리(@media max-width:1023.98px) 밖으로 이동.
   🔴 사고: 오너 기기 가로폭이 정확히 1024 라 이 미디어쿼리가 적용되지 않았고,
     JS 가 .is-open 을 붙여도 열림 CSS 가 없어 More 시트가 올라오지 않았다.
     (backdrop 은 opacity:0 인 채 inset-0 이라 클릭만 가로챘다)
   ⛔ 폭 기준 미디어쿼리 안으로 되돌리지 말 것 — 판정은 .vp-compact(SoT)를 쓴다. */
html.vp-compact body {
  padding-bottom: calc(64px + env(safe-area-inset-bottom, 0));
}

html.vp-compact #mobile-bottom-actionbar {
  /* 백드롭 블러 효과 (iOS 느낌) */
  -webkit-backdrop-filter: saturate(180%) blur(12px);
  backdrop-filter: saturate(180%) blur(12px);
  /* 키보드 활성화 시 가려짐 방지 — modal-open class를 body에 추가 시 */
  transition: transform 0.2s ease-out;
}

/* 모달/입력창 활성 시 액션바 숨김 (선택적 — 추후 입력 모달이 키보드 위에 가려질 때 */
/* body.mobile-modal-open #mobile-bottom-actionbar { transform: translateY(100%); } */

/* More 바텀시트 / 백드롭 (열림 상태) — 폭과 무관하게 항상 유효해야 한다. */
#mobile-more-sheet.is-open {
  transform: translateY(0);
}

#mobile-more-backdrop.is-open {
  opacity: 1;
}

/* ============================================================ */
/* [Mobile ActionBar 버튼 — 스타일 SoT 단일화] 2026-06-05 / #1 PC앱 */
/*                                                                 */
/*  근본 원인(이전 버그): 버튼 '모양'이 두 곳에서 정의됨            */
/*   - app-toolbar-html.js: flex-col + 라벨 (신 디자인)            */
/*   - app-view.js: className 통째 교체 'p-1.5 rounded-full'(구)   */
/*   → 선택 상태 변경 시 레이아웃이 깨져 "기능과 모양이 따로 놂"   */
/*                                                                 */
/*  해결: 버튼 모양의 SoT 를 본 CSS 로 단일화.                     */
/*   - 레이아웃/라벨/터치영역 = 본 CSS (.mobile-bottom-btn /       */
/*     .mobile-sheet-btn) 가 전담                                  */
/*   - 색상 = 의미 단위 [data-variant] 가 전담                     */
/*   - 상태 = JS 는 disabled 속성 + .is-hidden 클래스만 토글       */
/*     (className 을 더 이상 만들지 않음 → 어긋날 수 없음)         */
/* ============================================================ */

/* 공통 레이아웃 — 하단 액션바 / 바텀시트 버튼 (아이콘 위 + 라벨 아래) */
.mobile-bottom-btn,
.mobile-sheet-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  border-radius: 0.5rem; /* rounded-lg */
  -webkit-tap-highlight-color: transparent;
  user-select: none;
  -webkit-user-select: none;
  transition: background-color 0.15s ease, transform 0.1s ease;
  cursor: pointer;
  background: transparent;
  border: 0;
}

/* 하단 액션바 버튼 — 균등 너비 + 터치 최소 44px (Apple HIG) */
.mobile-bottom-btn {
  flex: 1 1 0%;
  gap: 0.125rem;       /* gap-0.5 */
  padding: 0.5rem 0.25rem; /* py-2 px-1 */
  min-height: 44px;
}

/* 바텀시트 버튼 — 그리드 셀, 터치 최소 64px */
.mobile-sheet-btn {
  gap: 0.25rem;        /* gap-1 */
  padding: 0.75rem 0.25rem; /* py-3 px-1 */
  min-height: 64px;
}

/* 아이콘 / 라벨 */
.mobile-bottom-btn > i,
.mobile-sheet-btn > i { line-height: 1; }
.mobile-bottom-btn > i { font-size: 1rem; }      /* text-base */
.mobile-sheet-btn > i { font-size: 1.125rem; }   /* text-lg */
.mobile-bottom-btn > span,
.mobile-sheet-btn > span {
  font-size: 10px;
  line-height: 1.1;
  text-align: center;
}

/* press 피드백 (active:scale-95) */
.mobile-bottom-btn:not(:disabled):active,
.mobile-sheet-btn:not(:disabled):active { transform: scale(0.95); }

/* ── 색상 = 의미 단위 variant (활성 상태) ─────────────────────── */
/* 기본(중립) */
.mobile-bottom-btn,
.mobile-sheet-btn { color: #374151; }            /* gray-700 */
.mobile-bottom-btn:not(:disabled):hover,
.mobile-sheet-btn:not(:disabled):hover { background-color: #f3f4f6; } /* gray-100 */

.mobile-bottom-btn[data-variant="primary"],
.mobile-sheet-btn[data-variant="primary"] { color: #2563eb; }  /* blue-600 */
.mobile-bottom-btn[data-variant="primary"]:not(:disabled):hover,
.mobile-sheet-btn[data-variant="primary"]:not(:disabled):hover { background-color: #eff6ff; }

.mobile-bottom-btn[data-variant="danger"],
.mobile-sheet-btn[data-variant="danger"] { color: #dc2626; }   /* red-600 */
.mobile-bottom-btn[data-variant="danger"]:not(:disabled):hover,
.mobile-sheet-btn[data-variant="danger"]:not(:disabled):hover { background-color: #fef2f2; }

.mobile-bottom-btn[data-variant="ai"],
.mobile-sheet-btn[data-variant="ai"] { color: #9333ea; }       /* purple-600 */
.mobile-bottom-btn[data-variant="ai"]:not(:disabled):hover,
.mobile-sheet-btn[data-variant="ai"]:not(:disabled):hover { background-color: #faf5ff; }

.mobile-sheet-btn[data-variant="multi"] { color: #4f46e5; }    /* indigo-600 */
.mobile-sheet-btn[data-variant="multi"]:not(:disabled):hover { background-color: #eef2ff; }

.mobile-sheet-btn[data-variant="seq"] { color: #9333ea; }      /* purple */
.mobile-sheet-btn[data-variant="seq"]:not(:disabled):hover { background-color: #faf5ff; }

.mobile-sheet-btn[data-variant="group"] { color: #6366f1; }    /* indigo-500 */
.mobile-sheet-btn[data-variant="group"]:not(:disabled):hover { background-color: #eef2ff; }

.mobile-sheet-btn[data-variant="unlink"] { color: #f97316; }   /* orange-500 */
.mobile-sheet-btn[data-variant="unlink"]:not(:disabled):hover { background-color: #fff7ed; }

/* ── 상태: disabled (variant 무관하게 중립 회색 + 흐림) ──────────── */
.mobile-bottom-btn:disabled,
.mobile-sheet-btn:disabled {
  color: #d1d5db !important;  /* gray-300 */
  opacity: 0.6;
  cursor: not-allowed;
  background: transparent !important;
}

/* ── 상태: 숨김 (JS 가 .is-hidden 토글) ─────────────────────────── */
.mobile-bottom-btn.is-hidden,
.mobile-sheet-btn.is-hidden { display: none !important; }

/* ── 다크모드 — variant 색상 보정 ──────────────────────────────── */
:root[data-theme="dark"] .mobile-bottom-btn,
.dark .mobile-bottom-btn,
:root[data-theme="dark"] .mobile-sheet-btn,
.dark .mobile-sheet-btn { color: #d1d5db; }      /* gray-300 */
:root[data-theme="dark"] .mobile-bottom-btn:not(:disabled):hover,
.dark .mobile-bottom-btn:not(:disabled):hover,
:root[data-theme="dark"] .mobile-sheet-btn:not(:disabled):hover,
.dark .mobile-sheet-btn:not(:disabled):hover { background-color: var(--dk-surface-2, #2c2e34); } /* gray-700 */

:root[data-theme="dark"] .mobile-bottom-btn[data-variant="primary"],
.dark .mobile-bottom-btn[data-variant="primary"],
:root[data-theme="dark"] .mobile-sheet-btn[data-variant="primary"],
.dark .mobile-sheet-btn[data-variant="primary"] { color: #60a5fa; }
:root[data-theme="dark"] .mobile-bottom-btn[data-variant="danger"],
.dark .mobile-bottom-btn[data-variant="danger"],
:root[data-theme="dark"] .mobile-sheet-btn[data-variant="danger"],
.dark .mobile-sheet-btn[data-variant="danger"] { color: #f87171; }
:root[data-theme="dark"] .mobile-bottom-btn[data-variant="ai"],
.dark .mobile-bottom-btn[data-variant="ai"],
:root[data-theme="dark"] .mobile-sheet-btn[data-variant="ai"],
.dark .mobile-sheet-btn[data-variant="ai"] { color: #c084fc; }
:root[data-theme="dark"] .mobile-sheet-btn[data-variant="multi"],
.dark .mobile-sheet-btn[data-variant="multi"] { color: #818cf8; }
:root[data-theme="dark"] .mobile-sheet-btn[data-variant="seq"],
.dark .mobile-sheet-btn[data-variant="seq"] { color: #c084fc; }
:root[data-theme="dark"] .mobile-sheet-btn[data-variant="group"],
.dark .mobile-sheet-btn[data-variant="group"] { color: #818cf8; }
:root[data-theme="dark"] .mobile-sheet-btn[data-variant="unlink"],
.dark .mobile-sheet-btn[data-variant="unlink"] { color: #fb923c; }
:root[data-theme="dark"] .mobile-bottom-btn:disabled,
.dark .mobile-bottom-btn:disabled,
:root[data-theme="dark"] .mobile-sheet-btn:disabled,
.dark .mobile-sheet-btn:disabled { color: var(--dk-surface-3, #3f4148) !important; } /* gray-600 */

/* 다크모드 — 하단 액션바 배경/테두리 보강 */
:root[data-theme="dark"] #mobile-bottom-actionbar,
.dark #mobile-bottom-actionbar {
  /* CSS variable로 이미 처리되지만 일부 브라우저 backdrop-filter 미지원 시 fallback */
  background-color: rgba(31, 41, 55, 0.95) !important;
  border-color: rgba(255, 255, 255, 0.08) !important;
}

:root[data-theme="dark"] #mobile-more-sheet,
.dark #mobile-more-sheet {
  background-color: rgba(31, 41, 55, 1) !important;
}

/* ===================================
   [스크롤바 슬림화 2026-07-16 · A1]
   사용자 요청: 작업 목록 표의 가로/세로 스크롤바가 너무 굵어
   현재(브라우저 기본 ~16px)의 25% 수준(4px)으로 축소한다.
   - 대상: #header-scroll-container(가로), #content-scroll-container(가로+세로)
   - WebKit(Chrome/Safari/Edge)은 ::-webkit-scrollbar 로 두께 지정,
     Firefox 는 scrollbar-width: thin 으로 얇게 처리.
   =================================== */
#header-scroll-container,
#content-scroll-container {
  scrollbar-width: thin; /* Firefox: 얇은 스크롤바 */
  scrollbar-color: rgba(107, 114, 128, 0.5) transparent; /* thumb / track */
}
#header-scroll-container::-webkit-scrollbar,
#content-scroll-container::-webkit-scrollbar {
  /* 기본 두께의 약 25% (16px → 4px) */
  width: 4px;
  height: 4px;
}
#header-scroll-container::-webkit-scrollbar-track,
#content-scroll-container::-webkit-scrollbar-track {
  background: transparent;
}
#header-scroll-container::-webkit-scrollbar-thumb,
#content-scroll-container::-webkit-scrollbar-thumb {
  background-color: rgba(107, 114, 128, 0.5); /* gray-500 반투명 */
  border-radius: 9999px;
}
#header-scroll-container::-webkit-scrollbar-thumb:hover,
#content-scroll-container::-webkit-scrollbar-thumb:hover {
  background-color: rgba(107, 114, 128, 0.8);
}
/* 다크모드 — thumb 색을 밝게 */
:root[data-theme="dark"] #header-scroll-container,
:root[data-theme="dark"] #content-scroll-container,
.dark #header-scroll-container,
.dark #content-scroll-container {
  scrollbar-color: rgba(209, 213, 219, 0.5) transparent;
}
:root[data-theme="dark"] #header-scroll-container::-webkit-scrollbar-thumb,
:root[data-theme="dark"] #content-scroll-container::-webkit-scrollbar-thumb,
.dark #header-scroll-container::-webkit-scrollbar-thumb,
.dark #content-scroll-container::-webkit-scrollbar-thumb {
  background-color: rgba(209, 213, 219, 0.5); /* gray-300 반투명 */
}

/* ──────────────────────────────────────────────────────────────
   [AI 분해 팝오버 위치 고정 2026-07-20]
   증상: "AI 분해" 버튼을 눌러도 개수 입력 팝오버가 버튼 아래가 아닌
         화면 좌상단 등 엉뚱한 위치에 떴다.
   원인: 팝오버 위치를 Tailwind CDN 유틸(.relative/.absolute/.top-full)에
         의존했는데, CDN JIT 가 top-full 등을 생성하지 못하면 position 기준점이
         무너져 팝오버가 body 기준으로 떠버린다.
   해결: id 기반 명시적 CSS 로 wrapper=relative, popover=absolute+위치를 고정.
         CDN 유틸 로드 상태와 무관하게 항상 버튼 바로 아래에 뜨도록 보장한다.
   ────────────────────────────────────────────────────────────── */
#ai-decompose-wrapper {
  position: relative !important;
  display: inline-block !important;
}
/* [팝오버 클리핑/겹침 재수정 2026-07-20]
   증상(1차 수정 후에도 잔존):
     · 팝오버가 다른 화면 요소 뒤로 깔려 보임(겹침).
     · 오른쪽 "해제" 버튼 옆에 가로 스크롤바가 생김.
   원인:
     · 팝오버의 조상 컨테이너 #desktop-selection-bar 가 overflow-x-auto 이다.
       overflow 가 visible 이 아닌 조상 안에서는 position:absolute 자식도
       컨테이너 경계에서 잘리고, 넘친 폭만큼 가로 스크롤바가 생긴다.
     · AI 버튼이 넓은 툴바의 오른쪽에 있어 left 기준 w-56 팝오버가 우측으로 넘친다.
   해결:
     · position: fixed 로 뷰포트 기준 배치 → 조상 overflow 클리핑에서 완전히 탈출.
       (열 때 JS 가 버튼 좌표 기준으로 top/right 를 계산해 인라인으로 지정한다.)
     · z-index 를 매우 높게(9999) 올려 헤더/툴바 등 다른 스택 컨텍스트 위로 올린다. */
#ai-decompose-popover {
  position: fixed !important;
  z-index: 9999 !important;
  margin-top: 0 !important;
}

/* [버튼 정돈 2026-07-21 · A1] 툴바 "작은 더보기(…)" 드롭다운 3종 클리핑/겹침 수정.
   (구 #sel-more-dropdown 을 대체 — 더보기 ▾ 는 해체하고 아래 3종으로 재배치)
   증상: …(더보기)를 눌러도 드롭다운이 툴바 뒤로 깔려 보이지 않음(PR #961 동일 현상).
   원인:
     · copy-more/checklist-more 의 조상 #desktop-selection-bar 가 overflow-x-auto 라
       position:absolute 자식이 컨테이너 경계에서 잘리고 뒤 레이어로 깔린다.
   해결(ai-decompose-popover 와 동일 패턴):
     · position: fixed 로 뷰포트 기준 배치 → 조상 overflow 클리핑에서 완전히 탈출.
       (열 때 app-toolbar-proxy-dd.js 가 버튼 좌표 기준 top/right 를 인라인 지정.)
     · z-index 를 매우 높게(9999) 올려 헤더/툴바 등 다른 스택 컨텍스트 위로 올린다. */
#copy-more-dropdown,
#checklist-more-dropdown,
#settings-more-dropdown {
  position: fixed !important;
  z-index: 9999 !important;
  margin-top: 0 !important;
}

/* ===================================
   부하율 겹침표시 (assignee_load overlay) — 2026-07-20 · A3
   요청(B안): 그래프를 셀 배경으로 진하게 깔고, 그 위에 숫자를 겹쳐 표시.
   숫자는 흰색 외곽선(text-stroke) + 반투명 흰 박스로 그래프 선 위에서도 또렷하게.
   =================================== */
.assignee-load-overlay {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  min-height: 22px;
  line-height: 1;
}
/* 그래프 레이어: 셀 전체를 배경처럼 채움 */
.assignee-load-overlay .load-spark-bg {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;   /* 호버 타깃은 숫자보다 뒤 — 필요 시 아래 숫자 레이어에서 통과시킴 */
}
.assignee-load-overlay .load-spark-bg svg.gtt-load-spark {
  width: 100%;
  height: 100%;
}
/* 숫자 레이어: 그래프 위에 겹쳐 배치 + 가독성 확보 */
.assignee-load-overlay .load-num-overlay {
  position: relative;
  z-index: 1;
  font-weight: 700;
  font-size: 11px;
  padding: 1px 4px;
  border-radius: 4px;
  /* 반투명 흰 박스 (그래프가 비쳐 보이되 숫자 대비 확보) */
  background: rgba(255, 255, 255, 0.72);
  /* 흰색 외곽선 — 지원 브라우저에서 숫자를 그래프 선 위로 띄움 */
  -webkit-text-stroke: 0.6px rgba(255, 255, 255, 0.9);
  paint-order: stroke fill;
  /* text-stroke 미지원 브라우저 폴백: 흰 그림자 외곽 */
  text-shadow:
    0 0 2px rgba(255, 255, 255, 0.95),
    0 0 2px rgba(255, 255, 255, 0.95);
  pointer-events: none;   /* 마우스는 그래프(스파크라인 호버 타이틀)로 통과 */
  white-space: nowrap;
}
.assignee-load-overlay .load-num-sep {
  color: #9ca3af;
  font-weight: 400;
  margin: 0 0.5px;
}
.assignee-load-overlay .load-num-pct {
  font-size: 9px;
  color: #9ca3af;
  font-weight: 400;
  margin-left: 0.5px;
}
/* 다크모드: 숫자 박스를 어둡게 반전 */
:root[data-theme="dark"] .assignee-load-overlay .load-num-overlay,
.dark .assignee-load-overlay .load-num-overlay {
  background: rgba(17, 24, 39, 0.66);
  -webkit-text-stroke: 0.6px rgba(17, 24, 39, 0.9);
  text-shadow:
    0 0 2px rgba(17, 24, 39, 0.95),
    0 0 2px rgba(17, 24, 39, 0.95);
}
:root[data-theme="dark"] .assignee-load-overlay .load-num-sep,
:root[data-theme="dark"] .assignee-load-overlay .load-num-pct,
.dark .assignee-load-overlay .load-num-sep,
.dark .assignee-load-overlay .load-num-pct {
  color: #6b7280;
}

/* ===================================
   부하율 3칼럼 분리 (2026-08-14 · A1 · 오너 요청)
   그래프+숫자 겹침(assignee-load-overlay) → 그래프 전용 / 숫자 전용 칼럼으로 분리.
   · .load-spark-only : '부하율 경향' 칼럼 — 그래프가 셀을 채움(겹칠 숫자가 없으므로
     text-stroke·반투명박스 같은 가독성 보정이 더 이상 필요 없습니다).
   · .load-num-cell   : '피크/평균 부하율' 칼럼 — 숫자만. 색은 지표별 구간색(인라인 style).
   ※ .assignee-load-overlay 규칙은 남겨둡니다 — 간트 등 다른 화면에서 쓰일 수 있어
     제거는 별도 확인이 필요합니다(이번 변경 범위는 리스트뷰 칼럼입니다).
   =================================== */
.load-spark-only {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  min-height: 22px;
  line-height: 1;
}
.load-spark-only svg.gtt-load-spark {
  width: 100%;
  height: 100%;
}
.load-num-cell {
  font-weight: 700;
  font-size: 11px;
  line-height: 1;
  white-space: nowrap;
  /* 숫자 폭이 흔들리지 않게(tabular-nums 는 클래스로도 지정) 자릿수 정렬 */
  font-variant-numeric: tabular-nums;
}
.load-num-cell .load-num-pct {
  font-size: 9px;
  color: #9ca3af;
  font-weight: 400;
  margin-left: 0.5px;
}
:root[data-theme="dark"] .load-num-cell .load-num-pct,
.dark .load-num-cell .load-num-pct {
  color: #6b7280;
}

/* ══════════════════════════════════════════════════════════════════════════
   [필터 잠금 화면 요동 개선 2026-09-04 · A3]  C안(행 소멸 애니메이션) + D안(스크롤 앵커)

   배경: 필터 잠금(🔒) 상태에서 작업을 완료 처리하면 필터가 재적용되며 행이
         사라진다. A안(렌더 게이트, app-render-gate.js)으로 중간 상태 페인트를
         없애면 "9줄이 짠 하고" 나타나지만, 그것만으로는 **무엇이 왜 사라졌는지**
         인지할 수 없다(완전 무음 소멸). 그래서:
           · C안 — 사라지는 행만 150ms 페이드+수축 → 원인(완료 처리)과
                    결과(행 소멸)를 시각적으로 연결한다.
           · D안 — 행 수가 줄어도 보고 있던 위치가 밀리지 않게 앵커를 고정한다.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── D안: 스크롤 앵커 고정 ──────────────────────────────────────────────────
   overflow-anchor 는 스크롤 컨테이너 안에서 내용이 줄어들 때 브라우저가
   "보고 있던 요소"를 기준으로 scrollTop 을 자동 보정하게 한다.
   기본값(auto)이라도 조상 어딘가에서 none 으로 꺼져 있으면 상속되어 무력화되므로
   대상 컨테이너/행에 명시적으로 auto 를 선언해 보장한다.
   (JS 측 정밀 보정은 RenderGate.captureScrollAnchor/restoreScrollAnchor 가 담당) */
#content-scroll-container {
  overflow-anchor: auto;
}
.task-tree-content,
.task-tree-content > .task-hierarchical {
  overflow-anchor: auto;
}
/* 소멸 중인 행은 앵커 후보에서 제외 — 사라질 요소를 기준점으로 삼으면
   애니메이션이 끝나는 순간 기준이 소실되어 오히려 점프한다. */
.task-tree-content > .task-hierarchical.task-row-exit {
  overflow-anchor: none;
}

/* ── C안: 사라지는 행 exit 애니메이션 — 페이드 + 수축 (150ms) ──────────────
   ⚠️ 이 duration(150ms)은 app-render-gate.js 의 EXIT_MS 상수와 **반드시 일치**해야
      한다. JS 가 EXIT_MS 뒤에 DOM 을 교체하므로, CSS 가 더 길면 애니메이션이
      중간에 잘리고 더 짧으면 빈 행이 남아 깜빡인다.

   ⚠️ from 키프레임을 **의도적으로 생략**했다. 행 높이는 renderFlatTask() 가
      style="height: ${state.rowHeight}px" 로 **인라인**에 넣기 때문에 CSS 에서
      시작 높이를 알 수 없다. from 을 비우면 브라우저가 암시적 키프레임(implicit
      from)으로 현재 계산값(=인라인 높이)을 시작점으로 삼아 0 까지 보간한다.
      (CSS 애니메이션은 캐스케이드에서 인라인 선언보다 우선하므로 height 를
       0 으로 덮어쓸 수 있다.) → 행 수가 다양한 rowHeight 설정에서도 동작한다.
   높이를 실제로 0 까지 줄이므로 아래 행들이 그 자리를 부드럽게 메운다
   = 사용자가 "방금 완료한 행이 접히며 빠졌다"는 인과를 인지할 수 있다. */
@keyframes taskRowExit {
  to {
    opacity: 0;
    height: 0;
    min-height: 0;
    padding-top: 0;
    padding-bottom: 0;
    border-top-width: 0;
    border-bottom-width: 0;
    transform: translateX(-8px);
  }
}
.task-hierarchical.task-row-exit {
  animation: taskRowExit 150ms ease-out forwards;
  /* 높이가 줄어드는 동안 셀 내용이 밖으로 삐져나오지 않게 잘라낸다 */
  overflow: hidden;
  /* 소멸 중 행은 클릭/드래그 대상이 되면 안 된다(이미 없는 데이터) */
  pointer-events: none;
  /* 합성 힌트 — 150ms 동안만 유효하므로 will-change 유지 비용이 미미하다 */
  will-change: opacity, height, transform;
}

/* ── 접근성: prefers-reduced-motion 존중 ───────────────────────────────────
   운영체제에서 "동작 줄이기"를 켠 사용자는 애니메이션 자체가 불편/어지럼을
   유발할 수 있다. JS(RenderGate)도 matchMedia 로 애니메이션을 건너뛰지만,
   CSS 에서도 이중으로 무력화해 어떤 경로로 클래스가 붙어도 즉시 사라지게 한다. */
@media (prefers-reduced-motion: reduce) {
  .task-hierarchical.task-row-exit {
    animation: none;
    opacity: 0;
    height: 0 !important; /* 인라인 height 를 덮어써 즉시 접는다 */
    overflow: hidden;
    transform: none;
    will-change: auto;
  }
}

/* ══════════════════════════════════════════════════════════════════════
   [모바일 가로보기 2026-09-10 · A2] 스마트폰 가로에서 PC UI 접기

   오너 제보: "가로보기에서 PC의 버튼들이 화면의 50%정도를 차지함".
   실측(Galaxy S21 가로 800×360): 사이드바 256px(32%) + PC헤더 85px
   → 실제 콘텐츠에 남는 면적 51.9%.

   ▶ 왜 CSS 로 하는가
     사이드바/헤더는 Tailwind `hidden md:flex` 로 제어된다. md 는 768px
     기준이라 스마트폰 가로(800px)가 PC 로 잡힌다. 이 판정을 JS 로 매번
     덮어쓰면 클래스 싸움이 되므로, **판정 결과 클래스(.vp-compact)** 를
     `<html>` 에 붙이고 CSS 가 그것만 보게 한다.
     → JS(app-viewport.js)와 CSS 가 **같은 판정 하나**를 공유한다.

   ⚠️ `!important` 는 Tailwind 의 `md:flex`(동일 특정도)를 확실히 이기기
      위한 것이다. 빼면 로드 순서에 따라 들쭉날쭉해진다.
   ⛔ `.vp-compact` 판정 로직을 CSS 미디어쿼리로 복제하지 말 것 —
      판정 지점이 둘이 되어 이번에 고친 문제가 그대로 재발한다.
   ══════════════════════════════════════════════════════════════════════ */

/* 사이드바: 가로 스마트폰에서 폭 256px 를 통째로 회수 */
html.vp-compact #sidebar {
  display: none !important;
}

/* PC 전용 헤더: 높이 85px 회수 (모바일 통합 헤더가 대신 뜬다) */
html.vp-compact header.md\:flex {
  display: none !important;
}

/* PC 전용 위젯(ETA 요약 등)도 함께 접는다 */
html.vp-compact [data-eta-summary="desktop"] {
  display: none !important;
}

/* ℹ️ 모바일 통합 헤더는 **따로 손대지 않는다.**
     그 컨테이너는 `lg:hidden`(1024 기준)이라 스마트폰 가로(667~932px)에서는
     이미 보이는 상태다. 실측으로 확인했다.
   ⛔ 여기서 `.md\:hidden` 을 display:block 으로 강제하지 말 것 —
      그 클래스는 칸반 카드의 상태 배지·진행바(app-view.js L524/L559)에만
      쓰이므로, 건드리면 무관한 UI 가 깨진다. */

/* 가로는 세로 공간이 극히 좁다(360~412px). 세로 여백을 줄여
   콘텐츠가 실제로 보이게 한다. 가로 여백은 건드리지 않는다. */
html.vp-phone-landscape #main-content {
  padding-top: 0.25rem !important;
  padding-bottom: 0.25rem !important;
}

/* ══════════════════════════════════════════════════════════════════════
   [보기모드 전환 2026-09-11 · A2] 전환 버튼 노출 조건

   오너 지시: "모바일에서 밖으로 빼놔야지, PC는 전환 버튼 불필요"
   → 폰 기기에서만 보인다. PC·태블릿에는 아예 렌더되지 않는다.

   🔴 조건이 `.vp-phone-device` 인 것이 핵심이다. `.vp-compact` 로 하면
      폰에서 PC 모드를 고른 순간 compact 가 꺼져 **버튼이 사라지고,
      되돌릴 수단이 없어 사용자가 갇힌다.** 기기 종류는 모드와 무관하게
      유지되므로 출구가 항상 남는다.
   ══════════════════════════════════════════════════════════════════════ */

/* ℹ️ 전용 헤더 버튼(.vp-phone-only)은 **철회**했다. 오너 지시에 따라
      보기모드는 칼럼설정과 같은 「설정 항목」(☰ 드로어 → 보기 모드)으로 두고,
      헤더에 버튼을 늘리지 않는다. 가로보기에서 버튼이 이미 3줄이었기 때문이다.
      → 드로어 항목은 모바일 헤더 안에 있으므로 별도 노출 규칙이 필요 없다. */

/* ── 모바일 전용 UI 노출 (.vp-mobile-only) ───────────────────────────
   🔴 사고 이력 두 건을 동시에 고친 규칙이다.

   ① 「메뉴가 없어」 — Tailwind `lg:hidden` 은 **1024px 이상에서 숨김**이다.
      오너 실기기 가로폭이 정확히 1024 라 모바일 헤더·하단 액션바가 **전부
      사라졌다**(스크린샷: 상태바 바로 아래에 테이블 헤더가 붙어 있었다).
      나는 앞선 주석에 *"폰 가로는 667~932 이므로 이미 보인다"* 고 적었는데,
      1024 급 기기를 빠뜨린 가정이었다.
      → Tailwind 브레이크포인트를 버리고 **우리 판정(.vp-compact)** 을 따르게
        바꿨다. 이제 폭과 무관하게 "모바일 UI 인가" 하나로 결정된다.

   ② 「PC모드로 했더니 갇혔어 — 아무것도 안됨」 — 되돌릴 출구를 만들려고
      `.lg\:hidden` **전체**에 display:block 을 걸었더니, 평소 닫혀 있어야 할
      전체화면 오버레이(#mobile-more-backdrop, fixed inset-0)까지 펼쳐져
      **모든 클릭을 가로챘다.** 실측 재현: 백드롭이 1024x461 로 화면을 덮고
      중앙 클릭 대상이 backdrop 이었다.
      → 클래스 전체에 거는 방식을 폐기했다. 지금은 각 요소가 자기 상태
        (.hidden, translate-y-full)를 그대로 유지한다.

   ⛔ 다시 `.lg\:hidden` 전체를 강제로 펼치지 말 것. 오버레이가 딸려 온다.
   ⛔ 여기에 미디어쿼리(max-width)를 쓰지 말 것 — 판정 지점이 둘이 되어
      ①번 문제가 그대로 재발한다. 판정은 app-viewport.js 하나다.
   ────────────────────────────────────────────────────────────────── */

/* 기본: 숨김. PC/태블릿에서는 모바일 UI 를 띄우지 않는다. */
.vp-mobile-only {
  display: none !important;
}

/* 모바일 UI 일 때만 노출.
   ⚠️ `:not(.hidden)` 필수 — .hidden 은 "지금 닫혀 있다" 는 **동작 상태**다.
      (더보기 백드롭이 대표적) 이를 무시하면 닫힌 오버레이가 펼쳐져 ②번
      사고가 재발한다. JS 가 .hidden 을 떼면 그때 자연히 보인다. */
html.vp-compact .vp-mobile-only:not(.hidden) {
  display: block !important;
}

/* 헤더 내부는 flex 레이아웃이라 block 으로 덮으면 정렬이 깨진다. */
html.vp-compact #mobile-header-integrated {
  display: flex !important;
}

/* 폰에서 PC 모드를 고른 경우 — 되돌릴 출구.
   ☰ 가 있는 모바일 헤더만 남긴다. 액션바·오버레이는 건드리지 않는다
   (PC 모드에서 하단 액션바까지 띄우면 화면을 또 잡아먹는다). */
html.vp-phone-device:not(.vp-compact) #mobile-header-integrated {
  display: flex !important;
}
html.vp-phone-device:not(.vp-compact) .vp-mobile-only:has(#mobile-header-integrated) {
  display: block !important;
}

/* ══════════════════════════════════════════════════════════════════════
   [가로 공간 효율 2026-09-11 · A2] 폰 가로에서 상·하단이 먹는 세로 회수

   오너 지시: "화면의 상단과 하단의 공간효울이 떨어져 해결해줘"

   ── 실측 (오너 기기 조건 1024x461, 실제 브라우저 · 데이터 45행) ────────
                        수정 전        수정 후
     상단 헤더 래퍼        58px    →     46px
     표 창(본문)          181px    →    311px
       화면 대비          39.3%    →    67.5%
     표 아래 죽은 공간     119px    →     13px
     하단 액션바           54px    →     46px
     세로·노트북          변화 없음 (영향 범위가 폰 가로로 한정됨)
   ───────────────────────────────────────────────────────────────────

   🔴 빈 119px 의 진짜 원인은 여백이 아니라 **상수로 박힌 max-height** 다.
     app-task-render.js L2838:  max-height: calc(100vh - 280px)
                        L2752:  max-height: calc(100vh - 220px)
     이 280 / 220 은 **세로 화면(높이 800~900px) 기준**으로 잡힌 값이다.
     세로에서는 800-280=520px 로 화면의 65% 라 문제가 없지만,
     가로는 높이가 461px 뿐이라 461-280=181px → **화면의 39%** 로 잘린다.
     화면이 낮아질수록 상수의 비중이 커지는 구조적 결함이다.

   🔴 `!important` 가 필요한 이유 — 저 max-height 는 **인라인 style 속성**이다.
     인라인은 외부 CSS 보다 우선하므로 !important 없이는 절대 덮이지 않는다.

   ⚠️ 적용 범위는 `html.vp-phone-landscape` — **폰 가로일 때만**이다.
     판정은 app-viewport.js(SoT) 한 곳에서만 한다.
   ⛔ @media (orientation: landscape) 로 옮기지 말 것 — 판정 지점이 2개가 되어
      W7 사고(두 경로 불일치)가 재발한다.
   ────────────────────────────────────────────────────────────────────── */

/* ① 상단 — 모바일 헤더 래퍼의 세로 여백 회수 (58px → 46px)
   가로 여백과 헤더 자체 높이(40px)는 건드리지 않는다.
   ☰ · 알림 · 채팅 버튼의 터치 목표를 줄이면 안 되기 때문이다. */
html.vp-phone-landscape .vp-mobile-only:has(#mobile-header-integrated) {
  padding-top: 0.125rem !important;
  padding-bottom: 0.125rem !important;
  margin-bottom: 0.25rem !important;
}

/* ② 본문 — 죽은 119px 회수.
   🔴 `height: auto` 가 max-height 와 **쌍으로** 있어야 한다.
     · max-height 만 덮으면 '상한'만 바뀌고 인라인 height 가 남아,
       작업이 적을 때 컨테이너가 그 높이를 유지해 빈 영역이 남는다.
     · height:auto 만 두면 행이 많을 때 늘어나 화면을 뚫는다.
     둘이 함께여야 "적으면 줄고, 많으면 멈춘다". */
html.vp-phone-landscape #content-scroll-container {
  max-height: calc(100vh - 150px) !important;
  height: auto !important;
}
html.vp-phone-landscape #tasks-container > div > div {
  max-height: calc(100vh - 115px) !important;
  height: auto !important;
}

/* ③ 하단 — 액션바 세로 압축 (54px → 46px)
   🔴 병목은 패딩이 아니라 버튼의 `min-height: 44px` 이다. 실측으로 확인했다.
     처음에 버튼 padding 만 줄였더니 min-height 가 이미 44px 이라 **버튼은
     그대로**인데 래퍼의 py-1(4+4)만 남아 오히려 54 → 57px 로 늘었다.
     → 그래서 손대는 곳은 **래퍼 패딩**이다.
   ⛔ min-height:44px 를 낮추지 말 것. 44px 은 터치 목표 최소 권장 크기이며
     (Apple HIG / WCAG 2.5.5), 더 줄이면 '삭제' 같은 파괴적 버튼을 잘못 누른다.
   라벨도 남긴다 — 아이콘만 두면 "새작업/상세/AI/삭제/해제/More" 구분이 어렵다. */
html.vp-phone-landscape #mobile-bottom-actionbar > div {
  padding-top: 0 !important;
  padding-bottom: 0 !important;
}

/* ══════════════════════════════════════════════════════════════════════════
   [sticky 회귀 수정 2026-09-12 · A2] 리스트뷰 높이 체인 연결

   🔴 오너 제보: "헤더행 고정 안되는 문제 / 마일스톤 행들 고정 안되는 문제"
     (PR #1255 · A3 의 side effect)

   ── 원인 ────────────────────────────────────────────────────────────────
   A3 는 목록 래퍼를 `height: 100%` + flex 로 바꿨다(고정 100vh 차감 제거).
   방향은 옳다. 그런데 `height: 100%` 는 **부모의 높이가 확정되어야** 의미가
   있고, 그 사이 래퍼들에 높이가 없어 100% 의 기준이 사라졌다.

   실측(노트북 1366×768, 데이터 45행 전개) — 체인이 #content-area 에서 끊긴다:

     #content-area              h= 684   ← 여기까지 화면 높이 기준 (정상)
       .mb-6                    h=1974   ← ❌ 높이 미상속, 내용만큼 늘어남
         #tasks-container       h=1837   ← ❌ 같이 늘어남
           .bg-transparent      h=1833   ← height:100% 가 1833px 로 풀림
             #content-scroll-container h=1800  ← flex:1 이 1800px 을 받음

   결과: clientHeight(1800) == scrollHeight(1800) →
     **목록이 자기 스크롤을 잃고** 스크롤이 #main-content 로 새어나갔다.
     `position: sticky` 는 "가장 가까운 스크롤 조상" 을 기준으로 붙으므로,
     그 기준 컨테이너가 사라지면 헤더행·마일스톤행이 따라 올라가지 못한다.
     → 오너가 본 "고정 안 됨" 증상이다.

   ── 왜 이 방법인가 ──────────────────────────────────────────────────────
   A3 의 flex 설계를 되돌리지 않는다(그건 하단 공백 문제를 되살린다).
   끊긴 두 칸만 이어 `height: 100%` 가 딛고 설 바닥을 만든다.

   🔴 `min-height: 0` 이 함께 필요하다. flex 자식의 기본 min-height 는 auto 라
     내용이 컨테이너를 밀어내 같은 누출이 다시 발생한다.

   ⚠️ 리스트뷰에만 적용한다 — `#tasks-container:has(#content-scroll-container)`.
     간트뷰는 #content-scroll-container 가 없어 이 규칙을 받지 않는다.
     (#tasks-container 는 리스트/간트가 공유하는 컨테이너다. 무조건 100% 를
      걸면 간트의 자체 높이 계산을 건드린다 — 그래서 :has() 로 좁힌다.)

   ⛔ 되돌리지 말 것. 되돌리면 sticky 가 다시 깨진다.
     회귀 테스트: tests/list-sticky-height-chain.test.ts
   ══════════════════════════════════════════════════════════════════════════ */

/* ① 최상위 래퍼(.mb-6) — buildProjectViewHTML() 의 바깥 껍데기.
      리스트뷰가 들어있을 때만 높이를 흘려보낸다. */
#content-area > .mb-6:has(#content-scroll-container) {
  height: 100%;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

/* ② 중간 래퍼(.mb-2 2겹) — .mb-6 과 #tasks-container 사이에 두 칸이 더 있다.
      실측으로 확인했다: .mb-6 만 이었을 때 .mb-6 은 684 로 잡혔지만
      그 아래 .mb-2 두 개가 1974 로 남아 체인이 다시 끊겼다.
      한 칸이라도 비면 height:100% 의 기준이 사라진다 — 전 구간이 이어져야 한다. */
#content-area > .mb-6:has(#content-scroll-container) .mb-2:has(#content-scroll-container) {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

/* ③ #tasks-container — 리스트뷰일 때만 남는 높이를 전부 받는다. */
#tasks-container:has(#content-scroll-container) {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

/* ④ 마지막 한 칸 — #tasks-container 와 A3 래퍼(.bg-transparent) 사이에
      **클래스 없는 <div>** 가 하나 더 있다(renderHierarchicalTaskList 의 껍데기).
      실측으로 드러났다: ①~③ 을 이은 뒤에도 이 칸이 1833 으로 남아
      바로 아래 A3 래퍼의 height:100% 가 다시 1833 을 기준으로 삼았다.
      클래스가 없으므로 자식 관계로 지목한다. */
#tasks-container:has(#content-scroll-container) > div:has(> .bg-transparent.rounded) {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

/* ══════════════════════════════════════════════════════════════════════════
   [가로/세로 액션바 분기 2026-09-12 · A2]

   오너 지시:
     세로 6개 : 완료 · 다중입력 · 새로고침 · 삭제 · 해제 · 더보기
     가로 11개: 새작업 · AI작업분해 · 복사 · 작업상세 · 별도창 + 위 6개

   ── 왜 CSS 로 감추는가 (조건부 HTML 이 아닌 이유) ────────────────────────
   회전 시 **액션바가 다시 그려지지 않는다.** 실측으로 확인했다
   (세로 412x915 → 가로 915x412):
     · renderProjectView 호출 횟수 = 0
     · 액션바 DOM 에 심은 표식이 그대로 남아 있음 = 재생성 안 됨
   즉 HTML 을 `isPhoneLandscape ? 11개 : 6개` 로 만들면 세로로 진입한
   사용자가 가로로 돌려도 6개에 머물고 새 버튼 5개는 영구히 안 나온다.

   그래서 11개를 항상 렌더하고, 세로에서 5개를 감춘다.
   회전은 html 클래스(vp-phone-landscape) 토글만으로 즉시 반영되며
   판정은 app-viewport.js(SoT) 한 곳에서만 한다.
   ⛔ 여기서 @media (orientation) 로 다시 판정하지 말 것 — 판정 지점이
     2개가 되어 W7 사고(두 경로 불일치)가 재발한다.

   ── 가로 11개가 들어갈 폭이 있는가 (실측 근거) ──────────────────────────
   오너 기기 가로폭 1024px 기준:
     래퍼 좌우 패딩 px-1 = 8px  →  가용 1016px
     11개로 나누면 버튼당 약 92px
   44px 터치 목표(Apple HIG / WCAG 2.5.5)를 폭에서도 여유롭게 넘는다.
   ⛔ 그래도 min-width 를 44px 밑으로 두지 않는다. 더 좁은 가로 기기
     (예: 폭 667px → 버튼당 60px)에서도 터치 목표를 지켜야 한다.

   🔴 max-w-screen-sm(640px) 제한을 가로에서 푼다.
     래퍼에 Tailwind `max-w-screen-sm` 이 걸려 있어, 1024px 화면에서도
     11개가 640px 안에 갇힌다 → 버튼당 58px 로 쪼그라들고 라벨이 겹친다.
     세로(최대 ~430px)에서는 이 제한이 애초에 걸리지 않으므로 무해했다.
   ══════════════════════════════════════════════════════════════════════════ */

/* ① 가로 전용 5개 — 기본(세로)에서는 숨긴다. */
.vp-landscape-only {
  display: none !important;
}

/* ② 폰 가로에서만 노출. 액션바 버튼은 flex-column 이므로 flex 로 되살린다
      (display:block 이면 아이콘과 라벨이 세로 정렬을 잃는다). */
html.vp-phone-landscape .vp-landscape-only {
  display: flex !important;
}

/* ③ 가로에서 래퍼 폭 제한 해제 — 11개가 화면 전체 폭을 쓰도록.
      위 주석의 max-w-screen-sm 문제를 푼다. */
html.vp-phone-landscape #mobile-bottom-actionbar > div {
  max-width: none !important;
}

/* ④ 가로 11개 버튼 — 라벨이 겹치지 않게 폭·글자를 조인다.
      🔴 min-height:44px 는 건드리지 않는다(터치 목표). 여기서 줄이는 것은
        가로 여백과 글자 크기뿐이다. */
html.vp-phone-landscape #mobile-bottom-actionbar .mobile-bottom-btn {
  min-width: 44px;              /* 터치 목표 하한 — 더 좁히지 말 것 */
  padding-left: 0.125rem;
  padding-right: 0.125rem;
}

/* ⑤ 가로에서 라벨 글자 축소 + 한 줄 유지.
      11개가 되면 "AI작업분해" 같은 긴 라벨이 두 줄로 접혀 버튼 높이를
      밀어올린다. 줄바꿈을 막고 넘치면 말줄임으로 처리한다. */
html.vp-phone-landscape #mobile-bottom-actionbar .mobile-bottom-btn > span {
  font-size: 0.625rem;          /* 10px */
  line-height: 1.1;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}

/* ══════════════════════════════════════════════════════════════════════════
   [합집합 복원 2026-09-12 · A2] 세로 전용 프록시 (.vp-portrait-only)

   🔴 오너 지적: "합집합 개념으로 가로보기의 하단매뉴와 더보기안 매뉴의 합은
     세로보기의 그것과 같아야 해" ('별도창' 누락을 직접 지목)

   가로/세로 분기를 넣을 때 5개(새작업·AI분해·복사·작업상세·별도창)를
   액션바의 가로 전용 구간으로 옮기고 시트에서 지웠다. 그런데 그 5개는
   세로에서 CSS 로 숨겨지므로, 세로 사용자는 더보기를 열어도 닿을 수 없었다.
   실측 확증(세로 412x915, 시트 열린 상태): 5개 전부 visible=false.

   그래서 더보기 시트에 '세로 전용' 프록시 버튼을 되살린다.
   가로에서는 액션바에 이미 있으므로 시트에서는 감춘다 — 중복 노출 방지.

   ⚠️ .vp-landscape-only 와 정확히 반대다. 둘을 합치면 항상 정확히 한쪽만
     보인다 → 합집합이 세로·가로에서 동일하게 유지된다.
   ⛔ @media (orientation) 으로 판정하지 말 것 — 판정은 app-viewport.js(SoT)
     한 곳뿐이어야 한다(W7 사고 방지).
   ══════════════════════════════════════════════════════════════════════════ */

/* 기본(세로) — 보인다. 시트 그리드가 grid-cols-4 이므로 flex 로 되살린다
   (display:block 이면 아이콘과 라벨의 세로 정렬이 깨진다). */
.vp-portrait-only {
  display: flex !important;
}

/* 폰 가로 — 액션바에 원본이 있으므로 시트에서는 숨긴다. */
html.vp-phone-landscape .vp-portrait-only {
  display: none !important;
}

/* ══════════════════════════════════════════════════════════════════════════
   [PC 사이드바 너비 드래그 조절 2026-09-14 · A1] 리사이즈 핸들

   오너 요청: "PC작업창 사이드바 너비 조절가능하도록" — 종전에는 Tailwind
   `w-64`(256px) 고정이라 사용자가 바꿀 수단이 0개였고, 프로젝트명이 길면
   잘렸다. 있는 것은 접기/펼치기 2단계 토글뿐이었다.

   ▶ 왜 간트 트리 리사이저(#tree-resizer)와 같은 형태인가
     같은 저장소에 이미 검증된 구현(PR #1117 · A3 · 2026-08-12)이 있다.
     새 감각을 만들면 두 화면의 조작법이 달라지므로 그 패턴을 이식한다.
     차이는 **경계 방향 하나**다 — 간트 트리는 오른쪽 경계에 border-right:2px
     가 있어 right:-3px 로 선 위에 겹쳤지만, 사이드바는 border-r 이 1px 이라
     right:-4px 로 8px 핸들의 정중앙을 경계선에 맞춘다.

   ⛔ 폭 판정/저장을 CSS 로 복제하지 말 것 — 값의 SoT 는 전용 리사이즈
      스크립트(localStorage 'sidebarWidth') 한 곳이다. 여기 선언은 **초기값이
      아니라 핸들의 외형**만 담당한다(사이드바 초기 폭은 Tailwind w-64).
   ══════════════════════════════════════════════════════════════════════════ */

#sidebar-resizer {
  position: absolute;
  top: 0;
  /* border-r 1px 의 정중앙에 8px 핸들을 얹는다 → 경계 기준 −4px~+4px.
     "선 위를 잡는" 감각을 주기 위한 것이다(간트와 동일 의도). */
  right: -4px;
  width: 8px;
  height: 100%;
  cursor: col-resize;
  /* 프로젝트 행(.project-item 등)보다 위에 있어야 행 클릭/드래그에
     핸들이 가려지지 않는다. 간트는 20 을 쓴다 — 같은 값으로 맞춘다. */
  z-index: 20;
  background: transparent;
  transition: background 0.15s;
  /* 드래그 중 텍스트 선택 방지(mousedown 의 preventDefault 와 이중 안전망) */
  user-select: none;
  -webkit-user-select: none;
  /* 모바일에서 사이드바 자체가 숨으므로 핸들도 함께 사라진다.
     다만 터치 환경에서 스크롤 제스처를 먹지 않도록 명시한다. */
  touch-action: none;
}

/* 평소엔 투명 — hover/드래그/포커스에서만 파란 선이 나타난다(발견성).
   키보드 사용자를 위해 :focus-visible 도 같이 표시한다. */
#sidebar-resizer:hover,
#sidebar-resizer:focus-visible,
#sidebar-resizer.dragging {
  background: rgba(13, 110, 253, 0.35);
}

/* 접힌 상태(w-0)에서는 잡을 경계가 없다 — 0px 사이드바의 핸들을 드래그하면
   사용자는 "접었는데 왜 늘어나지?" 를 겪는다. 접기 토글이 붙이는
   .collapsed 클래스를 그대로 재사용해 숨긴다(새 상태값을 만들지 않는다). */
#sidebar.collapsed #sidebar-resizer {
  display: none;
}

/* 드래그 중에는 폭 전환 애니메이션을 끈다.
   #sidebar 에는 접기/펼치기용 `transition-all duration-300` 이 걸려 있어,
   끄지 않으면 핸들이 커서를 300ms 늦게 따라와 "고무줄" 처럼 느껴진다.
   ⚠️ 클래스는 드래그 시작~종료 구간에만 붙인다(접기 애니메이션 보존). */
#sidebar.resizing {
  transition: none !important;
}

/* ══════════════════════════════════════════════════════════════════════════
   [제스처 영역분리 2026-09-16 · A1] 모바일 롱프레스 ↔ Drag & Drop 충돌 해소

   🔴 오너 지시: "모바일 롱프레스와 Drag&Drop 충돌 해결 해줘 - 영역분리 방식으로
      · 선택 : Drag & Drop  · 제목영역 : 롱프레스"

   Android Chrome / 삼성 인터넷은 **롱프레스를 native HTML5 드래그 시작
   제스처로 쓴다.** 행 전체가 draggable="true" 인 이상, 제목칸을 꾹 눌러도
   드래그가 승격되어 롱프레스 메뉴와 다툰다.

   JS 가드(app-view.js 의 dragstart, app-task-context-menu.js 의 touchstart)가
   1차 방어지만, CSS 로도 **브라우저가 애초에 드래그를 시작하지 않게** 막는다.
   JS 가드는 이미 시작된 드래그를 취소하는 것이고, 이쪽은 시작 자체를 막는다
   — 후자가 더 부드럽다(고스트 이미지가 한 프레임도 보이지 않는다).

   ⛔ `draggable="false"` 를 제목칸 마크업에 박으면 **데스크톱 마우스 드래그도
      함께 죽는다.** 그래서 마크업이 아니라 CSS 로, 그것도 포인터가 굵은
      (= 터치) 기기에서만 적용한다. 이것이 이 블록이 존재하는 이유다.
   ══════════════════════════════════════════════════════════════════════════ */

/* 터치 기기에서만 — 마우스가 있는 환경은 이 문제가 없다(우클릭 ≠ 드래그).

   `pointer: coarse` 는 **주 포인터가 손가락**이라는 뜻이다. 터치스크린 노트북은
   주 포인터가 마우스이므로 `pointer: fine` 으로 보고되어 여기에 걸리지 않는다
   — 즉 이 한 줄로 "폰/태블릿만" 이 정확히 잡힌다.

   ⛔ MQ Level 4 의 부정 구문(`not (...)` 로 any-pointer 를 배제하는 형태)을
      쓰지 말 것. 브라우저 지원이 고르지 않아 조건 전체가 무시되면
      **데스크톱에도 적용되어** 마우스 드래그가 죽는다. 단순한 쪽이 안전하다.
      (회귀 잠금: tests/mobile-gesture-zone-split.test.ts ⑤) */
@media (pointer: coarse) {

  /* ── 제목영역 = 롱프레스 전용. 터치 드래그 승격을 차단한다 ──────────────── */
  .task-hierarchical [data-column="task_title"] {
    /* native 드래그 시작을 막는 표준/벤더 속성.
       ⚠️ `-webkit-user-drag` 는 비표준이지만 이 문제의 당사자인
          Chromium(Android Chrome / 삼성 인터넷)이 이것만 이해한다. */
    -webkit-user-drag: none;
    -khtml-user-drag: none;
    user-drag: none;
    /* iOS Safari 의 "꾹 누르면 뜨는 링크/이미지 미리보기" 를 끈다.
       켜져 있으면 롱프레스 메뉴와 OS 팝업이 동시에 뜬다. */
    -webkit-touch-callout: none;
    /* 꾹 누를 때 텍스트가 파랗게 선택되는 것을 막는다(메뉴 UX 방해).
       ⚠️ 더블클릭 인라인 편집(app-inline-edit-v2)은 편집 진입 후 input 요소를
          새로 만들므로, 이 선언이 편집 중 텍스트 선택을 막지는 않는다. */
    -webkit-user-select: none;
    user-select: none;
    /* ══════════════════════════════════════════════════════════════════════
       [가로 스크롤 회귀 2026-09-17 · A1] 🔴 `pan-y` → `manipulation`

       🔴 오너 보고: "모바일에서 화면을 좌우로 스크롤 하는 것이 안됨"

       #1279 에서 여기에 `pan-y` 를 썼다. 그때 지키려던 것은 **세로** 스크롤
       이었고("none 이면 목록을 굴릴 수 없다"), 그 판단 자체는 옳았다.
       놓친 것은 **같은 컨테이너에 가로 스크롤도 있다**는 사실이다 —
       `#content-scroll-container` 는 `overflow-x: auto` 로 칼럼을 좌우로
       굴린다(app-task-render.js). 목록의 칼럼은 화면 폭보다 넓으므로
       가로 스크롤은 옵션이 아니라 **필수 조작**이다.

       `pan-y` 는 "세로 패닝만 허용" 이다 = **가로 패닝을 명시적으로 차단**한다.
       게다가 `touch-action` 의 실효값은 터치 지점부터 스크롤 컨테이너까지의
       **교집합**이므로, 제목칸에서 시작한 터치는 조상이 `overflow-x:auto`
       여도 가로로 움직이지 않는다. 제목칸은 목록에서 가장 넓은 칸이라
       손가락이 거기 닿을 확률이 가장 높아, 사용자 눈에는 "가로 스크롤이
       아예 안 되는" 것으로 보인다.

       ▶ 조치. `manipulation` = `pan-x pan-y pinch-zoom` 으로 넓힌다.
         · 가로·세로 패닝 모두 허용 (가로 스크롤 복구, 세로 스크롤 유지)
         · 핀치 줌도 함께 복구된다 — `pan-y` 는 이것까지 막고 있었다
         · 비표준 제스처(더블탭 줌)만 계속 차단 → 더블탭 인라인 편집
           (app.js setupMobileDoubleTap)과 다투지 않는다. 이 성질은
           `pan-y` 시절과 동일하므로 편집 동작은 **무변경**이다.

       ⛔ `auto` 로 되돌리지 말 것. 그러면 브라우저가 이 칸의 꾹 누르기를
          다시 제스처로 해석할 여지가 생긴다 — 영역분리(#1279)가 없애려던
          바로 그 경합이다. 제한을 유지하면서 축만 넓히는 것이 요점이다.

       ⛔ `pan-x pan-y pinch-zoom` 이라고 풀어 쓰지 말 것. `pinch-zoom`
          키워드를 모르는 구형 엔진은 **선언 전체를 무효**로 버려 `auto` 로
          떨어지고, 위의 경합이 되살아난다. `manipulation` 은 Chrome 36 /
          Safari 9.3 부터 있어 폴백 위험이 없다.

       ⛔ 제목칸의 native 드래그 차단은 이 속성이 아니라 위쪽
          `-webkit-user-drag: none` 이 담당한다. 그러므로 이 변경으로
          드래그 차단이 약해지지 않는다(영역분리 계약 유지).
       ══════════════════════════════════════════════════════════════════════ */
    touch-action: manipulation;
  }

  /* 제목칸 내부 요소까지 상속되지 않는 속성이 있어 명시적으로 내린다.
     (특히 -webkit-user-drag 는 상속되지 않는 구현이 있다) */
  .task-hierarchical [data-column="task_title"] * {
    -webkit-user-drag: none;
    -webkit-touch-callout: none;
    /* ══════════════════════════════════════════════════════════════════════
       [롱프레스 글자 불규칙 2026-09-19 · A3] 🔴 `touch-action` 도 상속되지 않는다

       🔴 오너 보고: "제목칼럼의 **공백**을 누르면 잘 뜨는데 **글자**가 있는
          부분을 누르면 여전히 불규칙해. 공백 누를때와 글자 누를때의 차이가 뭐니?"

       ▶ 실측한 차이 (Chromium, Pixel 7 에뮬레이션, 이 파일 그대로 렌더):

              요소                     touch-action
              [data-column=task_title]  manipulation   ← 위 규칙이 지정
              .task-link                auto           ← 🔴 아무도 지정 안 함
              제목 텍스트 span           auto           ← 🔴

         `touch-action` 은 **상속되지 않는 속성**이다. 위(#1294, 2026-09-17)에서
         `pan-y` → `manipulation` 으로 고칠 때 **셀 하나에만** 적용됐고, 그 안의
         글자 요소들은 계속 `auto` 로 남아 있었다.

       ▶ 왜 그것이 롱프레스를 깨뜨리나.
         `auto` 는 "이 지점의 제스처 해석을 브라우저에 전부 맡긴다" 는 뜻이다.
         그래서 글자 위에서는 Chromium 이 **자기 롱프레스 처리**(텍스트 선택
         핸들·확대·번역 등)를 자유롭게 시작하고, 그 과정에서 우리 터치 시퀀스에
         `touchcancel` 을 던진다. 공백 부분은 `manipulation` 이라 그 해석이
         차단되어 방해가 없었다 —— **오너가 본 "공백은 되고 글자는 불규칙"이
         정확히 이 경계선이다.**

         `manipulation` = `pan-x pan-y pinch-zoom`. 스크롤과 핀치줌은 그대로
         허용하고 **비표준 제스처(더블탭 줌·롱프레스 해석)만** 막는다.
         즉 스크롤을 잃지 않으면서 경합만 없앤다.

       ⚠️ 이 선언이 셀에만 있고 자손에 없으면 **셀의 값은 무의미해진다.**
          실효값은 터치가 **실제로 닿은 요소**에서 정해지기 때문이다. 제목칸은
          거의 전부가 글자이므로 사용자의 손가락은 대개 자손에 닿는다.

       ⛔ `auto` 로 되돌리지 말 것 — #1279 가 없애려던 제스처 경합이 되살아난다.
       ⛔ `pan-x pan-y pinch-zoom` 으로 풀어 쓰지 말 것 — `pinch-zoom` 미지원
          엔진이 선언 전체를 버려 `auto` 로 떨어진다(위 규칙과 같은 이유).
       ⛔ 선택칸(.task-checkbox-container)에는 **여전히 넣지 않는다.** 그쪽은
          `auto` 여야 브라우저가 꾹 누르기를 native 드래그로 승격시킨다(D&D).
          회귀 잠금: tests/mobile-gesture-zone-split.test.ts ⑤
       ══════════════════════════════════════════════════════════════════════ */
    touch-action: manipulation;
  }

  /* ── 선택영역 = Drag & Drop 전용 ────────────────────────────────────────
     여기서는 native 드래그를 **살려야** 하므로 user-drag 를 건드리지 않는다.
     대신 꾹 누를 때의 텍스트 선택/OS 팝업만 정리해 드래그 감각을 깔끔하게 한다. */
  .task-hierarchical .task-checkbox-container {
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    user-select: none;
    /* ⛔ `touch-action` 을 건드리지 않는다 — 기본값(auto)이어야 브라우저가
       꾹 누르기를 드래그로 승격시킬 수 있다. `pan-y` 로 좁히면 스크롤만
       남고 **드래그가 죽는다**(우리가 살리려는 바로 그 동작이다). */
  }

  /* ⚠️ 체크박스 자체는 예외다. 선택 토글이 본래 목적이므로 기본 동작을 지킨다.
     (app-view.js 의 dragstart 가드도 INPUT 에서 드래그를 차단한다 — 일관된다) */
  .task-hierarchical .task-checkbox-container input[type="checkbox"] {
    -webkit-user-select: auto;
    user-select: auto;
  }
}
