/* 추가 컴포넌트: 코어(components·app·modules)에 없던 조각과 그 상태.
   결과 화면 · 확인 창 · 화면 전환 · 누름 · 선택 칩 · 슬라이더 · 입력 도우미 · 페이지 이동 · 경로 · 키 · 코드 · 타임라인 · 대화 ·
   아바타 묶음 · 가격 · 알림 띠 · 진행 막대 · 숫자 폭 · 빛 · 축하 · 툴팁 · 구분선 · 비율 상자 · 본문 바로가기 · 숨는 헤더.
   코어와 같은 규칙을 따른다. 역할 토큰만 쓰고, 테두리와 그림자를 함께 주지 않고, 누르는 곳은 44px, 모션 축소면 완료 상태로 둔다.
   동작은 js/extras.js 가 data- 속성으로 붙인다. 문서는 docs/07_extras.md. */

/* ══ 결과 화면 ══════════════════════════════════════════
   일이 끝났다는 사실을 한 화면으로 알린다. 그림 → 제목 → 설명 → 요약 → 버튼 순으로 짧게 이어 나온다.
   전체가 1초 안에 끝난다. 더 끌면 사람을 기다리게 만든다. */
.result{display:flex;flex-direction:column;align-items:center;width:100%;max-width:var(--w-sm);margin-inline:auto;
  padding:var(--s-48) var(--s-20) var(--s-40);text-align:center}
/* 앱 화면처럼 결과만 있는 화면: 남는 높이의 가운데보다 조금 위에 둔다. svh 는 주소창이 움직여도 바뀌지 않는다. */
.result--fill{min-height:min(72svh,640px);justify-content:center;padding-bottom:var(--s-64)}
.result__art{display:grid;place-items:center;margin-bottom:var(--s-24)}
.result__title{font-size:var(--t-heading-md);line-height:var(--t-heading-md-lh);letter-spacing:var(--t-heading-md-ls);
  font-weight:700;color:var(--ink);text-wrap:balance}
.result__desc{margin-top:var(--s-10);max-width:26em;color:var(--ink-muted);
  font-size:var(--t-body-md);line-height:var(--t-body-md-lh);word-break:keep-all;text-wrap-style:pretty}
@media (min-width:768px){
  .result{padding-top:var(--s-64)}
  .result__title{font-size:var(--t-heading-lg);line-height:var(--t-heading-lg-lh);letter-spacing:var(--t-heading-lg-ls)}
}
/* 요약: 무엇이 어떻게 끝났는지 키·값 몇 줄. 면으로 묶고 줄 사이만 가는 선. */
.result__meta{width:100%;max-width:420px;margin-top:var(--s-32);padding:var(--s-4) var(--s-20);
  border-radius:var(--r-xl);background:var(--canvas-alt);text-align:left}
.surface-alt .result__meta,.on-alt .result__meta{background:var(--surface)}
.result__meta > div{display:flex;align-items:baseline;justify-content:space-between;gap:var(--s-16);padding-block:var(--s-12);
  font-size:var(--t-body-sm);line-height:var(--t-body-sm-lh)}
.result__meta > div + div{box-shadow:inset 0 1px 0 var(--line)}
.result__meta dt{flex:none;color:var(--ink-muted)}
.result__meta dd{min-width:0;color:var(--ink);font-weight:600;text-align:right;
  font-variant-numeric:tabular-nums;word-break:keep-all;overflow-wrap:anywhere}
.result__actions{display:flex;flex-direction:column;align-items:center;gap:var(--s-8);width:100%;max-width:420px;margin-top:var(--s-24)}
.result__actions > .btn{width:100%}
.result__actions > .btn--ghost,.result__actions > .link{width:auto}
@media (min-width:640px){
  .result__actions:has(> .btn:not(.btn--ghost) + .btn:not(.btn--ghost)){flex-direction:row}
  .result__actions:has(> .btn:not(.btn--ghost) + .btn:not(.btn--ghost)) > .btn{flex:1}
}
/* 등장 순서. 그림은 check-anim 이 스스로 그린다. 한 덩어리가 180ms 안팎으로 끝나 겹침이 짧다. */
@keyframes result-in{from{opacity:0;transform:translateY(var(--s-12))}to{opacity:1;transform:none}}
.result > :is(.result__title,.result__desc,.result__meta,.result__actions){animation:result-in var(--d-5) var(--ease-out) both}
.result > .result__title{animation-delay:var(--d-3)}
.result > .result__desc{animation-delay:var(--d-4)}
.result > .result__meta{animation-delay:var(--d-5)}
.result > .result__actions{animation-delay:calc(var(--d-5) + var(--d-1))}

/* ── 선으로 그리는 결과 그림 ──
   --good 완료(체크) · --bad 실패(X) · --wait 기다림(도는 호) · --accent 완료를 브랜드 색으로.
   원이 살짝 튀며 나오고, 테두리가 한 바퀴 그려진 뒤, 표시가 그어진다. 끝나면 옅은 빛이 한 번 퍼진다.
   pathLength="1" 로 길이를 1 로 맞춰 dash 값이 모양과 상관없이 같다. 멈춘 상태(애니메이션 없음)가 곧 완성된 그림이다. */
.check-anim{--ca:var(--good);--ca-soft:var(--good-soft);--ca-size:80px;
  display:block;flex:none;width:var(--ca-size);height:var(--ca-size);overflow:visible}
.check-anim--bad{--ca:var(--bad);--ca-soft:var(--bad-soft)}
.check-anim--wait{--ca:var(--accent);--ca-soft:var(--accent-soft)}
.check-anim--accent{--ca:var(--accent);--ca-soft:var(--accent-soft)}
.check-anim--sm{--ca-size:56px}
.check-anim--lg{--ca-size:96px}
.check-anim *{transform-box:fill-box;transform-origin:center}
.check-anim__disc{fill:var(--ca-soft);animation:ca-pop var(--d-5) var(--ease-spring) both}
.check-anim__halo{fill:none;stroke:var(--ca);stroke-width:2;opacity:0;animation:ca-halo var(--d-7) var(--ease-out) calc(var(--d-6) + var(--d-1)) both}
.check-anim__ring,.check-anim__mark,.check-anim__spin,.check-anim__track{fill:none;stroke-linecap:round;stroke-linejoin:round}
.check-anim__ring{stroke:var(--ca);stroke-width:3;stroke-dasharray:1;stroke-dashoffset:0;
  animation:ca-draw var(--d-6) var(--ease-out) var(--d-1) both}
.check-anim__mark{stroke:var(--ca);stroke-width:5;stroke-dasharray:1;stroke-dashoffset:0;
  animation:ca-draw var(--d-4) var(--ease-out) var(--d-4) both}
.check-anim__mark--2{animation-delay:calc(var(--d-4) + var(--d-1))}
.check-anim__track{stroke:var(--ca);stroke-width:3;opacity:.16}
.check-anim__spin{stroke:var(--ca);stroke-width:3;stroke-dasharray:.24 .76;
  animation:ca-spin 1.1s linear infinite,ca-fade var(--d-3) var(--ease-out) var(--d-1) both}
