/* ── 모바일 · 태블릿 셸 (2026-08-23, specs/2026-08-23-mobile-shell-design.md) ──
 *
 * `app.css` **다음**에 실린다. 여기 있는 것은 전부 ≤1024px 규칙이다 — 예외 둘,
 * 인쇄 규칙(폭과 무관하다)과 탭 바를 감추는 `(min-width:720.02px)`(상한이 없다).
 * 둘 다 **효과**는 PC 모양을 그대로 둔다 — 인쇄는 화면 폭과 상관없고,
 * 탭 바 숨김은 이 라운드가 새로 만든 요소 하나를 PC에서 지우는 것뿐이다
 * (안 지우면 PC에 없던 막대가 새로 생긴다).
 * **PC(>1024px)의 화면 모양은 이 파일이 건드리지 않는다** —
 * 2026-08-23 사용자 요청("PC는 그대로 둬도 좋다")이 이 파일의 경계다.
 *
 * `app.css`의 기존 미디어 쿼리 블록 넷(720·640·520·960)은 손대지 않는다.
 * `tests/test_lineup_reorder.py`가 그 파일의 **첫** 640px 블록을 정규식으로 찾는다.
 *
 * 구간 (스펙 §4): 폰 ≤720 · 태블릿 721~1024 · PC >1024.
 * `(pointer:coarse)`를 쓰지 않는 이유는 헤드리스 셸이 그 값을 주지 않아
 * `scripts/mobile_check.py`가 재현할 수 없기 때문이다.
 */

