merge: feature/ai-sentiment-rubric 진행분 (주변 안내 정본 반영, §8 인사이트) 반영

supporters/ 정본에 주변 안내(/stay·/en/stay)가 i18n 방식으로 들어왔다.
recoveryNotes 는 뷰 실데이터를 유지한다.
핸드오버 제목 날짜를 2026-09-12(토)로 맞추고, §8-1 과 §8-11 에 이번 통합에서
같은 사고를 되풀이한 기록과 재발 방지(머지 직전 브랜치 끝 재확인)를 남겼다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Haewon Kam 2026-09-12 15:20:46 +09:00
commit 0a0f64c29e
5 changed files with 208 additions and 5 deletions

View File

@ -1,4 +1,4 @@
# 다음 세션 핸드오버 · 2026-09-12 · INFINITH AI Discovery Product 빌드
# 세션 핸드오버 · 2026-09-12(토) · INFINITH AI Discovery Product 빌드
작성 2026-09-11 · 갱신 2026-09-12(토) · 작성자 Claude (haewon 세션) · 상태: §1 확인 완료, §4-1 완료
하위 문서: 서포터즈 사이트 빌드 절차 `~/orca/workspaces/INFINITH/Productize/docs/NEXT_SESSION_2026-09-12_PRODUCT_BUILD.md`, 자동 빌드 상세 `…/NEXT_SESSION_SUPPORTERS_AUTOBUILD_v3.md`, 자동 채점 `docs/AI_DISCOVERY_BUILD2_HANDOVER.md`, 회고 `docs/INFINITH_AI_Discovery_Retrospective_2026-09-08.md`.
@ -9,7 +9,7 @@
INFINITH AI Discovery Product 빌드 세션. 제품은 "병원 URL 하나 → AI 답변 준비도 진단(AEO/GEO v2) → 서포터즈 사이트 자동 빌드(글·영상·뉴스룸·회복 일정 플래너·오시는 길) → 병원 확인·재빌드 → 측정(질문뱅크·감성·GA4·크롤러 로그)" 파이프라인이다.
코드 위치: 프런트·스크립트·문서는 메인 체크아웃 ~/Claude/Agentic Marketing/INFINITH (브랜치 feature/ai-sentiment-rubric, 프로덕션 main → infinith-demo.vercel.app),
워커·템플릿·큐는 워크트리 ~/orca/workspaces/INFINITH/Productize (브랜치 feature/supporters-autobuild), 랜딩 리드 CTA 는 브랜치 feature/discovery-url-cta(미머지).
먼저 docs/NEXT_SESSION_2026-09-12_AI_DISCOVERY_PRODUCT.md 를 읽고, §1 표의 "확인" 칸을 git log·Supabase 로 실제 확인한 뒤, §5 결정 대기의 답을 받고 §4 순서대로 진행한다.
먼저 docs/NEXT_SESSION_2026-09-12_AI_DISCOVERY_PRODUCT.md 를 읽는다(§8 인사이트 포함). 시작 절차: (1) 두 저장소 git status 와 .vercel 연결을 확인하고 다른 세션이 열려 있지 않은지 haewon 에게 묻는다 (2) §1 표의 "확인" 칸을 git log·Supabase 로 실제 확인한다 (3) §5 결정 대기의 답을 받는다 (4) §4 순서대로 진행하되 1번 브랜치 통합을 먼저 끝낸다. 한 세션은 한 저장소·한 배포 대상만 다룬다.
원칙: 근거 없는 사실은 생성하지 않는다("확인 대기"). 사람 게이트(의학 검토·게시 승인·이미지 육안·채널 확인·구조 기준선)는 자동화하지 않는다. 언급률·점수는 "스키마를 넣으면 인용된다"고 약속하지 않는다(정직성 규약).
고객 화면에 운영자 어휘 금지. 존댓말, 엠대시 금지(커밋 메시지 포함), 은유 금지, 흰 배경 위 연보라 텍스트 금지(인디고 #1D0024). INFINITH 리포트 PDF 는 PPTX 덱(infinith-deck 스킬)으로 만든 뒤 변환한다.
클라이언트 격리: INFINITH 문서·코드에 다른 서비스 이름을 쓰지 않는다. 산출 전 grep.
@ -39,7 +39,7 @@ INFINITH AI Discovery Product 빌드 세션. 제품은 "병원 URL 하나 → AI
| 6 측정 | 질문뱅크 언급률(2엔진), 감성, GA4 AI 유입, AI 크롤러 히트, 공식 vs 서포터즈 재채점 | `scripts/run_question_bank_openai.py`·`merge_qb_results.py`·`sentiment_qb.py`·`ga4_ai_traffic.py`, `data/ai_channels.json`, 리포트 Integrity 절 "측정 가능 범위" | 메인(QB·감성) + Productize(GA4) | 스크립트 단위. 리포트 카드 자동 갱신 없음. GA4 는 GCP 계정 대기 |
| 7 계약·가격 | 상담 기반 계약 | 가격 문서 `docs/INFINITH_가격정책_사업부보고서_v1.1.docx`(2026-04, 이전 제품 INSIGHT/INTELLIGENCE 티어), AI Discovery 가격 제안은 `docs/INFINITH_vs_FrostAI_비교_및_가격제안_2026-09-08.html` | 메인 | **AI Discovery 전용 티어 미확정** |
2026-09-10~11 추가: 회복 일정 플래너(설계 v0.2 §13, 관광공사 데이터 합침), 오시는 길 지도·길찾기, 전역 언어 전환·영어 홈(뷰), 모바일 헤더. 전부 템플릿·워커에 통합됨.
2026-09-10~11 추가: 회복 일정 플래너 `/plan`(설계 v0.2 §13, 관광공사 데이터 합침), 주변 안내 `/stay`(장소·축제·동선 지도·날씨, 플래너와 별개 페이지로 복원), 오시는 길 지도·길찾기, 전역 언어 전환·영어 홈(뷰), 모바일 헤더, 발행(색인) 단계. 전부 템플릿·워커에 통합됨.
## 2. 코드·브랜치·배포 지도
@ -70,6 +70,7 @@ INFINITH AI Discovery Product 빌드 세션. 제품은 "병원 URL 하나 → AI
- 진단 결과가 코드 파일(`discovery_<id>.ts`)이라 병원마다 프런트 배포가 필요하다. 런타임 데이터(Supabase 또는 JSON 스토리지)로 옮겨야 "URL 입력 → 리포트 링크"가 된다.
- 측정 지표가 리포트 카드에 자동으로 들어가지 않는다(스크립트 산출물을 사람이 옮긴다).
- 워커가 Mac 에 묶여 있다. 규칙표 잠정값·큐레이션 장소·의료진 후보·이미지·채널 12건은 사람 확인 대기.
- 색인 작업의 사람 몫(네이버 등록·GSC 소유자·도메인·법무)이 체크리스트 파일로만 나오고, 담당자에게 알림·요청하는 UI 가 아직 없다(§4-9).
- AI Discovery 가격·계약 조건 미확정. 랜딩 FAQ 의 해지·환불 항목 삭제 상태.
## 4. Product 빌드 순서 (권장)
@ -84,6 +85,7 @@ INFINITH AI Discovery Product 빌드 세션. 제품은 "병원 URL 하나 → AI
6. **운영 이관**: Mac launchd 폴러 → Trigger.dev(또는 동등)로. 키·비용 로그(`status.json` costUsd)·실패 알림.
7. **가격·계약**: AI Discovery 티어(진단만 / 진단+서포터즈 / +측정·관리) 확정, 랜딩 FAQ·계약서·해지 조건. FrostAI 비교 문서의 제안을 출발점으로.
8. **다음 병원**: 톡스앤필·차앤박(피부과), 레지스트리 73곳 중 회귀 병원. 한 화면 비교 산출물은 만들지 않는다.
9. **발행(색인) 단계**: "서포터즈 사이트 완료"의 정의는 배포가 아니라 색인 작업과 사람 체크리스트까지 끝난 상태다(haewon 결정 2026-09-11). 워커 `publish` 단계(`supporters/scripts/publish_site.mjs`)가 자동으로 하는 것: 라이브 확인(홈·robots·sitemap·noindex 헤더), IndexNow 로 사이트맵 URL 전부 알림(빙·네이버 공용, 색인 허용 빌드일 때만), Google Search Console 사이트맵 제출과 URL 색인 상태 조회(서비스 계정 `GOOGLE_SERVICE_ACCOUNT_JSON` 있을 때), 검색엔진 소유 확인 메타 태그(site.json `googleSiteVerification`·`naverSiteVerification`), IndexNow 키 파일(`public/<key>.txt`) 자동 생성, `--indexable` 로 noindex 헤더 제거·승인 글만 발행, `--domain` 으로 정식 도메인 연결. 사람에게 남기는 것(`~/supporters-builds/<clinic>/publish-checklist.md`, `status.humanTasks`): 네이버 서치어드바이저 등록·소유 확인·사이트맵/RSS 제출(공개 API 없음), Google Search Console 소유자 추가(첫 1회), 정식 도메인 결정, 색인 허용 전환 전 법무 3건·게시 승인, 병원 자산에서 백링크·sameAs, 네이버 블로그 재작성 트랙. 하지 않는 것: Google Indexing API(채용·라이브 방송 전용), 색인 보장 약속. 제품에서는 이 체크리스트를 확인 패널(`ClinicInputsPanel`) 또는 운영자 알림으로 사람에게 요청하게 만드는 것이 다음 일이다.
## 5. haewon 결정 대기
@ -122,6 +124,7 @@ INFINITH AI Discovery Product 빌드 세션. 제품은 "병원 URL 하나 → AI
## 6. 함정 (제품 관점)
- **한 Vercel 프로젝트에 두 폴더**: 2026-09-11 Productize/supporters 와 메인 supporters 가 같은 프로젝트에 배포해 `/plan` 이 404 됐다. 지금은 Productize 쪽 연결을 껐다. 배포는 병원 폴더와 메인 supporters 에서만.
- **페이지를 덮어쓰지 않는다**: 플래너 통합 때 `/stay`(주변 안내)를 `/plan` 으로 리다이렉트해 세 사이트에서 주변 안내가 사라졌다(09-11 17시 복원). 새 기능이 기존 페이지와 '같은 주제'로 보여도 haewon 확인 전에는 기존 페이지를 지우거나 리다이렉트하지 않는다.
- **세 브랜치 분산**: 같은 파일(`supporters/`)이 두 브랜치에서 다르게 진화했다. 머지 전에 "템플릿이 정본"을 정하고 뷰 샘플을 옮긴다.
- **진단 결과가 코드**: `discovery_<id>.ts` 커밋 방식은 병원 수가 늘면 배포 병목이다(§4-3).
- **게이트는 고객 화면 전체를 본다**: 운영 어휘("실측"·"API 집계"·"임베드")가 데이터 값을 타고 화면에 나가면 빌드가 막힌다. 운영 기록은 데이터에만.
@ -140,3 +143,20 @@ INFINITH AI Discovery Product 빌드 세션. 제품은 "병원 URL 하나 → AI
- 측정: `scripts/run_question_bank_openai.py`, `scripts/sentiment_qb.py`, `docs/AI_ANSWER_SENTIMENT_RUBRIC_v0.1.md`, Productize `scripts/ga4_ai_traffic.py`, `data/ai_channels.json`
- 가격·경쟁: `docs/INFINITH_vs_FrostAI_비교_및_가격제안_2026-09-08.html`, `docs/INFINITH_가격정책_사업부보고서_v1.1.docx`
- PRD: `docs/prd/INFINITH_AI-Discovery_PRD_20260828_v02.md`, `docs/prd/INFINITH_Global_Medical_Hub_PRD_v0.1.html`(Productize 에 v0.2), `docs/prd/INFINITH_Dermatology_Supporters_PRD_v0.1.html`
## 8. 이번 이틀(09-10~11)의 인사이트 · 다음 개발에 그대로 적용할 것
1. **한 세션 = 한 저장소 = 한 배포 대상.** 두 세션이 같은 Vercel 프로젝트와 같은 `supporters/` 폴더를 다른 브랜치에서 건드려 `/plan` 404 와 주변 안내 실종이 났다. 세션 시작 때 `git status`·`.vercel/project.json`·열린 세션 여부를 확인하고, 그 세션이 맡지 않은 자산은 손대지 않는다. 브랜치 통합(§4-1)이 이 문제의 근본 해결이므로 첫 일로 했다. **2026-09-12 완료.**
- 다만 통합 중에 같은 사고를 한 번 더 냈다. 머지 기준으로 삼은 브랜치 끝이 세션 도중 9개 커밋만큼 앞서 있었고(주변 안내 복원·워커 publish 단계), 그것을 모르고 올린 첫 머지가 둘을 떨어뜨렸다. 푸시 전에 발견해 되돌렸다.
- **머지 직전에 브랜치 끝을 다시 읽는다.** 세션 시작 때 읽은 `git rev-parse`/`git log` 값은 머지 시점에 낡아 있을 수 있다. 다른 워크트리가 열려 있으면 특히 그렇다. 머지 뒤에는 `git rev-list --count main..<branch>` 가 0 인지 확인한다.
2. **"완료"를 산출물이 아니라 고객 결과로 정의하면 자동·사람 경계가 드러난다.** 서포터즈 사이트 완료 = 색인 작업 + 사람 체크리스트로 바꾸자 IndexNow·GSC 는 자동, 네이버 등록·소유자 추가는 사람으로 갈렸고 워커 단계가 그 경계를 코드로 가졌다. 진단(사람 판정 4항목), 병원 확인(재빌드 트리거)도 같은 방식으로 "완료 정의 → 자동 부분 → 사람 요청"을 명시한다.
3. **병원 원문은 인용 전에 걸러야 한다.** 원문 32문장 중 14건이 홍보문·정형 고지·표 조각이었다. 원문을 데이터로 쓰는 모든 경로(recoveryNotes, OCR, 뉴스 제목)에 화이트리스트 필터와 "제외된 문장 목록 눈으로 보기"를 둔다. 필터 없는 "병원 안내 기준" 배지는 의료광고 위반이다.
4. **게이트는 데이터 값도 본다.** 좌표 출처의 "실측"이 화면에 새어 나가 빌드가 막혔다. 데이터 필드마다 고객 노출 여부를 정하고 운영 기록(source·coordSource·note)은 렌더하지 않는다.
5. **규칙표를 먼저 쓰면 코드·테스트가 따라온다.** 설계 v0.2 §13 의 status(clinic/provisional)·출처 필수 규칙이 그대로 procedures.json 스키마와 게이트 테스트가 됐다. 새 기능은 데이터 규칙표부터 문서화한다.
6. **관광공사 데이터는 공급이지 수요가 아니다.** 선호는 화면에서 직접 묻고, 큐레이션이 1순위, 외부 데이터는 보강(2순위)이라는 우선순위를 점수로 코드화했다. 외부 API 를 붙일 때마다 같은 질문(공급인가 수요인가, 누가 1순위인가)을 먼저 한다.
7. **디자인 규칙은 텍스트가 아니라 리듬이다.** "흰 카드만 쌓인 페이지"는 홈·영상 페이지의 섹션 리듬(다크 히어로+글래스 통계 → 틴트 → 다크 → 라이트 → 틴트 → 다크)을 안 따라서 완성도가 낮아 보였다. 새 페이지는 기존 페이지 두 개의 구성을 먼저 분석하고 같은 리듬으로 짠다. 웜 그라디언트는 CTA 카드 한 장에만.
8. **전역 CSS 와 컴포넌트 클래스는 접두어로 분리한다.** `.kpi` 충돌로 그라디언트가 되살아났다. 컴포넌트 클래스는 `plan-`·`vm-` 처럼 접두어를 쓰고, 새 클래스는 전역 CSS 에 grep 한다.
9. **모바일 검증은 하네스로.** 헤드리스 크롬 창 최소 폭 때문에 400px 캡처가 틀렸고, 실제 문제(헤더 겹침)는 아이프레임 하네스에서만 보였다. 8페이지×3사이트 그리드 캡처를 배포 전 기본 절차로 둔다.
10. **언어 전환은 전역 하나.** 페이지별 토글은 사용자 관점에서 틀렸다. 영어 홈이 없는 사이트는 영어 입구(/en/plan)를 정해야 하고, 템플릿에 영어 홈을 넣을지는 §5-3 결정에 달렸다.
11. **기존 페이지는 haewon 확인 전에 지우거나 리다이렉트하지 않는다.** 새 기능이 기존 페이지와 같은 주제로 보여도 병존이 기본이다(주변 안내 `/stay` 와 회복 일정 `/plan`). **2026-09-12 재발.** 브랜치 통합 머지가 `/stay` 를 떨어뜨렸다. 페이지를 지우려는 의도가 없어도 머지 기준을 잘못 잡으면 같은 결과가 난다. 통합 뒤 페이지 목록을 이전 배포와 대조한다.
12. **제품 병목은 세 가지로 좁혀졌다.** 진단 결과가 코드 파일, 리드→큐 미연결, 브랜치 3개 분산. 이 셋을 풀어야 "URL 입력 → 리포트 링크 → 서포터즈 미리보기"가 사람 손 없이 이어진다.

View File

@ -16,6 +16,7 @@
"a",
"a",
"a",
"a",
"a.lang-switch",
"a.cta",
"main",
@ -287,6 +288,7 @@
"a",
"a",
"a",
"a",
"a.lang-switch",
"a.cta",
"div.backbar-wrap",
@ -515,6 +517,7 @@
"a",
"a",
"a",
"a",
"a.lang-switch",
"a.cta",
"div.backbar-wrap",

View File

@ -75,6 +75,7 @@ document.addEventListener('click',function(e){var a=e.target.closest&&e.target.c
<a href="/posts">{u.nav_posts}</a>
<a href="/videos">{u.nav_videos}</a>
<a href="/newsroom">{u.nav_news}</a>
<a href="/stay">{u.nav_stay}</a>
<a href="/visit">{u.nav_visit}</a>
<a href="/plan">{u.nav_plan}</a>
<a href="/about">{u.nav_about}</a>
@ -82,6 +83,7 @@ document.addEventListener('click',function(e){var a=e.target.closest&&e.target.c
) : (
<>
<a href="/en">{u.nav_home}</a>
<a href="/en/stay">{u.nav_stay}</a>
<a href="/en/plan">{u.nav_plan}</a>
<a href="/en#visit">{u.nav_visit}</a>
<a href="/en#clinic">{u.nav_clinic}</a>

View File

@ -6,6 +6,7 @@ export type Lang = 'ko' | 'en';
const PAIRS: [string, string][] = [
['/', '/en'],
['/plan', '/en/plan'],
['/stay', '/en/stay'],
];
export function switchHref(pathname: string, to: Lang): string {
const p = pathname.replace(/\/index\.html$/, '/').replace(/\.html$/, '').replace(/\/$/, '') || '/';
@ -21,7 +22,7 @@ export const UI: Record<Lang, Record<string, string>> = {
ko: {
sample: '샘플 사이트입니다. 정식 공개 전입니다.',
brand: '서포터즈',
nav_posts: '상담 전 질문', nav_videos: '의료진 영상', nav_news: '뉴스룸', nav_visit: '방문 안내', nav_plan: '회복 일정', nav_about: '매체 소개',
nav_posts: '상담 전 질문', nav_videos: '의료진 영상', nav_news: '뉴스룸', nav_stay: '주변 안내', nav_visit: '방문 안내', nav_plan: '회복 일정', nav_about: '매체 소개',
cta: '상담 예약',
back: '이전 페이지', backAria: '이전 페이지로 돌아가기', totop: '맨 위로',
switchLabel: 'EN', switchAria: 'Switch to English',
@ -33,7 +34,7 @@ export const UI: Record<Lang, Record<string, string>> = {
en: {
sample: 'Sample site. Not yet public.',
brand: 'Supporters',
nav_home: 'Home', nav_plan: 'Recovery Planner', nav_visit: 'Getting Here', nav_clinic: 'Clinic Facts', nav_korean: '한국어 전체 보기',
nav_home: 'Home', nav_stay: 'Nearby', nav_plan: 'Recovery Planner', nav_visit: 'Getting Here', nav_clinic: 'Clinic Facts', nav_korean: '한국어 전체 보기',
cta: 'Book a consultation',
back: 'Back', backAria: 'Go back to the previous page', totop: 'Back to top',
switchLabel: 'KO', switchAria: '한국어로 보기',

View File

@ -280,3 +280,180 @@ td { color: var(--slate-700); }
.site-header .nav a { margin-left: 0; flex: none; font-size: 0.9rem; }
}
/* ── /en/stay ── */
.stay { padding-bottom: 4rem; }
.stay .lede { color: var(--slate-600, #475569); margin-top: 0.6rem; }
.stay section { margin-top: 3.4rem; }
.stay .sec-head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
.stay .sec-head h2 { margin: 0; }
.stay .upd { font-size: 0.8rem; color: var(--slate-500, #64748B); }
.stay .sub { color: var(--slate-600, #475569); margin: 0.4rem 0 1rem; }
.stay .src { font-size: 0.84rem; color: var(--slate-500, #64748B); margin-top: 1rem; }
.stay .chips { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0.2rem 0 1.6rem; }
.stay .chip {
border: 1px solid var(--line, #E2E8F0); background: #fff; border-radius: 999px;
padding: 0.36rem 0.85rem; font: inherit; font-size: 0.86rem; color: var(--slate-600, #475569);
cursor: pointer; transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.stay .chip b { font-weight: 700; opacity: 0.55; margin-left: 0.25em; }
.stay .chip:hover { border-color: var(--primary-900, #0A1128); }
.stay .chip.on { background: var(--primary-900, #0A1128); border-color: var(--primary-900, #0A1128); color: #fff; }
.stay .chip.on b { opacity: 0.7; }
.stay .grp { margin-bottom: 2.2rem; }
.stay .grp h3 { margin: 0 0 0.9rem; }
.stay .grp h3 small { font-weight: 400; font-size: 0.82rem; color: var(--slate-500, #64748B); margin-left: 0.45em; }
.stay .cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(248px, 1fr)); gap: 1rem; }
.stay .card {
background: #fff; border: 1px solid var(--line, #E2E8F0); border-radius: 14px;
overflow: hidden; display: flex; flex-direction: column;
box-shadow: 3px 4px 12px rgba(0, 0, 0, 0.05);
}
.stay .card .thumb { position: relative; height: 148px; background: #EEF1F6; display: flex; align-items: center; justify-content: center; }
.stay .card .thumb img { width: 100%; height: 148px; object-fit: cover; display: block; }
.stay .card .nophoto {
font-size: 1.02rem; font-weight: 700; color: var(--slate-500, #64748B);
padding: 0 1rem; text-align: center; line-height: 1.35; word-break: keep-all;
}
.stay .card .badge {
position: absolute; left: 0.6rem; bottom: 0.6rem;
background: rgba(10, 17, 40, 0.82); color: #fff; border-radius: 6px;
padding: 0.18rem 0.45rem; font-size: 0.74rem; font-weight: 600;
}
.stay .card .badge em { font-style: normal; opacity: 0.7; margin-left: 0.25em; }
.stay .card .badge.mono { letter-spacing: 0.06em; }
.stay .card .body { padding: 0.85rem 0.95rem 1rem; display: flex; flex-direction: column; flex: 1; }
.stay .card h4 { margin: 0 0 0.35rem; font-size: 0.98rem; }
.stay .card .when { font-size: 0.8rem; color: var(--slate-600, #475569); margin: 0 0 0.4rem; }
.stay .card .desc { font-size: 0.86rem; color: var(--slate-600, #475569); margin: 0 0 0.5rem; }
.stay .card .addr { font-size: 0.78rem; color: var(--slate-500, #64748B); margin: 0 0 0.6rem; }
.stay .card .go { font-size: 0.82rem; margin-top: auto; }
.stay .route { border: 1px solid var(--line, #E2E8F0); border-radius: 16px; padding: 1.3rem 1.4rem; margin-bottom: 1.4rem; background: #fff; }
.stay .route-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 1rem; flex-wrap: wrap; }
.stay .route-head h3 { margin: 0; }
.stay .route .who { font-size: 0.84rem; color: var(--slate-500, #64748B); margin: 0.15rem 0 0; }
.stay .route .clock { font-size: 0.86rem; font-weight: 600; color: var(--primary-900, #0A1128); white-space: nowrap; }
.stay .route .blurb { color: var(--slate-600, #475569); margin: 0.7rem 0 1rem; }
.stay .route .map { width: 100%; height: auto; display: block; border-radius: 12px; }
.stay .route .maphint { font-size: 0.76rem; color: var(--slate-500, #64748B); margin: 0.4rem 0 1rem; }
.stay .timeline { list-style: none; padding: 0; margin: 0; }
.stay .timeline .move { font-size: 0.8rem; color: var(--slate-500, #64748B); margin: 0.35rem 0 0.35rem 1.05rem; }
.stay .timeline .stop { display: flex; gap: 0.8rem; align-items: flex-start; }
.stay .timeline .n {
flex: none; width: 26px; height: 26px; border-radius: 50%;
background: var(--primary-900, #0A1128); color: #fff;
font-size: 0.8rem; font-weight: 700; display: grid; place-items: center; margin-top: 0.1rem;
}
.stay .timeline .stop-t { font-size: 0.8rem; color: var(--slate-500, #64748B); }
.stay .timeline h4 { margin: 0.1rem 0 0.2rem; font-size: 0.98rem; }
.stay .timeline p { margin: 0; font-size: 0.86rem; color: var(--slate-600, #475569); }
.stay .botnote { margin-top: 1.6rem; }
/* 시술 후 관리 안내(병원 원문 인용). 동선 카드(.route)·정차(.stop) 룩을 그대로 쓰고 접이식 동작만 더한다. 새 색·크기 없음. */
.stay details.recovery > summary.btn { list-style: none; cursor: pointer; }
.stay details.recovery > summary.btn::-webkit-details-marker { display: none; }
.stay details.recovery > summary.btn .hide { display: none; }
.stay details.recovery[open] > summary.btn .show { display: none; }
.stay details.recovery[open] > summary.btn .hide { display: inline; }
.stay details.recovery[open] > summary.btn { margin-bottom: 1.1rem; }
.stay details.recovery > .timeline { margin-top: 0.2rem; }
.stay .recovery .timeline > li { border-top: 1px solid var(--line, #E2E8F0); padding: 0.7rem 0; }
.stay .recovery .timeline > li:first-child { border-top: 0; padding-top: 0; }
.stay .recovery summary { cursor: pointer; list-style: none; }
.stay .recovery summary::-webkit-details-marker { display: none; }
.stay .recovery summary .stop { display: flex; }
.stay .recovery summary h4 { margin: 0.1rem 0 0; }
.stay .recovery details[open] summary h4 { text-decoration: underline; text-underline-offset: 0.2em; text-decoration-color: var(--slate-200, #E2E8F0); }
.stay .recovery details > ul { margin: 0.7rem 0 0.5rem 2.2rem; padding-left: 1.1rem; }
.stay .recovery details > ul li { font-size: 0.9rem; color: var(--slate-700, #334155); margin-bottom: 0.35rem; }
.stay .recovery details > p { margin: 0 0 0 2.2rem; font-size: 0.84rem; color: var(--slate-500, #64748B); }
@media (max-width: 640px) { .cards { grid-template-columns: 1fr; } }
/* 섹션 점프. 머리말 아래에 붙어 스크롤을 따라온다. 헤더(68px) 아래에 걸리게 top 을 맞춘다. */
.stay .jump {
position: sticky; top: 68px; z-index: 9;
display: flex; flex-wrap: wrap; gap: 0.45rem;
margin: 1.4rem 0 2.4rem;
padding: 0.62rem 0.68rem;
background: rgba(255, 255, 255, 0.58);
backdrop-filter: blur(16px) saturate(1.5);
-webkit-backdrop-filter: blur(16px) saturate(1.5);
border: 1px solid rgba(255, 255, 255, 0.6);
border-radius: 18px;
box-shadow: 0 8px 28px rgba(10, 17, 40, 0.09), inset 0 1px 0 rgba(255, 255, 255, 0.7);
}
.stay .jump a {
display: inline-flex; align-items: center; gap: 0.42rem;
padding: 0.46rem 0.9rem;
border-radius: 999px;
font-size: 0.86rem; font-weight: 600; letter-spacing: -0.005em;
color: var(--primary-900, #0A1128);
text-decoration: none; white-space: nowrap;
background: #fff;
border: 1px solid var(--line, #E2E8F0);
box-shadow: 0 1px 2px rgba(10, 17, 40, 0.06);
transition: transform 0.14s ease, box-shadow 0.14s ease, background 0.14s, border-color 0.14s, color 0.14s;
}
.stay .jump a:hover {
text-decoration: none;
transform: translateY(-1px);
border-color: #C5CBF5;
box-shadow: 0 4px 12px rgba(10, 17, 40, 0.12);
}
.stay .jump a:active { transform: translateY(0); box-shadow: 0 1px 2px rgba(10, 17, 40, 0.08); }
.stay .jump a b {
font-weight: 700; font-size: 0.72rem; line-height: 1;
padding: 0.2rem 0.4rem; border-radius: 999px;
background: #EFF0FF; color: #3A3F7C;
}
/* 지금 보고 있는 섹션 */
.stay .jump a.on {
background: linear-gradient(to right, #4F1DA1, #021341);
border-color: transparent; color: #fff;
box-shadow: 0 4px 14px rgba(79, 29, 161, 0.32);
}
.stay .jump a.on b { background: rgba(255, 255, 255, 0.22); color: #fff; }
/* 점프로 이동했을 때 제목이 스티키 바에 가리지 않게 */
.stay section, .stay .grp { scroll-margin-top: 152px; }
@media (max-width: 640px) {
.stay .jump { top: 60px; gap: 0.35rem; padding: 0.5rem; border-radius: 14px; }
.stay .jump a { padding: 0.4rem 0.72rem; font-size: 0.8rem; }
}
/* 필터 칩도 눌리는 것처럼 보이게 */
.stay .chip {
box-shadow: 0 1px 2px rgba(10, 17, 40, 0.05);
transition: transform 0.14s ease, box-shadow 0.14s ease, background 0.15s, color 0.15s, border-color 0.15s;
}
.stay .chip:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(10, 17, 40, 0.1); }
.stay .chip:active { transform: translateY(0); }
.stay .chip.on { box-shadow: 0 4px 14px rgba(10, 17, 40, 0.22); }
/* 오늘 날씨. 값을 못 받으면 hidden 이라 아예 그려지지 않는다. */
.stay .wx {
display: flex; align-items: center; justify-content: space-between;
gap: 1.2rem; flex-wrap: wrap;
margin-top: 1.6rem; padding: 1rem 1.2rem;
background: linear-gradient(to right, #fff3eb, #e4cfff, #f5f9ff);
border-radius: 16px;
}
.stay .wx-now { display: flex; align-items: center; gap: 0.9rem; }
.stay .wx-temp { font-family: 'Playfair Display', serif; font-size: 2.5rem; font-weight: 700; line-height: 1; color: var(--primary-900, #0A1128); }
.stay .wx-unit { font-size: 1.1rem; vertical-align: 0.9rem; margin-left: 0.06em; opacity: 0.6; }
.stay .wx-cond { font-weight: 700; color: var(--primary-900, #0A1128); }
.stay .wx-meta { font-size: 0.8rem; color: var(--slate-600, #475569); margin-top: 0.15rem; }
.stay .wx-note { margin: 0; font-size: 0.88rem; color: var(--slate-700, #334155); max-width: 30rem; }
/* 동선 지도. OSM 타일을 좌표로 깔고 그 위에 핀을 얹는다. 출처 표기는 OSM 정책상 필수다. */
.stay .route .map {
position: relative; width: 100%; max-width: 680px; height: 340px;
border-radius: 12px; overflow: hidden; background: #E8EDF3;
border: 1px solid var(--line, #E2E8F0);
}
.stay .route .map-tiles { position: absolute; inset: 0; }
.stay .route .map-tiles .tile { position: absolute; width: 256px; height: 256px; background-size: 256px 256px; background-repeat: no-repeat; }
.stay .route .map-pins { position: absolute; inset: 0; width: 100%; height: 100%; }
.stay .route .map-credit {
position: absolute; right: 0; bottom: 0;
background: rgba(255, 255, 255, 0.82); color: var(--slate-600, #475569);
font-size: 0.66rem; padding: 0.1rem 0.35rem; border-top-left-radius: 6px;
}