@keyframes ca-pop{0%{opacity:0;transform:scale(.5)}60%{opacity:1}100%{opacity:1;transform:none}}
@keyframes ca-draw{from{stroke-dashoffset:1}to{stroke-dashoffset:0}}
@keyframes ca-halo{0%{opacity:.36;transform:scale(1)}100%{opacity:0;transform:scale(1.42)}}
@keyframes ca-spin{to{transform:rotate(1turn)}}
@keyframes ca-fade{from{opacity:0}to{opacity:1}}

/* ══ 확인 창 (dsConfirm · dsAlert · data-confirm) ═══════
   코어 .overlay + .modal 을 그대로 쓴다. 좁은 화면은 아래에서 올라오는 시트, 넓은 화면은 가운데 작은 창.
   되돌릴 수 없는 일만 묻는다. 모든 동작 앞에 확인을 끼우면 사람들은 읽지 않고 누른다. */
.confirm .modal{width:min(400px,calc(100vw - 32px));padding:var(--s-28) var(--s-24) var(--s-24)}
.confirm__title{font-size:var(--t-heading-sm);line-height:var(--t-heading-sm-lh);letter-spacing:var(--t-heading-sm-ls);
  font-weight:700;color:var(--ink);text-wrap:balance}
.confirm__desc{margin-top:var(--s-8);color:var(--ink-muted);font-size:var(--t-body-sm);line-height:var(--t-body-sm-lh);
  white-space:pre-line;word-break:keep-all;text-wrap-style:pretty}
.confirm__error{display:flex;align-items:flex-start;gap:var(--s-8);margin-top:var(--s-16);padding:var(--s-10) var(--s-12);border-radius:var(--r-md);
  background:var(--bad-soft);color:var(--bad-text);font-size:var(--t-caption);line-height:var(--t-caption-lh);font-weight:500}
.confirm__error[hidden]{display:none}
.confirm__error svg{flex:none;width:16px;height:16px;margin-top:2px}
.confirm__actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--s-8);margin-top:var(--s-24)}
.confirm__actions--one{grid-template-columns:minmax(0,1fr)}
.confirm__actions .btn{min-width:0}
/* 처리 중에는 막을 눌러도, Escape 를 눌러도 닫히지 않는다(extras.js). 막이 눌리지 않는다는 것도 모양으로 알린다. */
.confirm[data-busy] .overlay__scrim{cursor:progress}
/* 좁은 화면: 화면 아래에 붙는 시트. 코어 .modal 의 margin:auto 가 시트를 가운데로 띄우므로 위쪽 여백만 남긴다. 올라오고 내려가는 움직임도 시트와 같게. */
@media (max-width:640px){
  .confirm .modal{width:100%;margin:auto 0 0;padding:var(--s-28) var(--s-20) calc(var(--s-20) + var(--safe-b))}
  .confirm[data-state="open"] .modal{animation:sheet-up var(--d-5) var(--ease-sheet) both}
  .confirm[data-state="closing"] .modal{animation:sheet-down var(--d-3) var(--ease-in) both}
}

/* ══ 화면 전환 (View Transitions) ═══════════════════════
   방향은 html[data-nav] 가 정한다. forward 앞으로(오른쪽에서 들어옴) · back 뒤로(왼쪽에서) · tab 제자리 페이드 · none 움직이지 않음.
   Turbo 는 extras.js 가 방문마다 적고, 문서 사이(MPA)는 링크를 누를 때 적어 둔 값을 새 문서가 읽는다.
   움직임은 html[data-transitions] 가 붙은 문서에서만 보인다. 나가는 화면이 먼저 빠르게 사라지고 들어오는 화면이 뒤이어 떠올라
   두 화면의 글자가 겹쳐 보이지 않는다. 탭바·앱바·하단 동작 바·상단 내비는 이름을 붙여 제자리에 둔다. */
@media (prefers-reduced-motion:no-preference){@view-transition{navigation:auto}}
html:not([data-transitions])::view-transition-group(root),
html:not([data-transitions])::view-transition-old(root),
html:not([data-transitions])::view-transition-new(root){animation:none}
html[data-transitions]::view-transition-old(root){animation:vt-fade-out var(--d-1) var(--ease-in) both}
html[data-transitions]::view-transition-new(root){animation:vt-fade-in var(--d-3) var(--ease-out) both}
html[data-transitions][data-nav="forward"]::view-transition-old(root){animation-name:vt-out-left}
html[data-transitions][data-nav="forward"]::view-transition-new(root){animation-name:vt-in-right}
html[data-transitions][data-nav="back"]::view-transition-old(root){animation-name:vt-out-right}
html[data-transitions][data-nav="back"]::view-transition-new(root){animation-name:vt-in-left}
html[data-nav="none"]::view-transition-group(*),
html[data-nav="none"]::view-transition-old(*),
html[data-nav="none"]::view-transition-new(*){animation:none}
/* 제자리에 두는 바. 목업(.phone) 안의 것은 빼고, 한 문서에 둘 이상이면 extras.js 가 뒤의 것 이름을 지운다(이름이 겹치면 전환이 통째로 취소된다). */
html[data-transitions] .tabbar--fixed{view-transition-name:ds-tabbar}
html[data-transitions] .appbar:not(.phone .appbar){view-transition-name:ds-appbar}
html[data-transitions] .actionbar:not(.phone .actionbar){view-transition-name:ds-actionbar}
html[data-transitions] body > .nav{view-transition-name:ds-nav}
::view-transition-group(ds-tabbar),::view-transition-group(ds-appbar),
::view-transition-group(ds-actionbar),::view-transition-group(ds-nav){animation-duration:var(--d-2)}
@keyframes vt-fade-in{0%,35%{opacity:0}100%{opacity:1}}
@keyframes vt-fade-out{to{opacity:0}}
@keyframes vt-in-right{0%,35%{opacity:0;transform:translateX(var(--s-32))}100%{opacity:1;transform:none}}
@keyframes vt-out-left{to{opacity:0;transform:translateX(calc(var(--s-16) * -1))}}
@keyframes vt-in-left{0%,35%{opacity:0;transform:translateX(calc(var(--s-32) * -1))}100%{opacity:1;transform:none}}
@keyframes vt-out-right{to{opacity:0;transform:translateX(var(--s-16))}}

/* ══ 누르는 느낌 ═══════════════════════════════════════
   카드·행·타일을 누르면 살짝 줄어들고 옅은 덮개가 깔린다. 손가락 아래에서 무언가 일어났다는 걸 바로 알린다.
   스크롤하려고 댄 손가락에는 반응하지 않는다. extras.js 가 70ms 기다렸다가 data-pressed 를 붙이고, 움직이면 떼어 낸다.
   덮개는 ::before 다. 같은 요소의 ::before 를 쓰는 코어 조각(.spotlight, 작은 버튼의 적중 영역)과는 함께 쓰지 않는다. */
