/* ============================================================
 * 놀아:Play — 앱 런처 화면 (apps.css)
 * devplan11 part002(002b). tokens.css 변수만 사용(명시된 알파값/그라데이션만 예외).
 * 앱 화면(app/Views/apps/index.php)에서만 include 된다(layouts/main.php 조건부).
 *
 * 반응형은 --tile/--basis/--gx/--gy 4개 변수를 .app-grid 에서 선언하고
 * 미디어쿼리에서 "값만" 교체하는 방식으로만 구현한다(handoff.md §6).
 * ============================================================ */

/* 앱 화면 본문 배경 — 앱 화면에서만(이 파일이 앱 화면에만 include 되므로 body 전체에 걸어도 안전) */
body.app-bg {
  background: linear-gradient(175deg, #eef2ff 0%, #f7f8fc 38%, #fafafa 68%, #f6f4fb 100%);
  /* D-16: 시안은 루트에 전역 자간을 건다(현재는 제목류에만 있었다) — 앱 화면 전용으로만 확장.
     -webkit-line-clamp:2 인 .app-name 도 자간이 줄면 줄바꿈 지점이 바뀌므로 육안 확인 필수(part003). */
  letter-spacing: -0.012em;
}

/* ============================================================
 * 앱 화면 전용 상단바(GNB) — devplan12 part001(D-01 D-03 D-04 D-05 D-06).
 * app.css 의 .gnb/.gnb-inner 원본은 전 사이트 공통이라 직접 수정하지 않고(수정 금지 제약),
 * 이 파일이 앱 화면에서만 include 되는 걸 이용해 여기서만 값을 덮어쓴다. apps.css 는 app.css
 * 보다 <link> 순서상 항상 뒤에 로드되므로, 같은 클래스 단일 선택자(동일 우선순위)끼리는
 * 미디어쿼리 안에 있든 없든 항상 이 파일의 선언이 이긴다 — 그래서 아래엔 !important 가 없다.
 * ============================================================ */

/* D-04: 유리 질감 — 배경을 옅게 하면 스크롤 시 아이콘이 상단바에 비쳐 보이는 게 시안 의도.
   saturate(180%) 는 시안에 없어 제거(속성 전체를 새 값으로 교체 — 원본과 합쳐지지 않는다).
   border-bottom(경계선)은 app.css 값(var(--border-subtle)) 그대로 두어 배경이 옅어져도
   경계가 사라지지 않게 한다(part001 §3-3). */
.gnb {
  background: rgba(255, 255, 255, .62); /* 시안 원값. tokens.css 의 --alpha-white-* 계열엔 .62가 없어 하드코딩 */
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
}

/* D-05: 상단바 안쪽 최대폭 1240→1280 / 항목 간격 20→16 / 좌우 여백 24→32(≤900:24 / ≤720:16).
   1240px 처럼 이 코드베이스는 컨테이너 폭을 토큰화하지 않고 그대로 쓰는 게 기존 관례(app.css .wrap)라
   1280px 도 같은 방식으로 둔다. gap/padding 은 tokens.css 의 space 스케일로 표현 가능해 그대로 쓴다. */
.gnb-inner {
  max-width: 1280px;
  gap: var(--space-4); /* 16px. ≤720에서도 시안은 16px 고정이라 별도 미디어쿼리로 되돌리지 않는다 */
  padding-inline: var(--space-8); /* 32px */
}
@media (max-width: 900px) {
  .gnb-inner { padding-inline: var(--space-6); } /* 24px */
}
@media (max-width: 720px) {
  .gnb-inner { padding-inline: var(--space-4); } /* 16px */
}

/* D-13: 좌우 여백 32px(≤900:24 / ≤720:16) — .gnb-inner(D-05 위)와 같은 값으로 통일한다.
   상단바는 이미 .gnb-inner 가 담당하므로 여기서는 태그로 한정해(main/footer) 겹치지 않게 한다
   (겹치면 클래스 개수 차이로 명시도가 더 높은 쪽이 이겨 D-05 가 의도치 않게 밀릴 수 있다). */
body.app-bg main.wrap,
body.app-bg footer .wrap {
  padding-inline: 32px;
}
@media (max-width: 900px) {
  body.app-bg main.wrap,
  body.app-bg footer .wrap { padding-inline: 24px; }
}
@media (max-width: 720px) {
  body.app-bg main.wrap,
  body.app-bg footer .wrap { padding-inline: 16px; }
}

/* D-13: 본문 하단 여백 96px. layouts/main.php 의 main.wrap 은 인라인 style(padding-block:var(--space-8))로
   top/bottom 을 한 번에 32px 고정하는데, 인라인 style 은 이 파일의 어떤 규칙보다도 우선한다.
   top(32px, 시안과 동일)은 그대로 두고 bottom 만 늘려야 하므로 !important 로 인라인 style 을 덮는다
   (레이아웃의 인라인 선언 자체를 고치면 part001 완료 범위를 다시 건드리게 된다). */
body.app-bg main.wrap { padding-bottom: 96px !important; }

/* devplan12 part004(D-23): 토스트를 알약 모양으로 — NP.toast()(np.js)는 수정 금지 대상이라
   CSS 로만 덮는다. 아이콘을 숨기면 성공/실패 구분이 색으로만 남으므로, 실패(.err) 토스트는
   아이콘을 유지해 안전판을 남긴다(handoff §3-5). */
body.app-bg .toast { border-radius: var(--radius-pill); padding: 12px 20px; }
body.app-bg .toast .t-ico { display: none; }
body.app-bg .toast.err .t-ico { display: grid; }
/* D-26: 실행 취소(내 앱에서 빼기) — NP.toast() 호출 직후 apps.js 가 이 버튼을 토스트에 덧붙인다. */
body.app-bg .toast .toast-undo {
  margin-left: 4px;
  padding: 0;
  border: none;
  background: none;
  color: inherit;
  font-weight: var(--weight-bold);
  text-decoration: underline;
  cursor: pointer;
  flex-shrink: 0;
}

/* D-01: 공개 앱 ↔ 내 앱 알약 세그먼트 — layouts/main.php 가 $use_apps_css 일 때만 렌더(D-03: 그
   자리에 홈·카테고리·랭킹 링크 대신 이걸 넣는다. 링크 자체를 레이아웃이 안 그리므로 여기서 숨길 필요 없음). */
.app-tabs {
  display: flex;
  gap: 4px;
  padding: 4px;
  background: var(--surface-background-alt); /* 시안 --g1(#f7f7f8) = tokens.css cool-neutral-98 과 동일 */
  border-radius: var(--radius-pill);
  flex: none;
}
.app-tab {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 36px;
  padding: 0 14px;
  border: none;
  border-radius: var(--radius-pill);
  background: transparent;
  font-family: inherit;
  font-size: 14px;
  font-weight: var(--weight-semibold);
  color: var(--fg-tertiary);
  white-space: nowrap;
  text-decoration: none;
  cursor: pointer;
  transition: background var(--motion-fast), color var(--motion-fast);
}
.app-tab:hover { color: var(--fg-primary); }
.app-tab.is-active {
  background: #fff;
  color: var(--fg-primary);
  box-shadow: var(--shadow-1);
}

/* D-06: 편집 버튼·[취소][완료] 슬롯 — .app-head-actions/.app-edit-btn/.app-edit-controls 는
   본문(.app-head 안)에서 쓰던 것을 그대로 재사용한다(새 클래스 없음). 상단바 flex 행 안에서만
   줄어들지 않게 막아 준다. */
.gnb-inner > .app-head-actions { flex: none; }

/* D-07: 앱 화면엔 하단 모바일 탭바를 두지 않는다(대신 아바타·푸터가 이동 경로를 대신한다).
   app.css 는 이 규칙을 ≤720 미디어쿼리 안에서 display:flex 로 뒤집는데, 이 파일이 항상 뒤에
   로드되므로 미디어쿼리 없이도 모든 폭에서 이 선언이 이긴다. body 하단 패딩(has-tabbar)은
   애초에 안 붙도록 layouts/main.php 스크립트 쪽에서 막았다(빈 여백 방지, part001 §3-4). */
.mobile-tabbar { display: none; }

/* ---------- 정렬 select ---------- */
/* devplan12 part002(D-08): 대제목(.app-head/-title/-sub)이 사라지고 시안에 없던 이 블록은 통째로
   제거한다. 정렬 select 는 "전체 서비스" 섹션 헤더 우측(.app-sort-wrap, 아래 ---------- 섹션 ---------- 참고)
   으로 옮겨졌다 — select 자체 모양(아래 .app-sort)은 그대로 재사용. */
.app-sort {
  height: 40px;
  padding: 0 36px 0 14px;
  border-radius: var(--radius-lg);
  border: 1px solid var(--border-default);
  background-color: #fff;
  color: var(--fg-primary);
  font-family: inherit;
  font-size: var(--t-body-3-size);
  font-weight: var(--weight-medium);
  appearance: none;
  cursor: pointer;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='%2376787d' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 10px center;
  transition: border-color var(--motion-fast), box-shadow var(--motion-fast);
}
.app-sort:focus { outline: none; border-color: var(--border-focus); box-shadow: 0 0 0 3px var(--alpha-primary-08); }

/* D-09: 정렬 컨트롤을 감싸는 label — "정렬" 글자 자체를 라벨로 써서 클릭해도 select 가 포커스되게
   한다. .app-section-head 안에서 margin-left:auto 로 밀어 우측 정렬(스페이서 div 대신 이 방식). */
.app-sort-wrap {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--fg-tertiary);
}

