/* ==========================================================================
   NYC Taxi Traffic — dashboard layout
   톤: 포트폴리오(hollyriver.github.io)와 통일된 잉크+스틸블루 미니멀
   구조: 뷰포트 contain-fit — 대시보드를 비율 고정 블록(--dash-ratio)으로 두고
         뷰포트에 들어가는 최대 크기로 키운 뒤, 남는 세로·가로는 여백으로 환원.
         [좌: 헤더+캡슐 스위치+지도+KPI 칩 | 우: 컨트롤+시간대 차트+히트맵] → 1줄 푸터
         (해설 텍스트는 화면에서 제거 — README.md가 정본)
   ========================================================================== */

:root {
  --ink: #1c1c1e;
  --muted: #666;
  --accent: #345995;
  --accent-soft: #eaeff7;   /* 액센트 8% 근사 — hover/보조 배경 */
  --line: #e3e3e6;
  --bg-soft: #f7f8fa;
  --radius: 12px;
  --radius-pill: 999px;
  --maxw: 1200px;

  /* --- 뷰포트 contain-fit -------------------------------------------------
     --chrome: 대시보드 밖에서 세로를 먹는 총량 (body 상하 패딩 14+14 + 푸터 46)
     --fit-h : 뷰포트가 대시보드에 내줄 수 있는 세로 (--dash-maxh로 상한)
     --dash-w: 그 세로에 비율을 곱한 폭 — 단, 컨테이너 폭과 --maxw를 넘지 않음
     --dash-h: 그 폭에서 비율로 역산한 세로 (--maxw에 걸린 경우까지 반영)

     높이는 height가 아니라 min-height로 준다. height로 확정하면 짧은 뷰포트에서
     그리드 아이템이 상자 밖으로 32px 넘쳐 히트맵이 푸터를 덮었음 — min-height면
     콘텐츠가 더 필요할 때 상자가 자라 겹침 없이 스크롤로만 넘어감.
     (--dash-w에는 100%가 들어 있어 세로 계산에 재사용 불가 — 세로 백분율은
      컨테이너 높이 기준으로 풀리므로 --dash-h를 따로 둠) */
  --dash-ratio: 1.85;
  --dash-maxh: 860px;
  --chrome: 74px;
  --fit-h: min(calc(100vh - var(--chrome)), var(--dash-maxh));
  --fit-h: min(calc(100dvh - var(--chrome)), var(--dash-maxh));
  --dash-w: min(100%, var(--maxw), calc(var(--fit-h) * var(--dash-ratio)));
  --dash-h: min(var(--fit-h), calc(var(--maxw) / var(--dash-ratio)));
}

* { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  /* min-height 100dvh + flex column: .dash의 margin:auto가 남는 세로를
     위아래 대칭 여백으로 흡수하고, .foot은 항상 마지막 줄에 놓임 */
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  padding: 14px 20px;
  font-family: "Pretendard", -apple-system, "Segoe UI", "Noto Sans KR", "Malgun Gothic", sans-serif;
  font-size: 15px;
  line-height: 1.6;
  color: var(--ink);
  background: #fff;
}

/* 본문 블록 공통: 대시보드와 푸터가 같은 폭·같은 중앙선을 공유 */
.dash, .foot {
  width: var(--dash-w);
  margin-left: auto;
  margin-right: auto;
}

/* --------------------------------------------------------------------------
   Hero — 좌측 컬럼 최상단 한 줄 헤더: 제목+부제 인라인
   (전체 폭 밴드 제거 — 우측 패널이 페이지 상단부터 세로 공간 점유)
   -------------------------------------------------------------------------- */
.hero {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 2px 10px;
  min-width: 0;
  padding: 2px 2px 0;
}

.hero h1 {
  margin: 0;
  font-size: 1.2rem;
  font-weight: 800;
  letter-spacing: -0.02em;
  line-height: 1.25;
  white-space: nowrap;
}

.hero .sub {
  margin: 0;
  color: var(--muted);
  font-size: 0.83rem;
}

/* --------------------------------------------------------------------------
   KPI — 지도 아래 가로 스트립 (소형 칩 4개, 지도 폭에 균등 분배)
   -------------------------------------------------------------------------- */
.kpis {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  flex-shrink: 0; /* 지도(flex:1)가 세로를 채워도 KPI 스트립은 압축되지 않게 */
}

