o2o-site-AEO/solution/site/scripts/mockup/inject.css
Mina Choi 872d00f3c4 [feat] solution: 미니 블로그·이용후기·예약요청 추가, /s/stay 목업·발행 사이트 UI 다수 수정
인앱 미니 블로그(AI 자동 포스트, 이메일 승인)·이용후기(즉시 게시)·예약 요청(메일 발송)을
새로 붙였고, 병행해서 /s/stay 목업과 발행 사이트 공통 렌더러(UnitsSection·FestivalSection·
LocalGuideSection·WeatherSection 등)의 UI 버그를 다수 고쳤다. 범위가 넓지만 한 주 분량
작업을 한 커밋으로 묶어 달라는 요청에 따라 하나로 묶는다.

- solution/backend: post/review/booking_request 라우터·서비스·CRUD 추가, 스케줄러에
  블로그 초안 생성(새벽 4:10)·발송(아침 9:00) cron 등록, 마이그레이션 4건 추가
- solution/frontend, admin/frontend: 생성된 API 클라이언트 갱신, 리뷰 모더레이션·
  블로그 글 관리 페이지 추가
- solution/site/src: 객실 상세+실시간예약(날짜선택·연락처 폼)을 모달로 통합, 축제·
  주변안내 카드 클릭 시 모달 전환, 후기 목록 카드 UI, 공용 Modal 컴포넌트 신설,
  날씨 문구 동기화 버그 수정(하늘줄·기온줄 한 타이머로), 시설·편의 가능/불가 아이콘
  색상 하이라이트, 헤더 메뉴 순서를 실제 섹션 순서에 맞춤, 하단 탭바 아이콘 정렬 버그
  (line-height) 수정, 추천일정 점선 연결+데스크톱 자동펼침/모바일 축소, 채널 라벨에
  크롤링 원문("NOL")이 새던 것을 bookingLabel() 로 교체
- solution/site/scripts/mockup: /s/stay 패치 스크립트·주입 CSS·JS 다수 수정, stay4~6
  빌드 스크립트 추가(다른 세션 작업)

테스트: solution/site `npx tsc --noEmit` 통과, `npx vitest run` 93 passed,
solution/backend `pytest tests/test_booking_request.py` 6 passed(로컬 DB 대상).
예약 요청 메일은 실제 발송까지 확인(place 66894a1b 소유자 이메일 누락을 DB에서 보정).
2026-09-18 09:03:18 +09:00