/* devplan12 part004(D-19): 카테고리 필터 칩 — 시안 치수 그대로. 정렬(.app-sort-wrap)이
   margin-left:auto 로 우측에 붙으므로 이 칩은 항상 그 왼쪽, 개수(.app-section-count) 바로
   옆에 자연스럽게 놓인다(마크업 순서: 제목 → 개수 → 칩 → 정렬). */
.app-filter-clear {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  min-height: 28px;
  padding: 0 10px;
  border: 1px solid var(--border-default);
  border-radius: var(--radius-pill);
  background: #fff;
  font-family: inherit;
  font-size: 12px;
  font-weight: var(--weight-medium);
  color: var(--fg-secondary);
  cursor: pointer;
}
.app-filter-clear:hover { background: var(--surface-background-alt); }
/* [hidden] 함정 방어 — apps.js 가 필터 적용/해제 시 이 속성을 토글한다(D-19 3~4번). */
.app-filter-clear[hidden] { display: none; }

/* 내 앱 화면(account/apps.php) 상단 "편집" 버튼 · [취소][완료] — devplan11 part005b.
   드래그 재배치·폴더 조작(part005c)이 없는 상태라 위치를 바꿀 수단은 없지만, 진입/저장 왕복은 여기서 동작한다. */
.app-head-actions { display: flex; align-items: center; gap: var(--space-3); }
.app-edit-btn { display: inline-flex; align-items: center; gap: 6px; }
.app-edit-btn[disabled] { opacity: .5; cursor: not-allowed; }
.app-edit-controls { display: flex; align-items: center; gap: var(--space-2); }
/* [hidden] 은 UA 기본 규칙이라 위 display 선언(author 규칙)에 밀린다(알려진 함정과 동일) —
   apps.js 가 편집 모드 진입/종료 시 이 두 요소의 hidden 을 토글하므로 반드시 명시적으로 되돌려준다. */