:where(.pressable){position:relative;cursor:pointer;-webkit-tap-highlight-color:transparent;user-select:none;-webkit-user-select:none}
.pressable{transition:transform var(--d-3) var(--ease-out)}
.pressable::before{content:"";position:absolute;inset:0;z-index:2;border-radius:inherit;pointer-events:none;
  background:var(--tint-active);opacity:0;transition:opacity var(--d-3) var(--ease-out)}
@media (hover:hover){.pressable:hover::before{opacity:.4}}
.pressable[data-pressed],html:not(.js) .pressable:active{transform:scale(var(--press-scale,.98));
  transition-duration:var(--d-1);transition-timing-function:var(--ease-inout)}
.pressable[data-pressed]::before,html:not(.js) .pressable:active::before{opacity:1;transition-duration:var(--d-1)}
/* 가로로 긴 행은 줄어들면 가장자리가 흔들려 보인다. 덮개만 깐다. 작은 타일은 조금 더 줄인다. */
.pressable--flat{--press-scale:1}
.pressable--deep{--press-scale:.95}
.pressable:is(:disabled,[aria-disabled="true"]){cursor:not-allowed;opacity:.5;transform:none}
.pressable:is(:disabled,[aria-disabled="true"])::before{display:none}
/* 고른 상태(선택 카드·요금제 고르기): 강조 테두리는 덮개 층 위에 그린다 */
.pressable[aria-pressed="true"],.pressable[aria-checked="true"],.pressable[aria-selected="true"]{box-shadow:inset 0 0 0 2px var(--accent)}
.pressable.card--line[aria-pressed="true"]::after,.pressable.card--line[aria-checked="true"]::after{box-shadow:inset 0 0 0 2px var(--accent)}

/* ══ 슬라이더 ══════════════════════════════════════════
   <input type="range" class="range">. 채운 트랙은 --p(0~100)로 그린다. extras.js 가 값이 바뀔 때마다 적는다.
   손잡이 둘레에 면 색 고리를 둘러 채운 트랙 위에서도 떠 보이게 한다. 누르는 높이는 44px. */
.range{--p:50;--range-fill:var(--accent);--range-track:var(--line-strong);--thumb:24px;
  -webkit-appearance:none;appearance:none;display:block;width:100%;height:var(--c-md-h);margin:0;padding:0;
  background:transparent;cursor:pointer;touch-action:pan-y;-webkit-tap-highlight-color:transparent}
.range:focus{outline:none}
.range::-webkit-slider-runnable-track{height:6px;border-radius:var(--r-full);
  background:linear-gradient(to right,var(--range-fill) 0 calc(var(--thumb) / 2 + (100% - var(--thumb)) * var(--p) / 100),var(--range-track) 0 100%)}
.range::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:var(--thumb);height:var(--thumb);margin-top:calc((6px - var(--thumb)) / 2);
  border-radius:50%;background:var(--range-fill);box-shadow:0 0 0 3px var(--surface),var(--sh-sm);
  transition:box-shadow var(--d-2) var(--ease-inout),transform var(--d-2) var(--ease-spring)}
.range::-moz-range-track{height:6px;border-radius:var(--r-full);background:var(--range-track)}
.range::-moz-range-progress{height:6px;border-radius:var(--r-full);background:var(--range-fill)}
.range::-moz-range-thumb{width:var(--thumb);height:var(--thumb);border:0;border-radius:50%;background:var(--range-fill);
  box-shadow:0 0 0 3px var(--surface),var(--sh-sm);transition:box-shadow var(--d-2) var(--ease-inout),transform var(--d-2) var(--ease-spring)}
@media (hover:hover){
  .range:hover:not(:disabled)::-webkit-slider-thumb{box-shadow:0 0 0 3px var(--surface),0 0 0 9px var(--accent-soft)}
  .range:hover:not(:disabled)::-moz-range-thumb{box-shadow:0 0 0 3px var(--surface),0 0 0 9px var(--accent-soft)}
}
.range:active:not(:disabled)::-webkit-slider-thumb{transform:scale(1.12);box-shadow:0 0 0 3px var(--surface),0 0 0 10px var(--accent-soft)}
.range:active:not(:disabled)::-moz-range-thumb{transform:scale(1.12);box-shadow:0 0 0 3px var(--surface),0 0 0 10px var(--accent-soft)}
.range:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 3px var(--surface),0 0 0 5px var(--focus)}
.range:focus-visible::-moz-range-thumb{box-shadow:0 0 0 3px var(--surface),0 0 0 5px var(--focus)}
.range[aria-invalid="true"]{--range-fill:var(--bad-text)}
.range:disabled{--range-fill:var(--ink-disabled);--range-track:var(--line);cursor:not-allowed}
.range:disabled::-webkit-slider-thumb{box-shadow:0 0 0 3px var(--surface)}
.range:disabled::-moz-range-thumb{box-shadow:0 0 0 3px var(--surface)}
/* 이름·값 한 줄 + 슬라이더 + 양 끝 눈금 */
.range-field{display:flex;flex-direction:column;gap:var(--s-2)}
.range-field__head{display:flex;align-items:baseline;justify-content:space-between;gap:var(--s-12)}
.range-field__value{font-size:var(--t-body-sm);font-weight:700;color:var(--accent-text);font-variant-numeric:tabular-nums;white-space:nowrap}
.range-field:has(.range:disabled) .range-field__value{color:var(--ink-disabled)}
.range-field:has(.range[aria-invalid="true"]) .range-field__value{color:var(--bad-text)}
.range-field__scale{display:flex;justify-content:space-between;margin-top:calc(var(--s-6) * -1);
  font-size:var(--t-micro);color:var(--ink-subtle);font-variant-numeric:tabular-nums}

/* ══ 입력 도우미: 지우기 · 비밀번호 보기 · 글자 수 ═══════
   코어 .input-group 안의 .input 에 data-clearable · data-password-toggle 을 달면 extras.js 가 코어 뒤 칸(.input-group__end)에
   아이콘 버튼(.input-group__btn)을 넣는다. 모양·적중 영역·입력 오른쪽 여백은 코어가 잡고, 여기서는 상태만 정한다.
   data-maxlength-count 를 달면 입력칸 아래 오른쪽에 '12 / 100' 을 적는다. 끝에 가까우면 주의색, 넘치면 오류색과 aria-invalid. */
.input-clear svg,.input-reveal svg{width:20px;height:20px;flex:none}
.input-reveal[aria-pressed="true"]{color:var(--accent-text)}
.input-group:has(.input:disabled) :is(.input-clear,.input-reveal){display:none}
.field__foot{display:flex;align-items:flex-start;gap:var(--s-12)}
.field__foot > .field__help,.field__foot > .field__error{flex:1;min-width:0}
.field__count{flex:none;margin-left:auto;font-size:var(--t-caption);line-height:var(--t-caption-lh);color:var(--ink-subtle);
  font-variant-numeric:tabular-nums;white-space:nowrap;transition:color var(--d-2) var(--ease-inout)}