.kpi {
  display: flex;
  align-items: baseline;
  justify-content: center;
  flex: 1 1 0;
  /* 칩 4개가 한 줄에 들어가는 폭대를 최대한 넓히려고 압축한 값 —
     여백/자간을 줄일수록 2x2로 접히는 미디어쿼리 임계가 내려감 */
  gap: 6px;
  padding: 5px 8px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: var(--bg-soft);
  white-space: nowrap;
}

.kpi-value {
  font-size: 0.88rem;
  font-weight: 800;
  letter-spacing: -0.01em;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
  line-height: 1.3;
}

.kpi-label {
  font-size: 0.7rem;
  color: var(--muted);
  letter-spacing: 0.01em;
}

.kpi-label i {
  font-style: normal;
  font-size: 0.62rem;
  color: #999;
  margin-left: 2px;
}

/* 칩 4개가 한 줄에 안 들어가는 폭대에서는 3+1로 어긋나게 접히므로 2x2로 고정.
   임계값 역산 (ref/probe-kpi.mjs 실측): 칩 4개 필요폭 615px,
   좌측 컬럼 폭 = 0.62 * (--dash-w - 16) 이므로 --dash-w >= 1008px가 필요.
   --dash-w = min(100%, --maxw, --fit-h * --dash-ratio) 이므로
     세로 구속: --fit-h >= 1008/1.85 = 545  ->  100dvh >= 619px
     가로 구속: 100vw - 40 >= 1008          ->  100vw  >= 1048px
   KPI 라벨 길이나 칩 여백을 바꾸면 필요폭이 달라지므로 이 두 값도 다시 역산할 것.
   2x2로 접히면 스트립이 32 -> 69px가 되고 그만큼 지도가 줄어든다 — 한 줄 유지가
   지도를 키우는 쪽이라 임계를 여유 없이 실측 최소값에 붙여 둠 */