.app-edit-btn[hidden],
.app-edit-controls[hidden] { display: none; }

/* devplan12 part001 마감2: 편집 버튼 3종은 이제 항상 상단바 안(.gnb-inner .app-head-actions)에서만
   렌더된다(D-06). 위 .app-head-actions/.app-edit-btn/.app-edit-controls 규칙은 본문 어디서든 재사용될
   가능성에 대비해 범용으로 남겨두고, 시안 치수(36px 알약)는 상단바 스코프에서만 덮어쓴다 — 본문용
   .btn/.btn-secondary/.btn-primary 원본(app.css)은 손대지 않는다.
   .btn 원본이 height:40px 를 고정값으로 주므로 min-height 만으로는 줄어들지 않아 height 자체를 덮어쓴다. */
.gnb-inner .app-head-actions {
  /* 탭 세그먼트(.app-tabs)와의 간격을 .gnb-inner 공통 gap(16px = --space-4)에서 12px(--space-3)로:
     16 + margin(-4) = 12. 다른 .gnb-inner 자식 간 간격(예: 검색창↔탭 세그먼트)은 그대로 16px 유지. */
  margin-left: calc(var(--space-3) - var(--space-4));
}
.gnb-inner .app-head-actions .btn {
  height: 36px; /* 시안 "min-height:36px" */
  border-radius: var(--radius-pill);
  font-size: var(--t-body-3-size); /* 14px, 시안 값과 동일 토큰 */
  white-space: nowrap;
}
.gnb-inner .app-head-actions .btn-secondary {
  padding: 0 14px;
  border: 1px solid var(--border-default);
  background: #fff;
  font-weight: var(--weight-semibold);
  color: var(--fg-secondary);
}
.gnb-inner .app-head-actions .btn-primary {
  padding: 0 18px;
  border: none;
  background: var(--brand-primary);
  font-weight: var(--weight-bold); /* app.css .btn 기본값(semibold)보다 굵게 — 시안 값 */
  color: #fff;
}

/* ---------- 섹션 ---------- */
.app-section {
  margin-bottom: var(--space-10);
  /* D-12: 시안 --gridmax. .wrap 은 좌우 여백만 담당하고 실제 격자 폭은 이 섹션이 잡는다 — .wrap(1240
     또는 D-13 의 32/24/16 패딩) 안에서 이 max-width 가 항상 더 좁아 이중 중앙정렬처럼 보이지만
     실제 렌더 결과는 시안과 동일하다(part002 §3-3 주의사항). */
  max-width: 1120px;
  margin-inline: auto;
}
@media (max-width: 1199px) { .app-section { max-width: 940px; } }
@media (max-width: 900px)  { .app-section { max-width: 640px; } }
@media (max-width: 720px)  { .app-section { max-width: 400px; } }

/* D-10: 헤더 한 줄 — [h2 제목] [회색 개수] …spacer… [정렬]. 제목/개수를 두 요소로 분리했으므로
   검색 중 텍스트 교체도 apps.js 가 이 둘을 각각 갱신한다(중복 표기 방지 — apps.js initSearch 참고). */
.app-section-head {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}
[data-role="folder-section"] .app-section-head { margin: 0 0 16px; }
[data-role="all-section"] .app-section-head { margin: 0 0 20px; }

.app-section-title { font-size: 18px; font-weight: var(--weight-bold); letter-spacing: -0.02em; margin: 0; color: var(--fg-primary); }
.app-section-count { font-size: 13px; color: var(--fg-tertiary); }

/* devplan12 part004(D-21): 내 앱 화면 전용 안내 문구(account/apps.php only — 공개 화면엔 이 요소
   자체가 없다). 텍스트는 apps.js(initAppsHint)가 data-hint/data-hint-edit 속성에서 읽어 채운다. */
.app-hint { margin: 0 0 20px; font-size: 13px; line-height: 1.6; color: var(--fg-tertiary); }
.app-hint[hidden] { display: none; } /* [hidden] 함정 방어 — 검색 중엔 apps.js 가 숨긴다 */

.app-more { text-align: center; margin-top: var(--space-8); }

/* ---------- 그리드 ---------- */
.app-grid {
  --tile: 72px;
  --basis: 92px;
  --gx: 20px;
  --gy: 24px;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--basis), 1fr));
  gap: var(--gy) var(--gx);
  justify-items: center;
}
/* 901~1199: 타일은 72 유지, 셀 최소폭만 88로 */
@media (max-width: 1199px) {
  .app-grid { --basis: 88px; }
}
/* 721~900 */
@media (max-width: 900px) {
  .app-grid { --tile: 64px; --basis: 80px; --gx: 16px; --gy: 20px; }
}
/* ~720 */
@media (max-width: 720px) {
  .app-grid { --tile: 60px; --basis: 72px; --gx: 12px; --gy: 18px; }
}