.field__count[data-near]{color:var(--warn-text);font-weight:600}
.field__count[data-over]{color:var(--bad-text);font-weight:700}

/* ══ 페이지 이동 ═══════════════════════════════════════
   번호는 44px 칸. 지금 페이지는 연한 강조 면. 좁은 화면에서는 번호를 접고 '3 / 12' 로 줄인다. */
.pager{display:flex;align-items:center;justify-content:center;gap:var(--s-4)}
.pager__btn,.pager__num{position:relative;display:inline-flex;align-items:center;justify-content:center;gap:var(--s-4);
  min-width:var(--c-md-h);height:var(--c-md-h);padding-inline:var(--s-10);border-radius:var(--r-md);
  font-size:var(--t-body-sm);font-weight:600;color:var(--ink-muted);font-variant-numeric:tabular-nums;white-space:nowrap;
  -webkit-tap-highlight-color:transparent;
  transition:background-color var(--d-1) var(--ease-inout),color var(--d-1) var(--ease-inout),transform var(--d-1) var(--ease-inout)}
.pager__btn svg{flex:none}
@media (hover:hover){.pager__btn:hover,.pager__num:hover{background:var(--tint);color:var(--ink)}}
.pager__btn:active,.pager__num:active{background:var(--tint-active);transform:scale(.96)}
.pager__num[aria-current="page"]{background:var(--accent-soft);color:var(--accent-text);font-weight:700}
.pager__btn:is([aria-disabled="true"],:disabled){color:var(--ink-disabled);background:none;pointer-events:none}
.pager__gap{min-width:var(--s-24);text-align:center;color:var(--ink-subtle);letter-spacing:.1em;user-select:none}
.pager__status{display:none;min-width:var(--s-80);justify-content:center;font-size:var(--t-body-sm);font-weight:600;
  color:var(--ink-body);font-variant-numeric:tabular-nums}
.pager__status b{color:var(--ink);font-weight:700}
@media (max-width:639px){
  .pager__num,.pager__gap{display:none}
  .pager__status{display:inline-flex}
  .pager__label{display:none}
}
/* '더 보기' 한 줄: 무한 목록 대신 누를 때마다 더 불러온다. 처리 중은 코어 .btn[data-busy] */
.pager--more{flex-direction:column;gap:var(--s-8)}
.pager--more .btn{width:100%;max-width:360px}
.pager__hint{font-size:var(--t-caption);color:var(--ink-subtle);font-variant-numeric:tabular-nums}

/* ══ 경로 ═════════════════════════════════════════════
   지금 화면이 어디에 있는지 한 줄로. 넘치면 옆으로 밀리고, extras.js 가 끝(지금 화면)이 보이게 둔다. */
.crumbs{min-width:0}
.crumbs ol{display:flex;align-items:center;overflow-x:auto;scrollbar-width:none;white-space:nowrap}
.crumbs ol::-webkit-scrollbar{display:none}
.crumbs li{display:inline-flex;align-items:center;flex:none;font-size:var(--t-body-xs);color:var(--ink-muted)}
.crumbs li + li::before{content:"";flex:none;width:6px;height:6px;margin:0 var(--s-8) 0 var(--s-4);
  border:solid var(--ink-disabled);border-width:1.5px 1.5px 0 0;rotate:45deg}
.crumbs a{display:inline-flex;align-items:center;min-height:var(--c-md-h);padding-inline:var(--s-4);border-radius:var(--r-xs);
  color:var(--ink-muted);text-decoration:underline;text-decoration-color:transparent;text-underline-offset:3px;
  transition:color var(--d-1) var(--ease-inout),text-decoration-color var(--d-1) var(--ease-inout)}
@media (hover:hover){.crumbs a:hover{color:var(--ink);text-decoration-color:currentColor}}
.crumbs a:active{color:var(--ink)}
.crumbs a:focus-visible{outline-offset:-2px}
.crumbs [aria-current="page"]{display:inline-flex;align-items:center;min-height:var(--c-md-h);padding-inline:var(--s-4);color:var(--ink);font-weight:600}

/* ══ 키 ═══════════════════════════════════════════════
   단축키 안내. 아래쪽 선을 한 겹 더 둬서 눌리는 키처럼 보인다(선만 쓰고 그림자는 쓰지 않는다). */
.kbd{display:inline-flex;align-items:center;justify-content:center;min-width:22px;height:22px;padding:0 var(--s-6);
  border-radius:var(--r-xs);background:var(--surface);color:var(--ink-muted);
  font-family:var(--font-mono);font-size:var(--t-nano);font-weight:600;line-height:1;white-space:nowrap;vertical-align:middle;font-variant-numeric:tabular-nums;
  box-shadow:inset 0 0 0 1px var(--line-strong),inset 0 -1.5px 0 var(--line-strong)}
.kbd--lg{min-width:28px;height:28px;padding:0 var(--s-8);font-size:var(--t-caption)}
.kbd[data-on]{background:var(--accent-soft);color:var(--accent-text);box-shadow:inset 0 0 0 1px var(--accent-line),inset 0 -1.5px 0 var(--accent-line)}
.kbd-combo{display:inline-flex;align-items:center;gap:var(--s-4);white-space:nowrap}
.btn .kbd{margin-left:var(--s-4)}
.btn--primary .kbd,.btn--invert .kbd{background:transparent;color:inherit;box-shadow:inset 0 0 0 1px currentColor}

/* ══ 코드 상자 ═════════════════════════════════════════
   설정값·명령어·예시 코드를 그대로 옮겨 가게 한다. 복사는 코어 data-copy 위임을 쓴다(extras.js 가 본문을 넣는다).
   긴 줄은 줄을 바꾸지 않고 상자 안에서만 옆으로 민다. 페이지는 밀리지 않는다. */
.code{position:relative;min-width:0;border-radius:var(--r-lg);background:var(--canvas-sunken);overflow:hidden}
.surface-alt .code,.on-alt .code{background:var(--surface)}
.code__head{display:flex;align-items:center;justify-content:space-between;gap:var(--s-8);min-height:var(--c-md-h);
  padding:var(--s-2) var(--s-4) var(--s-2) var(--s-16);box-shadow:inset 0 -1px 0 var(--line)}
.code__lang{font-size:var(--t-micro);font-weight:600;letter-spacing:.02em;color:var(--ink-subtle)}
.code__copy{position:relative;display:inline-flex;align-items:center;gap:var(--s-6);height:var(--c-sm-h);padding-inline:var(--s-10);
  border-radius:var(--r-sm);font-size:var(--t-caption);font-weight:600;color:var(--ink-muted);cursor:pointer;
  -webkit-tap-highlight-color:transparent;transition:background-color var(--d-1) var(--ease-inout),color var(--d-1) var(--ease-inout)}