625 lines
27 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* /s/stay 목업 주입분 — 헤더 미니 플레이어 · 캐치프레이즈 전환.
★ 색·테두리·서체를 전부 템플릿 변수(--tpl-*)에서 가져온다. 값을 박으면 템플릿을 바꿀 때
플레이어만 옛 색으로 남는다.
★ 플레이어는 헤더 **안**이다. 하단 바는 모바일 탭바(fixed bottom:0 · 61px)를 덮었고,
상단 바는 헤더와 두 줄이 됐다. 헤더 안이면 화면에서 차지하는 줄이 늘지 않는다. */
/* ── 헤더에 꽂는 아이콘 세 개 ─────────────────────────────────────── */
#w4d-mini {
display: inline-flex;
align-items: center;
gap: 2px;
margin-right: 6px;
padding-right: 8px;
border-right: 1px solid var(--tpl-border, #bcb49e);
}
#w4d-mini button {
display: inline-flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
padding: 0;
color: var(--tpl-text, #1b1a15);
background: transparent;
border: 0;
border-radius: var(--tpl-radius, 0);
cursor: pointer;
opacity: 0.75;
}
#w4d-mini button:hover { opacity: 1; background: rgb(27 26 21 / 0.06); }
#w4d-mini svg { width: 16px; height: 16px; }
/* 지금 나오는 곡명. 좁은 화면에서는 헤더에 자리가 없어 접는다 —
실측(390px): 상호 115px + 오른쪽 묶음 94px 이라 남는 폭이 55px 뿐이다.
그 화면에서는 목록을 열면 재생 중인 줄에 곡명과 시간이 있다. */
#w4d-now {
max-width: 150px;
overflow: hidden;
font-size: 14px;
font-weight: 700;
white-space: nowrap;
text-overflow: ellipsis;
color: var(--tpl-accent, #bf2f1b);
padding-right: 2px;
}
#w4d-mini[data-playing='0'] #w4d-now { opacity: 0.85; font-weight: 400; }
@media (max-width: 860px) { #w4d-now { display: none; } }
/* 카세트 — 단추가 아니라 표지다. 이 자리가 무엇인지 한눈에 말해 준다. */
#w4d-tape {
display: inline-flex;
align-items: center;
padding: 0 4px 0 2px;
color: var(--tpl-text, #1b1a15);
opacity: 0.8;
}
#w4d-tape svg { width: 20px; height: 20px; }
#w4d-mini[data-playing='1'] #w4d-tape { color: var(--tpl-accent, #bf2f1b); opacity: 1; }
/* 재생 중에는 릴이 돈다 — 소리가 나고 있다는 것을 글자 없이 말한다. */
#w4d-mini[data-playing='1'] .w4d-reel {
transform-box: fill-box;
transform-origin: center;
animation: w4d-reel 2.6s linear infinite;
}
@keyframes w4d-reel { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .w4d-reel { animation: none !important; } }
/* 재생 중일 때만 강조색 — 헤더에 곡명을 안 두므로 색이 유일한 '지금 나온다' 표시다. */
#w4d-mini button[data-playing='1'] { color: var(--tpl-accent, #bf2f1b); opacity: 1; }
@media (max-width: 400px) {
#w4d-mini { margin-right: 2px; padding-right: 4px; }
#w4d-mini button { width: 27px; height: 27px; }
}
/* ── 목록 패널 (헤더 아래에 떠 있다) ──────────────────────────────
★ 좁은 화면에서는 좌우 8px 만 남기고 펼친다. 320px 고정이었을 때 390px 화면에서
제목이 두 줄로 접히고 시간이 잘렸다(2026-09-11 사장님: "모바일 대응이 안 되어 있음"). */
#w4d-panel {
position: fixed;
z-index: 45; /* 사이트 헤더가 z-40 이라 그 위 */
width: 344px;
max-width: calc(100vw - 16px);
max-height: min(64vh, 520px);
display: flex;
flex-direction: column;
overflow: hidden;
font-family: var(--tpl-font-body, serif);
color: var(--tpl-text, #1b1a15);
background: var(--tpl-card, #efe7d3);
border: var(--tpl-border-width, 2px) solid var(--tpl-primary, #1b1a15);
box-shadow: var(--tpl-shadow, 4px 4px 0 rgb(27 26 21 / 0.16));
}
#w4d-panel[hidden] { display: none; }
@media (max-width: 640px) { #w4d-panel { width: auto; } }
#w4d-panel .w4d-head {
display: flex;
align-items: center;
gap: 8px;
flex: 0 0 auto;
padding: 11px 8px 11px 14px;
border-bottom: var(--tpl-border-width, 2px) solid var(--tpl-primary, #1b1a15);
}
.w4d-head-t { font-size: 15px; font-weight: 700; letter-spacing: 0.06em; }
.w4d-head-n { flex: 1 1 auto; font-size: 14px; opacity: 0.5; }
#w4d-panel .w4d-head button {
display: inline-flex;
width: 28px;
height: 28px;
flex: 0 0 auto;
align-items: center;
justify-content: center;
color: inherit;
background: transparent;
border: 0;
cursor: pointer;
opacity: 0.6;
}
#w4d-panel .w4d-head button:hover { opacity: 1; }
#w4d-panel .w4d-head svg { width: 14px; height: 14px; }
#w4d-panel ol {
flex: 1 1 auto;
margin: 0;
padding: 4px 0;
overflow-y: auto;
list-style: none;
-webkit-overflow-scrolling: touch;
}
/* 한 줄 = 판 + 제목/가수 + 시간. 48px 아래로 내리지 않는다 — 손가락으로 누르는 자리다. */
.w4d-item {
display: flex;
width: 100%;
min-height: 52px;
align-items: center;
gap: 11px;
padding: 8px 14px;
color: inherit;
background: transparent;
border: 0;
font: inherit;
text-align: left;
cursor: pointer;
}
.w4d-item + .w4d-item { border-top: 1px dashed var(--tpl-border, #bcb49e); }
.w4d-item:hover { background: rgb(27 26 21 / 0.05); }
.w4d-item[aria-current='true'] { background: rgb(27 26 21 / 0.06); }
.w4d-item[aria-current='true'] .w4d-t { color: var(--tpl-accent, #bf2f1b); }
/* 판 자리. 재생 중인 줄에서는 판 대신 막대가 뛴다. */
.w4d-mark { position: relative; flex: 0 0 26px; height: 26px; }
.w4d-disc-sm {
position: absolute;
inset: 0;
border-radius: 50%;
background:
radial-gradient(circle at 50% 50%, var(--w4d-lbl, #bf2f1b) 0 34%, transparent 34%),
repeating-radial-gradient(circle at 50% 50%, rgb(255 255 255 / 0.12) 0 1px, transparent 1px 3px),
#14110f;
}
.w4d-disc-sm::after {
content: '';
position: absolute;
inset: 45%;
border-radius: 50%;
background: var(--tpl-card, #efe7d3);
}
.w4d-eq {
position: absolute;
inset: 0;
display: none;
align-items: flex-end;
justify-content: center;
gap: 3px;
padding-bottom: 4px;
}
.w4d-eq i {
width: 3px;
height: 6px;
background: var(--tpl-accent, #bf2f1b);
animation: w4d-eq 0.9s ease-in-out infinite;
}
.w4d-eq i:nth-child(2) { animation-delay: 0.18s; }
.w4d-eq i:nth-child(3) { animation-delay: 0.36s; }
@keyframes w4d-eq { 0%, 100% { height: 5px; } 50% { height: 17px; } }
.w4d-item[data-playing='1'] .w4d-disc-sm { display: none; }
.w4d-item[data-playing='1'] .w4d-eq { display: flex; }
@media (prefers-reduced-motion: reduce) { .w4d-eq i { animation: none; height: 11px; } }
.w4d-info { min-width: 0; flex: 1 1 auto; }
.w4d-t {
display: block;
overflow: hidden;
font-size: 15px;
font-weight: 700;
white-space: nowrap;
text-overflow: ellipsis;
}
.w4d-sub {
display: block;
margin-top: 2px;
overflow: hidden;
font-size: 14px;
white-space: nowrap;
text-overflow: ellipsis;
opacity: 0.85;
}
.w4d-time {
flex: 0 0 auto;
font-size: 14px;
opacity: 0.6;
font-variant-numeric: tabular-nums;
}
/* 가사 — 우리 곡만. 종이 위 인쇄물처럼 둔다. */
#w4d-lyrics-box {
padding: 10px 12px 14px;
border-top: 1px dashed var(--tpl-border, #bcb49e);
white-space: pre-line;
font-size: 15px;
line-height: 1.9;
}
#w4d-lyrics-box[hidden] { display: none; }
/* ★ 이용 정보 맨 아래 가로선을 지운다 (2026-09-10 사장님 지시).
<hr> 이 아니라 바로 뒤 '예약은 아래로 받습니다' 블록의 border-t 다 — 마지막 항목 밑에 선이
한 줄 더 그어져 빈 칸이 하나 더 있는 것처럼 보인다. 항목 자체는 그대로 둔다.
마크업을 고치면 하이드레이션이 되돌리므로 CSS 로 끈다. */
#root div.border-line.flex.flex-col.gap-3.border-t.pt-6 { border-top-width: 0; }
/* 사진 저작자 표시 — 출처 줄과 같은 크기, 한 단 더 옅게. */
.w4d-credit { margin-top: 2px; font-size: 14px; opacity: 0.85; }
/* ★ 모바일 헤더에서 전화 단추를 접는다 (2026-09-11 사장님: "저 전화 아이콘은 빼도 되잖아").
좁은 화면 헤더에 상호 + 플레이어 3개 + 전화 + 메뉴가 다 서서 난잡했다.
★ 전화가 사라지는 게 아니다 — 이 화면 폭에서는 **하단 탭바에 전화가 이미 있다**
(실측: NAV fixed bottom:0 · 위치 · 전화 · 네이버 예약). 같은 것이 두 번 서 있던 것이다.
★ 넓은 화면에서는 탭바가 없으므로 그대로 둔다. */
@media (max-width: 860px) {
#root header .shell > div:last-child > a[href^="tel:"] { display: none; }
}
/* 덧붙인 링크 — 본문 글자보다 눈에 띄되 강조색을 남발하지 않는다. */
.w4d-link {
color: color-mix(in oklab, var(--tpl-accent, #bf2f1b) 62%, var(--tpl-text, #1b1a15));
text-decoration: underline;
text-underline-offset: 3px;
white-space: nowrap;
}
/* ── 캐치프레이즈 등장 ────────────────────────────────────────────
★ 줄 전체를 페이드하지 않고 **단어가 마스크 안에서 올라온다.**
한 줄짜리 문장은 통째로 흐려지면 '바뀌었다'만 보이고 문장이 안 읽힌다 —
왼쪽부터 차례로 서면 눈이 첫 단어에서 읽기를 시작한다.
★ 나갈 때는 반대로 위로 빠지며 흐려진다. 들어오는 방향과 나가는 방향이 같으면
두 문장이 같은 자리에서 겹쳐 보인다.
★ 자리 흔들림: 처음 구워진 줄 높이를 min-height 로 박아 둔다(스크립트가 잰다).
문장 길이가 달라 한 줄↔두 줄이 오갈 때 히어로 아래가 튀는 것을 막는다. */
/* 대표 문구 아래에서 도는 줄 (2026-09-11). 대표 문구보다 한 단 작고 옅게 둔다 —
둘이 같은 크기·같은 농도면 어느 쪽이 숙소의 한 줄인지 안 읽힌다. */
.w4d-sub { margin-top: 0.5rem; font-size: var(--fs-sm, 0.9375rem); line-height: 1.6; opacity: 0.75; }
.w4d-line { display: block; }
.w4d-line--out {
opacity: 0;
transform: translateY(-5px);
filter: blur(3px);
transition: opacity 0.3s ease, transform 0.3s ease, filter 0.3s ease;
}
.w4d-line--in { opacity: 1; transform: none; filter: none; transition: opacity 0.3s ease, transform 0.3s ease, filter 0.3s ease; }
/* 단어 한 칸 = 마스크. 안쪽 글자가 아래에서 올라온다. */
.w4d-w { display: inline-block; overflow: hidden; vertical-align: bottom; }
.w4d-w > span {
display: inline-block;
transform: translateY(112%);
opacity: 0;
filter: blur(4px);
}
.w4d-line--in .w4d-w > span {
animation: w4d-rise 0.62s cubic-bezier(0.16, 0.84, 0.28, 1) forwards;
animation-delay: var(--w4d-d, 0ms);
}
@keyframes w4d-rise {
from { transform: translateY(112%); opacity: 0; filter: blur(4px); }
60% { filter: blur(0); }
to { transform: translateY(0); opacity: 1; filter: blur(0); }
}
@media (prefers-reduced-motion: reduce) {
.w4d-line--out, .w4d-line--in { transition: none; opacity: 1; transform: none; filter: none; }
.w4d-w > span { transform: none; opacity: 1; filter: none; animation: none; }
}
/* 가사 — 우리 곡만. 종이 위 인쇄물처럼 둔다. */
#w4d-lyrics-box {
max-width: 1120px;
margin: 0 auto;
padding: 12px 16px 16px;
border-top: 1px dashed var(--tpl-border, #bcb49e);
white-space: pre-line;
font-size: 15px;
line-height: 1.9;
max-height: 40vh;
overflow-y: auto;
}
#w4d-lyrics-box[hidden] { display: none; }
/*
* 일정 정거장 번호 — 제목 글자와 세로 중심을 맞춘다.
*
* 번호 동그라미는 inline-grid 라 **안에 든 숫자(10px)의 베이스라인**이 제목(14px)의
* 베이스라인에 붙는다. 숫자가 17px 상자 한가운데 있으니 동그라미 중심은 그만큼 아래로
* 내려가고, 렌더러가 얹은 translate-y-px 가 1px 더 내린다.
* 실측(1280·390 공통): 동그라미 중심이 제목 줄 중심보다 3.14px 아래.
* 2px 올리면 0.14px — 반올림해서 눈에 안 걸리는 자리다.
*/
#itinerary ol li span.inline-grid.rounded-full {
translate: 0 -2px;
transform: none;
}
/*
* ── 가요 다방 판꽂이 ──────────────────────────────────────────────────────
*
* ★ 되짚음 (2026-09-14 대표: "모바일 대응이 안 되어있는듯? 무슨 노래인지 모르잖아,
* 모바일에서는 hover가 없으니까"). 맞는 지적이고, **아래는 내가 만든 회귀였다.**
* 원래 렌더러(`SongsSection.tsx`)는 판 아래 곡명을 **항상** 보여준다 —
* `<span className="mt-2 block truncate text-[10px] ...">{song.title}</span>`,
* 숨기는 코드가 없다. 그런데 같은 날 앞선 지적("LP판이 많아져서 보기가 이상하잖아",
* 25곡이 `flex-wrap` 으로 3~9줄이 되던 문제)에 답하며 판을 겹쳐 꽂고 **곡명을
* hover 로만 띄우게** 고쳤다 — 데스크톱 벽은 없앴지만 손가락엔 hover 가 없으니
* 곡명이 아예 안 보이는 화면이 됐다. 데스크톱만 보고 고친 값이다.
* ★ 다시 짠다 — **곡명은 항상 보인다. 줄은 접지 않고 가로 한 줄로만 편다.**
* 판이 25곡이든 50곡이든 줄 수는 하나고, 안 보이는 나머지는 옆으로 스크롤한다.
* 이 페이지의 다른 레일(주변 명소·인물 열전)이 이미 쓰는 것과 같은 해법이라 —
* "hover 로 감추기" 는 애초에 필요 없던 처방이었다.
* ★ 마크업을 고치지 않는다(React 가 되돌린다). `:has()` 로 판이 든 상자만 집는다.
* 곡명 `<span>` 은 원래도 절대배치가 아니라 `block` 이라 손대지 않는다 — 화면에
* 보이려고 위치를 옮길 필요가 없다, 그냥 숨기지만 않으면 된다.
* ★ **화살표 + 실제 카로셀 라이브러리**(2026-09-14 대표: "옆으로 갈수있는 버튼도 주고
* slider.js 적용해줘"). `inject.js` `startSongsRail()` 이 이 판 목록을 감싸는 상자를
* 하나 만들어 `.slider-viewport` 로 표시하고(embla-carousel 코어를 그 위에 붙인다 —
* "군산 읽기" 탭과 같은 라이브러리, `vendor/embla-carousel.umd.js`), 그 안에 원래
* 판 목록(:has() 로 잡는 이 상자)을 그대로 넣는다. **가로 스크롤·snap 은 이제
* `.slider-viewport` 쪽 몫이다** — 그 클래스가 이미 그 CSS 를 갖고 있어(전역 번들)
* 여기서 다시 안 만든다. 이 규칙은 **안쪽 판 줄의 레이아웃(줄바꿈 금지·간격·판 크기)만** 잡는다.
*/
#songs div:has(> button > .w4-disc-mini) {
flex-wrap: nowrap;
justify-content: flex-start;
gap: 14px;
padding: 6px 4px 14px;
/* 렌더러가 이 div 자체에 준 mt-4 를 지운다 — 지금은 이 div 가 `.slider-viewport` 안에
들어가 있어, 그 여백이 화살표 줄(위)이 아니라 스크롤 상자 안쪽 빈 공간이 됐다.
간격은 화살표 줄의 mt-4(inject.js `startSongsRail`)가 대신 준다. */
margin-top: 0;
}
#songs button:has(> .w4-disc-mini) {
flex: 0 0 auto;
width: 68px; /* 원래 96px(`w-24`) — 한 줄로만 펴니 그만큼은 줄여도 된다 */
transition: transform 0.16s ease;
}
#songs button:has(> .w4-disc-mini) > .w4-disc-mini {
width: 56px;
height: 56px;
}
/* 곡명 — 렌더러가 원래 그리는 그 글자다. 여기서는 줄어든 판 폭에 맞춰 여백만 좁힌다. */
#songs button:has(> .w4-disc-mini) > .w4-disc-mini + span {
margin-top: 5px;
font-size: 14px;
}
/* 고른 판만 살짝 들어 올린다 — "뽑아 든 판" 표시. 선택 자체(테두리)는 React 가 인라인
스타일로 이미 그린다(`boxShadow`); 여기선 그 위에 얹는 동작만 더한다. */
#songs button:has(> .w4-disc-mini):hover { transform: translateY(-4px); }
#songs button[aria-pressed='true']:has(> .w4-disc-mini) { transform: translateY(-6px); }
@media (prefers-reduced-motion: reduce) {
#songs button:has(> .w4-disc-mini) { transition: none; }
}
/* `#songs .slider-viewport` 로 스코프한다 — `.slider-viewport` 를 전부 건드리면
"군산 읽기" 카로셀과 이 페이지 다른 실제 레일까지 같이 바뀐다. */
#songs .slider-viewport {
-webkit-mask-image: linear-gradient(90deg, #000 0, #000 calc(100% - 28px), transparent 100%);
mask-image: linear-gradient(90deg, #000 0, #000 calc(100% - 28px), transparent 100%);
}
/*
* ── 엽서 쓰기 (inject.js ⑥) ───────────────────────────────────────────────
* `#location` 과 `#festival` 사이의 **독립 섹션**이다(2026-09-14 대표 지시로 '오늘의
* 엽서' 탭에서 빠져나왔다). 다른 실제 섹션과 같은 껍데기(`.shell` · `--section-space`
* 세로 여백)를 쓰고, 제목·부제도 `ItemSection` 이 쓰는 것과 같은 전역 클래스(`.h2`
* `.measure`)라 이 섹션만 따로 CSS 를 두지 않는다 — 안쪽 위젯(캔버스·폼)만 여기서 잡는다.
*/
#w4d-pm-section {
width: 100%;
padding-block: var(--section-space, 64px);
background: var(--tpl-surface, #fafafa);
color: var(--tpl-text, #1b1a15);
}
.w4d-pm-layout {
display: flex;
flex-wrap: wrap;
gap: 20px;
align-items: flex-start;
}
.w4d-pm-canvas {
/* ★ 300px 이던 것을 키웠다(2026-09-14 대표: "엽서 작아서 깨지는데?"). 실제로 그리는
건 1080×1080 이라 화질 문제는 아니지만, 미리보기가 작으면 안의 문구·소인이 안 읽혀
"제대로 안 나온 것" 처럼 보인다 — 미리보기 크기를 실물 크기에 맞춘다. */
width: 420px;
max-width: 100%;
aspect-ratio: 1 / 1;
border: 1px solid var(--tpl-border, #bcb49e);
background: var(--tpl-card, #efe7d3);
}
.w4d-pm-form { flex: 1 1 320px; min-width: 260px; }
@media (max-width: 767px) {
.w4d-pm-form { order: -1; }
}
/* 썸네일 줄 — `.slider-viewport`(뷰포트) 안에 `.slider-track`(트랙, 이 클래스 자체가
display:flex 를 준다)이 있다. 여기서는 간격·안쪽 여백만 얹는다(2026-09-14 대표:
"이미지는 다 가져와서 그냥 카로셀로 보여주든가 하셈" — 9장 전체를 후보로 두면서
한 줄에 다 못 앉으니 카로셀이 됐다). */
.w4d-pm-thumbs-viewport { margin-bottom: 12px; }
.w4d-pm-thumbs { gap: 8px; padding: 2px; }
.w4d-pm-thumb {
width: 52px;
height: 52px;
flex: 0 0 auto;
padding: 0;
overflow: hidden;
border: 2px solid transparent;
border-radius: 4px;
cursor: pointer;
}
.w4d-pm-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.w4d-pm-thumb[aria-pressed='true'] { border-color: var(--tpl-accent, #bf2f1b); }
.w4d-pm-text {
width: 100%;
box-sizing: border-box;
padding: 10px 12px;
font: inherit;
font-family: var(--tpl-font-body, serif);
font-size: 14px;
line-height: 1.5;
color: var(--tpl-text, #1b1a15);
background: var(--tpl-surface, #fafafa);
border: 1px solid var(--tpl-border, #bcb49e);
resize: none; /* 2026-09-14 대표: "textarea 크기 못바꾸게" — 늘리면 옆 캔버스와 높이가 어긋난다 */
}
.w4d-pm-count { margin: 4px 0 14px; font-size: 14px; text-align: right; opacity: 0.85; }
.w4d-pm-actions { display: flex; flex-wrap: wrap; gap: 10px; }
.w4d-pm-actions button {
padding: 9px 16px;
font-size: 15px;
font-weight: 700;
border: 1px solid var(--tpl-primary, #1b1a15);
cursor: pointer;
}
.w4d-pm-share { background: var(--tpl-primary, #1b1a15); color: var(--tpl-bg, #fff); }
.w4d-pm-save { background: transparent; color: var(--tpl-text, #1b1a15); }
.w4d-pm-hint { margin: 10px 0 0; font-size: 14px; opacity: 0.6; }
@media (max-width: 480px) {
.w4d-pm-canvas { width: 100%; }
}
/*
* ── 군산 읽기 (inject.js ③) ───────────────────────────────────────────────
* 렌더러에 없는 섹션이라 카드 내용 모양은 여기서 만든다. 색·서체는 전부 템플릿 변수에서
* 가져온다 — 값을 박으면 템플릿을 바꿀 때 이 섹션만 남의 집처럼 보인다(이 파일의 규약).
*
* ★ **카로셀 껍데기는 새로 만들지 않았다** — `.slider-viewport` · `.slider-track` 은
* 이 사이트가 이미 쓰는 클래스다(`site/src/lib/ui/Carousel.tsx`, 컴파일된 전역 CSS에
* 이미 있다). 여기서 정의하는 CSS 는 **카드 안 내용물**뿐이다. 점(dots)·간격도 그 컴포넌트가
* 쓰는 Tailwind 유틸리티(`h-1.5 rounded-full bg-current opacity-70` 등)를 그대로 쓴다.
* ★ **포스트 하나씩** (2026-09-14 대표). 슬라이드 하나 = 꼭지 하나, 전체 폭을 다 쓴다 —
* 3~4개를 욱여넣었더니 카드가 좁아져 본문이 한 줄로 접혔었다.
*/
/* '군산 이야기' 탭 하나로 접혀 들어왔다(2026-09-14) — 독립 섹션의 배경·제호는 없다.
나머지 다섯 탭 패널과 같은 폭(`.shell`)에, 위아래 여백만 얹는다. */
#w4d-reading-panel {
padding: 26px 0 4px;
color: var(--tpl-text, #1b1a15);
font-family: var(--tpl-font-body, serif);
}
/* .w4d-read-kicker 삭제 — 2026-09-14 대표: "군산읽기만 타이틀이 없서 통일 시켜",
커스텀 캡션 대신 다른 네 탭과 같은 h2.h2 + p.measure 로 바꿨다(inject.js startReading()). */
/* ── 슬라이드 폭 ── `CarouselSlide` 의 관례와 같다: 모바일은 다음 장이 살짝 걸치게(86%),
데스크톱은 한 장이 크게 온다(560px 안팎). "다음 장이 걸쳐 보이는 것" 이 옆에 더 있다는
유일한 시각 신호라는 그 컴포넌트의 원칙을 그대로 따른다. */
.w4d-read-basis { width: 86%; }
@media (min-width: 640px) { .w4d-read-basis { width: min(600px, 56%); } }
/* ── 포스트 카드 — 슬라이드 하나 = 꼭지 하나. */
.w4d-read-card {
height: 100%;
padding: 18px 20px 16px;
background: var(--tpl-card, #efe7d3);
border: 1px solid var(--tpl-border, #bcb49e);
border-radius: var(--tpl-radius, 0);
}
.w4d-read-page {
margin: 0 0 8px;
font-size: 14px;
font-weight: 700;
letter-spacing: 0.18em;
color: color-mix(in oklab, var(--tpl-accent, #bf2f1b) 62%, var(--tpl-text, #1b1a15));
}
.w4d-read-year { font-weight: 400; letter-spacing: 0.06em; opacity: 0.7; }
.w4d-read-card h3 {
margin: 0 0 10px;
padding-bottom: 10px;
border-bottom: 2px solid var(--tpl-primary, #1b1a15);
font-family: var(--tpl-font-heading, var(--tpl-font-body, serif));
font-size: 21px;
font-weight: 700;
line-height: 1.3;
}
.w4d-read-body {
margin: 0;
font-size: 14.5px;
line-height: 1.8;
text-align: justify;
word-break: keep-all;
opacity: 0.88;
}
.w4d-read-src {
margin-top: 14px;
padding-top: 10px;
border-top: 1px dashed var(--tpl-border, #bcb49e);
font-size: 14px;
font-style: italic;
opacity: 0.6;
}
.w4d-read-src a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
/* .w4d-read-flag(확인필요 배지) 삭제 — 2026-09-14 대표: "확인필요 없애고 링크는
네이버 링크로", inject.js startReading() 도 같이 뺐다. */
@media (max-width: 640px) {
.w4d-read-card h3 { font-size: 18px; }
}
/* ══ 사진 갤러리 쪽 넘김 (데스크톱만) ═══════════════════════════════════════
* ★ 왜 (2026-09-14 사장님: "사진갤러리 전체 렌더링 말고 페이징 좀, 너무 많아 데스크톱에서는")
* 갤러리는 좁은 화면에서 한 장씩 미는 캐러셀이지만 넓은 화면에서는 4열 격자로 **전부**
* 깔린다(`GallerySection.tsx` — `lg:grid lg:grid-cols-4`). 사진 37장이면 열 줄이 되어
* 페이지가 사진으로 끊긴다.
* ★ 좁은 화면은 그대로 둔다 — 거기서는 한 장이 폭을 다 쓰는 캐러셀이라 이미 한 번에 하나다.
* 그래서 숨김 규칙 전부가 `min-width: 1024px`(Tailwind `lg`) 안에 있다.
* ★ 마크업을 지우지 않고 **속성으로만** 가린다. React 가 payload 로 다시 그리면 잘라낸
* <li> 는 돌아오지만, 그때는 주입 JS 가 속성을 다시 찍는다(inject.js ⑧).
* 그리고 사진이 DOM 에 남아 있어야 alt 문장이 문서에 남는다 — 이미지 검색이 그걸 읽는다
* (같은 이유가 `GallerySection.tsx` 머리주석에도 있다).
*/
.w4d-gal-pager { display: none; }
@media (min-width: 1024px) {
#gallery ul[data-w4d-gal] > li[data-w4d-gal-on='0'] { display: none; }
.w4d-gal-pager {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
margin-top: 18px;
}
.w4d-gal-pager button {
min-width: 30px;
height: 30px;
padding: 0 8px;
border: 1px solid var(--tpl-border, #bcb49e);
border-radius: 8px;
background: transparent;
color: inherit;
font-size: 15px;
font-variant-numeric: tabular-nums;
line-height: 1;
cursor: pointer;
transition: background-color 0.15s ease, opacity 0.15s ease;
}
.w4d-gal-pager button:hover:not(:disabled) { background: color-mix(in srgb, currentColor 8%, transparent); }
.w4d-gal-pager button:disabled { opacity: 0.3; cursor: default; }
.w4d-gal-pager button[aria-current='true'] {
background: var(--tpl-primary, #1b1a15);
border-color: var(--tpl-primary, #1b1a15);
color: var(--tpl-bg, #fff);
font-weight: 700;
}
.w4d-gal-total { margin-left: 8px; font-size: 14px; opacity: 0.85; }
}
/* ── 768~1023px 에서 하단 탭바가 푸터 마지막 줄을 덮는 것 막기 ─────────────────
★ 실측(2026-09-14 대표 스크린샷, 태블릿 폭 Edge): 푸터(SiteFooter.tsx)는 768px
부터 아래 여백을 줄이는데(pb-28 → md:pb-14), 하단 고정 탭바(MobileTabBar.tsx)는
1024px 이 되기 전엔 그대로 떠 있다(lg:hidden). 그래서 768~1023px 사이에서만
푸터 마지막 줄이 탭바에 깔린다 — 두 컴포넌트가 서로 다른 기준(md·lg)으로
"이제 모바일 아니다"를 판단해 생기는 폭이다.
★ 진짜 수정은 렌더러 쪽(`solution/site/src/sections/SiteFooter.tsx`)에서
`md:pb-14` 를 탭바와 같은 `lg:pb-14` 로 맞추는 것인데, 그건 발행 사이트 전부에
영향을 주는 변경이라 다시 굽고(`docker compose restart solution-prerender`)
전부 재발행(`republish_all.py`)해야 한다 — 이 목업만 먼저 막는다. */
@media (min-width: 768px) and (max-width: 1023.98px) {
footer { padding-bottom: 7rem !important; }
}
#w4d-mini[data-armed="1"] #w4d-play { animation: w4d-breathe 2.4s ease-in-out infinite; }
@keyframes w4d-breathe { 0%, 100% { opacity: 1 } 50% { opacity: .45 } }
@media (prefers-reduced-motion: reduce) {
#w4d-mini[data-armed="1"] #w4d-play { animation: none }
}
h3.border-line.mb-3.border-b.pb-2 { border-bottom: 0; padding-bottom: 0; }
#units .rounded-md.border-line.tpl-border {
border-radius: 999px;
border-width: 0;
padding-inline: 0.9rem;
background-color: color-mix(in srgb, var(--tpl-text, #1b1a15) 8%, transparent);
}
/* 축제 각주 삭제 (2026-09-17 대표 지시) — 렌더러는 그대로 두고 시연본에서만 감춘다. */
#festival > div > p.text-muted:last-child { display: none; }