/* ---------- 셀(아이콘+폴더 공통) ---------- */
.app-cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  max-width: var(--basis);
  gap: 8px;
  padding: 4px;
  border: none;
  background: none;
  color: inherit;
  text-decoration: none;
  text-align: center;
  cursor: pointer;
  border-radius: var(--radius-lg);
  /* part003b: 길게 누르기로 컨텍스트 메뉴를 여는 동안 iOS 콜아웃/텍스트 선택이 뜨지 않게 한다 */
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
  /* part005b: 편집 모드 ⓧ(.app-edit-remove)를 타일 좌상단에 절대 배치하기 위한 위치 기준점.
     기본 흐름(static)에는 영향이 없어 평상시 레이아웃은 그대로다. */
  position: relative;
}
.app-cell:focus-visible {
  outline: 3px solid var(--brand-primary);
  outline-offset: 2px;
}

/* ---------- 타일 ---------- */
.app-tile {
  width: var(--tile);
  height: var(--tile);
  border-radius: 25%;
  background: rgba(255, 255, 255, .78);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  /* D-15: 시안 v2 원값(헤어라인 없는 2겹). tokens.css 의 --shadow-1~4 는 각각 자체적으로
     "0 0 0 1px rgba(20,25,30,.04)" 헤어라인을 끼고 있어서, 그대로 쓰면(또는 --shadow-3 처럼 더 큰
     번호로 바꿔도) 위 inset 테두리와 겹쳐 3겹이 되어 여전히 테두리감이 남는다(직접 확인함) —
     그래서 이 규칙에서만 원값을 직접 쓴다(handoff.md §6 오기 정정, 2026-09-02 devplan12 part003). */
  box-shadow:
    inset 0 0 0 1px var(--border-subtle),
    0 1px 2px rgba(20, 25, 30, .04),
    0 8px 24px rgba(20, 25, 30, .08);
  position: relative;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  font-weight: var(--weight-bold);
  font-size: calc(var(--tile) * 0.4);
  flex-shrink: 0;
  transition: transform var(--motion-fast), box-shadow var(--motion-fast);
}
.app-tile::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(160deg, rgba(255, 255, 255, .22), transparent 50%);
  pointer-events: none;
}
.app-tile-icon {
  width: 60%;
  height: 60%;
  object-fit: contain;
  position: relative;
  z-index: 1;
}
.app-cell:hover .app-tile { transform: scale(1.06); }
.app-cell:active .app-tile { transform: scale(.94); }

/* ---------- 폴더 타일 ---------- */
.app-tile.is-folder {
  /* D-14: 시안 v2는 흰 젖빛 유리. handoff.md §6 의 --alpha-neutral-08(회색, rgba(112,115,124,.08))
     은 오기였다(2026-09-02 devplan12 part003 정정) — 아래 두 값으로 교체하고, blur 도 앱 타일
     (10px, .app-tile 기본값 유지)보다 짙게 16px 로 올린다. box-shadow 는 앱 타일과 동일하게
     유지한다(시안도 폴더/앱이 같은 그림자를 쓴다 — 위 .app-tile 규칙을 그대로 상속). */
  background: rgba(255, 255, 255, .36);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  padding: 8px;
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 1fr 1fr; /* 필수: 빠지면 미니 프리뷰 높이가 0이 된다(알려진 함정) */
  gap: 4px;
  /* 기본 .app-tile 이 flex 용으로 선언한 align-items/justify-content(center)가 그대로 남아있으면
     grid 항목이 세로로 stretch 되지 않아 미니 프리뷰 높이가 0이 된다 — 반드시 리셋한다. */
  align-items: stretch;
  justify-content: normal;
}
.app-mini {
  background: #fff;
  border-radius: 7px;
  overflow: hidden;
  min-width: 0;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
.app-mini.is-empty { background: rgba(112, 115, 124, .10); }
.app-mini img { width: 76%; height: 76%; object-fit: contain; }

/* ---------- 이름 / 개수 ---------- */
.app-name {
  width: 100%;
  font-size: 12px;
  line-height: 1.35;
  font-weight: var(--weight-medium);
  color: var(--fg-primary);
  text-align: center;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  word-break: keep-all;
  overflow-wrap: break-word;
}
.app-count { font-size: 11px; color: var(--fg-tertiary); }

/* 폴더 안 서비스 hidden DOM(part003 오버레이가 그대로 옮겨 씀) — 화면에는 보이지 않음 */
.app-folder-items { display: none; }

/* ============================================================
 * 폴더 오버레이 — devplan11 part003(003a). handoff.md §6 "폴더 오버레이".
 * DOM은 apps.js가 body 끝에 1회 생성해 붙인다(폴더별로 만들지 않고 재사용).
 * ============================================================ */
.folder-overlay {
  position: fixed;
  inset: 0;
  z-index: 150;
}
.folder-overlay-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(20, 25, 30, .78); /* ⚠ .62 아님. 대비 확보용으로 .78 확정(handoff.md §6) */
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  opacity: 0;
  transition: opacity 160ms cubic-bezier(.2, .8, .2, 1);
}
.folder-overlay.open .folder-overlay-backdrop { opacity: 1; }
.folder-overlay.is-closing .folder-overlay-backdrop { transition-duration: 160ms; }