.code__copy::before{content:"";position:absolute;inset:50% auto auto 50%;translate:-50% -50%;width:max(100%,44px);height:44px}
.code__copy svg{width:16px;height:16px;flex:none}
@media (hover:hover){.code__copy:hover{background:var(--tint);color:var(--ink)}}
.code__copy:active{background:var(--tint-active)}
.code__copy .code__copy-done{display:none}
.code__copy[data-state="done"]{color:var(--good-text)}
.code__copy[data-state="done"] .code__copy-ico{display:none}
.code__copy[data-state="done"] .code__copy-done{display:block;animation:pop var(--d-3) var(--ease-spring) both}
.code__copy[data-state="fail"]{color:var(--bad-text)}
.code__body{margin:0;padding:var(--s-16);overflow-x:auto;-webkit-overflow-scrolling:touch;
  font-family:var(--font-mono);font-size:var(--t-caption);line-height:1.75;color:var(--ink-body);
  white-space:pre;tab-size:2;font-variant-ligatures:none;font-variant-numeric:tabular-nums}
.code__body code{font:inherit;color:inherit}
.code__body:focus-visible{outline-offset:-3px;border-radius:var(--r-lg)}
.code--wrap .code__body{white-space:pre-wrap;overflow-wrap:anywhere}
/* 머리 없이 한 줄 명령어: 복사 버튼이 오른쪽에 붙는다 */
.code--bare{display:flex;align-items:center}
.code--bare .code__body{flex:1;min-width:0;padding-block:var(--s-12)}
.code--bare .code__copy{margin-right:var(--s-4)}
/* 손으로 칠하는 구문 색. 하이라이터 없이 필요한 곳만 감싼다. */
.code .tok-k{color:var(--accent-text)}
.code .tok-s{color:var(--good-text)}
.code .tok-n{color:var(--warn-text)}
.code .tok-t{color:var(--note-text)}
.code .tok-c{color:var(--ink-subtle);font-style:italic}
.code .tok-d{color:var(--bad-text)}
/* 글 안의 짧은 코드 */
.code-inline{padding:.12em .42em;border-radius:var(--r-xs);background:var(--tint);color:var(--ink);
  font-family:var(--font-mono);font-size:.88em;font-variant-ligatures:none;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}

/* ══ 타임라인 ══════════════════════════════════════════
   일이 어떤 순서로 진행됐는지. 표시(점) 사이를 선이 잇고, 끝난 구간의 선은 강조색으로 이어진다.
   상태는 data-state: done 끝남(체크) · now 진행 중(퍼지는 고리) · todo 아직(빈 원) · bad 문제(느낌표) · skip 건너뜀.
   색만으로 알리지 않도록 표시 모양이 다르고, 글에도 상태를 적는다(.sr-only 또는 설명). */
.timeline{display:flex;flex-direction:column}
.timeline__item{position:relative;display:grid;grid-template-columns:24px minmax(0,1fr);column-gap:var(--s-12);padding-bottom:var(--s-24)}
.timeline__item:last-child{padding-bottom:0}
.timeline__item::before{content:"";position:absolute;left:11px;top:28px;bottom:4px;width:2px;border-radius:2px;background:var(--line)}
.timeline__item:last-child::before{display:none}
.timeline__item[data-state="done"]::before{background:var(--accent-line)}
.timeline__mark{position:relative;display:grid;place-items:center;width:24px;height:24px}
.timeline__mark::before{content:"";width:10px;height:10px;border-radius:50%;background:var(--surface);box-shadow:inset 0 0 0 2px var(--line-strong)}
[data-state="done"] > .timeline__mark::before{width:20px;height:20px;background:var(--accent);box-shadow:none}
[data-state="done"] > .timeline__mark::after{content:"";position:absolute;width:8px;height:4.5px;margin-top:-2px;
  border:solid var(--accent-ink);border-width:0 0 2px 2px;rotate:-45deg}
[data-state="now"] > .timeline__mark::before{width:12px;height:12px;background:var(--accent);box-shadow:0 0 0 4px var(--accent-soft)}
[data-state="now"] > .timeline__mark::after{content:"";position:absolute;width:12px;height:12px;border-radius:50%;
  box-shadow:0 0 0 2px var(--accent);animation:tl-pulse 1.8s var(--ease-out) infinite}
[data-state="bad"] > .timeline__mark::before{width:20px;height:20px;background:var(--bad);box-shadow:none}
[data-state="bad"] > .timeline__mark::after{content:"!";position:absolute;color:var(--canvas);font-size:var(--t-caption);font-weight:800;line-height:1}
[data-state="skip"] > .timeline__mark::before{width:8px;height:8px;background:var(--ink-disabled);box-shadow:none}
@keyframes tl-pulse{0%{opacity:.8;transform:scale(1)}100%{opacity:0;transform:scale(2.2)}}
.timeline__body{min-width:0;padding-top:1px}
.timeline__head{display:flex;align-items:baseline;justify-content:space-between;gap:var(--s-12)}
.timeline__title{font-size:var(--t-body-sm);line-height:22px;font-weight:600;color:var(--ink);word-break:keep-all}
.timeline__time{flex:none;font-size:var(--t-micro);color:var(--ink-subtle);font-variant-numeric:tabular-nums;white-space:nowrap}
.timeline__desc{margin-top:var(--s-2);font-size:var(--t-caption);line-height:var(--t-caption-lh);color:var(--ink-muted);word-break:keep-all;overflow-wrap:anywhere}
.timeline__extra{margin-top:var(--s-10)}
[data-state="todo"] .timeline__title,[data-state="skip"] .timeline__title{color:var(--ink-muted);font-weight:500}
[data-state="skip"] .timeline__title{text-decoration:line-through;text-decoration-color:var(--ink-disabled)}
[data-state="bad"] .timeline__desc{color:var(--bad-text)}
.timeline--dense .timeline__item{padding-bottom:var(--s-16)}

/* ══ 대화 ═════════════════════════════════════════════
   .chat 안에 말풍선을 쌓는다. --me 내가 보냄(오른쪽 강조면) · --ai 상대·도우미(왼쪽 옅은 면) · --sys 안내(가운데 작은 글).
   같은 쪽 말풍선이 이어지면 간격을 좁히고, 마지막 것만 말꼬리 모서리를 작게 둔다.
   나중에 끼워 넣은 말풍선은 보낸 쪽 모서리에서 부풀며 올라온다(extras.js 가 bubble--new 를 붙인다). */
.chat{display:flex;flex-direction:column;gap:var(--s-4);padding-block:var(--s-8)}
.chat__day{align-self:center;margin-block:var(--s-12) var(--s-8);padding:var(--s-2) var(--s-10);border-radius:var(--r-full);
  background:var(--tint);color:var(--ink-muted-alt);font-size:var(--t-micro);line-height:var(--t-micro-lh);font-variant-numeric:tabular-nums}