/* ── 하단 탭 바 (스펙 §5-2) ────────────────────────────────────────────── */
@media (max-width:720px){
  .tabbar{
    position:fixed;left:0;right:0;bottom:0;z-index:30;
    display:grid;grid-template-columns:repeat(5,1fr);
    height:calc(var(--tabbar-h) + env(safe-area-inset-bottom));
    padding-bottom:env(safe-area-inset-bottom);
    background:var(--surface);border-top:1px solid var(--border)
  }
  .tab-item{
    display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;
    min-height:var(--tabbar-h);text-decoration:none;color:var(--muted);
    -webkit-tap-highlight-color:transparent
  }
  .tab-icon{width:24px;height:24px;stroke:currentColor;stroke-width:1.8;fill:none;
    stroke-linecap:round;stroke-linejoin:round}
  /* **`rem`은 언제나 루트(`<html>`)를 기준으로 푼다.** 이 저장소의 `font-size:15px`은
     `body`에 있고(app.css:4-8) `:root`·`html`에는 아무 선언이 없다 — 그래서 1rem은
     15px이 아니라 브라우저 기본값 16px이다. 즉 `.75rem` = **12px**(하한에 딱 걸린다),
     `.8rem` = **12.8px**. 여기서 `.8rem`을 쓰는 이유는 `.75rem`이 하한 미달이어서가
     아니라 하한에 **여유 없이** 붙기 때문이다(글꼴 배율·반올림에 바로 밑돈다).
     2026-08-23까지 이 자리에 "`.75rem`은 11.25px"라고 적혀 있었다 — `body`의 15px을
     기준으로 잘못 곱한 값이다. 값은 그대로 두고 셈만 바로잡는다. */
  .tab-label{font-size:.8rem;font-weight:650;line-height:1}
  /* 색만으로 가르지 않는다 — 굵기와 획 두께가 함께 간다. */
  .tab-item[aria-current="page"]{color:var(--brand)}
  .tab-item[aria-current="page"] .tab-label{font-weight:800}
  .tab-item[aria-current="page"] .tab-icon{stroke-width:2.4}

  /* ── 헤더: 한 줄 56px (스펙 §5-1) ──────────────────────────────────────
   * 지금(2026-08-23 실측) 430px에서 172px · 3줄이다. 메뉴·사용자·로그아웃을
   * 헤더에서 빼면 한 줄이 된다 — 뺀 것들의 갈 곳이 탭 바와 '내 계정'이다.
   * 마크업은 한 글자도 안 바꾼다(대시보드 라운드가 gnb.html에 '홈'을 더한다). */
  /* 노치 인셋은 **`.gnb`(바깥, 배경을 칠하는 쪽)** 에 준다. `.gnb-inner`에 주면
     안 된다 — `*{box-sizing:border-box}`(app.css:3)라 `height:56px`이 테두리 상자
     높이여서 padding이 56px을 **깎아 먹는다.** 아이폰 standalone의 인셋은 44px
     (노치)·59px(다이내믹 아일랜드)이라 내용 상자가 12px 또는 0px이 되고, 44px짜리
     자식들이 상태 표시줄 아래로 되레 들어간다. 이 블록 위쪽의 `.tabbar`(스펙 §5-2)가
     맞는 본이다 — 상자를 인셋만큼 **키우고** 그만큼 패딩을 준다. 헤드리스 셸은 인셋이 0이라
     `scripts/mobile_check.py`가 이것을 절대 못 잡는다. */
  .gnb{padding-top:env(safe-area-inset-top)}
  .gnb-inner{height:56px;padding:0 16px;gap:10px;flex-wrap:nowrap}
  .gnb-nav,.gnb-user,.gnb-logout{display:none}
  .brand-logo{width:40px;height:32px}
  /* 로고만 남은 홈 링크는 **40×44**였다(기하 검사 2026-08-24: 13페이지 × 폰 폭
     셋 = 39건). `app.css:30`의 `.brand{min-height:44px}`는 세로만 잡는다 —
     `.brand-name`이 이 구간에서 `display:none`(app.css:218)이라 가로는 로고
     40px가 그대로 상자 폭이 된다. 스펙 §8 ③의 하한은 44**×**44다.
     명시도는 `.brand`(0,1,0)로 app.css와 같고 이 파일이 뒤라 이긴다.
     `justify-content:center`는 늘어난 4px를 좌우로 나눠 로고가 왼쪽에
     붙어 보이지 않게 한다. */
  .brand{min-width:44px;justify-content:center}
  /* 팀 이름이 길면(‘(주)네이처텍’) 칩이 헤더를 밀어낸다. 잘라서 한 줄을 지킨다.
     선택자는 `.team-chip-name`이다 — `.gnb-team-chip`은 이 저장소에 없다.
     그 클래스에 이미 overflow/text-overflow/white-space가 있으므로(app.css:109)
     여기서는 max-width만 덮는다(기본 11em). */
  .team-chip-name{max-width:min(52vw,220px)}

  /* ── 본문과 바닥 여백 (스펙 §5-3 · §5-4) ─────────────────────────────
   * 탭 바가 fixed라 문서 흐름에서 빠져 있다. 여백이 없으면 마지막 카드와
   * 푸터가 탭 바 아래에 깔린다. `body.auth-body`(로그인 전)는 탭 바가 없으므로
   * 제외한다 — 주면 빈 띠만 생긴다. */
  body.app{padding-bottom:calc(var(--tabbar-h) + env(safe-area-inset-bottom))}
  .wrap{padding:16px;gap:16px}
  .card-body{padding:14px}
  .card-head{padding:12px 14px}
  .page-title{font-size:1.35rem}
  /* 오류 플래시는 탭 바 위로. 아래 FAB(Task 5)·라인업 막대(Task 7)와 **같은
     기준선**을 쓴다 — 기준선이 하나여야 셋이 서로 겹치지 않는다(스펙 §5-3).
     떠 있는 알약 둘(플래시·FAB)만 거기에 `+ 16px`를 더하고, 전폭 막대는 탭 바에
     붙느라 더하지 않는다(아래 `.lineup-primary` 절). */
  #error-flash{bottom:calc(var(--tabbar-h) + env(safe-area-inset-bottom) + 16px)}
  /* ③에서는 그 16px 위가 **비어 있지 않다.** 라인업 고정 막대가 탭 바 위
     `--lineup-bar-h`만큼을 차지하는데 플래시는 `z-index:60`(app.css:298)이라
     막대(20)를 덮는다 — 한 줄짜리 배너가 '오더지 확정'을 통째로 가리고, 그
     배너는 8초를 머문다(`base.html`). 막대가 있는 화면에서만 그만큼 더 띄운다.
     `:has()`는 이 파일이 이미 쓰고 있다(아래 FAB 여백 절의 전제 그대로).
     **`#panel-lineup:not([hidden])`까지 적는 이유**: 워크스페이스는 패널 다섯을
     한 번에 그리고 `hidden` 속성만 토글하므로(`game_workspace.html:34`),
     `:has(.lineup-primary)`만으로는 ①②④⑤에서도 참이 되어 아무것도 없는 자리
     위로 플래시가 144px 뜬다. `:has()`는 그려지는지가 아니라 **DOM**을 보므로
     감춰진 패널 속 막대도 잡는다. 이 조건은 `hidden` 토글에 매여 있다 —
     `tests/test_workspace.py`가 그 토글을 여러 곳에서 못 박고 있고, 방식이
     바뀌면 이 규칙과 아래 `scroll-padding-bottom`이 함께 죽는다.
     명시도 (2,3,1)이라 위 규칙(1,0,0)을 순서와 무관하게 이긴다. */
  body.app:has(#panel-lineup:not([hidden]) .lineup-primary) #error-flash{
    bottom:calc(var(--tabbar-h) + env(safe-area-inset-bottom) + var(--lineup-bar-h) + 16px)
  }

  /* ── 포커스가 새 크롬 **밑으로** 스크롤되지 않게 한다 (품질 검토 B-1) ──────
   * `lineup.js`가 `element.focus()`를 `preventScroll` 없이 부른다(포지션 고르기·
   * ▲▼ 재정렬·비고 상자). 화면 밖 요소에 포커스가 가면 브라우저는 그 요소의
   * **아래 모서리를 레이아웃 뷰포트 바닥에 맞춰** 굴리는데, 그 바닥은 이제 탭
   * 바 56px 뒤이고 ③에서는 거기에 고정 막대까지 얹혀 있다 — 방금 연 포지션
   * 목록이 가려진 띠 안에 떨어진다. 이 라운드 전에는 가려진 띠가 56px였다.
   *
   * JS는 이 라운드의 경계 밖이라 **스크롤포트 쪽에 띠를 선언한다.**
   * `scroll-padding-*`은 스크롤 컨테이너에 주는 값이고, 루트 요소에 준 것이
   * 곧 뷰포트의 값이다(html·body 어디에도 `overflow`가 없어 문서 스크롤러가
   * 뷰포트다). 포커스가 실제로 굴릴 때만 쓰이므로 이미 보이는 요소에는
   * 아무 일도 하지 않는다.
   *
   * 값의 유래: 탭 바(`--tabbar-h`) + 노치 인셋 + ③의 고정 막대(`--lineup-bar-h`).
   * 막대가 없는 화면은 앞의 둘까지다. 조건이 둘 붙는다:
   *   · `#panel-lineup:not([hidden])` — 위 오류 플래시와 같은 이유다(패널 다섯이
   *     늘 DOM에 있다). 여기 없으면 ①②④⑤에서도 128px을 더 띄운다.
   *   · `.lineup-primary .btn` — 아래 읽기 전용 규칙과 한 벌이다. 총무·선수에게는
   *     막대가 아예 고정되지 않으므로 그만큼 띄우면 안 된다.
   * 명시도 (1,3,1) > `html`(0,0,1). */
  html{scroll-padding-bottom:calc(var(--tabbar-h) + env(safe-area-inset-bottom))}
  html:has(#panel-lineup:not([hidden]) .lineup-primary .btn){
    scroll-padding-bottom:calc(var(--tabbar-h) + env(safe-area-inset-bottom) + var(--lineup-bar-h))
  }
  /* ③ 고정 막대의 높이. **실측값**이라 내용이 바뀌면 같이 바뀐다 —
     `scripts/mobile_check.py`가 매 실행마다 "③ 라인업 @375: … 높이 NNNpx"로
     찍으므로 드리프트는 거기서 보인다. 2026-08-24 실측: 확정된 오더지에서
     128px(저장 상태 19 + 확정됨 19 + 확정 버튼 44 + 간격 둘 16 + 안여백 26),
     확정 전에는 '확정됨' 줄이 없어 약 99px. **큰 쪽을 쓴다** — 모자라면 포커스가
     가려진 띠에 떨어지고, 남으면 조금 더 위로 굴러갈 뿐이라 해가 없다. */
  :root{--lineup-bar-h:128px}

  /* ── 주 동작은 알약 FAB (스펙 §7-1 · 2026-08-23 사용자 확정 ④) ───────────
   * 엄지 영역에 둔다. 글자를 그대로 남긴다 — 아이콘만 남은 FAB는 무엇을 하는
   * 버튼인지 눌러 봐야 안다(§3 안티패턴 2).
   *
   * 선택자가 `.btn-primary.btn-lg`인 것이 중요하다: `.btn-lg`만 쓰면
   * audit·members의 '설정으로'(btn-ghost btn-lg)까지 떠 있는 알약이 된다.
   * bottom의 **기준선**은 오류 플래시·라인업 막대와 같다(스펙 §5-3) — 셋 다
   * `var(--tabbar-h) + env(safe-area-inset-bottom)` 위에서 잰다. 다만 **식까지
   * 같지는 않다**: 떠 있는 알약 둘(이것과 플래시)만 `+ 16px`로 화면에서 뜨고,
   * 전폭 막대(`.lineup-primary`)는 탭 바에 붙어야 해서 더하지 않는다 — Task 8의
   * 합격선이 "막대 아래 모서리가 탭 바 위 ±2px"라 16px를 더하면 그 검사가
   * 떨어진다. 2026-08-24까지 이 자리에 "**같은 식**"이라고 적혀 있었는데, 그
   * 문장을 믿고 막대에 16px를 더하면 기하 검사가 빨간불이 된다
   * (`tests/test_mobile_shell.py`의 같은 오해를 이 라운드가 이미 한 번 고쳤다). */
  .page-head .btn-primary.btn-lg{
    position:fixed;right:16px;
    bottom:calc(var(--tabbar-h) + env(safe-area-inset-bottom) + 16px);
    z-index:25;border-radius:999px;height:52px;padding:0 20px;box-shadow:var(--shadow)
  }
  /* FAB가 뜬 화면은 바닥을 그만큼 더 비운다. `body.app`의 여백은 탭 바까지만이라
     맨 아래로 스크롤하면 마지막 68px(16px 띄움 + 52px 높이)이 알약 밑에 깔린다 —
     그 자리가 오른쪽이라 선수단 행의 버튼 줄이 정확히 거기다. FAB가 없는 화면에
     빈 띠를 만들지 않으려고 `:has()`로 좁힌다.
     `:has()`는 이 파일에서 처음 쓰는 것이 아니다 — app.css의 `.filter-chip:has(...)`·
     `.pos-chip:has(...)`가 이미 같은 셀렉터를 쓴다. Task 8의 기하 검사가 여는
     헤드리스 셸이 `:has()`를 지원할 만큼 새 버전이어야 한다는 전제가 여기 딸려
     간다 — 지원하지 않으면 이 패딩이 안 걸린 채로 재서, 없는 겹침을 있다고
     보고할 수 있다. 폴백은 두지 않는다(기록만). */
  body.app:has(.page-head .btn-primary.btn-lg){
    padding-bottom:calc(var(--tabbar-h) + env(safe-area-inset-bottom) + 68px)
  }
  /* `position:fixed` 자식은 flex 항목이 아니라서 애초에 빈 칸이 생기지 않는다 —
     버튼이 빠져나간 자리를 메우는 gap이 아니다. 아직 두 자식이 인라인으로 남은
     page-head(예: `/settings/audit`의 '설정으로' 뒤로가기 링크)를 좁히는 것이 이 줄의
     실제 효과다. */
  .page-head{gap:8px}
  /* 폼 버튼 줄: primary 먼저 전체 폭, 삭제는 맨 마지막에 띄워서 (스펙 §8 ④). */
  .form-actions{flex-direction:column;align-items:stretch}
  .form-actions .btn{width:100%;justify-content:center}
  .form-actions .btn-danger{margin-top:12px}
  /* 카드 하나가 225px → 약 170px. 22경기가 세로로 5,687px이던 목록이 줄어든다. */
  .game-card{padding:14px;gap:6px}
  /* 리스트 보기(2026-08-24 요청 ④)는 폰에서 압축 카드다 — PC의 4열 grid를
     좁은 폭에 강요하면 문서가 가로로 밀린다(넘침 0 규칙). 정보는 하나도
     감추지 않고 배치만 세로로 접는다. **≤1024 블록에 두지 말 것** — 위
     `.game-card`의 gap·padding을 겹치는 폭에서 되받아
     `test_no_later_block_takes_back_what_an_earlier_one_said`가 실패한다.
     그래서 태블릿(720.02~1024) 블록에 같은 규칙이 한 벌 더 있다. */
  .board-view.is-rows .game-card{display:flex;flex-direction:column;gap:8px;padding:13px 14px}
  .board-view.is-rows .gc-top{flex-direction:row;align-items:center}
  .board-view.is-rows .gc-steps{justify-content:flex-start}

  /* ── 표 → 카드 행 (스펙 §8 ⑤) ─────────────────────────────────────────
   * `min-width:640`이 남으면 카드로 바꿔도 가로 스크롤이 그대로다 — 실측에서
   * 기록·선수단·팀 표가 전부 스크롤 안에서 잘려 있었다.
   *
   * 명시도를 app.css와 **맞춘다.** 저쪽 `.log-table th,.log-table td`(app.css:396)는
   * (0,1,1)이라 홑 `td`(0,0,1)로 적으면 이 파일이 나중에 실려도 진다 — 파일 순서는
   * 명시도가 같을 때만 승부를 가른다(이 파일 아래쪽 `.att-stat .k` 주석과
   * 같은 함정). `.log-name`도 마찬가지라 `.log-table .log-name`으로 쓴다.
   * (2026-08-23까지 이 자리에 (0,2,0)이라고 적혀 있었다 — 고른 선택자와 결론은
   *  맞았고 숫자만 틀렸다.) */
  .log-table{min-width:auto;font-size:.9rem}
  .log-table thead{display:none}
  .log-table tr{display:grid;grid-template-columns:1fr;gap:4px;padding:12px 0;
    border-bottom:1px solid var(--border)}
  .log-table td{display:block;padding:0;border-bottom:0}
  .log-table td[data-label]::before{
    content:attr(data-label);font-size:.8rem;color:var(--muted);margin-right:6px
  }
  /* 이름 칸은 카드의 제목이다 — 라벨 없이 굵게.
     **제목이 카드를 넓히지 못하게 한다.** `app.css:402`의
     `.log-name{white-space:nowrap}` 위에 회원 표의 긴 이메일(`.log-name` 안
     둘째 줄)이 얹히면 그 칸의 min-content가 그대로 표의 바닥 폭이 되고,
     표의 최소 폭은 행마다가 아니라 **표 하나 전체**에 걸리므로 카드 스물몇
     개가 다 같이 화면 밖으로 흐른다. 셋이 다 필요하다:
     `white-space:normal`이 줄바꿈 자리를 열고, `overflow-wrap:anywhere`가
     띄어쓰기 없는 이메일 안에서도 끊게 하며(`break-word`는 min-content 계산에
     반영되지 않아 바닥이 그대로 남는다), `min-width:0`이 grid 항목의 자동
     최소 크기를 푼다. */
  .log-table .log-name{font-weight:750;font-size:1rem;
    min-width:0;white-space:normal;overflow-wrap:anywhere}
  /* 선수단·팀 카드의 제목 이름은 **한 줄로 자른다** — 제목 줄에서 이름이 접히면
     아래 본문 줄이 통째로 밀리고 칩이 이름 중간 높이에 뜬다. `overflow:hidden`이
     grid 항목의 자동 최소 크기도 함께 푼다(overflow가 `visible`이 아니면
     `min-width:auto`가 0이다) — 잘린 이름이 트랙을 넓히지 못한다.
     **회원 표는 일부러 뺐다.** 그쪽 `.log-name`은 `이름<br>이메일`이라 바로 위
     `white-space:normal` · `overflow-wrap:anywhere`가 긴 이메일이 표의 min-content
     바닥을 정하는 것을 막고 있는 처방 그 자체다 — 여기에 회원 표를 넣으면 그것을
     되돌린다.

     **알려진 한계 — 쫓지 말 것(2026-08-23 실측).** 18자 이상 이름은 여전히 제
     카드를 44px쯤 가로로 흘린다. 트랙 배치 넷을 재 봤고, 어느 길이에서도 안
     흐르는 유일한 배치(`minmax(0,1fr) auto auto`)는 칩을 카드 오른쪽 끝에 못박아
     흔한 2~4자 한국어 이름에서 스펙의 `이름 #번호 [칩]` 붙임을 깨뜨린다.
     `minmax(0,auto)`는 Blink에서 줄어들지 않는다. 트랙은 지금 그대로 두고, 남은
     넘침은 Task 8의 기하 스윕이 긴 이름을 심어 보고한다. */
  .log-table.roster-table .log-name,
  .log-table.team-table .log-name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  /* 칸이 블록이 되면 그 안의 **블록 레벨** 자식이 카드 폭을 통째로 먹는다 —
     `.mini`는 `display:flex`(app.css:262), `.slot-no`는 `display:grid`(app.css:591)라
     알약이 아니라 **띠**가 되고, 라벨은 제 줄로 밀려난다. 실측 430px에서
     `.mini`가 368px, 곧 카드 폭 전체였다. (본이 되었던 `.chart-count .mini`는
     포지션 분포 그래프와 함께 사라졌다 — 2026-08-27 A안 결정 ④.)

     제목 줄의 알약은 **제 내용만큼만** 선다 — 이름이 길어져도 글자마다 줄바꿈되지
     않는다. 한국어는 글자마다 줄바꿈 자리가 있어서 그냥 두면 실측 31×49px의
     세로로 쌓인 알약이 된다(375px·18자 이름; 430px에서도 35×49). 아래
     `.filter-chip{flex:none}`이 필터 줄에서 고친 것과 같은 결함이고, **여기서
     일하는 것은 `white-space:nowrap`이다** — 지금 마크업의 `.mini`는 어느 칸에서도
     flex 항목이 아니라(칸이 `display:block`이다) `flex:none`은 실효가 없다.
     칸이 flex가 되는 자리를 위한 짝으로 함께 둔다. */
  .log-table .mini{display:inline-flex;white-space:nowrap;flex:none}
  .log-table .slot-no{display:inline-grid}
  /* `.log-memo{max-width:22em}`(app.css:498)은 **데스크톱 표**의 열이 넓어지는 것을
     막는 값이다 — 그 파일의 바로 위 주석이 그렇게 적고 있다. 폰 카드에는 열이
     없다: 368px 카드 안에서 316.8px로 잘려 한 줄 요약이 51px 일찍 말줄임됐다. */
  .log-table .log-memo{max-width:none}
  /* 버튼 칸은 한 줄에 나눠 갖는다.
     **`:not([data-label])`이 필요하다.** 이 규칙은 '버튼 칸'을 노렸는데 `:last-child`
     만으로는 관리 칸이 **없는** 표의 마지막 값 칸까지 잡는다 — `pages/audit.html`의
     IP 칸이 그렇다. 라벨과 값 사이가 14px(flex gap 8 + `::before`의 6)로 다른
     칸(6px)과 어긋나고 위로 6px이 더 붙었다.
     **`:not(.empty)`도 필요하다.** 빈 표의 안내 줄도 `:last-child`다. 예전에는
     아래 `td.empty`를 **뒤에** 두어 같은 명시도끼리 나중 것이 이기게 막았는데,
     `:not([data-label])`이 붙으면서 이 선택자가 (0,3,1)로 올라가 `.log-table
     td.empty`(0,2,1)를 순서와 무관하게 이겨 버린다. 순서로는 더 못 막으므로
     **명시적으로 뺀다** — 안 그러면 '등록된 선수가 없습니다'와 '전체 명단 보기'
     버튼이 한 줄에 나란히 붙는다.
     `> *`가 따로 있는 이유: `member_lists.html`은 `<td>`와 버튼 사이에
     `.member-actions`(app.css:79, `display:flex`)가 끼어 있어 td의 flex **항목**은
     그 상자다. 상자가 내용 크기(368px 카드에서 212.5px)로 서면 아래 `.btn{flex:1}`은
     아무 효과가 없다 — 스펙 §7-7의 "버튼은 전체 폭"이 표 넷에서만 지켜지고 회원
     표 둘에서만 안 지켜지는, 표끼리 어긋난 상태였다. 자식을 **하나도 가리지 않고**
     늘리는 것이 가장 작은 수정이다: 버튼이 직접 자식인 표 넷에서는 `.btn{flex:1}`과
     같은 값이라 아무것도 안 바뀌고, 끼어 있는 상자가 있는 곳만 늘어난다.
     **`:not(.is-blank)`도 필요하다.** 편집 권한이 없는 사람에게 선수단의 관리
     칸은 통째로 빈다 — 카드마다 바닥에 10px짜리 죽은 띠가 남는다(23명이면
     230px). 아래 `td.is-blank{display:none}`가 그것을 지우려 하는데, `:not()`이
     명시도를 얹어 이 규칙이 (0,3,1) → `is-blank`(0,2,1)보다 세다. 순서로는 못
     막으므로 여기서도 **명시적으로 뺀다**(위 `:not(.empty)`와 같은 이유).
     **`:not(.cho-cell)`도 같은 갈래다** (2026-08-27 A안 Task 5). 초성 그룹
     머리(`<tr class="cho-head"><td class="cho-cell" colspan="5">`)의 칸도 제
     행에 혼자라 `:last-child`다 — 안 빼면 이정표 줄마다 `margin-top:6px`이
     붙고 `display:flex`가 아래 `tr.cho-head td`의 블록 배치를 이긴다
     ((0,5,2) 대 (0,4,2)). 순서로는 못 막는다. */
  .log-table td:last-child:not([data-label]):not(.empty):not(.is-blank):not(.cho-cell){display:flex;gap:8px;margin-top:6px}
  .log-table td:last-child:not([data-label]):not(.empty):not(.is-blank):not(.cho-cell)>*{flex:1;min-width:0}
  .log-table td:last-child:not([data-label]):not(.empty):not(.is-blank):not(.cho-cell) .btn{flex:1;justify-content:center}
  /* 빈 표의 한 줄. 위 규칙이 `:not(.empty)`로 이 줄을 빼므로 순서에 기대지 않는다.
     **`grid-column:1/-1`이 아래 "조립된 카드" 때문에 필요하다.** 표 넷이 여러
     열짜리 grid가 되므로, 자리를 안 주면 안내 문장이 첫 트랙(예: 이름 폭)
     안으로 찌그러진다. 아래 자리 규칙들이 `:nth-child(1)`을 **비워 두는** 것도
     이 줄 때문이다 — 빈 안내 줄도 제 행의 첫 칸이라 같은 명시도(0,2,1)로
     부딪치고, 그러면 순서 싸움이 된다. */
  .log-table td.empty{display:block;text-align:center;grid-column:1/-1}

  /* ── 조립된 카드: 제목 줄 (스펙 §7-5 · §7-6 · §7-7) ──────────────────────
   * 위의 일반 규칙(§8 ⑤)은 **모든** 칸을 제 줄에 세운다 — 선수단 카드가
   * `이름` / `등번호: 7` / `소화 포지션: …` / `장단점 · 특이사항: —` /
   * `상태: 활동`, 다섯 줄짜리 키-값 목록이었다. 스펙이 손으로 설계한 것은
   * **제목 줄 하나 + 본문 줄 몇**이다.
   *
   * 마크업은 한 글자도 안 옮긴다. `tr`이 이미 `display:grid`라 `<td>`가 전부
   * grid 항목이므로 **자리만** 옮기면 된다 — 그래서 720px 위의 데스크톱 표는
   * 이 블록 밖이고 한 픽셀도 안 바뀐다.
   *
   * **데이터는 하나도 숨기지 않는다(2026-08-23 사용자 결정).** 스펙 §7-7은
   * 폰에서 감사 상세·IP와 회원 마지막 로그인·연결 선수를 빼라고 적었지만,
   * 그 칸들은 라벨 줄로 남는다 — 폰에서만 정보가 사라지는 것은 되돌리기
   * 어렵다. 스펙의 **모양**은 따르고 스펙의 **데이터**는 지킨다.
   *
   * 선택자 두 가지 규칙:
   * ① `tr` 규칙은 `.log-table.X-table`로 쓴다. 홑 `.X-table tr`은 위
   *    `.log-table tr{…gap:4px}`와 명시도가 같아(둘 다 0,1,1) 순서 싸움이 된다.
   * ② 자리 규칙은 `:nth-child(1)`을 **쓰지 않는다.** 첫 칸은 자동 배치가
   *    이미 (1행 1열)에 놓고, 규칙을 쓰면 빈 표의 안내 줄(`td.empty`도 제
   *    행의 첫 칸이다)과 같은 명시도로 부딪친다(위 `td.empty` 주석).
   */
  /* ── 선수단만 카드가 아니라 **압축 행**이다 (2026-08-27 A안 Task 4, 스펙 §5-1) ──
   * 실측에서 이 카드가 평균 143px이었고 23명이 4,717px, 곧 폰 화면 5.6개였다.
   * 60px 한 줄로 되돌린다: 등번호는 1열 배지, 이름과 포지션 요약이 오른쪽에
   * 두 줄, 상태 알약은 **예외일 때만** 이름 줄 오른쪽 끝.
   *
   * 메모 칸은 여기서 안 그린다 — 전문은 상세(`/roster/{id}/detail`)가 갖고,
   * 그 자리는 행을 한 번 누르면 닿는다. 2026-08-23의 "폰에서만 정보가
   * 사라지지 않게 한다"는 결정은 **닿을 길이 없어지는 것**을 막는 것이었다.
   *
   * 나머지 표 셋과 모양이 달라져서 `tests/test_mobile_shell.py`의 모형도
   * `COMPOSED_TABLES`(제목 줄 + 카드 폭 줄들)가 아니라 `ROSTER_COMPRESSED`가
   * 따로 잰다. */
  .log-table.roster-table tr{grid-template-columns:38px minmax(0,1fr);
    grid-template-rows:24px auto;
    column-gap:11px;row-gap:2px;align-items:center;min-height:60px;padding:8px 14px}
  .log-table.player-log-table tr{grid-template-columns:auto auto minmax(0,1fr);
    column-gap:8px;align-items:center}
  .log-table.team-table tr{grid-template-columns:auto minmax(0,1fr);
    column-gap:8px;align-items:center}
  /* 회원 표만 `start`다. 그 표의 제목 칸은 `이름<br>이메일`이라 **늘 두 줄
     이상**인데, `center`면 상태 칩이 그 덩어리의 한가운데 — 곧 이름과 이메일
     **사이** — 에 뜬다. 이메일이 길어 `overflow-wrap:anywhere`로 세 줄이 되면
     칩이 이메일 둘째 줄 옆까지 내려간다. 칩은 이름에 묶여야 뜻이 통한다.
     제 줄을 통째로 쓰는 나머지 칸들은 제 행에 혼자라 정렬이 아무 차이도 안 낸다. */
  .log-table.member-table tr{grid-template-columns:minmax(0,1fr) auto;
    column-gap:8px;align-items:start}

  /* 선수단 압축 행 — `[번호] 이름 [상태]` / `소화 포지션 요약` (스펙 §5-1).
     명시도를 **(0,2,1)로 맞춘다**: 아래 `.log-table td.is-blank{display:none}`가
     같은 (0,2,1)이고 파일에서 뒤에 있어야 이기는데, 여기에 `.log-table`을
     앞세워 (0,3,1)로 올리면 등번호 없는 선수의 **빈 배지**가 되살아난다. */
  .roster-table td.roster-no{grid-column:1;grid-row:1/3;
    width:38px;height:38px;border-radius:11px;background:var(--surface-3);
    color:var(--muted);display:grid;place-items:center;
    font-size:.86rem;font-weight:700;font-variant-numeric:tabular-nums}
  /* 이름 칸은 **두 줄을 다 걸친다.** 그 안의 버튼이 이 행의 진짜 터치 타깃인데,
     44px 하한을 한 줄로는 못 채운다 — 60px 안에 44px짜리 줄과 17px짜리 포지션
     줄이 함께 들어가지 않는다(8+44+2+17+8 = 79px). 두 줄을 합친 44px을 버튼이
     통째로 갖고, 글자는 첫 줄에만 얹힌다(`line-height:22px`). 둘째 줄 자리에는
     아래 포지션 칸이 그려진다 — 버튼은 그 위를 덮는 투명한 히트 영역이다.
     `scripts/mobile_check.py`가 `<button>`의 **상자**를 재므로 ::after로 덮는
     방법으로는 안 된다(그 파일의 "예외를 늘리고 싶어지면" 주석).

     `grid-template-rows`를 못 박은 것도 이것 때문이다. 자동이면 상태 알약이
     **없는** 행에서 첫 트랙이 0이 되고, 배지의 38px과 `min-height:60px`이
     남은 자리를 두 트랙에 반씩 나눠 줘서 포지션 글자가 이름 글자 위로 올라온다
     (실제로 겹쳤다). 24px은 알약(≈25.5px)과 이름 줄(22px)을 함께 담는 값이다. */
  .roster-table td.log-name{grid-column:2;grid-row:1/3;align-self:stretch;
    font-size:1rem;min-width:0}
  /* 이 선택자에 `.log-name`을 안 적는 이유는
     `test_only_the_two_short_name_tables_clip_their_title_to_one_line`이
     "`.log-name`을 한 줄로 자르는 표"를 그 낱말로 세기 때문이다.
     `display:block`이라야 말줄임이 돈다 — flex 컨테이너에는 안 걸린다. */
  .log-table.roster-table .row-open{display:block;width:100%;height:100%;
    line-height:22px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .roster-table td:nth-child(3){grid-column:2;grid-row:2;
    font-size:.79rem;color:var(--muted);
    white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  /* 상태 알약은 이름 줄의 오른쪽 끝. 예외가 아니면 칸이 비어 아무 자리도 안 쓴다. */
  .roster-table td:nth-child(5){grid-column:2;grid-row:1;justify-self:end}
  /* 메모는 상세에서 본다 — 목록의 한 줄 요약이 60px 행에 들어갈 자리가 없다. */
  .log-table.roster-table td.log-memo{display:none}
  /* 포지션은 서버가 전문·요약을 **둘 다** 내려보내고 폭이 고른다(app.css의 짝). */
  .pos-full{display:none}
  .pos-short{display:inline}

  /* ── 초성 그룹 머리 (2026-08-27 A안 Task 5, 스펙 §5-2) ────────────────────
   * 초성이 바뀌는 자리에 서는 이정표 줄이다. **명시도가 (0,3,1)이라야 한다** —
   * 위 `.log-table.roster-table tr`(0,2,1)의 `display:grid`와
   * `grid-template-rows:24px auto`를 순서와 무관하게 이겨야 한다. `tr.cho-head`가
   * 클래스 셋(log-table · roster-table · cho-head)이라 그 값이 나온다.
   *
   * **스티키가 아니다** (2026-08-27 Task 6·7 눈확인이 잡았다). 이 표는
   * `.table-scroll`(`overflow-x:auto`) 안에 있고, 한 축이라도 `auto`면 다른
   * 축도 `auto`로 계산되므로 **스크롤포트가 뷰포트가 아니라 그 상자**다. 그
   * 안에서는 세로 스크롤이 0이라 이정표가 붙을 일이 애초에 없었는데, `top:57px`가
   * **첫 이정표만 57px 아래로 밀어** 첫 행 위에 겹쳐 그렸다. 상태 칩으로 좁혀
   * 결과가 한 행이면 그 한 행이 통째로 가려져 **목록이 비어 보였다.**
   *
   * 되돌리는 길 둘 중 흐름 배치를 골랐다: `.table-scroll`의 오버플로를 푸는 쪽은
   * 저장소의 표 여섯과 `mobile_check`의 `.table-scroll` 검사가 함께 걸린다.
   * 이정표가 안 붙어도 잃는 것이 없다 — 목록을 건너뛰는 일은 레일이 하고,
   * 레일이 뛴 자리를 헤더 아래로 내리는 것은 아래 `scroll-margin-top`이 한다.
   *
   * `scroll-margin-top`도 **여기 `<tr>`에** 있어야 한다. 처음에는 `<td>`에
   * 적었는데 `scrollIntoView`를 부르는 대상은 id를 가진 `<tr>`이라 아무 효과가
   * 없었다 — 그 속성은 **스크롤되는 요소 자신**의 것이다.
   *
   * 글자는 `.78rem`(=12.48px)이다. `.75rem`이면 정확히 12px이라 `mobile_check`의
   * 하한에 여유 없이 붙는다 — OS 글꼴 배율을 내리는 사람에게 그 여유가 없다.
   */
  .log-table.roster-table tr.cho-head{display:block;
    scroll-margin-top:57px;
    min-height:0;padding:5px 14px;background:var(--surface-2);
    font-size:.78rem;font-weight:800;color:var(--faint);
    border-bottom:1px solid var(--border)}
  /* `cho-cell`은 위 '버튼 줄' 규칙에서 `:not(.cho-cell)`으로 빠진 그 클래스다. */
  .log-table.roster-table tr.cho-head td.cho-cell{padding:0;border:0}

  /* ── 초성 레일 (스펙 §5-2) ────────────────────────────────────────────────
   * 누르는 자리는 **44×44**, 보이는 것은 그 안의 26px 알약이다.
   * `scripts/mobile_check.py`가 `<button>`의 상자를 예외 없이 재므로 **폭도**
   * 44여야 한다 — 알약만 키우고 상자를 26으로 두면 그 검사가 빨간불이 된다.
   *
   * **화면 한가운데(`top:50%; translateY(-50%)`)로 잡으면 안 된다**
   * (2026-08-27 눈확인, 667×375 = 아이폰 SE 가로). 그 화면에는 고정된 것이
   * **셋**이다 — 스티키 헤더 56px(app.css의 `.gnb`) · 탭 바 56px · 알약 FAB의
   * 띠 68px. 남는 세로는 195px인데 초성 여섯만 해도 264px다. 가운데로 잡으면
   * 마지막 초성이 탭 바 밑에 깔려 **영영 안 눌리고**(헤드리스 클릭이 30초를
   * 기다리다 죽었다), 상자만 위로 올리면 이번에는 첫 초성이 헤더 밑에 깔린다.
   * 셋 다 z-index가 레일보다 높으니 z로는 못 이긴다(헤더 30 · 탭 바 30 · FAB 25).
   *
   * 그래서 **상자를 '쓸 수 있는 띠'로 자르고, 안 들어가면 왼쪽으로 접는다.**
   *   · `top:56px` — 스티키 헤더 아래. 그 56은 이 파일 위쪽 `.gnb-inner`의
   *     `height:56px`(폰 헤더 한 줄)과 같은 수다.
   *   · `bottom`의 `+ 68px` — 바로 위 `body.app:has(.page-head .btn-primary.btn-lg)`가
   *     쓰는 것과 같은 수이고 같은 뜻이다(알약 FAB = 16px 띄움 + 52px 높이).
   *   · `flex-wrap:wrap-reverse` — 띠보다 길어지면 둘째 **열**이 생기는데,
   *     `reverse`라 그 열이 오른쪽이 아니라 **왼쪽**에 선다. 상자는 `right:0`에
   *     붙어 내용만큼만 넓으므로 새 열이 화면 밖으로 나가지 않는다.
   *     `wrap`(reverse 없이)이면 정확히 반대로 화면 밖에 그린다.
   * 375px 가로에서 초성 일곱이 4+3 두 열이 되고, 세로 화면(844px)에서는 띠가
   * 664px이라 열다섯까지 한 열로 선다 — 실제 명단(초성 여섯)은 접히지 않는다.
   *
   * `z-index:24`는 탭 바(30)·FAB(25) **아래**다. 겹치지 않게 잘라 두었으므로
   * 순서 싸움이 날 일이 없고, 만에 하나 겹치면 이정표가 그 둘을 가리는 것보다
   * 뒤로 들어가는 편이 낫다.
   *
   * `pointer-events`가 컨테이너에서 `none`, 버튼에서 `auto`인 것이 요점이다.
   * 레일 상자는 이제 띠 높이만 한 세로 상자라 그 뒤의 목록 오른쪽이 통째로
   * 눌리지 않게 되는데, 거기에 상태 알약과 행의 히트 영역이 있다.
   */
  .cho-rail{display:flex;position:fixed;right:0;top:56px;
    bottom:calc(var(--tabbar-h) + env(safe-area-inset-bottom) + 68px);
    z-index:24;flex-direction:column;flex-wrap:wrap-reverse;
    justify-content:center;align-content:center;align-items:center;
    pointer-events:none}
  .cho-rail button{width:44px;height:44px;padding:0;border:0;background:none;
    display:grid;place-items:center;pointer-events:auto;cursor:pointer}
  .cho-rail button span{width:26px;height:26px;border-radius:999px;display:grid;
    place-items:center;font-size:.78rem;font-weight:700;color:var(--brand);
    background:var(--surface);border:1px solid var(--border)}

  /* ── 선수 상세는 폰에서 **바텀 시트**다 (스펙 §7-3) ───────────────────────
   * 규칙 넷은 ⑤ 기록 입력의 `.pa-picker`가 이미 값을 치른 것이다(이 파일의
   * 그 절 주석에 사정이 길게 적혀 있다). 베끼는 것이 아니라 **같은 결함을 두
   * 번 겪지 않으려고** 옮겨 온다:
   *   ① `z-index:50` — 탭 바 30 위, `#error-flash` 60 아래.
   *   ② 판에 `max-height:calc(100% - 24px)` + `overflow-y:auto` +
   *      `overscroll-behavior:contain`. 없으면 667×375(작은 폰 가로)에서 판이
   *      화면을 넘겨 ×가 화면 밖으로 나가고, 그때 판이 가장자리까지 덮으므로
   *      어둠막이 가려져 바깥 클릭까지 죽는다 — 닫는 길 셋 중 둘이 한꺼번에
   *      사라진다. `100%`가 아니라 `-24px`인 것은 화면이 아무리 낮아도 폭
   *      전체짜리 어둠막 띠를 남겨 **바깥 클릭을 살리기** 위해서다(딱 `100%`면
   *      어둠막이 위쪽 둥근 모서리의 호 안쪽에만 남는다).
   *   ③ 바닥 여백 `calc(18px + env(safe-area-inset-bottom))`.
   *   ④ 닫는 길 셋 — 어둠막 · 손잡이 · ×. Esc는 `roster.js`가 넷째로 더한다.
   *
   * **선택자에 `.roster-main`을 앞세워 (0,2,0)으로 올린다.** 아래
   * `(max-width:1024px)` 블록이 파일에서 **뒤**이고 거기 `.detail-host`(0,1,0)가
   * `position:static`을 선언한다 — 같은 명시도로 적으면 폰에서도 그쪽이 이겨
   * 시트가 그냥 표 아래 카드로 되돌아간다(`test_no_later_block_takes_back_what_
   * an_earlier_one_said`가 그것을 잡는다). `margin`을 단축으로 적는 것도 같은
   * 이유다 — 그쪽 `margin-top:16px`이 `inset:0`을 16px 밀어 어둠막이 위쪽을
   * 못 덮는다.
   *
   * `.detail-host:empty{display:none}`(app.css)는 그대로 이긴다 — 여기서
   * `display`를 선언하지 않으므로 비어 있는 동안에는 화면에 아무것도 없다.
   */
  .roster-main .detail-host{position:fixed;inset:0;z-index:50;margin:0;width:auto}
  .dsheet-dim{display:block;position:absolute;inset:0;background:rgba(2,6,23,.45)}
  .dsheet-pane{position:absolute;left:0;right:0;bottom:0;
    max-height:calc(100% - 24px);overflow-y:auto;overscroll-behavior:contain;
    border:0;border-radius:20px 20px 0 0;
    padding:0 16px calc(18px + env(safe-area-inset-bottom));
    box-shadow:var(--shadow)}
  /* 손잡이도 **버튼**이라 44px 하한을 받는다(`mobile_check`는 예외가 없다).
     보이는 것은 가운데 36×4 막대이고, 그 위아래 20px이 손가락 자리다 —
     시트 맨 위를 아무 데나 눌러도 닫힌다. */
  .dsheet-grab{display:block;width:100%;height:44px;padding:0;border:0;
    background:none;cursor:pointer}
  .dsheet-grab::before{content:"";display:block;width:36px;height:4px;
    margin:20px auto 0;border-radius:999px;background:var(--border-strong)}

  /* 선수별 기록 — `[타순 배지] 이름 #번호 · 포지션` / 타격 / 투구 / 버튼 줄 (§7-5) */
  .player-log-table td:nth-child(2){grid-column:2;grid-row:1}
  .player-log-table td:nth-child(3){grid-column:3;grid-row:1}
  .player-log-table td:nth-child(4),.player-log-table td:nth-child(5),
  .player-log-table td:nth-child(6){grid-column:1/-1}

  /* 팀 — `팀 이름 [기본 팀 칩]` / 임원 / 버튼 줄 (§7-7).
     둘째 칸은 모양이 둘이다: 기본 팀이면 칩, 아니면 '기본 팀으로' **버튼**.
     칩만 제목 줄에 올린다 — 44px 버튼은 제목이 아니고, 제목 줄에 얹으면
     팀 이름과 같은 줄에서 눌리는 것이 된다. 템플릿이 `is-chip`으로 알려 준다
     (CSS는 칸 안에 무엇이 그려졌는지 못 본다). 기본은 전체 폭이고 `.is-chip`이
     (0,3,1)로 (0,2,1)을 순서와 무관하게 이긴다. */
  .team-table td.team-default{grid-column:1/-1}
  .team-table td.team-default.is-chip{grid-column:2;grid-row:1}
  .team-table td:nth-child(3),.team-table td:nth-child(4){grid-column:1/-1}

  /* 회원 — `이름/이메일 [상태 칩]` / 역할 / 마지막 로그인 / 연결 선수 / 버튼 줄.
     마지막 로그인·연결 선수는 스펙 §7-7이 폰에서 빼라고 한 칸이다 — 남긴다
     (2026-08-23 사용자 결정). 이름 트랙이 `minmax(0,1fr)`인 것은 이메일이
     길어서다: 칩을 오른쪽에 고정하고 이름/이메일이 남은 폭 안에서 접힌다. */
  .member-table td:nth-child(3){grid-column:2;grid-row:1}
  .member-table td:nth-child(2),.member-table td:nth-child(4),
  .member-table td:nth-child(5),.member-table td:nth-child(6){grid-column:1/-1}

  /* 승인 대기 표와 감사 로그 표는 자리를 안 옮긴다 — 제목 줄이 첫 칸 하나뿐이라
     기본 1열 grid로 이미 스펙의 모양이다. 감사 로그는 그 첫 칸(시각)이
     `.log-name`이 아니라서 제목처럼 보이게 하는 것만 따로 준다. */
  .log-table.audit-table td.audit-at{font-weight:750;font-size:1rem;color:var(--text)}
  /* 감사 카드가 제 상자를 76~131px 흘렸다(기하 검사 2026-08-24: 375/412/430).
     범인은 '대상' 칸의 `member:2 very.long.address.for.the…` 한 덩어리다 —
     그 칸을 빼면 표 폭이 뷰포트 폭에 정확히 맞는다. 이메일은 띄어쓰기가 없어
     min-content가 문자열 통째이고, 표의 최소 폭은 행마다가 아니라 **표 하나
     전체**에 걸리므로 카드 스물몇 개가 다 같이 옆으로 흐른다. 위
     `.log-name`이 회원 표에서 받은 처방과 같은 이유·같은 약이다
     (`break-word`는 min-content 계산에 안 들어가므로 `anywhere`여야 한다).
     **30자짜리 실제 회원 이메일이면 운영에서도 그대로 난다** — 검사용 데이터가
     길어서 생긴 것이 아니다.
     `overflow-wrap`은 상속되므로 `<code class="audit-action">` 안에서도 끊긴다.
     `.audit-at`·`.audit-ip`(app.css:85)는 `white-space:nowrap`이라 이 값이
     줄바꿈 자리를 만들지 못한다 — 시각과 IP는 그대로 한 줄이다(의도한 것이다).
     명시도 (0,2,1)로 위 `.log-table td`(0,1,1)를 순서와 무관하게 이긴다. */
  .log-table.audit-table td{overflow-wrap:anywhere}

  /* 제목 줄에 오른 칸은 **자기 이름을 앞에 찍지 않는다** — 제목이 `등번호: 7`
     이면 제목이 아니다. `data-label` 속성 자체는 그대로 둔다(데스크톱 `<th>`와
     한 벌이라는 사실도, "어느 칸이 이름을 갖는가"를 재는 테스트도 안 바뀐다).
     명시도 (0,3,2)로 위 `.log-table td[data-label]::before`(0,2,2)를 순서와
     무관하게 이긴다. */
  .log-table.roster-table td:nth-child(2)::before,
  .log-table.roster-table td:nth-child(3)::before,
  .log-table.roster-table td:nth-child(5)::before,
  .log-table.player-log-table td:nth-child(1)::before,
  .log-table.team-table td:nth-child(2)::before,
  .log-table.member-table td:nth-child(3)::before,
  .log-table.audit-table td:nth-child(1)::before{content:none}

  /* (2026-08-27 A안 Task 4: 선수단 등번호의 `#` 규칙은 여기 있었다. 이제
     등번호는 1열 **배지**라 상자 자체가 "등번호"라고 말한다 — `#7`도 라벨도
     안 찍는다. 배지 상자는 위 `.roster-table td.roster-no`가 잡는다.) */
  /* 포지션은 이름 뒤에 `· 포지션`으로 붙는다(스펙 §7-5의 제목 줄). */
  .log-table.player-log-table td:nth-child(3)::before{content:"·";font-size:inherit;
    color:var(--muted);margin-right:4px}
  /* 포지션이 없으면 그 구분점을 끈다 — `[1] 홍길동 #7 · —`는 제목이 아니라 잡음이다.
     **아래 `is-blank`를 쓰지 않는다**: 포지션의 `—`는 "아직 없다"는 실제 진술이라
     칸째로 지우면 2026-08-23 사용자 결정을 어긴다. 칸 안에 무엇이 그려졌는지는
     CSS가 못 보므로 템플릿이 `no-sep`으로 알려 준다(팀 표의 `is-chip`과 같은 꼴).
     `:nth-child(3)`을 함께 적어 (0,4,2)로 바로 위 규칙(0,3,2)을 **순서와 무관하게**
     이긴다. */
  .log-table.player-log-table td:nth-child(3).no-sep::before{content:none}

  /* ── 값이 없는 칸은 폰에서 지운다 (스펙 §7-6 "장단점·특이사항(있을 때만)") ──
   * `장단점 · 특이사항: —` 한 줄은 카드에서 아무것도 말하지 않는다.
   *
   * **딱 두 칸에만 쓴다** — 선수단의 메모와 등번호다. 다른 `—`는 전부 남는다:
   * 포지션·타격·투구·대상·상세의 em 대시는 "아직 기록이 없다"는 실제 진술이고,
   * 2026-08-23 사용자 결정이 "폰에서 데이터를 빼지 않는다"였다. 등번호가
   * 예외인 이유는 하나뿐이다 — 압축 행의 1열 배지가 비면 뜻이 없다(2026-08-27
   * A안 전에는 제목 줄의 `#—`가 그랬다).
   * `.log-table td.is-blank`는 (0,2,1)이라 위 `.log-table td`(0,1,1)를 순서와
   * 무관하게 이긴다. */
  .log-table td.is-blank{display:none}

  /* ── 선수단 필터 (스펙 §7-6) ─────────────────────────────────────────
   * 칩 10개가 `flex-wrap:wrap`이라 세 줄을 먹어 표가 밀린다 → 한 줄 가로 스크롤.
   *
   * **`.filter-bar`의 `flex-wrap:wrap`을 폰에서 끈다.** 그러지 않으면 스크롤이
   * 아예 안 생긴다: 세로 flex 컨테이너가 `wrap`이면 flex **줄**의 가로 크기가
   * 내용을 따라 정해져서, 줄이 칩 열 개의 max-content(537px)까지 자라고 모든
   * 항목을 거기에 맞춰 늘린다. 항목이 줄어든 게 아니라 줄이 자란 것이라
   * `app.css:458`의 `min-width:0`으로는 못 막는다. 실측(430px): 문서가
   * 552px로 122px 옆으로 흘렀고 `.filter-chips`는 scrollWidth==clientWidth==537,
   * 즉 스크롤러가 생기지도 않았다.
   *
   * `.filter-chip`에 `flex:none`이 필요하다 — 한국어는 글자마다 줄바꿈 자리가
   * 있어서 기본 `flex-shrink:1`이면 칩이 한 글자 폭(43px)으로 찌그러지고
   * 세로로 82px까지 늘어난다.
   *
   * 위아래 여백 4px는 포커스 링 자리다. `overflow-x:auto`를 주면 다른 축이
   * `visible`에서 `auto`로 계산되어 **세로로도 잘린다** — `--focus`는
   * `0 0 0 3px`에 오프셋이 없어서 링의 위아래가 잘려 나간다(스펙 §8 ⑧).
   * 음수 마진으로 되돌려 줄 높이는 그대로 둔다.
   *
   * 좌우 14px은 스펙의 -16px이 아니다 — 폰 `.card-body`가 14px이라(위 `.card-body`
   * 규칙) 16px이면 카드 테두리를 2px 넘어선다. 실측 x=17 / right=413 대 카드
   * 16..414로 카드 안쪽 가장자리에 정확히 맞는다. */
  .filter-bar .form-field{max-width:none}
  .filter-bar{flex-wrap:nowrap}
  .filter-chips{flex-wrap:nowrap;overflow-x:auto;margin:-4px -14px;padding:4px 14px;
    scrollbar-width:none}
  .filter-chip{flex:none}
  .filter-chips::-webkit-scrollbar{display:none}

  /* ── ③ 라인업: **핵심 둘만** 탭 바 위에 붙인다 (스펙 §7-4 · 2026-08-24 결정) ──
   * 2026-08-18 지시로 막대는 패널 **맨 위**에 있다. 폰에서만 편집기 끝으로
   * 보내되 화면에 붙여 늘 보이게 한다 — 되돌리는 것이 아니다(`lineup_editor.html`의
   * 주석 한 벌이 두 지시의 관계를 적고 있다). PC·태블릿은 한 픽셀도 안 바뀐다:
   * 이 파일에서 `.lineup-actions`·`.lineup-primary`·`.lineup-secondary`를 건드리는
   * 곳은 이 블록 하나뿐이어야 하고, `test_the_desktop_keeps_the_bar_at_the_top`이
   * 나머지 전부를 훑어 지킨다.
   *
   * **일곱을 다 고정하지 않는다.** 실측(헤드리스 크로미움)에서 일곱을 담은 막대는
   * 375px·430px 모두 256px이었다 — 375×812에서 탭 바까지 빼면 아홉 칸을 편집할
   * 자리가 488px만 남는다. 2026-08-24 사용자 결정으로 고정되는 것은
   * `.lineup-primary`(저장 상태 · 확정됨 · 오더지 확정)뿐이고
   * `.lineup-secondary`(수비 초기화 · 초기화 · 오더지 삭제 · 인쇄 힌트 ·
   * 인쇄 미리보기)는 편집기 끝에 평범한 블록으로 앉는다.
   *
   * `.lineup-actions{display:contents}`가 그 분리를 만든다 — 껍데기의 테두리·배경·
   * 안여백이 그리지 않고 두 상자가 편집기의 flex 항목이 되어 `order`를 각자 받는다.
   * `order`는 **같은 flex 컨테이너의 형제**에서만 들으므로 Alpine 루트를 flex로
   * 만든다(그 div에 클래스가 없어서 Task 7이 `lineup-editor`를 붙였다).
   * `app.css`의 `.lineup-actions ~ .alert`(형제)와 `.lineup-actions .btn`(자손)은
   * 둘 다 DOM으로 고르므로 `display:contents` 아래에서도 그대로 맞는다.
   *
   * `bottom`은 `#error-flash`·FAB와 같은 기준선이되 `+ 16px`이 **없다** — 떠
   * 있는 알약 둘과 달리 전폭 막대는 탭 바에 붙는다(계획서 §3-A ⑦은 셋이 글자까지
   * 같아야 한다고 적었는데, 그 줄이 Task 8의 합격선 "막대 아래 모서리가 탭 바 위
   * ±2px"와 부딪힌다 — Task 8이 계획서를 고친다). 둘은 애초에 함께 뜨지 않는다:
   * FAB 선택자는 `/games`·`/roster`·`/settings`에만 걸린다.
   *
   * `.lineup-primary`에는 아래쪽 마진을 주지 않는다 — sticky의 제약 사각형은
   * **마진 상자**라 아래 마진이 남으면 막대가 그만큼 떠서 위 합격선을 벗어난다.
   * (예전에는 `.lineup-actions`가 막대여서 `app.css`의 14px를 `margin-bottom:0`으로
   * 지웠다. 지금 막대는 `app.css`에 마진이 없는 새 클래스라 지울 것이 없다.)
   * `z-index`는 탭 바(30)보다 아래, `#error-flash`(app.css 60)보다 아래다:
   * 플래시는 이 막대 위 16px에 떠서 실제로 겹친다 — 그 겹침을 실제로 푸는 것은
   * 이 `z-index`가 아니라 위쪽 오류 플래시 절의
   * `body.app:has(#panel-lineup:not([hidden]) .lineup-primary) #error-flash`다. */
  .lineup-editor{display:flex;flex-direction:column}
  .lineup-actions{display:contents}
  /* `grid-template-columns:1fr 1fr`의 **두 트랙은 아무것도 배치하지 않는다.**
     이 상자의 자식은 넷인데 셋(`.lineup-save-state`/`.lineup-readonly-note` ·
     `.lineup-state` · 확정 버튼)이 전부 `grid-column:1/-1`이고 `.lineup-spacer`는
     `display:none`이다 — 한 열짜리 격자와 **그려지는 모양이 같다.**
     그래도 `1fr 1fr`인 것은 바닥 폭 때문이다: `1fr`은 `minmax(auto,1fr)`이라
     칸을 가로지르는 항목의 자동 최소 크기가 트랙 바닥이 되는데,
     `.lineup-save-state{min-width:15em}`(app.css)이 192px이라 두 트랙이 그것을
     나눠 받아 막대가 절대 그 아래로 찌그러지지 않는다. `minmax(0,1fr)`로
     바꾸면 바닥이 0이 되어 320px 폭에서 저장 상태가 막대를 넘쳐 나간다 —
     아래 `.lineup-secondary`와 정반대 이유로 여기서는 `1fr`이 맞다.
     (2026-08-24까지 이 자리 아래 주석이 "두 열 중 한 칸에 안 들어간다"라고
     적어 항목이 열에 **배치되는** 것처럼 읽혔다. 배치되는 항목은 없다.) */
  .lineup-primary{
    order:99;position:sticky;z-index:20;
    bottom:calc(var(--tabbar-h) + env(safe-area-inset-bottom));
    border-top:1px solid var(--border);background:var(--surface);
    box-shadow:0 -6px 18px -12px rgba(15,23,42,.35);
    padding:13px 16px;
    display:grid;grid-template-columns:1fr 1fr;gap:8px
  }
  /* ── 누를 것이 하나도 없는 막대는 고정하지 않는다 (품질 검토 B-5) ──────────
     `LINEUP_EDIT`가 없는 역할(총무·선수)에게 Jinja는 저장 상태도 확정 버튼도
     그리지 않는다(`lineup_editor.html`의 `{% if lineup_editable %}` 둘). 남는
     것은 `.lineup-readonly-note` 한 문장 — 절대 바뀌지 않는 글이다. 그것을
     70px짜리 테두리·그림자 상자로 화면에 못 박으면, 헤더와 탭 바까지 합쳐
     667px 폰의 27%가 **읽기만 하는 화면**에서 크롬으로 나간다.
     고정의 명분(확정이 엄지 아래 · 저장 상태 가시성)이 둘 다 없는 상태다.
     그래서 평범한 블록으로 되돌리고 안내는 `.lineup-secondary`(인쇄 미리보기)
     위의 한 줄로 흐른다. `bottom`은 static에서 무시되므로 지울 것이 없다.
     `:not(:has(.btn))`의 명시도는 (0,2,0)이다 — `:not()`은 제 안쪽에서 가장 센
     인자의 명시도를 그대로 얹으므로 `.btn`의 (0,1,0)이 `.lineup-primary`에
     더해진다. 위 규칙(0,1,0)을 순서와 무관하게 이긴다. */
  .lineup-primary:not(:has(.btn)){
    order:97;position:static;border-top:0;box-shadow:none;background:none;
    padding:0;margin:14px 0
  }
  /* 고정되지 않는 다섯. **이 상자도 카드다** — 껍데기(`.lineup-actions`)가
     `display:contents`로 걷히면서 그 테두리·배경·모서리가 사라졌고,
     `.lineup-primary`는 그것을 제 값으로 다시 선언했는데 이쪽은 안 했다.
     버튼 다섯이 카드 하나 없는 화면 바닥에 그냥 떠 있었다(품질 검토 B-6).
     `padding:13px 16px`은 `.lineup-primary`와 같은 값이다 — 두 무리의 버튼
     왼쪽 모서리가 x=32로 맞는다(예전에는 이 상자에 가로 안여백이 없어 16px과
     32px로 어긋나 있었다). `margin`은 예전 막대가 `app.css`에서 갖고 있던
     14px — 위로는 비고 상자와, 아래로는 고정 막대와 붙지 않는다.

     **트랙이 `minmax(0,1fr)`인 이유는 여기 두 열이 진짜로 쓰이기 때문이다.**
     `1fr`(=`minmax(auto,1fr)`)이면 `.btn{white-space:nowrap}`(app.css) 때문에
     '🖨️ 인쇄 미리보기'의 max-content 약 138px가 트랙 바닥이 된다. 375px에서
     칸이 167px라 아직 남지만 320px에서는 140px 대 140px로 딱 붙고, OS 글꼴
     배율을 올리면 그대로 넘쳐 **문서 전체**가 옆으로 흐른다 — 이 라운드가
     선수단 필터에서 이미 한 번 맞은 122px 결함과 같은 갈래다. */
  .lineup-secondary{
    order:98;margin:14px 0;padding:13px 16px;
    border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);
    display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:8px;align-items:center
  }
  /* 상태 셋은 제 줄을 통째로, '오더지 확정'은 전체 폭, 삭제는 맨 뒤 (§7-4).
     이 넷은 위 `.lineup-primary`의 두 트랙을 **가로지른다** — 그래서 그 격자에
     열로 배치되는 항목이 하나도 없다(위 주석). `.lineup-spacer`는 격자에서
     `margin-left:auto`가 아무 일도 안 하면서 칸 하나를 먹으므로 지운다.
     확정 버튼의 `order:1`은 `app.css`에 있다(마크업 이동을 되돌리는 값이라
     PC에서도 살아야 한다) — 여기서는 그 값이 저장 상태 **아래**를 뜻한다.
     (2026-08-24까지 이 목록에 `.lineup-dirty`가 넷째로 있었다. **어느
     템플릿에도 없는** 클래스였다(품질 검토 B-6) — `app.css`의 짝과
     `tests/test_mobile_shell.py`의 이름까지 한 번에 걷어냈다.) */
  .lineup-save-state,.lineup-readonly-note,.lineup-state{grid-column:1/-1}
  .lineup-spacer{display:none}
  .lineup-actions .btn-primary{grid-column:1/-1}
  .lineup-actions .btn-danger{grid-column:1/-1;order:2}

  /* 부제('11명 확정')는 패널 안 요약이 같은 말을 한다. 폰에서만 뺀다 —
     태블릿은 탭이 넓어 부제가 들어간다(아래 ≤1024 블록의 스텝퍼 절). */
  .st-sub{display:none}
  /* **⑤만은 예외다 (품질 검토 B-3).** 스펙 §7-2가 부제를 빼도 된다고 한 근거는
     "패널 안 요약이 같은 말을 한다"인데, 그 근거가 성립하지 않는 탭이 하나
     있다 — ⑤는 **패널을 열 수가 없는** 탭이다(`enabled=finished`,
     `game_view.py:538`). 눌러도 `workspace.js`가 클릭을 삼키고 아무 말도 하지
     않으며, 남은 신호는 `cursor:not-allowed`(터치에서는 뜻이 없다)와 옅어진
     색뿐이다. 왜 안 열리는지를 적고 있던 것이 `.st-sub`의
     `경기 후 활성화` · `취소된 경기`다.
     `enabled=True`인 ①②③④에는 안 걸리므로 부제가 되돌아오지 않는다 —
     `aria-disabled`가 붙는 탭은 ⑤뿐이다(같은 파일의 StepView 다섯).
     `white-space`를 푸는 것이 함께 필요하다: app.css:597이 `nowrap`이라
     375px에서 한 칸이 75px인데 `경기 후 활성화`는 82px다. `keep-all`이 낱말
     안에서 끊지 않게 해 `경기 후` / `활성화` 두 줄이 된다.
     명시도 (0,3,0)으로 바로 위 `.st-sub`(0,1,0)를 이긴다. */
  .step-tab[aria-disabled="true"] .st-sub{display:block;white-space:normal;word-break:keep-all}
  .st-name{font-size:.8rem}

  /* ── 스텝퍼: 폰에서만 세로로 쌓는다 (스펙 §7-2) ─────────────────────────
   * 격자 다섯 칸은 ≤1024 공통이지만 **쌓는 것은 폰뿐이다.** 태블릿(721~1024)은
   * 스펙 §7-2가 "가로 배치(아이콘 + 이름 + 부제)를 유지한다 — `min-width:0`만
   * 풀면 들어간다"고 못박았다. Task 7이 두 문장을 한 블록에 합치면서 태블릿
   * 탭까지 세 줄로 쌓였다.
   *
   * **이 규칙들을 아래 ≤1024 블록에 적지 말 것.** 그 블록이 파일에서 **뒤**라
   * 같은 속성을 거기서 다시 선언하면 명시도가 같아 폰에서도 그쪽이 이긴다 —
   * 여기 있는 속성은 ≤1024 블록이 한 번도 건드리지 않는 것들이어야 한다
   * (`test_the_stepper_stacks_only_on_a_phone`이 두 규칙의 속성 집합이 겹치지
   * 않는지 본다). */
  .step-tab{flex-direction:column;gap:4px;padding:8px 2px;
    text-align:center;align-items:center;border-right:0}

  /* ── ⑤ 선수 카드 (2026-08-25 A안, 스펙 §4-6) ────────────────────────────
   * 피커는 PC·태블릿에서 칩 행 바로 아래 인라인이고(app.css의
   * `.pa-picker` margin-top:12px · `.pa-picker-dim` display:none) **폰에서만**
   * 바텀시트다. 그래서 이 규칙들은 전부 이 블록 안에 있어야 한다.
   *
   * z-index 50인 이유: 탭 바가 30(이 파일 맨 위)이고 `#error-flash`가
   * 60(app.css)이다. 30 이하면 막대가 시트를 뚫고 올라오고, 60이면 배너와
   * 같아지는데 둘 다 position:fixed라 나중에 칠해진 쪽이 이긴다 — 오류 배너가
   * 시트 뒤로 숨을 수 있다. 계획서 초안이 적었던 값이 그 60이다.
   * 바깥 클릭은 `.pa-picker-dim`이 받는다(닫기).
   *
   * **판에 `max-height`·`overflow-y`가 없으면 작은 폰 가로에서 덫이 된다**
   * (2026-08-26 검토). 판은 내용만큼 462px인데, 667×375(아이폰 SE 2·3 가로,
   * 640×360 안드로이드)에서는 화면이 375px뿐이라 판 위쪽 87px이 잘려 나가고
   * ×(닫기)가 화면 밖으로 사라진다. 그때 판이 화면을 가장자리까지 덮으므로
   * `.pa-picker-dim`이 100% 가려져 Alpine의 `@click.outside`가 아예 안 뜨고,
   * `pick()`은 일부러 피커를 열어 두므로 결과를 골라도 안 닫힌다 — 하드웨어
   * 자판이 없으면 나갈 길이 새로 고침뿐이었다. 320×568 아이폰 사파리는 **세로**
   * 가용 높이가 450~460px이라 462px에 몇 px 차로 걸린다 — 그래서 가로 전용
   * 미디어 쿼리가 아니라 높이 자체를 묶는다. 판은 `inset:0`인 `.pa-picker`
   * 안의 `absolute`라 `100%`가 곧 화면 높이다.
   *
   * **`100%`가 아니라 `100% - 24px`인 이유**(2026-08-26 실측): 딱 `100%`로
   * 묶으면 667×375에서 판이 화면을 위아래로 꽉 채워, ×는 되살아나지만
   * `.pa-picker-dim`이 위쪽 둥근 모서리 두 곳(반지름 20px)의 **호 안쪽으로만**
   * 남는다. 바깥을 눌러 닫는 길이 사실상 사라지는 것이라, 닫는 길 셋(스펙 §4-4:
   * Esc · 바깥 클릭 · ×) 가운데 둘이 죽는다. 24px을 남기면 화면이 아무리 낮아도
   * 폭 전체짜리 어둠막 띠가 남는다. 판이 화면보다 낮은 흔한 경우
   * (375×667에서 462px)에는 이 값이 아무 일도 하지 않는다.
   *
   * `.record-split`는 **여기서 손대지 않는다.** app.css가 이미 1열이 기본이고
   * 2열은 `(min-width:1024.02px)`에서만 걸린다(Task 4 검토 I-4). 이 파일이
   * app.css보다 뒤에 실리므로 여기서 다시 선언하면 PC의 2열까지 되받는다. */
  .pa-picker{position:fixed;inset:0;bottom:0;z-index:50;margin:0}
  .pa-picker-dim{display:block;position:absolute;inset:0;background:rgba(2,6,23,.45)}
  .pa-picker-pane{position:absolute;left:0;right:0;bottom:0;max-height:calc(100% - 24px);overflow-y:auto;overscroll-behavior:contain;border:0;border-radius:20px 20px 0 0;padding:14px 14px calc(18px + env(safe-area-inset-bottom));background:var(--surface);box-shadow:var(--shadow)}
  .pa-picker-grid{grid-template-columns:repeat(3,minmax(0,1fr))}
  .pa-pick{min-height:58px;font-size:1rem}
  /* 스테퍼는 3열. 한 칸이 좁아져도 되는 이유는 `.counter-row`를 세로로 쌓아
     `−`·값·`+`가 46px 폭 하나만 요구하기 때문이다 — 가로로 두면 46+숫자+46이
     한 칸에 안 들어가 375px에서 넘친다. */
  .counters,.counters.six{grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}
  .counter{padding:8px 8px 10px;align-items:center}
  .counter-row{flex-direction:column;gap:6px}
  .counter button{width:46px;height:46px}
  .counter output{font-size:1.8rem}
  .pa-chip{min-width:66px;min-height:54px}
  .pcard .sec,.pcard-head{padding:12px}
  /* 카드 머리의 한 줄 요약. 바로 아래 칩 행이 같은 말을 하고, 폰에서는 이것이
     이름을 둘째 줄로 밀어낸다. */
  .pcard-live{display:none}
  .score-card{padding:12px 12px 8px;gap:8px}
  /* 2.6rem(=41.6px)이면 44px 바닥을 깬다 — `line-height:1`이라 글자 크기가 곧
     칸 높이인 자리다(Task 4 검토 I-3). 3rem = 48px. */
  .score-digit{font-size:3rem}
  .score-step{width:44px;height:44px}
  .record-tally{padding:8px 12px 10px}

  /* ── 선수 폼 오버레이 (2026-08-27 A안 Task 8, 스펙 §8-2 · §8-4) ─────────
   * 판 자체의 기하는 아래 `(max-width:1024px)` 블록에 있다(태블릿도 같은
   * 전폭 시트라서다). 여기 둘은 **폰에서만** 다른 것들이다.
   *
   * ① 등번호 칸을 104px로 줄인다 — PC의 130px(app.css)은 375px 폭에서 이름
   *    칸을 두 글자마다 접는다. 세 자리 숫자에 필요한 것은 104px이다.
   *
   * ② **폼이 열리면 FAB를 숨긴다.** 오버레이가 화면을 덮으므로 겹치지는
   *    않지만, 알약의 그림자가 어둠막 뒤로 비친다(스펙 §8-4). `:has()`는 이
   *    파일이 이미 쓰고 있다(바로 위 `body.app:has(.page-head …)` 패딩 규칙).
   *
   *    **그 패딩 규칙은 일부러 안 건드린다.** 알약이 `display:none`이어도
   *    `:has()`는 여전히 맞으므로 바닥 68px이 남는데, 그 띠는 전폭·전높이
   *    시트 **뒤**라 화면에 나타나지 않는다. 규칙을 `:not(:has(…))`로 좁히면
   *    `test_the_extra_bottom_padding_matches_the_fabs_own_geometry`의 정규식
   *    (`:has(…)` 바로 뒤에서 여는 중괄호를 요구한다)이 그 규칙을 통째로 못 찾아,
   *    68px이 FAB 기하를 따라간다는 단언이 조용히 눈을 감는다. 보이지 않는
   *    여백보다 살아 있는 단언이 낫다. */
  .pf-grid{grid-template-columns:minmax(0,1fr) 104px}
  body.app:has(#player-form-slot > *) .page-head .btn-primary.btn-lg{display:none}
}
/* 폰 밖에서는 없다. 태블릿은 두 줄 헤더가, PC는 지금의 GNB가 맡는다(스펙 §12 ④).
 * `720.02px`인 이유: 폰 블록은 `max-width:720px`에서 닫히므로 `min-width:721px`을
 * 쓰면 720px < w < 721px 사이(확대·배율 조정 화면에서 실제로 생긴다)가 어느
 * 블록에도 안 걸려 탭 바가 꾸미지 않은 채로 그려진다. */
@media (min-width:720.02px){ .tabbar{display:none} }

/* ── 태블릿 721~1024px (스펙 §6) ───────────────────────────────────────────
 * 탭 바 없이 두 줄 헤더다(§12 ④: iPadOS는 탭 바를 위로 올렸고 Material은 레일을
 * 권한다 — 둘 다 웹에서 새 부품이라, PC와 같은 IA를 유지하는 두 줄을 택했다).
 * 지금의 "메뉴 1줄 + 사용자 블록이 2줄째 오른쪽에 홀로"는 flex-wrap이 **우연히**
 * 만든 모양이다. */
@media (min-width:720.02px) and (max-width:1024px){
  .gnb-inner{padding:0 20px;min-height:56px;row-gap:0}
  /* 메뉴가 둘째 줄을 통째로 쓰고, 항목이 같은 폭으로 한 줄을 채운다. */
  /* `repeat(auto-fit,minmax(0,1fr))`을 쓰지 않는다 — 자동 반복 횟수는 트랙의
     **최대** 크기가 확정일 때만 정해지는데 `1fr`은 확정이 아니라 최소(0px)로
     떨어지고, 그러면 몇 열이 되는지가 명세로 정해지지 않는다(UA가 정한다).
     메뉴 항목이 셋(비관리자)·넷(관리자)으로 갈리는 자리라 그 불확정을 두지
     않는다. `grid-auto-flow:column` + `grid-auto-columns:1fr`은 개수와 무관하게
     항목마다 같은 폭 한 열을 준다. */
  .gnb-nav{order:3;flex-basis:100%;display:grid;
    grid-auto-flow:column;grid-auto-columns:1fr;gap:0;
    border-top:1px solid var(--border)}
  .gnb-item{justify-content:center;min-height:48px}
  /* 이름 자체가 '내 계정' 링크다 — 힌트 글자까지 두면 첫 줄이 넘친다. */
  .gnb-user-hint{display:none}
  .wrap{padding:20px}
  /* 리스트 보기 압축 카드 — ≤720 블록의 사본. 두 벌인 이유는 그쪽 주석에. */
  .board-view.is-rows .game-card{display:flex;flex-direction:column;gap:8px;padding:13px 14px}
  .board-view.is-rows .gc-top{flex-direction:row;align-items:center}
  .board-view.is-rows .gc-steps{justify-content:flex-start}
}

/* ── 폰·태블릿 공통 터치 규칙 (스펙 §8) ────────────────────────────────────
 * PC(>1024px)는 지금의 36px 컨트롤을 그대로 쓴다 — 마우스는 44가 필요 없고,
 * 2026-08-23 요청의 경계가 "PC는 그대로"다.
 *
 * 목록은 **실측에서 나온 것만**이다(2026-08-23, 430/744/820/1024).
 * `.btn`(app.css)과 `.time-clear`는 이미 44라 여기 없다 — 스펙 §8 ③의 손
 * 목록이 그 둘을 잘못 실었다. 12px 하한의 권위는 `scripts/mobile_check.py`
 * (Task 8)의 getComputedStyle 순회다.
 */
@media (max-width:1024px){
  /* 선수단 상세는 ≤1024px에서 표 **아래**에 카드로 붙는다 (2026-08-27 A안 Task 4).
     PC의 340px 오른쪽 패널(app.css)은 태블릿 폭에서 표를 반으로 접는다. 여기서
     `display:block`이 필요한 이유는 `.roster-main`이 flex라서다 — `width:auto`만
     주면 `flex:none`인 채로 내용 폭만큼 옆에 그대로 선다.
     폰의 바텀 시트 기하는 **Task 5**가 ≤720 블록에 얹는다. */
  .roster-main{display:block}
  .detail-host{width:auto;position:static;margin-top:16px}
  /* ── 포지션 내비게이션은 폭마다 다른 몸을 쓴다 (2026-08-27 A안 Task 7) ──
     PC의 216px 사이드바는 태블릿 폭에서 표를 반으로 접고(위 `.detail-host`와
     같은 사정), 폰에서는 아예 설 자리가 없다 — 대신 카드 안의 접힌 보드가
     같은 데이터를 그린다. **둘은 서로의 폴백이라 이 두 줄이 한 쌍이다**:
     한 줄만 지우면 그 폭에서 포지션 필터가 통째로 사라지거나(칩 띠는 app.css가
     이미 지웠다) 같은 목록이 두 번 선다.
     `display`를 ≤720 블록에서 다시 선언하지 않는다 — 여기 하나로 두 폭을 다
     덮으므로, 거기 적으면 블록 충돌 감시에 걸리기만 하고 얻는 것이 없다. */
  .pos-board{display:block}
  .pos-side{display:none}
  /* 명단의 행을 여는 버튼. 손가락이 닿는 폭에서는 44×44다 — 폰에서는 위 ≤720
     블록이 이것을 두 줄을 걸친 행 전체로 늘리고(그쪽 주석), 태블릿에서는 표가
     아직 진짜 `<table>`이라 이름 칸 안의 보통 버튼이다. 그 상태의 실측이
     40.8×21.8이었다. **예외를 만들지 않는다** — `scripts/mobile_check.py`가
     `<button>`의 상자를 그대로 잰다. */
  .row-open{min-height:44px;min-width:44px}
  .plan-note-input{min-height:44px}
  .plan-add-sel,.plan-add-btn{min-height:44px}
  /* 워크스페이스 '← 경기 목록'은 21px이었다. `display:flex`도 min-height는 받는다
     (app.css:307의 `.ws-back`은 이미 flex다) — `inline-flex`로 바꾸는 진짜 이유는
     상자를 한 줄 전체 폭에서 글자 폭으로 줄이는 것이다. 폭 전체가 탭 타깃이면
     줄 오른쪽 빈 자리를 눌러도 반응해야 하는데 지금은 아무것도 없다. */
  .ws-back{display:inline-flex;min-height:44px}
  /* 카드 전체를 덮는 ::after가 실제 히트 영역이지만, 링크 자체도 44를 갖게 해서
     기하 검사에 예외를 만들지 않는다(스펙 §10 ④). */
  .gc-open{min-height:44px}

  /* 글자 12px 하한 (§8 ②). **`rem`은 루트 기준이라 1rem = 16px이다** — `font-size:15px`은
   * `body`에 있고(app.css:4-8) 루트에는 없다. 그래서 `.8rem` = 16 × .8 = **12.8px**로
   * 하한을 넘긴다(`.75rem`이면 정확히 12px, 하한에 여유 없이 붙는다). `.st-name`은 이미 .86이라 없다.
   * `.gnb-user-hint`는 여기 없다 — ≤720에서는 부모 `.gnb-user`가, 721~1024에서는
   * 자신이(위 태블릿 블록) 이미 `display:none`이라 이 구간 전체에서 한 번도
   * 보이지 않는다. 안 보이는 글자의 크기를 여기서 고쳐 봐야 아무 효과가 없다.
   *
   * `.slot-warn`(app.css:608)은 자기 `font-size`가 없다 — `lineup_editor.html`에서
   * `.slot-sub` 안에 얹혀 11.4px를 물려받는다. `.slot-warn`만 올리면 한 문장
   * 안에서 글자 크기가 갈린다(`#12 · 선발투수`는 12px, `· 소화 포지션 아님`은
   * 11.4px). `.slot-sub`도 함께 올려야 문장 전체가 같이 움직인다.
   *
   * **이 목록은 측정된 부분집합이다.** 2026-08-23 실측(430/744/820/1024)에서
   * 잡힌 것만 있다 — 검토가 더 찾아낸 것들은 일부러 안 넣었다. 12px 하한의
   * 권위는 Task 8의 `scripts/mobile_check.py`(getComputedStyle 순회)이고, 그
   * 스윕이 실제 전체 집합을 내놓으면 그때 이 목록을 완성한다.
   *
   * **자손 선택자를 쓸 자리에 홑 클래스를 쓰면 조용히 진다.** `.att-stat .k`는
   * app.css에서 명시도 (0,2,0)이다. 나중에 여기 그냥 `.k`(0,1,0)를 더해도 이
   * 파일이 app.css **뒤**에 실린다는 사실은 도움이 안 된다 — 파일 순서는 명시도가
   * 같을 때만 승부를 가른다. 목록 끝의 `.pa-chip small`·`.pa-pick small`을 홑
   * `small`이 아니라 자손 선택자로 적은 것도 같은 이유다(그쪽 주석). */
  .mini,.gc-dday,.ws-badge,.role-chip,.plan-note,.plan-note-input,
  .log-num,.memo-tag,.att-sub,.note,.log-table th,.slot-warn,.slot-sub,
  .admin-link-desc,.st-sub,.fn-badge,.fn-name,.plan-role,.sp-swap,
  .plan-add-sel,.plan-add-btn,.field-note,
  .pa-chip small,.pa-pick small{font-size:.8rem}
  /* `.fn-badge`부터 `.sp-swap`까지 넷은 2026-08-24 기하 스윕이 채운 나머지다(그때까지 이 목록은 손 실측의
     부분집합이었다 — 바로 위 주석의 약속대로 스윕이 전체 집합을 내놓았다).
     `.fn-badge` 11.2px(`.7rem`) · `.fn-name` 11.68px(`.73rem`) · `.plan-role`
     11.84px(`.74rem`)는 ③ 라인업에서 폭 일곱 전부 걸렸고, `.sp-swap`
     11.52px(`.72rem`)는 교체 후보 시트를 **열어야** 그려져 정적 스윕이 못 본다 —
     그래서 스윕의 보고가 아니라 CSS를 읽어서 함께 올린다.
     넷 다 app.css에서 홑 클래스 (0,1,0)이고 더 센 font-size 선언이 없다
     (`.field-node.on .fn-name`·`.plan-row.is-starter .plan-role`·
     `.sp-pick:hover .sp-swap`은 색만 바꾼다) — 그래서 여기 홑 클래스로 적어도
     파일 순서로 이긴다. `.att-stat .k` 함정에 걸리는 것이 하나도 없다.

     마지막 둘(`.pa-chip small` · `.pa-pick small`, 2026-08-25 A안 Task 6)은
     ⑤ 선수 카드의 타석 칩·피커 속 작은 글자다. app.css에서 이미 `.8rem`이라
     지금은 값이 안 바뀐다 — **하한 목록에 이름을 올려 두는 것**이 목적이다.
     칩은 카드를 열어야 그려져 정적 스윕이 못 보므로(`.sp-swap`과 같은 사정),
     app.css에서 누가 `.7rem`으로 줄이면 아무도 못 본 채로 나간다. 둘 다
     자손 선택자 (0,1,1)로 적어 app.css의 같은 모양과 명시도를 맞춘다 —
     홑 `small`로 적으면 (0,0,1)이라 파일 순서와 무관하게 진다. */

  /* ── 알약은 제 내용만큼만 선다 (기하 스윕 2026-08-24) ─────────────────────
   * `app.css:262`의 `.mini{display:flex}`는 `flex:0 1 auto` + `white-space:normal`
   * 이라, **부모가 flex인 자리**에서 알약이 제 max-content 아래로 줄어들고
   * 한국어는 글자마다 줄바꿈 자리가 있어 세로로 쌓인다. 실측 셋:
   *   · ② 참석 `.att-toggle`(app.css:366, flex)의 「재활」 — 폭 일곱 전부 48.9px 2줄
   *   · 설정 `.admin-link`(app.css:74, flex)의 「승인 대기 1」 — 폰 폭 셋에서 2줄
   *   · 744~1024 선수단·회원 표의 「활동」/「활성」 2줄, 「비활동」 **69.4px 3줄**
   *     (표 자동 배치가 상태 열에 53px만 준다)
   * `.team-default .mini`가 이미 `inline-flex` + `nowrap`이라 멀쩡했다 — 그것이
   * 답이고, 여기서는 그 처방을 알약 전체로 넓힌다. 부모가 진짜 flex라
   * `flex:none`이 실제로 일한다(카드 칸의 `.log-table .mini`에서는 칸이
   * `display:block`이라 실효가 없었고, 그 사실이 그쪽 주석에 적혀 있다).
   *
   * (2026-08-27 A안 결정 ④로 포지션 분포 그래프가 사라지면서 `.chart-count .mini`
   * 예외도 함께 없어졌다. 그 자리를 대신하는 `.pos-tile`·`.pos-row` 속의 '얇음'
   * 알약은 app.css에서 `display`를 따로 선언하지 않으므로 이 규칙이 그대로 걸린다.)
   * ≤720의 `.log-table .mini`(0,2,0)는 이제 이 규칙에 포함되지만 지우지 않는다 —
   * 카드 칸에서 **블록이 띠가 되는** 다른 결함을 함께 막고 있고, 그 값을
   * `tests/test_mobile_shell.py`가 이름으로 찾는다. */
  .mini{display:inline-flex;white-space:nowrap;flex:none}

  /* ── 스텝퍼: 5칸 같은 폭 (스펙 §7-2) ─────────────────────────────────
   * 430px에서 2.5칸만 보였다(`min-width:150px`). 나머지 셋이 있다는 사실이
   * 화면에 나타나지 않아서, 사용자는 ④⑤가 없다고 읽는다.
   * `role="tablist"`·로빙 tabindex·aria-disabled는 마크업이라 손대지 않는다.
   *
   * `1fr`은 `minmax(auto,1fr)`이라 자동 최소 크기가 트랙 바닥을 정한다 —
   * `.step-tab{min-width:0}`이 그것을 푼다(app.css의 `min-width:150px`도 함께
   * 덮는다). 태블릿은 여기까지다 — 스펙 §7-2: "태블릿도 `repeat(5,1fr)` 격자로
   * 하되 **가로 배치(아이콘 + 이름 + 부제)를 유지한다** — `min-width:0`만 풀면
   * 들어간다." 세로로 쌓는 것·간격·안여백·가운데 정렬·칸막이 제거는 전부 위 폰
   * 블록에 있다. 그것들을 여기로 옮기면 태블릿 탭이 세 줄로 쌓인다.
   *
   * `.st-no`는 24px다(§7-2). app.css의 26px를 폰·태블릿 양쪽에서 줄인다 —
   * 다섯 칸으로 좁아진 탭에서 배지가 이름을 밀어낸다. */
  .stepper{display:grid;grid-template-columns:repeat(5,1fr);overflow:visible}
  .step-tab{min-width:0;min-height:56px}
  .st-no{width:24px;height:24px}
  .ws-top{padding:12px 14px}
  .ws-title{font-size:1.15rem}

  /* ── 선수 폼은 폰·태블릿에서 **전폭 전높이 시트**다 (스펙 §8-1) ─────────
   * PC의 620px 가운데 판(app.css)을 여기서 화면 전체로 편다. 태블릿까지 함께
   * 거는 것은 스펙 §8-1의 표가 그렇게 갈라 놓았기 때문이다 — 744px에서
   * 가운데 판을 띄우면 폼 하나를 고치려고 화면의 3할을 어둠막에 준다.
   *
   * **판의 `max-height`를 여기서 푸는 것이 안전한 이유**: `height:100%`가
   * 대신 들어간다. host가 `inset:0`인 고정 상자라 그 100%가 곧 화면 높이이고,
   * 판은 세로 flex라 머리·발이 `flex:none`으로 남고 몸만 줄어든다 — 667×375
   * (작은 폰 가로)에서도 ×와 저장 버튼이 화면 안이다. `overflow-y`와
   * `overscroll-behavior`는 app.css의 값이 그대로 산다(여기서 안 건드린다).
   *
   * 발은 `position:sticky`다. 판이 화면보다 높아질 수 있는 유일한 경로는
   * 브라우저가 `height:100%`를 늦게 계산하는 순간(iOS 주소창 접힘)인데, 그때
   * 몸의 스크롤이 아니라 **판**이 스크롤되면 발이 아래로 밀려 나간다 —
   * sticky가 그 한 경우를 막는다. `bottom:0`은 판 안쪽 기준이라 탭 바(30)
   * 위에 뜨는 알약들의 기준선(`var(--tabbar-h) + …`)과 상관이 없다: 시트가
   * z-index 50으로 탭 바를 덮으므로 그 아래 자리를 비울 이유가 없다.
   * 안전 영역만 안여백에 더한다(홈 인디케이터). */
  #player-form-slot{padding:0}
  .sheet-pane{width:100%;height:100%;max-height:none;border:0;border-radius:0}
  .sheet-foot{position:sticky;bottom:0;padding-bottom:calc(12px + env(safe-area-inset-bottom))}
}

/* 누르면 즉시 반응한다 (§8 ⑥). 반응이 없으면 한 번 더 누르고, 참석 토글에서는
   그 두 번째 누름이 방금 바꾼 상태를 되돌린다. */
@media (hover:none) and (max-width:1024px){
  .btn:active,.tab-item:active,.game-card:active,.att-item:active,.admin-link:active{
    transform:scale(.985);filter:brightness(.96)
  }
  .btn,.tab-item,.game-card,.att-item{-webkit-tap-highlight-color:transparent}
}
@media (prefers-reduced-motion:reduce) and (max-width:1024px){
  .btn:active,.tab-item:active,.game-card:active,.att-item:active,.admin-link:active{
    transform:none;filter:brightness(.96)
  }
}

/* ── 인쇄 ──────────────────────────────────────────────────────────────────
 * 탭 바는 종이에 나가지 않는다. **이 규칙은 print.css에 두지 않는다** —
 * print.css를 싣는 것은 `print/order_sheet.html` 하나뿐이고 그 템플릿은
 * base.html을 상속하지 않아 탭 바가 애초에 없다(order_sheet.html:10 주석).
 * 브라우저에서 평범한 페이지를 인쇄할 때 탭 바가 나가는 것이 실제 문제이고,
 * 그 페이지가 싣는 것은 이 파일이다. `app.css:161`의 .site-footer와 같은 자리.
 */
@media print { .tabbar { display: none; } }