.folder-overlay-stage {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-6);
  transform: scale(.55);
  opacity: 0;
  transition: transform 200ms cubic-bezier(.2, .8, .2, 1), opacity 200ms cubic-bezier(.2, .8, .2, 1);
}
.folder-overlay.open .folder-overlay-stage { transform: scale(1); opacity: 1; }
/* 닫힘은 역방향 160ms(열림 200ms보다 빠르게) — transform-origin은 apps.js가 열 때마다 인라인으로 갱신 */
.folder-overlay.is-closing .folder-overlay-stage { transition-duration: 160ms; }

.folder-overlay-panel {
  position: relative;
  width: min(680px, 92vw);
  max-height: min(80vh, 720px);
  display: flex;
  flex-direction: column;
  padding: 56px 28px 28px;
}
/* ✕ 는 패널이 아니라 화면 우상단에 고정한다.
   패널 기준이면 폴더 안 앱 개수에 따라 버튼이 위아래로 움직여 눈에 거슬린다. */
.folder-overlay-close {
  position: fixed;
  /* D-24: 40px/top·right 20px → 44px/24px(handoff 미기재분 정정) */
  top: 24px;
  right: 24px;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: rgba(255, 255, 255, .24);
  border: 1px solid rgba(255, 255, 255, .45);
  color: #fff;
  display: grid;
  place-items: center;
  cursor: pointer;
  flex-shrink: 0;
}
.folder-overlay-close:focus-visible {
  outline: 3px solid #fff;
  outline-offset: 2px;
}
.folder-overlay-title {
  margin: 0;
  font-size: 22px;
  line-height: 1.4;
  /* D-24: 600→700(handoff 오기 정정) */
  font-weight: var(--weight-bold);
  color: #fff;
  text-align: center;
}
.folder-overlay-count {
  margin: 4px 0 0;
  font-size: 13px;
  /* D-24: .72→.82(handoff 오기 정정) */
  color: rgba(255, 255, 255, .82);
  text-align: center;
}
/* 격자: app-grid 를 상속(타일 반응형 변수 재사용)하되 auto-fit + justify-content:center 로
   앱이 몇 개뿐이어도 화면 중앙에 모이게 한다(auto-fill+justify-items 조합인 기본 .app-grid와 다름). */
.folder-overlay-grid {
  margin-top: var(--space-6);
  overflow-y: auto;
  padding: 4px 4px var(--space-2);
  grid-template-columns: repeat(auto-fit, minmax(var(--basis), 1fr));
  justify-content: center;
}
.folder-overlay-grid .app-name { color: #fff; }
.folder-overlay-more {
  margin: var(--space-6) auto 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  /* D-24: 14px/600, 그림자 없음 → 15px/700 + shadow-2, min-height 44px + padding 0 22px(handoff 미기재분 정정) */
  min-height: 44px;
  padding: 0 22px;
  border-radius: var(--radius-pill);
  background: #fff; /* 불투명 흰색 — 반투명은 대비 미달(handoff.md §6) */
  color: var(--cool-neutral-30);
  font-size: 15px;
  font-weight: var(--weight-bold);
  box-shadow: var(--shadow-2);
  text-decoration: none;
  flex-shrink: 0;
}
/* devplan11 part005b: 내 앱 화면의 개인 폴더는 /categories/f{id} 가 존재하지 않아 이 링크를 숨긴다
   (apps.js openFolder()가 hidden 을 토글). [hidden] 은 UA 기본 규칙이라 위 display:inline-flex 같은
   author 규칙에 밀린다 — 알려진 함정과 동일하게 명시적으로 되돌려줘야 한다. */
.folder-overlay-more[hidden] { display: none; }

/* ============================================================
 * 컨텍스트 메뉴 — devplan11 part003(003b). handoff.md §6 "컨텍스트 메뉴".
 * DOM은 apps.js가 body 끝에 1회 생성해 재사용한다(폴더 오버레이와 같은 패턴).
 * 트리거: 아이콘 우클릭 / 0.5초 길게 누르기. 셀렉터가 [data-service-id] 를 요구하므로
 * 폴더 타일(.app-cell[data-folder])에는 붙지 않는다(이번 파트 범위 아님 — part005).
 * ============================================================ */
.app-ctx-menu {
  position: fixed;
  z-index: 210; /* 폴더 오버레이(150), 토스트 호스트(200) 보다 위 */
  width: 200px;
  background: #fff;
  border-radius: 14px;
  box-shadow: var(--shadow-4);
  overflow: hidden;
  transform: scale(.92);
  opacity: 0;
  transition: transform 140ms cubic-bezier(.2, .8, .2, 1), opacity 140ms cubic-bezier(.2, .8, .2, 1);
}
.app-ctx-menu.open { transform: scale(1); opacity: 1; }

.app-ctx-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  height: 44px;
  padding: 0 16px;
  border: none;
  border-bottom: 1px solid var(--border-subtle);
  background: #fff;
  color: var(--fg-primary);
  font-family: inherit;
  font-size: var(--t-body-3-size);
  font-weight: var(--weight-medium);
  text-align: left;
  cursor: pointer;
}
.app-ctx-item:last-child { border-bottom: none; }
.app-ctx-item:hover { background: var(--interaction-hover); }
.app-ctx-item:focus-visible {
  outline: 2px solid var(--brand-primary);
  outline-offset: -2px; /* 부모의 overflow:hidden(둥근 모서리) 안에서도 잘리지 않게 안쪽으로 */
}
.app-ctx-icon {
  display: flex;
  flex-shrink: 0;
  color: var(--fg-tertiary);
}
/* 내 앱 화면 "내 앱에서 빼기" / "폴더 삭제" — handoff.md §6 컨텍스트 메뉴, 빨간 글씨 */
.app-ctx-item.is-danger { color: var(--status-danger); }
.app-ctx-item.is-danger .app-ctx-icon { color: var(--status-danger); }