.chat__who{align-self:flex-start;margin:var(--s-12) 0 var(--s-2) var(--s-4);font-size:var(--t-micro);font-weight:600;color:var(--ink-muted)}
.bubble{position:relative;display:flex;flex-direction:column;gap:var(--s-6);max-width:min(82%,34rem);padding:var(--s-10) var(--s-16);
  border-radius:var(--r-xl);font-size:var(--t-body-md);line-height:var(--t-body-sm-lh);word-break:keep-all;overflow-wrap:anywhere}
.bubble--me{align-self:flex-end;background:var(--accent);color:var(--accent-ink);transform-origin:100% 100%}
.bubble--ai{align-self:flex-start;background:var(--tint);color:var(--ink);transform-origin:0 100%}
.bubble--sys{align-self:center;max-width:100%;padding:var(--s-4) var(--s-12);background:transparent;color:var(--ink-muted);
  font-size:var(--t-caption);line-height:var(--t-caption-lh);text-align:center}
.bubble--me:not(:has(+ .bubble--me)){border-bottom-right-radius:var(--r-xs)}
.bubble--ai:not(:has(+ .bubble--ai)){border-bottom-left-radius:var(--r-xs)}
:is(.bubble--me,.chat__who) + .bubble--ai,.bubble--ai + .bubble--me,.bubble--sys + .bubble:not(.bubble--sys),.bubble:not(.bubble--sys) + .bubble--sys{margin-top:var(--s-12)}
.chat__who + .bubble--ai{margin-top:0}
.bubble__body{white-space:pre-wrap;text-wrap-style:pretty}
.bubble__body a{color:inherit;text-decoration:underline;text-underline-offset:2px}
.bubble__time{align-self:flex-end;margin-top:calc(var(--s-2) * -1);font-size:var(--t-nano);line-height:var(--t-nano-lh);
  color:var(--ink-muted);font-variant-numeric:tabular-nums}
.bubble--me .bubble__time{color:inherit}
.bubble__img{display:block;width:100%;max-width:240px;border-radius:var(--r-md);background:var(--canvas-sunken)}
.bubble__btns{display:flex;flex-wrap:wrap;gap:var(--s-6);padding-top:var(--s-4)}
.bubble__btns .btn{min-height:var(--c-md-h)}
/* 보내는 중 · 못 보냄: 글은 그대로 두고 상태 한 줄을 붙인다. 다시 보내기는 누르는 칸 44px. */
.bubble--sending{opacity:.62}
.bubble--failed.bubble--me{background:var(--bad-soft);color:var(--ink)}
.bubble__status{display:flex;align-items:center;justify-content:flex-end;gap:var(--s-4);margin-top:calc(var(--s-4) * -1);
  font-size:var(--t-micro);font-weight:600;color:var(--bad-text)}
.bubble--sending .bubble__status{color:inherit;opacity:.8;font-weight:500}
.bubble__retry{position:relative;display:inline-flex;align-items:center;min-height:28px;padding-inline:var(--s-8);border-radius:var(--r-sm);
  color:var(--bad-text);font-weight:700;text-decoration:underline;text-underline-offset:2px}
.bubble__retry::before{content:"";position:absolute;inset:50% auto auto 50%;translate:-50% -50%;width:max(100%,44px);height:44px}
@media (hover:hover){.bubble__retry:hover{background:var(--bad-soft)}}
/* 입력 중: 점 셋이 차례로 뛴다 */
.bubble--typing{padding-block:var(--s-12)}
.typing{display:inline-flex;align-items:center;gap:var(--s-4);height:var(--s-20);padding-inline:var(--s-2);color:var(--ink-muted)}
.typing > i{display:block;width:var(--s-6);height:var(--s-6);border-radius:var(--r-full);background:currentColor;opacity:.35;
  animation:typing-dot calc(var(--d-7) + var(--d-3)) var(--ease-inout) infinite}
.typing > i:nth-child(2){animation-delay:var(--d-2)}
.typing > i:nth-child(3){animation-delay:calc(var(--d-2) * 2)}
@keyframes typing-dot{0%,60%,100%{opacity:.35;transform:none}30%{opacity:1;transform:translateY(calc(var(--s-4) * -1))}}
.bubble--new{animation:bubble-in var(--d-3) var(--ease-out) both}
.bubble--sys.bubble--new{animation:fade-in var(--d-3) var(--ease-out) both}
@keyframes bubble-in{from{opacity:0;transform:translateY(var(--s-8)) scale(.92)}to{opacity:1;transform:none}}

/* ══ 아바타 묶음 ═══════════════════════════════════════
   겹친 얼굴. 둘레 고리는 놓인 바탕색이어야 겹친 선이 깔끔하다(--av-ring). 마우스를 올리면 살짝 벌어진다.
   코어 .avatar-stack 의 다듬은 판이다. 크기·고리·이니셜·남은 수를 한 번에 맞춘다. */
.avatars{--av:36px;--av-ring:var(--canvas);display:inline-flex;align-items:center;flex:none}
.surface-alt .avatars,.on-alt .avatars{--av-ring:var(--canvas-alt)}
.card .avatars,.group .avatars{--av-ring:var(--surface)}
.avatars > *{display:grid;place-items:center;flex:none;width:var(--av);height:var(--av);margin-left:calc(var(--av) * -.3);
  border-radius:var(--r-full);background:var(--canvas-alt);box-shadow:0 0 0 2px var(--av-ring);object-fit:cover;overflow:hidden;
  font-size:calc(var(--av) * .38);font-weight:700;line-height:1;letter-spacing:-.02em;color:var(--ink-muted);
  transition:margin var(--d-3) var(--ease-out)}
.avatars > :first-child{margin-left:0}
.avatars--sm{--av:28px}
.avatars--lg{--av:44px}
.avatars--xl{--av:56px}
@media (hover:hover){.avatars:hover > :not(:first-child){margin-left:calc(var(--av) * -.14)}}
.avatars__more{background:var(--tint-hover);color:var(--ink-muted-alt);font-size:calc(var(--av) * .32);font-variant-numeric:tabular-nums}
/* 사진이 없을 때 이니셜. 역할색 면 네 가지(누구인지 색으로만 구분하지 않는다. 글자가 다르다). */
.avatar--text{display:grid;place-items:center;font-weight:700;color:var(--ink-muted);background:var(--canvas-alt)}
.avatar--accent{background:var(--accent-soft);color:var(--accent-text)}
.avatar--good{background:var(--good-soft);color:var(--good-text)}
.avatar--warn{background:var(--warn-soft);color:var(--warn-text)}
.avatar--note{background:var(--note-soft);color:var(--note-text)}

/* ══ 가격 ═════════════════════════════════════════════
   숫자가 주인공이다. 단위·기간은 한 단계 작게, 정가는 위에 흐리게 긋는다. 숫자 폭은 고정해 값이 바뀌어도 흔들리지 않는다. */