@media (max-height: 618px), (max-width: 1047px) {
  .kpis {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* --------------------------------------------------------------------------
   Dashboard grid — 좌: 정방형 지도(~62%), 우: 패널(컨트롤+시간대 차트)
   -------------------------------------------------------------------------- */
.dash {
  display: grid;
  grid-template-columns: minmax(0, 62fr) minmax(0, 38fr);
  gap: 16px;
  /* 우측 패널이 높이를 정하던 관계가 뒤집혀, 이제 .dash가 높이를 정하고
     지도(좌)·시간대 차트(우)가 잔여 세로를 흡수. 콘텐츠 최소 높이가 --dash-h를
     넘는 짧은 뷰포트(대략 610px 미만)에서만 상자가 자라며 스크롤이 생김. */
  min-height: var(--dash-h);
  /* 남는 세로를 위아래 대칭 여백으로 (좌우는 .dash,.foot 공통 규칙에서 auto) */
  margin: auto;
  flex: 0 0 auto;
}

/* 좌측 컬럼: 캡슐 스위치 → 지도 → KPI 스트립 세로 스택 */
.map-col {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

#map {
  width: 100%;
  /* 정방형 고정 대신 좌측 컬럼의 잔여 세로(제목/스위치/KPI 제외)를 채움
     — 우측 패널과 하단 정렬. 세로가 가로보다 길어져도 허용 */
  flex: 1 1 auto;
  /* 하한은 짧은 뷰포트에서만 구속됨 — 이 폭대에서는 KPI 칩 4개가 2줄로 접혀
     좌측 컬럼이 커지므로, 지도가 그만큼 양보해야 한 화면이 유지됨 */
  min-height: 360px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  /* JS 로드 전에도 빈 영역이 깨져 보이지 않게 */
  background:
    radial-gradient(circle at 30% 30%, #eef1f6 0%, transparent 55%),
    var(--bg-soft);
}

.panel {
  display: flex;
  flex-direction: column;
  gap: 14px;
  min-width: 0;
}

/* --------------------------------------------------------------------------
   Layer switch — 좌측 컬럼 상단(지도 위) 캡슐형 세그먼트 컨트롤 (플로우 | 헥스빈)
   -------------------------------------------------------------------------- */
.layer-switch {
  display: flex;
  width: 100%;
  height: 40px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: #fff;
  overflow: hidden;
  flex-shrink: 0;
}

.layer-switch button {
  appearance: none;
  flex: 1 1 0;
  min-width: 0;
  font: inherit;
  font-size: 0.88rem;
  font-weight: 600;
  line-height: 1;
  border: none;
  border-radius: 0;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  transition: background 0.2s ease, color 0.2s ease;
}

.layer-switch button + button { border-left: 1px solid var(--line); }

.layer-switch button:hover {
  background: var(--accent-soft);
  color: var(--accent);
}

.layer-switch button.on {
  background: var(--accent);
  color: #fff;
  font-weight: 700;
}

.layer-switch button.on:hover { background: #2c4c80; }

.layer-switch button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -3px;
  border-radius: var(--radius-pill);
}

/* --------------------------------------------------------------------------
   Controls — 패널 내부 카드
   -------------------------------------------------------------------------- */
.controls {
  padding: 14px 16px;
  background: var(--bg-soft);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  display: flex;
  flex-direction: column;
  gap: 10px;
  /* 두 모드(헥스 3줄 / 플로우 2줄)의 카드 높이를 동일 고정 — 모드 전환 시
     아래 차트(chart-hour·chart-heat)의 y좌표가 0px 이동하도록 헥스 자연 높이로 맞춤
     (ref/verify7.mjs로 CDP 실측: 헥스 3줄 자연 높이 ≈132px). 플로우(2줄)는 이 min-height까지 채워짐 */
  min-height: 133px;
  flex-shrink: 0;
}

.ctl-group {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
}

/* 모드별 표시 전환: 플로우 모드 → .ctl-flow만, 헥스 모드 → 요일/시간/지표만 */
.ctl-group[hidden] { display: none; }

.ctl-label {
  min-width: 44px;
  font-size: 0.78rem;
  font-weight: 700;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.ctl-sep {
  width: 1px;
  height: 18px;
  background: var(--line);
  margin: 0 6px;
}

/* 플로우 로딩/실패 상태 표시 전용 (기본은 빈 텍스트 — main.js가 채움) */
.ctl-note {
  font-size: 0.73rem;
  color: var(--muted);
  margin-left: 4px;
}

/* 버튼: pill, 기본 보더만 / .on 액센트 채움 */
.controls button {
  appearance: none;
  font: inherit;
  font-size: 0.82rem;
  line-height: 1;
  padding: 7px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: #fff;
  color: var(--ink);
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

.controls button:hover {
  background: var(--accent-soft);
  border-color: var(--accent);
}

.controls button.on {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
  font-weight: 600;
}

.controls button.on:hover { background: #2c4c80; }

.controls button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* 시간대 줄: 버튼 내부 [라벨/시간범위] 2단 — 5버튼이 한 줄에 수납 */
.ctl-band button[data-band] {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-height: 40px;
  padding: 5px 11px;
}

.ctl-band button[data-band] small {
  font-size: 0.64rem;
  font-weight: 400;
  line-height: 1;
  color: #9aa1ab;
  letter-spacing: 0.01em;
  font-variant-numeric: tabular-nums;
}

.ctl-band button[data-band].on small { color: rgba(255, 255, 255, 0.82); }

/* 요일 줄: 7버튼 + "전체 요일" 토글을 반드시 한 줄에 수납 (소형 정폭 버튼) */
.ctl-days { flex-wrap: nowrap; gap: 4px; }

.ctl-days button[data-day] {
  flex: 1 1 30px;
  min-width: 0;
  max-width: 34px;
  padding: 6px 0;
  font-size: 0.78rem;
  text-align: center;
}

.ctl-days #days-all {
  flex: 0 0 auto;
  white-space: nowrap;
  margin-left: 2px;
}

/* 전체 시간·전체 요일 토글: 다른 토글과 같은 계열의 소형 pill */
.controls button.ctl-toggle {
  font-size: 0.78rem;
  padding: 6px 11px;
}

.controls button.ctl-toggle.on { font-weight: 700; }

/* 시간 슬라이더 */
.ctl-hour input[type="range"] {
  flex: 1 1 120px;
  max-width: 260px;
  accent-color: var(--accent);
  cursor: pointer;
}

.ctl-hour input[type="range"]:disabled {
  opacity: 0.4;
  cursor: default;
}

#hour-label {
  font-size: 0.83rem;
  font-weight: 700;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
  min-width: 34px;
}

/* --------------------------------------------------------------------------
   Charts — 우측 패널 내부 (시간대별 트립 수 + 요일×시간 히트맵)
   -------------------------------------------------------------------------- */
.chart-box {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 14px 16px 10px;
  background: #fff;
}

.chart-box h2 {
  margin: 0 0 8px;
  font-size: 0.92rem;
  font-weight: 700;
  letter-spacing: -0.01em;
}

.chart-sub {
  font-size: 0.76rem;
  font-weight: 400;
  color: var(--muted);
}

/* 시간대별 트립 수 — 우측 패널의 잔여 세로를 전부 흡수하는 유일한 블록.
   (히트맵은 폭 기준 정방 비율을 지켜야 하므로 고정, 컨트롤은 모드 전환
    안정성 때문에 고정 → 남는 세로는 여기로만 흘러감) */
.chart-box--hour {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.chart-box--hour h2 { flex-shrink: 0; }

#chart-hour {
  flex: 1 1 auto;
  min-height: 140px;
  background: var(--bg-soft);
  border-radius: 8px;
}

.chart-box--heat { flex-shrink: 0; }

/* 패널 폭 compact 히트맵 — 정확한 높이는 renderHeatChart()가
   컨테이너 폭 기준으로 재산정 (아래는 no-JS 폴백) */
#chart-heat {
  height: 150px;
  background: var(--bg-soft);
  border-radius: 8px;
}

/* --------------------------------------------------------------------------
   Map custom control — 초기 시점 리셋
   -------------------------------------------------------------------------- */
.maplibregl-ctrl button.map-reset-btn {
  font-size: 17px;
  line-height: 1;
  color: #333;
}

/* --------------------------------------------------------------------------
   Footer — 제작·저작권 1줄 (해설 텍스트는 README.md로 이관)
   세로 총량 44px(margin 12 + padding-top 10 + 본문 1줄 22)이 :root --chrome
   산정에 들어감 — 이 값을 바꾸면 --chrome도 같이 조정할 것
   -------------------------------------------------------------------------- */
.foot {
  flex-shrink: 0;
  margin-top: 12px;
  padding-top: 10px;
  border-top: 1px solid var(--line);
  color: var(--muted);
  font-size: 0.8rem;
  line-height: 1.7;
}

.foot p { margin: 0; }

.foot a { color: var(--accent); }

/* --------------------------------------------------------------------------
   Responsive
   -------------------------------------------------------------------------- */
@media (max-width: 900px) {
  /* 모바일·태블릿: 한 화면 고정 해제 — 세로 스택 + 스크롤 허용 */
  body {
    display: block;
    min-height: 0;
    padding: 0 20px 40px;
  }

  .dash, .foot { width: auto; max-width: var(--maxw); }

  .dash {
    grid-template-columns: 1fr;
    min-height: 0;
    margin: 12px auto 0;
  }

  /* 세로 스택에서는 잔여 세로 개념이 없으므로 고정 높이 폴백 */
  #map {
    flex: 0 0 auto;
    height: min(100vw, 480px);
    min-height: 360px;
  }

  .chart-box--hour { flex: 0 0 auto; display: block; }
  #chart-hour { flex: 0 0 auto; height: 240px; }

  .foot { margin-top: 24px; padding-top: 14px; }
}

@media (max-width: 560px) {
  body { padding: 0 14px 40px; }


  .hero { padding-top: 10px; }
  .hero h1 { font-size: 1.15rem; }

  .kpi { padding: 4px 10px; gap: 6px; }

  /* 480px 이하에서는 각 그룹이 세로로 접혀 두 모드 높이차가 커지므로
     min-height 고정을 해제(auto)해 불필요한 여백을 막음 */
  .controls { padding: 12px 14px; gap: 10px; min-height: auto; }
  .ctl-group {
    flex-direction: column;
    align-items: flex-start;
    gap: 6px;
  }
  .ctl-group > .ctl-label { min-width: 0; }
  /* 라벨 아래 버튼들은 가로로 흐르게 */
  .ctl-days, .ctl-metric, .ctl-flow, .ctl-hour {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
  }
  .ctl-label { flex-basis: 100%; }
  .ctl-hour input[type="range"] { flex: 1 1 140px; }
}

/* 초기 데이터 로드 실패 안내 */
.load-error { color: #a63232; font-size: 14px; margin-top: 10px; }