/* ============================================================
 * 검색 — devplan11 part004. handoff.md §5 "검색 사양(클라이언트)".
 * .gnb-search 자체는 app.css 소유지만, 이 파일이 앱 화면에서만 include 되므로 apps.css 가
 * 뒤에 로드되는 걸 이용해 이 화면 한정으로만 살짝 확장한다(다른 화면은 app.css 그대로).
 * ============================================================ */

/* 검색어와 일치한 부분 — handoff.md §5 "일치 조각은 color:var(--brand-primary); font-weight:700" */
.app-name .app-hl { color: var(--brand-primary); font-weight: 700; }

/* 편집 모드 중 검색 잠금 — devplan11 part005c. 드래그로 만든 폴더/재배치가 완료 전까지는 DOM에만
   존재하는데, 검색은 ✕/ESC/전부지우기 복귀 시 최초 1회 스냅샷(originalGridHTML)으로 되돌리는 구조라
   편집 중 검색을 허용하면 방금 만든 폴더가 사라져 보일 수 있다 — 그래서 편집 중엔 잠근다. */
.gnb-search.is-disabled { opacity: .5; pointer-events: none; }

/* 지우기(✕) 버튼 — 입력값이 있을 때만(JS가 hidden 토글). app.css의 42px 좌측 패딩(아이콘용)은
   유지하고 우측에만 여유를 준다. */