.price{display:flex;flex-wrap:wrap;align-items:baseline;column-gap:var(--s-2);row-gap:var(--s-2);color:var(--ink);font-variant-numeric:tabular-nums}
.price__was{flex-basis:100%;font-size:var(--t-body-sm);line-height:var(--t-body-sm-lh);color:var(--ink-subtle);
  text-decoration:line-through;text-decoration-thickness:1.5px;text-decoration-color:var(--ink-disabled)}
.price__num{font-size:var(--t-display-sm);line-height:1.1;letter-spacing:var(--t-display-sm-ls);font-weight:700}
.price__unit{font-size:var(--t-heading-sm);line-height:1.2;font-weight:700;letter-spacing:var(--t-heading-sm-ls)}
.price__per{margin-left:var(--s-4);font-size:var(--t-body-sm);color:var(--ink-muted)}
.price__save{align-self:center;margin-left:var(--s-8);padding:var(--s-2) var(--s-8);border-radius:var(--r-full);
  background:var(--accent-soft);color:var(--accent-text);font-size:var(--t-micro);font-weight:700;white-space:nowrap}
.price__note{flex-basis:100%;margin-top:var(--s-4);font-size:var(--t-caption);line-height:var(--t-caption-lh);color:var(--ink-muted)}
.price--lg .price__num{font-size:var(--t-display-md);letter-spacing:var(--t-display-md-ls)}
.price--sm .price__num{font-size:var(--t-heading-lg);letter-spacing:var(--t-heading-lg-ls)}
.price--sm .price__unit{font-size:var(--t-body-md)}
.price--muted{color:var(--ink-muted)}
@media (max-width:640px){
  .price__num{font-size:var(--t-heading-xl);letter-spacing:var(--t-heading-xl-ls)}
  .price--lg .price__num{font-size:var(--t-display-sm);letter-spacing:var(--t-display-sm-ls)}
  .price--sm .price__num{font-size:var(--t-heading-md);letter-spacing:var(--t-heading-md-ls)}
}

/* ══ 맨 위 알림 띠 ═════════════════════════════════════
   서비스 전체에 알릴 한 줄(점검·새 기능·혜택). 닫으면 기억한다(data-announce="키", 브라우저 저장소).
   기본은 반전면. --accent 강조, --soft 연한 강조, --warn 주의. 상단 내비보다 위에 두고 내비는 그 아래에서 붙는다. */
.announce{position:relative;overflow:hidden;background:var(--surface-invert);color:var(--canvas)}
.announce[hidden]{display:none}
.announce__inner{display:flex;align-items:center;justify-content:center;gap:var(--s-10);min-height:var(--c-md-h);padding-block:var(--s-6)}
.announce__text{min-width:0;font-size:var(--t-body-xs);line-height:var(--t-body-xs-lh);font-weight:500;text-align:center;word-break:keep-all;text-wrap:balance}
/* 배지는 글 안 맨 앞에 둔다(글과 붙어 읽히게). <p class="announce__text"><span class="announce__badge">새 기능</span>…</p> */
.announce__badge{display:inline-block;margin-right:var(--s-8);padding:0 var(--s-8);border-radius:var(--r-full);
  background:color-mix(in srgb,currentColor 14%,transparent);font-size:var(--t-micro);line-height:20px;font-weight:700;white-space:nowrap;vertical-align:1px}
.announce__link{position:relative;margin-left:var(--s-6);font-weight:700;white-space:nowrap;color:inherit;
  text-decoration:underline;text-underline-offset:3px;text-decoration-color:color-mix(in srgb,currentColor 45%,transparent);
  transition:text-decoration-color var(--d-1) var(--ease-inout)}
.announce__link::before{content:"";position:absolute;inset:50% auto auto 50%;translate:-50% -50%;width:calc(100% + var(--s-16));height:44px}
@media (hover:hover){.announce__link:hover{text-decoration-color:currentColor}}
.announce__close{flex:none;display:grid;place-items:center;width:var(--c-md-h);height:var(--c-md-h);margin-right:calc(var(--s-12) * -1);
  border-radius:var(--r-md);color:inherit;opacity:.7;cursor:pointer;transition:opacity var(--d-1) var(--ease-inout),background-color var(--d-1) var(--ease-inout)}
.announce__close svg{width:18px;height:18px}
@media (hover:hover){.announce__close:hover{opacity:1;background:color-mix(in srgb,currentColor 12%,transparent)}}
.announce__close:active{opacity:1;background:color-mix(in srgb,currentColor 18%,transparent)}
.announce__close:focus-visible{outline-color:currentColor;outline-offset:-4px}
/* 닫기 버튼이 있으면 가운데 글이 치우치지 않게 왼쪽에도 같은 폭을 비운다 */
.announce__inner:has(.announce__close)::before{content:"";flex:none;width:calc(var(--c-md-h) - var(--s-12))}
.announce__inner:has(.announce__close) .announce__text{flex:1}
.announce--accent{background:var(--accent);color:var(--accent-ink)}
.announce--soft{background:var(--accent-soft);color:var(--accent-text)}
.announce--warn{background:var(--warn-soft);color:var(--warn-text)}
.announce[data-closing]{transition:height var(--d-4) var(--ease-inout),opacity var(--d-2) var(--ease-in)}
@media (max-width:640px){
  .announce__inner:has(.announce__close)::before{display:none}
  .announce__text{text-align:left}
}

/* ══ Turbo 진행 막대 ═══════════════════════════════════
   Turbo 가 늦은 방문에 띄우는 막대를 강조색 2px 로 맞춘다(코어 .progressbar 와 같은 두께). */
.turbo-progress-bar{height:2px;background:var(--accent);box-shadow:0 0 8px var(--accent-line)}

/* ══ 숫자 폭 고정 ══════════════════════════════════════
   바뀌는 숫자(시간·금액·카운트다운)는 글자 폭을 고정해 옆 글자가 흔들리지 않게 한다. 코어 .num 과 같다. */
.tnum{font-variant-numeric:tabular-nums}

/* ══ 빛 ═══════════════════════════════════════════════
   강조 버튼·글자 위로 빛이 한 번 지나간다. 화면에 들어올 때 한 번(extras.js), dsShine(el) 로 다시.
   계속 반복하지 않는다. 반복하는 빛은 금방 소음이 된다. 빛은 늘 흰빛이라 두 테마 모두 흰색인 --media-ink 를 섞는다.
   그라데이션을 요소 폭의 세 배로 두고 위치를 100% → 0% 로 옮긴다. 이 범위에서는 그림이 늘 요소를 다 덮고(글자가 비지 않는다)
   빛 띠(40~60%)는 양 끝에서 요소 밖에 있다. */
.shine{position:relative;isolation:isolate}
.shine::after{content:"";position:absolute;inset:0;z-index:1;border-radius:inherit;pointer-events:none;opacity:0;
  background:linear-gradient(110deg,transparent 40%,color-mix(in srgb,var(--media-ink) 46%,transparent) 50%,transparent 60%) no-repeat;
  background-size:300% 100%;background-position:100% 0}