.gnb-search { position: relative; }
.gnb-search input { padding-right: 34px; }
.gnb-search-clear {
  display: none;
  position: absolute;
  right: 8px;
  top: 50%;
  transform: translateY(-50%);
  width: 20px;
  height: 20px;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: var(--alpha-neutral-08);
  color: var(--fg-tertiary);
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
/* [hidden] 은 UA(user-agent) 기본 규칙이라 author 규칙(위의 display:none)보다 우선순위가
   낮다 — 그래서 hidden 이 아닐 때만 보이도록 반드시 :not([hidden])로 되돌려줘야 한다. */
.gnb-search-clear:not([hidden]) { display: flex; }
.gnb-search-clear:hover { background: var(--interaction-hover); color: var(--fg-primary); }

/* 뒤로가기(←)/돋보기 토글 버튼 — 기본(데스크톱)엔 없다. 모바일에서만 나타난다. */
.gnb-search-back { display: none; }
@media (min-width: 721px) {
  .gnb-search-toggle { display: none; }
}

/* ---------- 모바일(~720px): 검색창 대신 돋보기, 탭하면 상단바를 덮으며 펼쳐짐 ---------- */
@media (max-width: 720px) {
  .gnb-search { display: none; }
  .gnb-search.is-open {
    display: flex;
    align-items: center;
    gap: 6px;
    position: absolute;
    inset: 0;
    z-index: 5;
    height: 56px;
    padding: 0 var(--space-4);
    background: #fff;
    max-width: none;
  }
  .gnb-search.is-open .gnb-search-back {
    display: grid;
    place-items: center;
    flex-shrink: 0;
    width: 32px;
    height: 32px;
    border: none;
    border-radius: 8px;
    background: transparent;
    color: var(--fg-secondary);
  }
  .gnb-search.is-open .gnb-search-back:hover { background: var(--interaction-hover); }
  .gnb-search.is-open .s-ico { display: none; }
  .gnb-search.is-open input { flex: 1; height: 40px; padding: 0 34px 0 14px; }
}

/* 검색 섹션 요소들 — 기반 클래스(.app-grid/.empty)가 이미 display 를 명시하므로(cascade 상 author
   규칙이 UA [hidden] 규칙을 이긴다) 이 두 선택자만 명시적으로 되돌려준다. */
.app-grid[hidden],
.app-search-empty[hidden] { display: none; }
.app-search-empty { margin-top: var(--space-4); }

/* devplan12 part004(D-22): 빈 상태를 앱 화면 한정으로 낮춘다 — 공통 .empty/.empty-art/.empty-title
   원본(app.css)은 건드리지 않고 이 스코프에서만 덮어쓴다. 96px 회색 박스 대신 아이콘만(40px, 이미
   data-icon-size="40"으로 그 크기다), 제목은 16px/700. */
.app-search-empty .empty-art {
  width: auto;
  height: auto;
  background: none;
  color: var(--cool-neutral-70);
}
.app-search-empty .empty-title {
  font-size: 16px;
  font-weight: var(--weight-bold);
}

/* ============================================================
 * 편집 모드 — devplan11 part005b/c. handoff.md §6 "편집 모드".
 * 진입 표시(wiggle) + ⓧ(북마크 해제/폴더 삭제) + 드래그 재배치·병합 예고 + 폴더 오버레이 관리 버튼.
 * ============================================================ */
@keyframes np-app-wiggle {
  0%, 100% { transform: rotate(-1.2deg); }
  50%      { transform: rotate(1.2deg); }
}
/* 모든 타일(앱+폴더)에 적용 — apps.js 가 편집 모드 진입 시 body 에 이 클래스를 붙인다.
   폴더 오버레이 안(클론된 노드)에는 [data-role="all-grid"] 스코프가 없어 적용되지 않는다. */
body.app-edit-mode [data-role="all-grid"] .app-cell .app-tile {
  animation: np-app-wiggle 0.28s ease-in-out infinite;
}
/* 편집 모드에서는 아이콘을 눌러도 스케일 트랜지션(hover/press)이 wiggle 과 겹쳐 어색해지므로 끈다 —
   단, 병합 예고 중인 타일(.is-merge-target)의 scale(1.16)은 덮어쓰면 안 된다(드래그 중 포인터가
   실제로 그 타일 위에 있어 :hover 가 동시에 매치될 수 있다). */
body.app-edit-mode [data-role="all-grid"] .app-cell:hover:not(.is-merge-target) .app-tile,
body.app-edit-mode [data-role="all-grid"] .app-cell:active:not(.is-merge-target) .app-tile {
  transform: none;
}

/* 편집 모드에서만 터치 스크롤을 끈다(평소엔 페이지 스크롤 정상 — handoff.md §6
   "touch-action:none은 편집 모드의 타일에만"). */
body.app-edit-mode [data-role="all-grid"] .app-cell {
  touch-action: none;
  cursor: grab;
}

/* 드래그 중인 원본 셀 — 그리드 안에서 자리(레이아웃 공간)는 그대로 차지하되 시각적으로만 숨긴다.
   실제로 보이는 건 body 에 별도로 붙는 .app-drag-ghost 뿐이다(재배치는 이 원본 노드를 실제로
   grid.insertBefore() 로 옮겨서 구현하므로 자리를 지워버리면 안 된다). */
.app-cell.is-drag-source {
  opacity: 0;
  pointer-events: none;
}

/* 드래그 중 포인터를 따라다니는 고스트(원본 셀의 복제본) — handoff.md §6 "드래그 중 scale(1.12) +
   shadow-4 + opacity .92, 포인터를 따라 이동". position:fixed 라 grid 레이아웃과 무관하게 움직이고,
   transform(translate3d)은 apps.js 가 매 pointermove 마다 갱신한다. */
.app-drag-ghost {
  position: fixed;
  top: 0;
  left: 0;
  margin: 0;
  z-index: 300; /* 컨텍스트 메뉴(210)·폴더 오버레이(150)보다도 위 — 드래그 중엔 항상 최상단 */
  opacity: .92;
  pointer-events: none;
  will-change: transform;
}
.app-drag-ghost .app-tile {
  transform: scale(1.12);
  box-shadow: var(--shadow-4);
  transition: none; /* 고스트는 매 프레임 위치가 바뀌므로 트랜지션이 오히려 지연을 만든다 */
}
.app-drag-ghost .app-edit-remove { display: none; }

/* 병합(폴더 생성/합류) 예고 — 중심 거리 타일폭×0.5 이내에서 400ms 머물렀을 때(handoff.md §6).
   기존 box-shadow 레이어(inset 테두리 + shadow-1/2)를 유지한 채 파란 링만 앞에 추가한다
   (알려진 함정: box-shadow 목록 안에 none을 넣으면 선언 전체가 무효가 된다 — 그래서 유지). */
.app-cell.is-merge-target .app-tile {
  transform: scale(1.16);
  /* D-15: .app-tile 의 2겹 원값(위 "---------- 타일 ----------" 참고)과 맞춘다 — var(--shadow-1/2)
     그대로 두면 병합 예고 때만 헤어라인이 겹쳐 그림자가 튄다(part003 완료 기준 6). */
  box-shadow:
    0 0 0 2px var(--brand-primary),
    inset 0 0 0 1px var(--border-subtle),
    0 1px 2px rgba(20, 25, 30, .04),
    0 8px 24px rgba(20, 25, 30, .08);
}

/* 폴더 오버레이 관리 버튼(이름 변경/폴더 삭제) — 내 앱 화면 전용. handoff.md §6 "관리 버튼: 불투명
   흰 배경 #fff + 글씨 cool-neutral-30, 폴더 삭제는 status-danger(반투명 흰색은 대비 미달)". */
.folder-overlay-actions {
  margin: var(--space-6) auto 0;
  display: flex;
  gap: var(--space-3);
  flex-shrink: 0;
}
.folder-overlay-action {
  padding: 10px 20px;
  border: none;
  border-radius: var(--radius-pill);
  background: #fff;
  color: var(--cool-neutral-30);
  font-size: var(--t-body-3-size);
  font-weight: var(--weight-semibold);
  font-family: inherit;
  cursor: pointer;
}
.folder-overlay-action.is-danger { color: var(--status-danger); }
.folder-overlay-action:focus-visible { outline: 2px solid var(--brand-primary); outline-offset: 2px; }
/* [hidden] 은 UA 기본 규칙이라 위 display:flex(author 규칙)에 밀린다(알려진 함정과 동일) */
.folder-overlay-actions[hidden] { display: none; }

/* ⓧ — 22px 원형, 타일 좌상단 -6px 오프셋(handoff.md §6). .app-cell 이 position:relative 라
   기준점이 되고, 좌우 offset 은 --basis/--tile 차이의 절반으로 계산해 타일 정중앙 위쪽에 정확히 앉힌다.
   .app-edit-remove(최상위 그리드: 북마크 해제/폴더 삭제)와 .app-folder-remove(폴더 오버레이 안:
   폴더에서만 빼기)는 클릭 핸들러만 다르고 모양은 완전히 같다(handoff.md §6). */
.app-edit-remove,
.app-folder-remove {
  position: absolute;
  /* 절대 위치의 기준(containing block)은 .app-cell 의 padding 경계다(padding:4px). 그 안쪽에서
     타일이 시작하므로, 타일 상단 기준 -6px 오프셋을 맞추려면 4px(padding) - 6px = -2px 여야 한다.
     가로는 padding이 좌우 대칭이라 (basis-tile)/2 공식이 padding과 무관하게 그대로 성립한다. */
  top: -2px;
  left: calc((var(--basis) - var(--tile)) / 2 - 6px);
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 2px solid #fff;
  background: var(--cool-neutral-30);
  color: #fff;
  display: grid;
  place-items: center;
  padding: 0;
  cursor: pointer;
  box-shadow: var(--shadow-1);
  z-index: 1;
}
.app-edit-remove:focus-visible,
.app-folder-remove:focus-visible {
  outline: 2px solid var(--brand-primary);
  outline-offset: 1px;
}

/* ============================================================
 * 확인/입력 모달 — devplan12 part004(D-26). window.confirm/prompt/alert 전량 대체.
 * DOM은 apps.js가 body 끝에 1회 생성해 재사용한다(폴더 오버레이·컨텍스트 메뉴와 같은 패턴).
 * z-index는 컨텍스트 메뉴(210)·드래그 고스트(300)보다 위(handoff §3-7 지시).
 * ============================================================ */
.app-modal-overlay {
  position: fixed;
  inset: 0;
  z-index: 320;
  display: grid;
  place-items: center;
  padding: var(--space-4);
}
.app-modal-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(20, 25, 30, .32); /* 시안 값 — 폴더 오버레이의 .78과는 별개(그쪽은 접근성 확보용 K-01) */
}
.app-modal {
  position: relative;
  width: min(360px, 100%);
  max-width: 360px;
  border-radius: 16px;
  padding: 24px;
  background: #fff;
  box-shadow: var(--shadow-4);
  display: flex;
  flex-direction: column;
  gap: 6px;
  opacity: 0;
  transform: translateY(12px) scale(.98);
  transition: opacity 200ms cubic-bezier(.2, .8, .2, 1), transform 200ms cubic-bezier(.2, .8, .2, 1);
}
.app-modal-overlay.open .app-modal { opacity: 1; transform: none; } /* 시안: fadeup 200ms */
.app-modal-title { margin: 0; font-size: 16px; font-weight: var(--weight-bold); color: var(--fg-primary); }
.app-modal-desc { margin: 0 0 10px; font-size: 13px; line-height: 1.5; color: var(--fg-tertiary); }
.app-modal-desc[hidden] { display: none; } /* [hidden] 함정 방어 — 확인형 모달은 desc가 항상 있어 실사용은 드물지만 방어적으로 */
.app-modal-field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 4px; }
.app-modal-field[hidden] { display: none; } /* [hidden] 함정 방어 — 확인형(mode:"confirm")일 때 입력칸을 감춘다 */
.app-modal-input { min-height: 44px; border-radius: 8px; }
.app-modal-error[hidden] { display: none; } /* [hidden] 함정 방어 — 이름 길이 오류(D-26) 발생 시에만 노출 */
.app-modal-actions { display: flex; justify-content: flex-end; gap: var(--space-3); margin-top: 14px; }
.app-modal-actions .btn { min-height: 44px; } /* 시안: 버튼 44px — 공용 .btn 기본 40px 위에 덮어쓴다 */
/* [hidden] 은 UA 기본 규칙이라 위 display:grid(author 규칙)에 밀린다(알려진 함정과 동일) */
.app-modal-overlay[hidden] { display: none; }