.shine.is-shining::after{animation:shine-sweep var(--d-7) var(--ease-inout) both}
.shine[data-busy]::after{animation:spin .62s linear infinite;opacity:1;background:none}
@keyframes shine-sweep{0%{opacity:1;background-position:100% 0}100%{opacity:1;background-position:0 0}}
/* 글자: 제목 위로 강조색 빛이 한 번 지나간다. 스크립트가 없어도 글자는 --shine-ink 로 그대로 읽힌다. */
.shine--text{--shine-ink:var(--ink);--shine-glow:var(--accent);
  background:linear-gradient(110deg,var(--shine-ink) 40%,var(--shine-glow) 50%,var(--shine-ink) 60%) no-repeat;
  background-size:300% 100%;background-position:100% 0;-webkit-background-clip:text;background-clip:text;color:transparent}
.shine--text::after{display:none}
.shine--text.is-shining{animation:shine-text calc(var(--d-7) + var(--d-4)) var(--ease-inout) both}
@keyframes shine-text{from{background-position:100% 0}to{background-position:0 0}}
@media (forced-colors:active){.shine--text{background:none;color:CanvasText}}

/* ══ 축하 입자 (dsCelebrate) ═══════════════════════════
   extras.js 가 조각 20~30개를 만들어 1초 안에 흩뿌리고 걷는다. 모양만 여기서 정한다. 색은 역할색 네 가지(--accent --good --warn --note-text). */
.celebrate{position:fixed;inset:0;z-index:var(--z-tip);pointer-events:none;overflow:hidden;contain:strict}
.celebrate__bit{position:absolute;top:0;left:0;width:8px;height:12px;margin:-6px 0 0 -4px;border-radius:2px;
  background:var(--c,var(--accent));will-change:transform,opacity}
.celebrate__bit--dot{width:8px;height:8px;margin:-4px 0 0 -4px;border-radius:50%}
.celebrate__bit--strip{width:4px;height:14px;margin:-7px 0 0 -2px;border-radius:2px}

/* ══ 툴팁 (data-tip) ═══════════════════════════════════
   모양·등장 움직임·다크 테두리는 코어 .tip. extras.js 가 화면 좌표(position:fixed)로 띄우고(위, 자리가 없으면 아래 data-side="bottom") 꼬리를 단다.
   마우스를 올리고 잠깐 뒤, 또는 키보드 초점이 오면 바로 뜬다. Escape 로 닫힌다. 누르는 동작의 이름을 대신하지 않는다(aria-label 은 따로). */
.tip[data-floating]{position:fixed;top:0;left:0}
.tip[data-floating]::after{content:"";position:absolute;left:var(--tip-x,50%);bottom:-4px;width:8px;height:8px;margin-left:-4px;
  background:inherit;border-radius:1px;rotate:45deg}
.tip[data-floating][data-side="bottom"]::after{bottom:auto;top:-4px}
.tip-trigger{position:relative;display:inline-grid;place-items:center;width:20px;height:20px;border-radius:50%;
  color:var(--ink-subtle);vertical-align:middle;cursor:help}
.tip-trigger::before{content:"";position:absolute;inset:50% auto auto 50%;translate:-50% -50%;width:44px;height:44px}
@media (hover:hover){.tip-trigger:hover{color:var(--ink-body)}}

/* ══ 구분선 ═══════════════════════════════════════════
   '또는' 처럼 글이 가운데 끼는 선. 글이 없으면 그냥 선이다. */
.divider{display:flex;align-items:center;gap:var(--s-12);color:var(--ink-subtle);font-size:var(--t-caption);line-height:var(--t-caption-lh);white-space:nowrap}
.divider::before,.divider::after{content:"";flex:1;height:1px;background:var(--line)}
.divider:empty{gap:0}

/* ══ 비율 상자 ═════════════════════════════════════════
   사진·영상·지도가 불러와지기 전에도 자리를 잡아 글이 밀려 내려가지 않게 한다. */
.ratio{position:relative;aspect-ratio:var(--ratio,16/9);overflow:hidden;border-radius:inherit;background:var(--canvas-sunken)}
.ratio > *{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;border:0}
.ratio--1x1{--ratio:1}
.ratio--4x3{--ratio:4/3}
.ratio--3x2{--ratio:3/2}
.ratio--3x4{--ratio:3/4}
.ratio--9x16{--ratio:9/16}
.ratio--21x9{--ratio:21/9}

/* ══ 본문 바로가기 ═════════════════════════════════════
   키보드 사용자가 첫 Tab 에서 긴 내비를 건너뛴다. 초점이 올 때만 보인다. <a class="skip-link" href="#main">본문 바로가기</a> */
.skip-link{position:fixed;top:calc(var(--safe-t) + var(--s-12));left:var(--s-16);z-index:calc(var(--z-tip) + 1);
  padding:var(--s-10) var(--s-16);border-radius:var(--r-md);background:var(--ink);color:var(--canvas);
  font-size:var(--t-body-sm);font-weight:600;
  translate:0 calc(-100% - var(--s-24) - var(--safe-t));transition:translate var(--d-2) var(--ease-out)}
/* 그림자는 보일 때만 준다. 숨어 있는 동안 그림자가 화면 위쪽에 번져 보이지 않게. */
.skip-link:focus,.skip-link:focus-visible{translate:0 0;outline-offset:3px;box-shadow:var(--sh-lg)}

/* ══ 스크롤하면 숨는 헤더 (data-autohide) ═══════════════
   아래로 읽는 동안은 비켜 주고, 위로 조금만 올려도 다시 나온다. 좁은 화면에서 읽는 자리를 넓힌다.
   초점이 헤더 안에 있으면 숨지 않는다. 숨는 동안 html[data-header-hidden] 이라 구역 내비(.subnav)가 맨 위로 붙는다. */
[data-autohide]{transition:translate var(--d-4) var(--ease-out),background-color var(--d-3) var(--ease-inout),box-shadow var(--d-3) var(--ease-inout)}
[data-autohide][data-hidden]:not(:focus-within){translate:0 calc(-100% - 2px)}
html[data-header-hidden] .subnav{top:0;transition:top var(--d-4) var(--ease-out)}

/* ══ 모션 축소 ════════════════════════════════════════
   움직임은 빼고 완성된 모습만 남긴다. 그림은 다 그려진 채, 빛·입자·퍼지는 고리는 없다. */
@media (prefers-reduced-motion:reduce){
  .result > *,.check-anim *,.bubble--new,.typing > i,.timeline__mark::after,.shine::after,.shine--text,
  .code__copy .code__copy-done{animation:none !important}
  .check-anim__halo{display:none}
  .pressable,.pressable[data-pressed]{transform:none}
  ::view-transition-group(*),::view-transition-old(*),::view-transition-new(*){animation:none !important}
  .celebrate{display:none}
}