@media (prefers-reduced-motion: reduce) {
  .app-tile { transition: none; }
  .folder-overlay-backdrop,
  .folder-overlay.is-closing .folder-overlay-backdrop {
    transition-duration: 120ms;
  }
  .folder-overlay-stage,
  .folder-overlay.is-closing .folder-overlay-stage {
    transition: opacity 120ms cubic-bezier(.2, .8, .2, 1);
    transform: none !important;
  }
  .app-ctx-menu {
    transition: opacity 120ms cubic-bezier(.2, .8, .2, 1);
    transform: none !important;
  }
  .app-modal {
    transition: opacity 120ms cubic-bezier(.2, .8, .2, 1);
    transform: none !important;
  }
  /* wiggle 대신 점선 테두리(handoff.md §6) — outline 이라 레이아웃에 영향 없음 */
  body.app-edit-mode [data-role="all-grid"] .app-cell .app-tile {
    animation: none;
    outline: 2px dashed var(--cool-neutral-30);
    outline-offset: 2px;
  }
  /* 재배치 FLIP 애니메이션은 apps.js 쪽에서 reducedMotion() 체크로 건너뛴다(transform 즉시 적용,
     트랜지션 없음) — 여기서는 혹시 남는 transition 만 방어적으로 제거한다. */
  .app-drag-ghost .app-tile { transition: none; }
}
