/* Default skin — the reference implementation every other skin is copied from.
 *
 * TOKENS LIVE IN THE CORE, NOT HERE.
 * static/core/tokens.css defines every --mg-* default and the shell loads it
 * before this file (docs/DECISIONS.md D9-1). This stylesheet only consumes
 * them. Redefine a token in a :root block here to override a core default —
 * do not restate the whole set, or the fallback layer stops being observable.
 *
 * NO HARDCODED COLORS OR SIZES (CLAUDE.md 코드 스타일). Everything below reads
 * --mg-*. A literal value here would be invisible to the theme editor and to
 * every skin that inherits this file's structure.
 *
 * SECTIONS
 *   1. base elements
 *   2. shell — layout shared by all four header placements
 *   3. shell — per-placement rules (top / bottom / left / right)
 *   4. content typography
 *   5. components
 *   6. forms
 *   7. block engine
 *   8. boards
 *   9. characters
 *  10. manage
 *  11. narrow viewports
 */

/* ------------------------------------------------------------------ *
 * 1. base elements
 * ------------------------------------------------------------------ */

* {
  box-sizing: border-box;
}

/* Two background layers. --mg-bg-image is the admin's own choice and sits on
   TOP; ROOT's paper texture is baked underneath as part of the skin's identity
   (the design system puts it on body itself). With no admin image the token is
   `none`, which draws an empty layer and lets the texture through — so baking
   it in never blocks a background the admin sets later.
   ⚠ repeat/position/size/attachment are single values and apply to BOTH
   layers; that is why the texture is authored full-bleed at 1920x1080. */
body {
  margin: 0;
  background-color: var(--mg-bg);
  /* 층 순서: 관리자 배경 → 밝히는 베일 → 종이 질감. 베일이 가운데라 질감만
     밝아지고 관리자가 올린 이미지는 손대지 않는다. 세기는 토큰이다. */
  background-image:
    var(--mg-bg-image),
    linear-gradient(rgb(var(--mg-bg-texture-veil) / var(--mg-bg-texture-lighten)) 0 100%),
    url("bg-paper-texture.0fd4ffd7c6d7.webp");
  background-repeat: var(--mg-bg-repeat);
  background-position: var(--mg-bg-position);
  background-size: var(--mg-bg-size);
  background-attachment: var(--mg-bg-attachment);
  color: var(--mg-text);
  font-family: var(--mg-font-body);
  font-size: var(--mg-font-size-base);
  line-height: var(--mg-line-height);
  word-break: keep-all;
}

/* ROOT's display voice is heavy squared geometric: flat terminals, wide
   counters, set tight. It never reads delicate, so headings start at 800 and
   only step down at h3. Weight 400 would fall back to the body voice. */
h1, h2, h3, h4, h5, h6 {
  font-family: var(--mg-font-heading);
  font-weight: 800;
  line-height: 1.24;
  letter-spacing: var(--mg-track-tight);
  margin: 0 0 var(--mg-space-3);
}

h1 { font-size: var(--mg-font-size-xl); }
h2 { font-size: var(--mg-font-size-lg); }
h3 { font-size: var(--mg-font-size-md); font-weight: 500; line-height: 1.38; }

/* h4 hands off to the body voice: below h3 a squared display face stops
   reading as structure and starts reading as noise. */
h4 {
  font-family: var(--mg-font-body);
  font-size: var(--mg-font-size-base);
  font-weight: 600;
  letter-spacing: var(--mg-track-wide);
}

/* h5 and h6 are system labels, not titles: mono, uppercase, tracked out. */
h5, h6 {
  font-family: var(--mg-font-mono);
  font-size: var(--mg-font-size-xs);
  font-weight: 700;
  letter-spacing: var(--mg-track-wider);
  text-transform: uppercase;
  color: var(--mg-text-muted);
}

a {
  color: var(--mg-link);
  text-decoration: none;
}

a:hover {
  color: var(--mg-link-hover);
  text-decoration: underline;
}

img {
  max-width: 100%;
}

/* A ROOT rule is a bar, not a line: a solid ink block with a gold tail. The
   tail is what keeps a divider from reading as a border. */
hr {
  border: 0;
  height: var(--mg-bar-h);
  background: var(--mg-bar-ink);
  margin: var(--mg-space-6) 0;
  position: relative;
}

hr::after {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-end: 0;
  width: 22%;
  background: var(--mg-bar-gold);
}

/* Gold is rationed to one element per view, so selection is where it can be
   spent freely: it exists only while the reader is holding it. */
::selection {
  background: var(--mg-accent);
  color: var(--mg-accent-text);
}

/* 2px ink edge plus a soft gold halo. The halo is what makes the focus ring
   visible against both the paper ground and a white card. */
:focus-visible {
  outline: var(--mg-stroke-1) solid var(--mg-border-strong);
  outline-offset: 2px;
  box-shadow: 0 0 0 4px var(--mg-accent-soft);
}

/* ROOT motion primitives. Enter slow and eased out, exit fast; nothing
   bounces and nothing loops. Names carry the mg- prefix for the same reason
   tokens do — admin-pasted CSS shares this namespace. */
@keyframes mg-rise {
  from { opacity: 0; transform: translateY(18px); }
  to { opacity: 1; transform: none; }
}

@keyframes mg-wipe {
  from { clip-path: inset(0 100% 0 0); }
  to { clip-path: inset(0 0 0 0); }
}

@keyframes mg-pulse {
  0%, 100% { opacity: 0.35; }
  50% { opacity: 1; }
}

/* A sweep is a READ, not a shine: one pass, left to right, never looped
   (design system guidelines/frame-variants). mg-flash is the press
   acknowledgement -- it starts at the tone and leaves. */
@keyframes mg-sweep {
  from { background-position: -70% 0; }
  to { background-position: 170% 0; }
}

@keyframes mg-flash {
  from { opacity: 0.4; }
  to { opacity: 0; }
}

@keyframes mg-fade-out {
  from { opacity: 1; }
  to { opacity: 0; }
}

@keyframes mg-fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* ------------------------------------------------------------------ *
 * 1-b. navigation — one rule for the whole site
 *
 * Two effects with separate jobs. The View Transitions API crosses the
 * old screen out over --mg-transition-fast; the elements of the new one
 * arrive over --mg-transition with the ladder below. The crossing is the
 * shorter of the two on purpose, so it reads as "the old screen left"
 * rather than as a second entrance competing with the ladder.
 *
 * Nothing here is scoped to a screen: #content is the only node hx-boost
 * ever replaces, so one rule set reaches every page, board, character and
 * manage screen. The shell is excluded by the same fact.
 *
 * Contract: docs/SKIN_API.md "화면 전환·등장 계약".
 * ------------------------------------------------------------------ */

/* ⛔ Both halves are retimed together, with the same duration token and the
   same easing. The browser isolates the pair and blends it with plus-lighter,
   so the two opacities are added: matching curves keep the sum at exactly 1
   for the whole crossing, while retiming one side alone makes the middle of
   the transition visibly brighter -- a flash with nothing in the console.
 *
 * ⚠ The incoming half is mostly bare while this runs -- every #content child
 * past the first is still held at opacity 0 by its stagger delay -- so the
 * crossing reads as the old screen leaving rather than as a second fade
 * layered over the ladder. Lengthening it past --mg-transition would undo
 * that. */
::view-transition-old(root) {
  animation: mg-fade-out var(--mg-duration-fast) var(--mg-ease-out) both;
}

::view-transition-new(root) {
  animation: mg-fade-in var(--mg-duration-fast) var(--mg-ease-out) both;
}

/* Everything hx-boost swaps in, plus everything a full page load parses, is a
   direct child of #content -- so a single rule covers every screen. No script:
   a CSS animation restarts by itself whenever the nodes are freshly parsed.
 *
 * ⛔ The hidden starting state lives in the keyframes with fill-mode backwards,
 * never in this declaration. An `opacity: 0` here would leave a blank screen
 * anywhere animations do not run (print, blockers, older browsers) with no
 * error to show for it.
 *
 * ⛔ No will-change / translateZ hint. The lightbox overlay is a position:fixed
 * element inside #content, and a transform that outlives the animation would
 * become its containing block and stop it covering the viewport. mg-rise ends
 * on `transform: none`, so nothing survives the 240ms.
 *
 * ⚠ A board with custom CSS carries a <link> as the first child of #content.
 * It is display:none but nth-child still counts it, so that one board's
 * stagger starts a step late. Left alone: the link has to stay inside #content
 * or a boosted navigation would drop the board's stylesheet. */
#content > * {
  animation: mg-rise var(--mg-duration) var(--mg-ease-out) backwards;
}

#content > :nth-child(2) { animation-delay: var(--mg-enter-stagger); }
#content > :nth-child(3) { animation-delay: calc(2 * var(--mg-enter-stagger)); }
#content > :nth-child(4) { animation-delay: calc(3 * var(--mg-enter-stagger)); }
#content > :nth-child(5) { animation-delay: calc(4 * var(--mg-enter-stagger)); }
#content > :nth-child(6) { animation-delay: calc(5 * var(--mg-enter-stagger)); }
/* Cap. The home screen puts every block straight into #content, so an
   uncapped ladder would make the last block of a long page arrive seconds
   after the first. */
#content > :nth-child(n+7) { animation-delay: calc(6 * var(--mg-enter-stagger)); }

/* ------------------------------------------------------------------ *
 * 2. shell — shared by all four header placements
 *
 * The shell DOM is core-owned and never re-rendered by hx-boost
 * (CLAUDE.md 금지 8·9). Placement is switched with a <body> class alone, so
 * the markup is identical in all four modes.
 * ------------------------------------------------------------------ */

.mg-shell {
  display: grid;
  min-height: 100vh;
}

.mg-shell-header {
  grid-area: header;
  background: var(--mg-surface);
  display: flex;
  align-items: center;
  gap: var(--mg-space-5);
  padding: var(--mg-space-3) var(--mg-space-5);
}

.mg-shell-brand {
  font-family: var(--mg-font-heading);
  font-size: var(--mg-font-size-lg);
  color: var(--mg-link);
  text-decoration: none;
  white-space: nowrap;
}

.mg-shell-brand:hover {
  color: var(--mg-link-hover);
  text-decoration: none;
}

.mg-shell-nav-toggle {
  display: none;
  background: transparent;
  border: var(--mg-border-width) solid var(--mg-border);
  border-radius: var(--mg-radius-pill);
  color: var(--mg-text);
  font: inherit;
  padding: var(--mg-space-1) var(--mg-space-3);
  cursor: pointer;
}

.mg-shell-nav {
  display: flex;
  align-items: center;
  gap: var(--mg-space-2);
  flex: 1;
  justify-content: flex-end;
}

.mg-shell-nav-item {
  position: relative;
}

/* ⛔ `.mg-shell-nav span` 처럼 후손 전체를 잡지 않는다. 그 선택자는 명시도가
   (0,1,1)이라 `.mg-icon`(0,1,0)의 font-size를 이기고, 메뉴 아이콘만 라벨 크기로
   눌린다 — 오류는 없고 계정 링크의 아이콘과 크기만 달라진다. 링크가 없는 항목의
   껍데기(그룹 헤더와 하위 항목)만 직접 자식으로 집는다. */
.mg-shell-nav a,
.mg-shell-nav > .mg-shell-nav-item > span,
.mg-shell-subnav > span,
.mg-shell-account a {
  display: block;
  color: var(--mg-text-muted);
  font-family: var(--mg-font-heading);
  font-size: var(--mg-font-size-sm);
  font-weight: 500;
  text-decoration: none;
  padding: var(--mg-space-2) var(--mg-space-3);
  border-radius: var(--mg-radius-pill);
  white-space: nowrap;
  transition: background var(--mg-transition-fast), color var(--mg-transition-fast);
}

.mg-shell-nav a:hover,
.mg-shell-account a:hover {
  color: var(--mg-link);
  background: var(--mg-accent-soft);
  text-decoration: none;
}

.mg-shell-nav a:not([aria-current="page"]) {
  position: relative;
}

.mg-shell-nav a:not([aria-current="page"])::after {
  content: "";
  position: absolute;
  inset-inline-start: 0;
  bottom: 0;
  width: 28px;
  height: var(--mg-bar-h);
  background: var(--mg-bar-gold);
  opacity: 0;
  transition: width var(--mg-transition), opacity var(--mg-transition-fast);
}

.mg-shell-nav a:not([aria-current="page"]):hover::after {
  width: 100%;
  opacity: 1;
}

/* Active menu item. ROOT marks the current page with a 4px gold bar on the
   edge facing the stage, so the active item points at the content it opened.
   The bar is gold; the label is not — gold type fails contrast on this ground
   (docs/SKIN_API.md 토큰 목록). */
.mg-shell-nav [aria-current="page"] {
  color: var(--mg-link);
  background: var(--mg-accent-soft);
  text-decoration: none;
  position: relative;
}

.mg-shell-nav [aria-current="page"]::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  height: var(--mg-bar-h);
  background: var(--mg-bar-gold);
}

.mg-shell-subnav {
  display: none;
  background: var(--mg-surface);
  border: var(--mg-border-width) solid var(--mg-border);
  border-radius: var(--mg-radius-sm);
  box-shadow: var(--mg-shadow-lift);
  padding: var(--mg-space-1);
  min-width: 100%;
}

.mg-shell-nav-item:hover > .mg-shell-subnav,
.mg-shell-nav-item:focus-within > .mg-shell-subnav {
  display: block;
  position: absolute;
  inset-inline-start: 0;
  z-index: 1;
}

.mg-shell-account {
  display: flex;
  gap: var(--mg-space-1);
}

#mango-player-root:empty {
  display: none;
}

/* Dark mode switch (docs/SKIN_API.md "다크모드 계약"). Same round dock target as
   the BGM button, and deliberately its neighbour: that spot has already been
   proven in all four header placements. */
.mg-theme-toggle {
  flex: none;
  display: grid;
  place-items: center;
  inline-size: var(--mg-bgm-button-size);
  block-size: var(--mg-bgm-button-size);
  padding: 0;
  border: var(--mg-stroke-1) solid transparent;
  border-radius: var(--mg-radius-pill);
  background: transparent;
  color: var(--mg-text);
  cursor: pointer;
  transition: color var(--mg-transition-fast), border-color var(--mg-transition-fast);
}

/* ⛔ Hover moves the ICON, not the box (2026-09-02 사람 지시). Growing a ring
   and a plate under a bare dock icon made the widget jump around as the pointer
   crossed it. ⚠ The transparent border stays so nothing shifts when a state
   rule paints it, and :focus-visible is deliberately NOT here -- the global
   focus ring near the top of this file already draws one, and a colour-only
   change is too weak an indicator for keyboard users on its own.
   ⛔ Gold as a FOREGROUND steps to --mg-link, never --mg-accent: that is the
   ROOT rule that keeps gold type legible in both themes (tokens.css). */
.mg-theme-toggle:hover {
  color: var(--mg-link);
}

/* ⛔ The glyph is chosen here, not by JS swapping text. Material Symbols render
   by ligature, so a script that rewrites the word would put "dark_mode" on
   screen as letters whenever the icon CDN is blocked -- one more place with
   that failure (docs/OPEN-ISSUES.md). Both glyphs ship; CSS picks one.
   The icon shows the DESTINATION, not the current state. */
.mg-theme-toggle__light,
:root[data-mg-theme="dark"] .mg-theme-toggle__dark {
  display: none;
}

:root[data-mg-theme="dark"] .mg-theme-toggle__light {
  display: block;
}

/* BGM widget (docs/plans/v0.3-c-1-bgm-widget.md §4-5, docs/DECISIONS.md D2-1).
   One round icon the size of a dock target: click toggles play, hover opens the
   volume panel beside it. The YouTube frame fills the same box behind the
   button and is hidden with opacity — it keeps its size and stays rendered and
   composited, so playback is unaffected, and unlike an opaque plate it assumes
   nothing about the colour behind the widget.
   ⛔ Never display:none, visibility:hidden or a zero size on the mount — the
   browser may stop rendering the frame and playback stops with no error. */
.mg-bgm {
  position: relative;
  flex: none;
  inline-size: var(--mg-bgm-button-size);
  block-size: var(--mg-bgm-button-size);
  isolation: isolate;
}

.mg-bgm__mount {
  position: absolute;
  inset: 0;
  z-index: 0;
  opacity: 0;
  pointer-events: none;
}

.mg-bgm__mount iframe {
  inline-size: 100%;
  block-size: 100%;
  border: 0;
  font-family: var(--mg-font-body);
}

.mg-bgm__toggle {
  position: relative;
  z-index: 2;
  display: grid;
  place-items: center;
  inline-size: 100%;
  block-size: 100%;
  padding: 0;
  border: var(--mg-stroke-1) solid transparent;
  border-radius: var(--mg-radius-pill);
  background: transparent;
  color: var(--mg-text);
  cursor: pointer;
  transition: color var(--mg-transition-fast), border-color var(--mg-transition-fast);
}

/* Same rule as the theme toggle beside it -- see the note there for why hover
   moves the icon and not the box. */
.mg-bgm__toggle:hover {
  color: var(--mg-link);
}

/* ⚠ This one is STATE, not pointer feedback: the gold ring says music is
   playing, which hover has no way to express. It stays. */
.mg-bgm[data-bgm-state="playing"] .mg-bgm__toggle {
}

.mg-bgm[data-bgm-state="error"] .mg-bgm__toggle {
  color: var(--mg-text-muted);
  cursor: default;
}

/* Volume flyout. It touches the button with no gap so the pointer can travel
   into it without crossing dead space and closing it. */
.mg-bgm__panel {
  position: absolute;
  z-index: 3;
  top: 100%;
  inset-inline-end: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--mg-space-2);
  inline-size: var(--mg-bgm-panel-width);
  padding: var(--mg-space-2) var(--mg-space-3);
  border-radius: var(--mg-radius-sm);
  background: var(--mg-text);
  color: var(--mg-bg);
  box-shadow: var(--mg-shadow-float);
  opacity: 0;
  pointer-events: none;
  transform: translateY(-6px);
  transition: opacity var(--mg-transition-fast), transform var(--mg-transition);
}

:where(.mg-bgm:hover, .mg-bgm:focus-within) .mg-bgm__panel {
  opacity: 1;
  pointer-events: auto;
  transform: translateY(0);
}

.mg-bgm__title {
  flex: 1 0 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--mg-font-mono);
  font-size: var(--mg-font-size-xs);
  letter-spacing: var(--mg-track-wide);
}

/* No track title yet (or the API never answered): the row would otherwise be
   an empty line pushing the slider down. */
.mg-bgm__title:empty {
  display: none;
}

.mg-bgm__volume-icon {
  flex: none;
}

.mg-bgm__volume {
  flex: 1;
  min-inline-size: 0;
  accent-color: var(--mg-accent);
}

/* Bottom dock opens upward, vertical rails open sideways — the same flyout
   geometry the rail already uses for its labels. */
body.mg-header-bottom .mg-bgm__panel {
  top: auto;
  bottom: 100%;
  transform: translateY(6px);
}

body.mg-header-bottom :where(.mg-bgm:hover, .mg-bgm:focus-within) .mg-bgm__panel {
  transform: translateY(0);
}

body.mg-header-left .mg-bgm__panel,
body.mg-header-right .mg-bgm__panel {
  top: 50%;
  bottom: auto;
}

body.mg-header-left .mg-bgm__panel {
  inset-inline: 100% auto;
  transform: translateY(-50%) translateX(-10px);
}

body.mg-header-right .mg-bgm__panel {
  inset-inline: auto 100%;
  transform: translateY(-50%) translateX(10px);
}

body.mg-header-left :where(.mg-bgm:hover, .mg-bgm:focus-within) .mg-bgm__panel,
body.mg-header-right :where(.mg-bgm:hover, .mg-bgm:focus-within) .mg-bgm__panel {
  transform: translateY(-50%) translateX(0);
}

/* The rail draws every target at one size; the player is one of them. */
body.mg-header-left .mg-bgm,
body.mg-header-right .mg-bgm {
  inline-size: var(--mg-shell-icon);
  block-size: var(--mg-shell-icon);
}

.mg-shell-scrim {
  display: none;
}

/* Back-to-top. Present in the DOM in all four placements, revealed by the
   runtime body.mg-scrolled class. */
.mg-shell-totop {
  display: none;
  position: fixed;
  inset-inline-end: var(--mg-space-5);
  bottom: var(--mg-space-5);
  z-index: 2;
  border: var(--mg-border-width) solid var(--mg-border);
  border-radius: var(--mg-radius-pill);
  background: var(--mg-surface);
  color: var(--mg-text);
  box-shadow: var(--mg-shadow-lift);
  padding: var(--mg-space-2) var(--mg-space-4);
  cursor: pointer;
  font-size: inherit;
  transition: transform var(--mg-transition-fast);
}

.mg-shell-totop:active {
  transform: scale(0.96);
}

body.mg-scrolled .mg-shell-totop {
  display: block;
}

.mg-shell-error {
  position: fixed;
  inset-inline: var(--mg-space-5);
  bottom: var(--mg-space-5);
  z-index: 5;
  border: var(--mg-border-width) solid var(--mg-danger);
  border-radius: var(--mg-radius-sm);
  background: var(--mg-danger-soft);
  color: var(--mg-text);
  box-shadow: var(--mg-shadow-lift);
  padding: var(--mg-space-3) var(--mg-space-4);
  text-align: center;
}

.mg-shell-error[hidden] {
  display: none;
}

/* The sidebar is a shell node, so it cannot live inside #content: hx-boost
   replaces that element on every navigation. This wrapper owns the box
   #content used to own, keeping the header gutter aligned without sidebar-
   aware variants. With the sidebar off, #content gets the old box back. */
.mg-shell-main {
  grid-area: content;
  display: flex;
  gap: var(--mg-space-5);
  width: 100%;
  max-width: var(--mg-content-width);
  margin: 0 auto;
  padding: var(--mg-space-6) var(--mg-space-5);
}

#content {
  flex: 1 1 auto;
  min-width: 0;
}

/* Screens a visitor reads straight through get the narrower box (a page body,
   a novel chapter); lists, the home screen and profiles are scanned rather than
   read and keep the full width.

   ⛔ Keyed off a marker INSIDE #content, never a <body> class the server wrote:
   hx-boost swaps #content and nothing else, so a server-set class goes stale the
   moment a boosted link crosses between a page and a board list. Same reason the
   manage sidebar switch is written this way.

   Fails open — a browser without :has() keeps the wider box, which is the state
   the site had before. */
.mg-shell-main:has(#content .mg-page-body),
.mg-shell-main:has(#content .mg-reader) {
  max-width: var(--mg-reading-width);
}

/* ⭐ 캐릭터 프로필은 세 칸을 나란히 두므로 훑는 폭보다도 넓다 (2026-09-06 사람 지시).
   ⛔ 판정은 위와 **같은 방식** — #content 안의 표식이다. 두 표면 모두 잡으려고
      선택자가 둘이다(블록 배열은 .mg-profile-layout, 폴백은 .mg-profile).
   ⚠ :has() 를 모르는 브라우저는 --mg-content-width 로 남는다. 좁아질 뿐 안 깨진다. */
.mg-shell-main:has(#content .mg-profile-layout),
.mg-shell-main:has(#content .mg-profile) {
  max-width: var(--mg-profile-width);
}

/* Hidden is the base state so an unknown or missing sidebar mode fails closed. */
.mg-shell-sidebar {
  display: none;
  flex: 0 0 var(--mg-shell-sidebar-width);
  min-width: 0;
  flex-direction: column;
  gap: var(--mg-space-5);
}

body.mg-sidebar-left .mg-shell-sidebar,
body.mg-sidebar-right .mg-shell-sidebar {
  display: flex;
}

/* DOM order remains content first. The left placement reverses only the visual
   row, so tab order still follows the reading order and needs no narrow-screen
   reorder. */
body.mg-sidebar-left .mg-shell-main {
  flex-direction: row-reverse;
}

/* The render tag must be flush with the aside tags or whitespace prevents
   :empty from releasing this otherwise empty column. */
body.mg-sidebar-left .mg-shell-sidebar:empty,
body.mg-sidebar-right .mg-shell-sidebar:empty {
  display: none;
}

/* Management screens keep the node but opt out with CSS so shell markup never
   branches. The switch has to live inside #content: hx-boost swaps that element
   and nothing else, so a <body> class set by the server goes stale the moment a
   boosted link crosses into or out of /manage/ - the sidebar stayed visible on
   manage screens and then stayed hidden after leaving them. Keying off the
   manage wrapper follows the swap for free, and the #content in :has() outranks
   the placement rules, so this no longer depends on file order either. */
.mg-shell-main:has(#content .mg-manage) .mg-shell-sidebar {
  display: none;
}

/* The sidebar remains narrow at every viewport, so its columns bridge to one
   track without a media query. */
.mg-shell-sidebar .mg-block-columns {
  grid-template-columns: 1fr;
}

/* Measured article-column widths at each boundary (960px content width,
   18rem sidebar, 24px gap, 24px gutters):

     header top/bottom   @64rem -> 600px
     header left/right   @64rem -> 408px    the 16rem vertical nav is a column
     header left/right   @75rem -> 584px    outside .mg-shell-main

   A container query would express this once, but container-type implies
   contain: layout and would make .mg-shell-main a containing block for fixed
   descendants. The gallery lightbox inside #content must cover the viewport,
   so the two media queries stay separate. */
@media (max-width: 75rem) {
  body.mg-header-left .mg-shell-main,
  body.mg-header-right .mg-shell-main {
    flex-direction: column;
  }

  body.mg-header-left .mg-shell-sidebar,
  body.mg-header-right .mg-shell-sidebar {
    flex: 0 0 auto;
  }
}

@media (max-width: 64rem) {
  body.mg-sidebar-left .mg-shell-main,
  body.mg-sidebar-right .mg-shell-main {
    flex-direction: column;
  }

  body.mg-sidebar-left .mg-shell-sidebar,
  body.mg-sidebar-right .mg-shell-sidebar {
    flex: 0 0 auto;
  }
}

/* ------------------------------------------------------------------ *
 * 3. shell — per-placement rules
 *
 * Each placement is self-contained below. A distributed skin that supports
 * only some placements can drop the blocks it does not want and keep the
 * rest; nothing outside this section reads the placement class except the
 * two dock offsets noted inline.
 * ------------------------------------------------------------------ */

/* --- top ---------------------------------------------------------- */

body.mg-header-top .mg-shell {
  grid-template-areas: "header" "content";
  grid-template-rows: auto 1fr;
}

/* The header is full-bleed but its contents line up with .mg-shell-main. There is no
   inner wrapper element in the shell markup, so the gutter is computed instead
   of nested. */
body.mg-header-top .mg-shell-header,
body.mg-header-bottom .mg-shell-header {
  border-bottom: var(--mg-border-width) solid var(--mg-border);
  padding-inline: max(
    var(--mg-space-5),
    calc((100% - var(--mg-content-width)) / 2)
  );
}

/* Only meaningful for the top placement: left/right are already sticky and
   bottom is a fixed dock. */
body.mg-headerfix-on.mg-header-top .mg-shell-header {
  position: sticky;
  top: 0;
  z-index: 2;
}

/* --- bottom ------------------------------------------------------- */

body.mg-header-bottom .mg-shell {
  grid-template-areas: "content" "header";
  grid-template-rows: 1fr auto;
  padding-block-end: var(--mg-shell-dock-height);
}

body.mg-header-bottom .mg-shell-header {
  position: fixed;
  inset-inline: 0;
  bottom: 0;
  height: var(--mg-shell-dock-height);
  z-index: 2;
  border-bottom: 0;
  border-top: var(--mg-border-width) solid var(--mg-border);
}

body.mg-header-bottom .mg-shell-subnav {
  bottom: 100%;
}

/* Floating elements clear the dock. */
body.mg-header-bottom .mg-shell-totop,
body.mg-header-bottom .mg-shell-error {
  bottom: calc(var(--mg-shell-dock-height) + var(--mg-space-5));
}

/* --- left / right ------------------------------------------------- */

body.mg-header-left .mg-shell {
  grid-template-areas: "header content";
  grid-template-columns: var(--mg-shell-rail-width) 1fr;
}

body.mg-header-right .mg-shell {
  grid-template-areas: "content header";
  grid-template-columns: 1fr var(--mg-shell-rail-width);
}

/* Pin the vertical sidebar to the viewport. As a grid item its default
   align-self: stretch makes the column as tall as the content, so on a long
   page the bottom of the menu can only be reached by scrolling. align-self
   must drop to start for sticky to have room to move. */
/* Only the axis survives here. The capsule block below owns position, size,
   padding and overflow for these two placements — leaving the old sticky
   100vh geometry in place meant `overflow-y: auto` was still declared and only
   source order kept it from clipping the rail's tooltips. */
body.mg-header-left .mg-shell-header,
body.mg-header-right .mg-shell-header {
  flex-direction: column;
}

body.mg-header-left .mg-shell-header {
  border-inline-end: var(--mg-border-width) solid var(--mg-border);
}

body.mg-header-right .mg-shell-header {
  border-inline-start: var(--mg-border-width) solid var(--mg-border);
}

body.mg-header-left .mg-shell-nav,
body.mg-header-right .mg-shell-nav {
  align-items: stretch;
  flex-direction: column;
  justify-content: flex-start;
}

body.mg-header-left .mg-shell-account,
body.mg-header-right .mg-shell-account {
  flex-direction: column;
}

body.mg-header-left .mg-shell-subnav,
body.mg-header-right .mg-shell-subnav {
  border: 0;
  box-shadow: none;
  background: transparent;
  margin-inline-start: var(--mg-space-3);
  padding: 0;
}

/* Vertical placements expand submenus in place instead of flying out. The
   base hover rule sets position: absolute; it has to be undone here or the
   open list floats and covers the next menu item.

   A flyout is not an option: the sidebar is viewport-height with
   overflow-y: auto, so anything escaping its box gets clipped. */
body.mg-header-left .mg-shell-nav-item:hover > .mg-shell-subnav,
body.mg-header-left .mg-shell-nav-item:focus-within > .mg-shell-subnav,
body.mg-header-right .mg-shell-nav-item:hover > .mg-shell-subnav,
body.mg-header-right .mg-shell-nav-item:focus-within > .mg-shell-subnav {
  position: static;
}

/* ROOT dock — the floating capsule. The vertical placements detach from the
   edge and leave the gutters open, which only reads as a dock when it is a
   SHORT column centred in the viewport — not a full-height panel. The earlier
   pass painted the capsule onto the existing 100vh × 236px sticky column and
   produced a blob the height of the screen.

   `fixed` rather than `sticky`: the capsule has to centre on the viewport, and
   sticky can only pin an element once scrolling reaches it. The grid column
   above still reserves the space, so content never slides underneath.
   ⚠ The header already created a stacking context as a sticky element, so this
   does not introduce one (docs/PITFALLS.md — drawer vs scrim z-index).

   Styling only: every shell node still renders in every placement
   (CLAUDE.md 금지 8·9) and the switch is a <body> class, never markup. */
body.mg-header-left .mg-shell-header,
body.mg-header-right .mg-shell-header {
  position: fixed;
  top: 50%;
  transform: translateY(-50%);
  height: auto;
  inline-size: calc(var(--mg-shell-rail-width) - 2 * var(--mg-shell-rail-inset));
  overflow: visible;
  align-items: center;
  align-self: center;
  gap: var(--mg-space-1);
  padding: var(--mg-space-2);
  background: var(--mg-surface-plate-strong);
  backdrop-filter: var(--mg-frame-blur);
  border: 0;
  border-radius: var(--mg-radius-pill);
  box-shadow: var(--mg-shadow-float);
  z-index: 20;
}

body.mg-header-left .mg-shell-header { inset-inline-start: var(--mg-shell-rail-inset); }
body.mg-header-right .mg-shell-header { inset-inline-end: var(--mg-shell-rail-inset); }
/* The brand becomes a mark. A wordmark cannot ride a 44px rail, so it clips to
   its first glyphs rather than widening the capsule. */
body.mg-header-left .mg-shell-brand,
body.mg-header-right .mg-shell-brand {
  inline-size: var(--mg-shell-icon);
  block-size: var(--mg-shell-icon);
  display: grid;
  place-items: center;
  overflow: hidden;
  padding: 0;
  font-size: var(--mg-font-size-xs);
  letter-spacing: var(--mg-track-wide);
}

body.mg-header-left .mg-shell-nav,
body.mg-header-right .mg-shell-nav {
  align-items: center;
  inline-size: auto;
}

body.mg-header-left .mg-shell-nav-item,
body.mg-header-right .mg-shell-nav-item { position: relative; }

body.mg-header-left .mg-shell-account a:hover,
body.mg-header-right .mg-shell-account a:hover { z-index: 3; }

/* Each item is a 44px round target. The label is not shown on the rail — it
   arrives as a tooltip on hover and focus, so the icon is never a riddle and
   the item stays reachable by keyboard. */
body.mg-header-left .mg-shell-nav a,
body.mg-header-left .mg-shell-nav > .mg-shell-nav-item > span,
body.mg-header-right .mg-shell-nav a,
body.mg-header-right .mg-shell-nav > .mg-shell-nav-item > span {
  display: grid;
  place-items: center;
  inline-size: var(--mg-shell-icon);
  block-size: var(--mg-shell-icon);
  padding: 0;
  border: var(--mg-stroke-1) solid transparent;
  border-radius: var(--mg-radius-pill);
}

body.mg-header-left .mg-shell-nav a:hover,
body.mg-header-left .mg-shell-nav a:focus-visible,
body.mg-header-right .mg-shell-nav a:hover,
body.mg-header-right .mg-shell-nav a:focus-visible {
  border-color: var(--mg-border-strong);
  background: var(--mg-surface);
  color: var(--mg-text);
}

/* ⭐ 2026-08-30 사람 결정 — 세로 독의 hover는 밑줄이 아니라 금색 아이콘이다.
   The gold bar is ROOT's marker for a horizontal row of labels; on a round
   icon target it draws a line across the button instead of under a word. */
body.mg-header-left .mg-shell-nav a::after,
body.mg-header-right .mg-shell-nav a::after {
  content: none;
}

body.mg-header-left .mg-shell-nav a:hover .mg-icon,
body.mg-header-left .mg-shell-nav a:focus-visible .mg-icon,
body.mg-header-left .mg-shell-account a:hover .mg-icon,
body.mg-header-left .mg-shell-account a:focus-visible .mg-icon,
body.mg-header-right .mg-shell-nav a:hover .mg-icon,
body.mg-header-right .mg-shell-nav a:focus-visible .mg-icon,
body.mg-header-right .mg-shell-account a:hover .mg-icon,
body.mg-header-right .mg-shell-account a:focus-visible .mg-icon {
  color: var(--mg-accent);
}

/* A hovered item paints over its neighbours so the tooltip is never covered
   by the next button in the rail. */
body.mg-header-left .mg-shell-nav-item:hover,
body.mg-header-left .mg-shell-nav-item:focus-within,
body.mg-header-right .mg-shell-nav-item:hover,
body.mg-header-right .mg-shell-nav-item:focus-within { z-index: 3; }

body.mg-header-left .mg-shell-account a:hover,
body.mg-header-left .mg-shell-account a:focus-visible,
body.mg-header-right .mg-shell-account a:hover,
body.mg-header-right .mg-shell-account a:focus-visible {
  border-color: var(--mg-border-strong);
  background: var(--mg-surface);
}

/* An item with no icon keeps its text, clipped to the target — better a
   cramped word than an invisible button. */
body.mg-header-left :where(.mg-shell-nav, .mg-shell-account) .mg-shell-nav__label,
body.mg-header-right :where(.mg-shell-nav, .mg-shell-account) .mg-shell-nav__label {
  position: absolute;
  z-index: 2;
  top: 50%;
  inline-size: max-content;
  max-inline-size: 12rem;
  padding: 0 var(--mg-space-3);
  block-size: 1.875rem;
  display: flex;
  align-items: center;
  border-radius: var(--mg-radius-sm);
  background: var(--mg-text);
  color: var(--mg-bg);
  font-size: var(--mg-font-size-sm);
  font-weight: 500;
  box-shadow: var(--mg-shadow-float);
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--mg-transition-fast), transform var(--mg-transition);
}

body.mg-header-left :where(.mg-shell-nav, .mg-shell-account) .mg-shell-nav__label {
  inset-inline-start: 100%;
  transform: translateY(-50%) translateX(-10px);
}

body.mg-header-right :where(.mg-shell-nav, .mg-shell-account) .mg-shell-nav__label {
  inset-inline-end: 100%;
  transform: translateY(-50%) translateX(10px);
}

body.mg-header-left :where(.mg-shell-nav, .mg-shell-account) a:hover .mg-shell-nav__label,
body.mg-header-left :where(.mg-shell-nav, .mg-shell-account) a:focus-visible .mg-shell-nav__label,
body.mg-header-right :where(.mg-shell-nav, .mg-shell-account) a:hover .mg-shell-nav__label,
body.mg-header-right :where(.mg-shell-nav, .mg-shell-account) a:focus-visible .mg-shell-nav__label {
  opacity: 1;
  transform: translateY(-50%) translateX(0);
}

/* No icon means the label is all there is: keep it visible and small. */
body.mg-header-left .mg-shell-nav a:not(:has(.mg-icon)) .mg-shell-nav__label,
body.mg-header-right .mg-shell-nav a:not(:has(.mg-icon)) .mg-shell-nav__label,
body.mg-header-left .mg-shell-nav > .mg-shell-nav-item > span .mg-shell-nav__label,
body.mg-header-right .mg-shell-nav > .mg-shell-nav-item > span .mg-shell-nav__label {
  position: static;
  transform: none;
  opacity: 1;
  background: none;
  color: inherit;
  box-shadow: none;
  padding: 0;
  block-size: auto;
  font-size: 0.625rem;
  line-height: 1.1;
  text-align: center;
  overflow: hidden;
}

body.mg-header-left .mg-shell-account,
body.mg-header-right .mg-shell-account { flex-direction: column; align-items: center; }

body.mg-header-left .mg-shell-account a,
body.mg-header-right .mg-shell-account a {
  position: relative;
  inline-size: var(--mg-shell-icon);
  block-size: var(--mg-shell-icon);
  display: grid;
  place-items: center;
  padding: 0;
  border: var(--mg-stroke-1) solid transparent;
  border-radius: var(--mg-radius-pill);
}

body.mg-header-left .mg-shell-account,
body.mg-header-right .mg-shell-account { overflow: visible; }

/* ⭐ Folded dock on desktop (2026-10-11 사람 지시):
   ① a group header with an icon shows the icon only — its name stays for screen readers;
   ② its submenu FLIES OUT beside the dock over the page, instead of opening in place and
      pushing the items below. (The old note "a flyout gets clipped" is about the drawer;
      the desktop header is `overflow: visible`, the label tooltips already escape it.) */
@media (min-width: 48rem) {
  body.mg-header-left:has(#mg-shell-collapse:checked) .mg-shell-nav > .mg-shell-nav-item > span:has(.mg-icon) .mg-shell-nav__label,
  body.mg-header-right:has(#mg-shell-collapse:checked) .mg-shell-nav > .mg-shell-nav-item > span:has(.mg-icon) .mg-shell-nav__label {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  body.mg-header-left:has(#mg-shell-collapse:checked) .mg-shell-nav-item:hover > .mg-shell-subnav,
  body.mg-header-left:has(#mg-shell-collapse:checked) .mg-shell-nav-item:focus-within > .mg-shell-subnav,
  body.mg-header-right:has(#mg-shell-collapse:checked) .mg-shell-nav-item:hover > .mg-shell-subnav,
  body.mg-header-right:has(#mg-shell-collapse:checked) .mg-shell-nav-item:focus-within > .mg-shell-subnav {
    position: absolute;
    z-index: 30;
    inset-block-start: 50%;
    transform: translateY(-50%);
    display: flex;
    flex-direction: column;
    gap: var(--mg-space-1);
    min-inline-size: max-content;
    margin: 0;
    padding: var(--mg-space-1);
    background: var(--mg-surface);
    border: var(--mg-border-width) solid var(--mg-border);
    border-radius: var(--mg-radius-sm);
    box-shadow: var(--mg-shadow-lift);
  }
  body.mg-header-left:has(#mg-shell-collapse:checked) .mg-shell-nav-item > .mg-shell-subnav {
    inset-inline-start: calc(100% + var(--mg-space-2));
  }
  body.mg-header-right:has(#mg-shell-collapse:checked) .mg-shell-nav-item > .mg-shell-subnav {
    inset-inline-end: calc(100% + var(--mg-space-2));
  }
  /* A bridge over the gap, so moving the pointer from the icon to the list keeps it open. */
  body.mg-header-left:has(#mg-shell-collapse:checked) .mg-shell-subnav::before,
  body.mg-header-right:has(#mg-shell-collapse:checked) .mg-shell-subnav::before {
    content: "";
    position: absolute;
    inset-block: 0;
    inline-size: calc(var(--mg-space-2) + var(--mg-border-width));
  }
  body.mg-header-left:has(#mg-shell-collapse:checked) .mg-shell-subnav::before { inset-inline-end: 100%; }
  body.mg-header-right:has(#mg-shell-collapse:checked) .mg-shell-subnav::before { inset-inline-start: 100%; }
  /* Its links are plain rows, not the dock's round icon buttons. */
  body.mg-header-left:has(#mg-shell-collapse:checked) .mg-shell-subnav a,
  body.mg-header-right:has(#mg-shell-collapse:checked) .mg-shell-subnav a {
    display: block;
    inline-size: auto;
    block-size: auto;
    min-block-size: 0;
    padding: var(--mg-space-2) var(--mg-space-3);
    border: 0;
    border-radius: var(--mg-radius-sm);
    background: transparent;
    color: var(--mg-text);
    font-size: var(--mg-font-size-sm);
    line-height: 1.3;
    text-align: start;
    white-space: nowrap;
  }
  body.mg-header-left:has(#mg-shell-collapse:checked) .mg-shell-subnav a:hover,
  body.mg-header-left:has(#mg-shell-collapse:checked) .mg-shell-subnav a:focus-visible,
  body.mg-header-right:has(#mg-shell-collapse:checked) .mg-shell-subnav a:hover,
  body.mg-header-right:has(#mg-shell-collapse:checked) .mg-shell-subnav a:focus-visible {
    background: var(--mg-surface-tint);
    color: var(--mg-text-strong);
    text-decoration: none;
  }
  body.mg-header-left:has(#mg-shell-collapse:checked) .mg-shell-subnav a::after,
  body.mg-header-right:has(#mg-shell-collapse:checked) .mg-shell-subnav a::after { content: none; }
}

.mg-shell-nav__label {
  min-inline-size: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ------------------------------------------------------------------ *
 * 4. content typography
 * ------------------------------------------------------------------ */

#content > h1:first-child {
  margin-bottom: var(--mg-space-5);
}

#content p {
  margin: 0 0 var(--mg-space-4);
}

#content ul,
#content ol {
  padding-inline-start: var(--mg-space-5);
}

#content dl {
  display: grid;
  grid-template-columns: minmax(6rem, max-content) 1fr;
  gap: var(--mg-space-2) var(--mg-space-5);
  margin: 0 0 var(--mg-space-5);
  padding: var(--mg-space-4) var(--mg-space-5);
  background: var(--mg-surface-tint);
  border-radius: var(--mg-radius);
}

#content dt {
  font-weight: 700;
  font-size: var(--mg-font-size-sm);
  color: var(--mg-text-muted);
}

#content dd {
  margin: 0;
}

/* ROOT's heading signature: a gold mono kicker, the heavy display title with a
   Latin echo set in mono beside it, then a 4px gold bar. The echo is rhythm,
   not translation — it is never something the reader is meant to read, so it
   stays one or two words and never carries meaning the Korean does not.
   The bar is drawn, not marked up, so a skin gets it without new nodes. */
.mg-heading {
  margin: 0 0 var(--mg-space-5);
}

.mg-heading::after {
  content: "";
  display: block;
  width: 56px;
  height: var(--mg-bar-h);
  background: var(--mg-bar-gold);
  margin-top: var(--mg-space-3);
}

/* Gold type fails contrast on white, so the kicker steps down to the --mg-link
   ramp rather than using --mg-accent directly. */
.mg-heading__kicker {
  display: block;
  font-family: var(--mg-font-mono);
  font-size: var(--mg-font-size-xs);
  font-weight: 700;
  letter-spacing: var(--mg-track-widest);
  text-transform: uppercase;
  color: var(--mg-link);
  margin-bottom: var(--mg-space-2);
}

.mg-heading__echo {
  font-family: var(--mg-font-mono);
  font-size: var(--mg-font-size-xs);
  font-weight: 500;
  letter-spacing: var(--mg-track-wider);
  text-transform: uppercase;
  color: var(--mg-text-muted);
  margin-inline-start: var(--mg-space-3);
  white-space: nowrap;
}

/* Icons — Material Symbols Outlined, self-hosted, addressed by NAME.
   The glyph is chosen by the ligature text inside the element, so a menu can
   use any of the ~3,700 icons without anyone vendoring a file for it first.
   A name with no glyph renders as its own text, which is a louder failure
   than a mask icon's empty box (static/vendor/material-symbols/README.md).

   ROOT's rules: 18-24px, currentColor, stroke 2-2.5 and never thinner — the
   500-weight instance is what holds its own beside a 2px border. An icon never
   carries brand gold on its own; gold reaches it only inside a gold container. */
.mg-icon {
  font-family: var(--mg-font-icon);
  font-weight: 500;
  font-style: normal;
  font-size: var(--mg-icon-size);
  line-height: 1;
  letter-spacing: normal;
  text-transform: none;
  white-space: nowrap;
  word-wrap: normal;
  direction: ltr;
  flex: none;
  -webkit-font-feature-settings: "liga";
  font-feature-settings: "liga";
  -webkit-font-smoothing: antialiased;
  user-select: none;
}


/* Mono metadata. ROOT sets every date, count and id in mono — a number in the
   body sans reads as a bug — and every Latin system label uppercase and
   tracked out, because Korean has no case to carry that signal. */
.mg-meta {
  font-family: var(--mg-font-mono);
  font-size: var(--mg-font-size-xs);
  font-weight: 500;
  letter-spacing: var(--mg-track-wide);
  color: var(--mg-text-muted);
  font-variant-numeric: tabular-nums;
}

.mg-muted {
  color: var(--mg-text-muted);
  font-size: var(--mg-font-size-sm);
}

/* ------------------------------------------------------------------ *
 * 5. components
 * ------------------------------------------------------------------ */

.mg-card {
  background: var(--mg-surface);
  border: var(--mg-border-width) solid var(--mg-border);
  border-radius: var(--mg-radius);
  box-shadow: var(--mg-shadow);
  padding: var(--mg-space-5);
  margin-bottom: var(--mg-space-4);
}

/* ROOT's hover contract: a surface acknowledges the cursor by rising 3px into
   a harder shadow. Nothing bounces and nothing changes hue. */
.mg-card {
  transition: transform var(--mg-transition-fast), box-shadow var(--mg-transition-fast);
}

.mg-card:hover {
  transform: translateY(-3px);
  box-shadow: var(--mg-shadow-lift);
}

.mg-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--mg-space-1);
  padding: var(--mg-space-2) var(--mg-space-5);
  border-radius: var(--mg-radius);
  border: var(--mg-border-width) solid var(--mg-accent);
  cursor: pointer;
  text-decoration: none;
  font-family: var(--mg-font-body);
  font-weight: 700;
  font-size: var(--mg-font-size-sm);
  line-height: 1.6;
  transition: transform var(--mg-transition-fast), background var(--mg-transition-fast),
    color var(--mg-transition-fast), border-color var(--mg-transition-fast);
}

.mg-btn:hover {
  text-decoration: none;
}

/* ROOT presses do not change colour: the element settles and drops its
   shadow. .985 is the system's press scale — 0.96 reads as a bounce. */
.mg-btn:active {
  transform: scale(0.985);
  box-shadow: none;
}

.mg-btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

/* Button mode is a <body> class, not a token (D9-1: modes are class toggles
   because they bundle several rules). Outline is the default when no mode
   class is present. */
body.mg-button-outline .mg-btn,
body:not([class*="mg-button-"]) .mg-btn {
  background: transparent;
  color: var(--mg-link);
}

body.mg-button-outline .mg-btn:hover,
body:not([class*="mg-button-"]) .mg-btn:hover {
  background: var(--mg-accent-soft);
}

body.mg-button-solid .mg-btn {
  background: var(--mg-accent);
  color: var(--mg-accent-text);
}

body.mg-button-solid .mg-btn:hover {
  background: var(--mg-accent-hover);
  border-color: var(--mg-accent-hover);
}

body.mg-button-ghost .mg-btn {
  background: transparent;
  color: var(--mg-link);
  border-color: transparent;
}

body.mg-button-ghost .mg-btn:hover {
  background: var(--mg-accent-soft);
}

.mg-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--mg-font-size-sm);
}

/* Colour sample for admin-picked colours (raid terrain templates). The colour
   itself comes from the page as an inline background, validated as #RRGGBB. */
.mg-swatch {
  display: inline-block;
  inline-size: var(--mg-space-4);
  block-size: var(--mg-space-4);
  border: var(--mg-border-width) solid var(--mg-border);
  border-radius: var(--mg-radius-sm);
  vertical-align: middle;
}

/* Raid authoring grid (manage). Terrain colour arrives as --mg-raid-terrain from
   a validated #RRGGBB value. Cell size is a token step, not a literal. */
.mg-raid-grid-wrap {
  overflow-x: auto;
}

.mg-raid-grid {
  border-collapse: collapse;
  font-size: var(--mg-font-size-sm);
}

.mg-raid-grid th {
  color: var(--mg-text-muted);
  padding: var(--mg-space-1);
}

.mg-raid-grid__cell {
  border: var(--mg-border-width) solid var(--mg-border);
  padding: 0;
}

.mg-raid-grid__cell button {
  inline-size: calc(var(--mg-space-4) * 2);
  block-size: calc(var(--mg-space-4) * 2);
  border: 0;
  background: transparent;
  color: var(--mg-text);
  cursor: pointer;
  font-size: var(--mg-font-size-sm);
}

.mg-raid-grid__cell--damage,
.mg-raid-grid__cell--heal,
.mg-raid-grid__cell--obstacle {
  background: var(--mg-raid-terrain);
}

.mg-raid-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--mg-space-3);
  margin-block: var(--mg-space-3);
}

.mg-raid-tabs a[aria-current="page"] {
  font-weight: 700;
  text-decoration: underline;
}

.mg-raid-candidate {
  display: inline-flex;
  gap: var(--mg-space-1);
  margin-inline-end: var(--mg-space-3);
}

/* Raid control screen (u-8a). One screen: the action table and the side panel scroll
   inside themselves. ⛔ The two-column rule and sticky live inside the media query —
   stacked on narrow screens, a sticky side panel would hold the table under it. */
.mg-raid-control {
  display: grid;
  gap: var(--mg-space-4);
}

.mg-raid-rows {
  max-block-size: var(--mg-raid-panel-height);
  overflow: auto;
  border: var(--mg-border-width) solid var(--mg-border);
}

.mg-raid-row {
  display: grid;
  grid-template-columns: minmax(8rem, 1.2fr) minmax(8rem, 1fr) minmax(10rem, 2fr) minmax(6rem, 0.8fr);
  gap: var(--mg-space-2);
  align-items: start;
  padding: var(--mg-space-2);
  border-block-end: 1px solid var(--mg-border);
}

.mg-raid-row--head {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--mg-surface);
  font-weight: 700;
}

.mg-raid-row--monster {
  background: var(--mg-danger-soft);
}

.mg-raid-row__targets {
  border: 0;
  margin: 0;
  padding: 0;
}

.mg-raid-row__targets legend {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  overflow: hidden;
  clip-path: inset(50%);
}

/* ⛔ [hidden] must win over the label's inline-flex (CLAUDE.md: display beats hidden). */
.mg-raid-row [hidden] {
  display: none;
}

.mg-raid-row__result--error,
.mg-raid-notice--error {
  color: var(--mg-danger);
}

.mg-raid-scroll {
  max-block-size: calc(var(--mg-raid-panel-height) / 2);
  overflow: auto;
}

@media (min-width: 75rem) {
  .mg-raid-control {
    grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
    align-items: start;
  }

  .mg-raid-control__side {
    position: sticky;
    top: var(--mg-space-5);
    max-block-size: calc(100vh - var(--mg-space-6));
    overflow: auto;
  }
}

/* A fixed top header and a sticky panel fight over the same offset (same as the profile rail). */
body.mg-headerfix-on.mg-header-top .mg-raid-control__side { position: static; max-block-size: none; }

.mg-raid-slot--mismatch {
  color: var(--mg-danger);
  background: var(--mg-danger-soft);
}

.mg-table th,
.mg-table td {
  border-bottom: var(--mg-border-width) solid var(--mg-border);
  padding: var(--mg-space-3);
  text-align: center;
}

.mg-table th {
  color: var(--mg-text-muted);
  font-weight: 700;
  font-size: var(--mg-font-size-md);
  letter-spacing: 0.03em;
}

.mg-table tbody tr:hover td {
  background: var(--mg-surface-tint);
}

.mg-comment {
  border-top: var(--mg-border-width) solid var(--mg-border);
  padding: var(--mg-space-4) 0;
}

.mg-comment--reply {
  margin-left: var(--mg-space-6);
  border-top: 0;
  background: var(--mg-surface-tint);
  border-radius: var(--mg-radius-sm);
  padding: var(--mg-space-3) var(--mg-space-4);
  margin-top: var(--mg-space-2);
}

.emoticon {
  display: inline-block;
  width: var(--mg-emoticon-size);
  height: var(--mg-emoticon-size);
  object-fit: contain;
  vertical-align: middle;
}

.mg-emoticon-picker {
  margin: var(--mg-space-2) 0;
  padding: var(--mg-space-3);
  border: var(--mg-border-width) solid var(--mg-border);
  border-radius: var(--mg-radius);
  background: var(--mg-surface);
}

.mg-emoticon-picker h2 {
  font-size: var(--mg-font-size-sm);
  margin: var(--mg-space-2) 0;
}

.mg-emoticon-picker__items {
  display: flex;
  flex-wrap: wrap;
  gap: var(--mg-space-2);
}

.mg-emoticon-picker__item {
  border: 0;
  padding: 0;
  background: transparent;
}

.mg-emoticon-picker__item img {
  width: calc(var(--mg-emoticon-size) * 2);
  height: calc(var(--mg-emoticon-size) * 2);
}

.toastui-editor-defaultUI-toolbar button.mg-emoticon-picker__toolbar-btn,
.toastui-editor-defaultUI-toolbar button.mg-editor-align-btn {
  font-size: 16px;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  margin: 0;
}

/* 정렬 버튼. ⛔ 아이콘 폰트가 아니라 인라인 SVG 다 — 아이콘 CDN 이 막힌 망에서
   리거처 폰트는 "format_align_left" 라는 **영문 단어**로 보인다
   (CLAUDE.md "절대 하지 말 것" 15). 색은 툴바 아이콘과 같은 축으로 둔다. */
.toastui-editor-defaultUI-toolbar button.mg-editor-align-btn {
  border: 0;
  padding: 0;
  background: transparent;
  color: var(--mg-text-muted);
}

.toastui-editor-defaultUI-toolbar button.mg-editor-align-btn:hover,
.toastui-editor-defaultUI-toolbar button.mg-editor-align-btn:focus-visible {
  color: var(--mg-text);
}

/* Spoiler curtain. UI only — never a permission boundary (D22). The title is
   still in the HTML source; the <details> element does the hiding with no
   JavaScript so it cannot fail open. */
.mg-spoiler {
  display: block;
  border: var(--mg-border-width) solid var(--mg-border);
  border-radius: var(--mg-radius);
  background: var(--mg-surface-tint);
  padding: var(--mg-space-4);
}

/* Lists keep the curtain in the text flow so the badge and meta stay on one
   line — <details> defaults to block and would push them onto the next row. */
.mg-spoiler--inline {
  display: inline-block;
  vertical-align: baseline;
  border-radius: var(--mg-radius-pill);
  padding: 0 var(--mg-space-3);
}

.mg-spoiler__label {
  color: var(--mg-text-muted);
  font-size: var(--mg-font-size-sm);
  cursor: pointer;
}

/* Rating badge. Part of the title, so it sits inside <h1> (D22-3). */
.mg-rating {
  display: inline-flex;
  align-items: center;
  vertical-align: middle;
  border-radius: var(--mg-radius-pill);
  background: var(--mg-surface-tint);
  color: var(--mg-text-muted);
  font-family: var(--mg-font-body);
  font-size: var(--mg-font-size-xs);
  font-weight: 700;
  line-height: 1.6;
  padding: 0 var(--mg-space-3);
  margin-inline-start: var(--mg-space-2);
}

.mg-rating--teen {
  background: var(--mg-green-soft);
  color: var(--mg-green-hover);
}

.mg-rating--adult {
  background: var(--mg-accent-soft);
  color: var(--mg-link-hover);
}

/* ------------------------------------------------------------------ *
 * 6. forms
 *
 * Core screens render Django's form.as_p (D8-2), so these have to reach bare
 * elements. :where() drops the specificity to zero on purpose: vendor editor
 * stylesheets (Toast UI, Editor.js) use class selectors and must keep winning
 * over anything here.
 * ------------------------------------------------------------------ */

/* Control metrics (10px 14px, 19px box, 6px box radius) are literal because the
   design system specifies them outside the --mg-space-* rhythm. Colors stay on
   tokens. */

:where(#content) :where(input, select, textarea) {
  font-family: var(--mg-font-body);
  font-size: var(--mg-font-size-base);
  padding: 10px 14px;
  border-radius: var(--mg-radius-sm);
  border: var(--mg-border-width) solid var(--mg-border);
  background: var(--mg-surface);
  color: var(--mg-text);
  outline: none;
  transition: border-color var(--mg-transition-fast);
  width: 100%;
  max-width: 100%;
}

:where(#content) :where(input, select, textarea):focus {
  border-color: var(--mg-accent);
}

:where(#content) :where(select):hover {
  border-color: var(--mg-border-strong);
}

:where(#content) :where(textarea) {
  resize: vertical;
  min-height: 90px;
}

/* form.as_p wraps each field in a <p>; that is our .ds-field.
 *
 * ⛔⛔ 편집기 안은 반드시 뺀다. Toast UI 의 본문 문단도 `<form>` 안의 `<p>` 라
 * 이 규칙이 그대로 걸리는데, 그러면 문단이 **세로 flex 컨테이너**가 되어
 * 자식이 전부 각자 한 줄을 차지하고 인라인 서식이 flex 아이템으로 **블록화**된다
 * (`display: block` 으로 계산된다). 화면에는 *"볼드를 넣자마자 그 앞뒤로 줄이
 * 바뀌는"* 것으로 보이고, 저장된 값은 멀쩡해서 **글 보기 화면에서는 정상**이다 —
 * 그래서 원인이 CSS 라는 단서가 아무 데도 안 남는다 (2026-09-03 사람 신고).
 * ⚠ 이 저장소에서 `#content` 범위의 태그 규칙이 컴포넌트를 이긴 것이 여섯 번째다.
 * ⛔ 제외 목록은 체크박스 규칙과 **같은 것**을 쓴다 — 편집기가 하나 더 생기면
 * 두 곳을 함께 고쳐야 한다는 뜻이고, 그것이 두 벌로 갈라지는 것보다 낫다. */
:where(#content) :where(form p):not(
  :where(.codex-editor *, .toastui-editor-defaultUI *)
) {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-bottom: var(--mg-space-4);
}

:where(#content) :where(label) {
  font-weight: 700;
  font-size: var(--mg-font-size-sm);
}

/* Django renders help_text as <span class="helptext">. */
:where(#content) :where(.helptext) {
  color: var(--mg-text-muted);
  font-size: var(--mg-font-size-xs);
}

:where(#content) :where(button, input[type="submit"]) {
  cursor: pointer;
}

/* Checkbox and radio. Django emits a bare <input>, with no sibling box element
   to paint, so the control is drawn on the input itself via appearance:none.
   The editors ship their own controls and must keep them. */
:where(#content) :where(input[type="checkbox"], input[type="radio"]):not(
  :where(.codex-editor *, .toastui-editor-defaultUI *)
) {
  appearance: none;
  -webkit-appearance: none;
  flex: none;
  width: 19px;
  height: 19px;
  padding: 0;
  border-radius: 6px;
  border: var(--mg-border-width) solid var(--mg-border-strong);
  background: var(--mg-surface);
  display: inline-grid;
  place-content: center;
  vertical-align: text-bottom;
  transition: background var(--mg-transition-fast), border-color var(--mg-transition-fast);
}

:where(#content) :where(input[type="radio"]):not(
  :where(.codex-editor *, .toastui-editor-defaultUI *)
) {
  border-radius: var(--mg-radius-pill);
}

:where(#content) :where(input[type="checkbox"], input[type="radio"]):hover:not(
  :where(.codex-editor *, .toastui-editor-defaultUI *)
) {
  border-color: var(--mg-accent);
}

:where(#content) :where(input[type="checkbox"], input[type="radio"]):checked:not(
  :where(.codex-editor *, .toastui-editor-defaultUI *)
) {
  background: var(--mg-accent);
  border-color: var(--mg-accent);
}

:where(#content) :where(input[type="checkbox"])::after {
  content: "";
  width: 5px;
  height: 9px;
  border-right: 2px solid var(--mg-accent-text);
  border-bottom: 2px solid var(--mg-accent-text);
  transform: rotate(45deg) translate(-1px, -1px);
  opacity: 0;
  transition: opacity 0.12s ease;
}

:where(#content) :where(input[type="radio"])::after {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: var(--mg-radius-pill);
  background: var(--mg-accent-text);
  opacity: 0;
  transition: opacity 0.12s ease;
}

:where(#content) :where(input[type="checkbox"], input[type="radio"]):checked::after {
  opacity: 1;
}

:where(#content) :where(input[type="checkbox"], input[type="radio"]):focus-visible {
  box-shadow: 0 0 0 3px var(--mg-accent-soft);
}

:where(#content) :where(input, select, textarea):disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* A checkbox/radio field is a row, not a stacked label-over-control group.
   ⛔ 위 규칙과 같은 제외를 단다 — 편집기 안에서 이 선택자가 맞을 일은 없지만,
   두 규칙이 같은 대상을 다르게 잡으면 다음 사람이 어느 쪽이 진짜인지 모른다. */
:where(#content) :where(form p):has(> input[type="checkbox"], > input[type="radio"]):not(
  :where(.codex-editor *, .toastui-editor-defaultUI *)
) {
  flex-direction: row;
  align-items: center;
  gap: 9px;
}

/* Dropdown — built over a native <select> by static/core/js/dropdown.js.
   The select is hidden HERE, inside the wrapper the script creates, and
   nowhere else: with the script absent there is no wrapper, so the plain
   select stays visible and usable. */
.mg-dropdown {
  position: relative;
  width: 100%;
}

.mg-dropdown > select {
  display: none;
}

.mg-dropdown__trigger {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  font-family: var(--mg-font-body);
  font-size: var(--mg-font-size-base);
  padding: 10px 14px;
  border-radius: var(--mg-radius-sm);
  border: var(--mg-border-width) solid var(--mg-border);
  background: var(--mg-surface);
  color: var(--mg-text);
  cursor: pointer;
  text-align: left;
  transition: border-color var(--mg-transition-fast);
}

.mg-dropdown__trigger:hover {
  border-color: var(--mg-border-strong);
}

.mg-dropdown__trigger:focus-visible {
  border-color: var(--mg-accent);
  outline: none;
  box-shadow: 0 0 0 3px var(--mg-accent-soft);
}

.mg-dropdown__trigger[aria-expanded="true"] {
  border-color: var(--mg-accent);
}

.mg-dropdown__trigger[aria-disabled="true"] {
  opacity: 0.45;
  cursor: not-allowed;
}

.mg-dropdown__value {
  flex: 1;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  overflow: hidden;
  text-overflow: ellipsis;
}

.mg-dropdown__value--empty {
  color: var(--mg-text-muted);
}

.mg-dropdown__chevron {
  flex: none;
  width: 14px;
  height: 14px;
  border-right: 2px solid var(--mg-text-muted);
  border-bottom: 2px solid var(--mg-text-muted);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform var(--mg-transition-fast);
}

.mg-dropdown__trigger[aria-expanded="true"] .mg-dropdown__chevron {
  transform: rotate(-135deg) translate(-4px, -4px);
}

.mg-dropdown__menu {
  position: absolute;
  /* ⛔⛔ 리치텍스트 편집기보다 위여야 한다 (2026-09-09 사람 신고: 캐릭터 수정
     화면에서 드롭다운이 편집기 상자에 가려 고를 수 없었다). Toast UI 벤더 시트가
     자기 내부에 최대 40 을 쓰고, 편집기가 DOM 상 **뒤**에 있어 20 으로는 같은
     값에서도 진다. 층 관례: 셸 20 · **드롭다운 50** · 툴팁 60 · 라이트박스 100.
     ⚠ 가드가 벤더 시트의 최댓값을 다시 세어 이 값과 대조한다 — 벤더를 올릴 때
     같이 올리지 않으면 **오류 없이 다시 가려진다.** */
  z-index: 50;
  top: calc(100% + 6px);
  left: 0;
  right: 0;
  background: var(--mg-surface);
  border: var(--mg-border-width) solid var(--mg-border);
  border-radius: var(--mg-radius-sm);
  box-shadow: var(--mg-shadow-lift);
  padding: 6px;
  max-height: 260px;
  overflow: auto;
}

.mg-dropdown__menu[hidden] {
  display: none;
}

.mg-dropdown__option {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 9px 12px;
  border-radius: 8px;
  font-size: var(--mg-font-size-base);
  color: var(--mg-text);
  cursor: pointer;
}

.mg-dropdown__option:hover {
  background: var(--mg-surface-tint);
}

.mg-dropdown__option--selected {
  background: var(--mg-surface-tint);
  font-weight: 700;
}

/* Keyboard position. Kept distinct from --selected so arrowing through a
   multi-select still shows which rows are actually picked. */
.mg-dropdown__option--active {
  outline: 2px solid var(--mg-accent-soft);
  outline-offset: -2px;
}

.mg-dropdown__option[aria-disabled="true"] {
  opacity: 0.45;
  cursor: not-allowed;
}

.mg-dropdown__option[aria-selected="true"] .mg-dropdown__check::before {
  content: "✓";
}

.mg-dropdown__check {
  flex: none;
  width: 16px;
  color: var(--mg-link);
  font-weight: 700;
}

/* Raid list (/raids/, 2026-10-11). Cards stack; the whole card is the link. */
.mg-raid-list { margin-block-end: var(--mg-space-6); }
.mg-raid-list__item {
  display: grid;
  gap: var(--mg-space-2);
  margin-block-end: var(--mg-space-3);
  color: inherit;
  text-decoration: none;
}
.mg-raid-list__item:hover,
.mg-raid-list__item:focus-visible { border-color: var(--mg-border-strong); }
.mg-raid-list__name { font-weight: 700; font-size: var(--mg-font-size-md); }
.mg-raid-list__tags { display: flex; flex-wrap: wrap; gap: var(--mg-space-2); }
.mg-chip.mg-raid-list__test { background: var(--mg-surface-sunken); color: var(--mg-text-muted); }

.mg-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 6px 3px 10px;
  border-radius: var(--mg-radius-pill);
  background: var(--mg-accent-soft);
  color: var(--mg-link-hover);
  font-size: var(--mg-font-size-sm);
  font-weight: 700;
}

.mg-chip button {
  border: none;
  background: none;
  color: inherit;
  cursor: pointer;
  font-size: 14px;
  line-height: 1;
  padding: 0 2px;
}

.errorlist,
.mg-field-error,
.mg-editor-error {
  color: var(--mg-danger);
}

.errorlist {
  list-style: none;
  padding: 0;
  margin: 0 0 var(--mg-space-2);
  font-size: var(--mg-font-size-sm);
}

/* Toast UI writes an inline height on the host, but when it fails before that
   the editing area collapses to 0px and only the toolbar is left. */
[data-mg-editor] {
  min-height: 300px;
}

/* The host min-height is not enough on its own: Toast UI's internals are a
   height:100% chain, so an auto-height host still collapses the editing area. */
[data-mg-editor] .toastui-editor-ww-container,
[data-mg-editor] .toastui-editor-md-container {
  min-height: 250px;
}

.mg-post-field {
  margin-bottom: var(--mg-space-5);
}

.mg-post-form--gallery {
  display: flex;
  flex-direction: column;
}

.mg-post-form--gallery .mg-post-attachments {
  order: -1;
}

.mg-gallery-upload__preview {
  display: flex;
  flex-wrap: wrap;
  gap: var(--mg-space-3);
}

.mg-gallery-upload__preview img {
  border: var(--mg-border-width) solid var(--mg-border);
  border-radius: var(--mg-radius-sm);
  max-width: 100%;
}

/* ------------------------------------------------------------------ *
 * 7. block engine (docs/BLOCKS.md)
 * ------------------------------------------------------------------ */

.mg-block-heading {
  margin-top: var(--mg-space-6);
}

.mg-block-text,
.mg-block-list,
.mg-block-image,
.mg-block-gallery,
.mg-block-columns,
.mg-block-timeline,
.mg-block-page-link,
.mg-block-toc,
.mg-block-quote,
.mg-block-table {
  margin: var(--mg-space-5) 0;
}

.mg-block-image {
  margin-inline: 0;
}

.mg-block-image img,
.mg-block-gallery img {
  max-width: 100%;
  height: auto;
  border-radius: var(--mg-radius-sm);
}

/* The <svg> that holds the dark-mode colour matrix. It draws nothing; taking it
   out of flow keeps a 0x0 inline element from contributing a line box. */
.mg-filter-defs {
  position: absolute;
  inline-size: 0;
  block-size: 0;
  overflow: hidden;
}

/* Dark-mode image inversion (docs/SKIN_API.md "다크모드 계약").
 *
 * ⛔⛔ TWO selectors and no more. This filter belongs to the `image` and
 * `slideshow` blocks and nothing else -- character art, board galleries,
 * emoticons, covers, button images and the lightbox are all left alone by
 * decision (2026-09-01 사람 결정). Widening this to something like
 * `#content img` would repaint every illustration on the site.
 *
 * ⛔ A filter makes its element a containing block for position:fixed
 * descendants. Neither block has one -- the lightbox hooks onto `gallery`,
 * `character_gallery` and the board galleries, none of which are here -- but a
 * fixed child added inside either block later would start scrolling with the
 * page, with no error. Same trap the page-transition rules avoid in #content.
 *
 * ⛔⛔ The filter is an SVG colour matrix, NOT `invert(1) hue-rotate(180deg)`.
 * The matrix, why CSS cannot express it, and why it looked obviously inverted
 * before are all in templates/core/_dark_invert_filter.html, which is also the
 * node this url() resolves against. ⚠ Change the id in one place and the other
 * stops matching: the browser then draws the image unfiltered, which in dark
 * mode looks like the block simply opted out. No error either way -- a guard in
 * apps/blocks/tests.py ties the two together.
 *
 * ⚠ Some colour still clips: a fully saturated blue cannot be reproduced by any
 * linear approximation and comes back lighter than it started. The block's
 * `dark: "keep"` remains the way out. Measurements: apps/blocks/types/common.py. */
:root[data-mg-theme="dark"] .mg-block-image--dark-invert img,
:root[data-mg-theme="dark"] .mg-block-slideshow--dark-invert .mg-block-slideshow__slide img,
:root[data-mg-theme="dark"] .mg-block-button__image--dark-invert {
  filter: url("#mg-dark-invert");
}

.mg-block-image figcaption,
.mg-block-gallery figcaption {
  color: var(--mg-text-muted);
  font-size: var(--mg-font-size-sm);
  margin-top: var(--mg-space-2);
}

/* ------------------------------------------------------------------ *
 * Block prose voice (v0.3-c-3b)
 *
 * The ROOT heading ladder and list markers lived only in core/prose.css, which
 * is scoped to `.root-prose` -- the board post viewer and the markdown editor.
 * A PAGE is an Editor.js block array with `mg-block-*` classes and no
 * `.root-prose` anywhere, so pages were getting the plain global h2/h3/h4 and
 * the browser's default bullets (2026-08-31 사람 보고). Same visual language,
 * expressed on the block classes.
 *
 * ⚠ Blocks have something prose does not: alignment. The left-anchored marks
 * (H2's gold bar, H3's diamond) are attached to `--left` only -- centred text
 * with a mark stuck to the far left reads as a mistake, not as a system.
 * ------------------------------------------------------------------ */

/* H2 -- 장 표제. Gold block bar on the leading edge, ink rule under, gold tail. */
h2.mg-block-heading {
  padding-block-end: var(--mg-space-2);
  border-block-end: var(--mg-stroke-1) solid var(--mg-border-strong);
  position: relative;
}

h2.mg-block-heading--left {
  padding-inline-start: var(--mg-space-4);
  border-inline-start: var(--mg-stroke-3) solid var(--mg-accent);
}

h2.mg-block-heading::after {
  content: "";
  position: absolute;
  inset-inline-start: 0;
  bottom: calc(-1 * var(--mg-stroke-1));
  width: 64px;
  height: var(--mg-stroke-1);
  background: var(--mg-bar-gold);
}

/* H3 -- 절 표제. The measuring diamond. */
h3.mg-block-heading--left {
  padding-inline-start: 22px;
  position: relative;
}

h3.mg-block-heading--left::before {
  content: "";
  position: absolute;
  inset-inline-start: 0;
  top: 0.42em;
  width: 9px;
  height: 9px;
  background: var(--mg-accent);
  border: var(--mg-stroke-1) solid var(--mg-border-strong);
  transform: rotate(45deg);
}

/* H4 -- 소표제. A short rule above, tracked out. */
h4.mg-block-heading {
  padding-block-start: var(--mg-space-2);
  border-block-start: var(--mg-stroke-1) solid var(--mg-border-strong);
  letter-spacing: var(--mg-track-wide);
}

/* h5·h6 — 2026-09-03 에 열었다. ⛔ 프로즈(.root-prose)는 둘을 이미 그리고
   있었지만 **페이지 블록은 다른 표면**이라 여기 없으면 h4 와 구분이 사라진다
   (활자 언어가 둘인 기존 함정). 프로즈의 같은 단계를 블록 어휘로 옮긴다. */
h5.mg-block-heading {
  font-family: var(--mg-font-mono);
  font-size: var(--mg-font-size-xs);
  letter-spacing: var(--mg-track-wider);
  text-transform: uppercase;
  color: var(--mg-link);
  border-block-start: 0;
  padding-block-start: 0;
}

/* 가장 조용한 단계. 점선으로 h5 와 갈린다. */
h6.mg-block-heading {
  font-family: var(--mg-font-mono);
  font-size: var(--mg-font-size-xs);
  letter-spacing: var(--mg-track-widest);
  text-transform: uppercase;
  color: var(--mg-text-faint);
  padding-block-end: var(--mg-space-1);
  border-block-start: 0;
  padding-block-start: 0;
  border-block-end: var(--mg-stroke-1) dashed var(--mg-text-faint);
}

/* Lists -- gold square markers and mono 01 numbering.
   ⛔ ::marker, not `list-style: none` plus an absolutely positioned ::before
   the way prose does it. Blocks can be centred, and an absolute marker would
   stay pinned to the left edge while the text moved away from it. ::marker
   follows list-style-position, so the alignment rules below keep working.
   ⚠ `content` on ::marker is Chrome/Firefox; Safari falls back to plain
   decimal numbers, which is a downgrade and not a break. */
ul.mg-block-list {
  list-style-type: square;
}

/* ⛔ --mg-link, not --mg-accent. A ::marker is a GLYPH, so it is type, and
   ROOT steps gold type down to the gold-700 ramp -- bright gold fails contrast
   on this ground (test_gold_is_never_a_text_colour caught this one).
   prose.css gets to use the bright gold for the same bullet because there it
   is a background on a real square: material, not type. */
ul.mg-block-list li::marker {
  color: var(--mg-link);
}

ol.mg-block-list li::marker {
  content: counter(list-item, decimal-leading-zero) ". ";
  font-family: var(--mg-font-mono);
  font-size: var(--mg-font-size-xs);
  font-weight: 700;
  color: var(--mg-link);
}

/* 중첩 목록 (v0.3-k-1). 마커는 위 규칙이 자손으로 잡으므로 여기서 다시 칠하지
   않는다 — 색을 두 곳에 적으면 한쪽만 고쳐진다.
   ⛔ 위 여백을 지운다. 블록 목록의 바깥 여백(.mg-block-list)은 **블록 사이**를
   벌리는 값이라, 그것이 부모 항목과 자식 목록 사이에 들어가면 한 항목이 둘로
   갈라져 보인다. */
.mg-block-list__nested {
  margin-block: 0;
  padding-inline-start: var(--mg-space-5);
}

/* 깊이를 마커로 구분한다. ⚠ list-style-type 은 상속되므로 이 선언이 없으면
   2단계가 1단계와 똑같은 사각형이 되어 층이 안 보인다. */
ul .mg-block-list__nested {
  list-style-type: circle;
}

/* Block alignment (v0.3-c-2). image has no justify: its align is a placement
   axis, not a text-flow one -- see blocks/types/common.py */
.mg-block-heading--left,
.mg-block-text--left,
.mg-block-quote--left,
.mg-block-list--left { text-align: left; }

.mg-block-heading--center,
.mg-block-text--center,
.mg-block-quote--center,
.mg-block-list--center { text-align: center; }

.mg-block-heading--right,
.mg-block-text--right,
.mg-block-quote--right,
.mg-block-list--right { text-align: right; }

.mg-block-heading--justify,
.mg-block-text--justify,
.mg-block-quote--justify,
.mg-block-list--justify { text-align: justify; }

/* 문단 들여쓰기 (2026-09-01 사람 지시). 첫 줄 2em.

   ⛔ 표면을 가려 건다. 활자 언어는 두 곳이다 — 게시판 글은 `.root-prose`(core/
   prose.css)이고 페이지 본문은 `mg-block-*`(스킨)이다. 사람이 "페이지 내 텍스트
   요소에만" 이라고 지정했으므로 플레이트 안의 text 블록만 잡는다. `.mg-plate__inner`
   는 게시판 글 상세도 쓰지만 그쪽 안에는 `.mg-block-text` 가 없다.
   ⚠ 메인·사이드바·프로필의 text 블록은 플레이트 밖이라 해당 없다.

   ⛔ 가운데·오른쪽 정렬에는 걸지 않는다 — 들여쓴 만큼 중심이 밀려서 정렬이
   어긋나 보이는데, `text-align` 은 멀쩡해서 CSS 만 읽으면 원인을 못 찾는다.
   ⚠ `text-indent` 는 **블록의 첫 줄**만 들여쓴다. 한 블록 안의 `<br>` 다음 줄은
   들여쓰지 않는다 — `each-line` 은 크로미움이 지원하지 않아 쓰지 않았다. */
.mg-plate__inner :is(.mg-block-text--left, .mg-block-text--justify) { text-indent: 2em; }
.mg-plate__inner b{
  font-weight: 700;
  color: var(--mg-link);
}
/* With outside markers only the text would move and the bullets would stay put. */
.mg-block-list--center,
.mg-block-list--right,
.mg-block-list--justify { list-style-position: inside; }

/* .mg-table th/td pin text-align: left, so inheritance alone cannot win. */
.mg-block-table--left th, .mg-block-table--left td { text-align: left; }
.mg-block-table--center th, .mg-block-table--center td { text-align: center; }
.mg-block-table--right th, .mg-block-table--right td { text-align: right; }
.mg-block-table--justify th, .mg-block-table--justify td { text-align: justify; }

.mg-block-image--left { text-align: left; }
.mg-block-image--center { text-align: center; }
.mg-block-image--right { text-align: right; }

.mg-block-image--normal img { max-width: 50%; }
.mg-block-image--wide img { max-width: 75%; }
.mg-block-image--full img { max-width: 100%; }

/* ------------------------------------------------------------------ *
 * Block frame (v0.3-c-3) -- the eight ROOT box edges, ported from the
 * design system's Frame component (project d7baae82, components/core).
 *
 * ROOT does not put one rounded box around everything: routine, mission,
 * document and warning are told apart by SHAPE. All eight share the same
 * 2px --mg-border-strong stroke, or they stop reading as one system.
 *
 * ⭐ STATIC ONLY -- no hover sweep, no lift, no click flash.
 * The design system drives those from React state (Frame.jsx), and a block on
 * a page is not a button: pointer feedback on a box invites a click that does
 * nothing. Frame.prompt.md already defines this state -- it is
 * `interactive={false}`, "모든 피드백이 꺼지고 정적 블록이 된다".
 *
 * ⚠ The design system also caps usage: "한 화면에 3종 이상 혼용 금지" and
 * notch is the default. The core cannot enforce that -- the admin picks per
 * block -- so it is documented in docs/SKIN_API.md, not coded here.
 * ------------------------------------------------------------------ */

.mg-frame {
  position: relative;
  box-sizing: border-box;
  /* Half the block rhythm. The frame carries the gap because the block's own
     margin is zeroed inside it (rule below), and framed cards sit closer to
     each other than plain blocks do (2026-08-31 사람 결정). */
  margin: var(--mg-space-3) 0;
  padding: var(--mg-space-3);
}

/* ⛔ Trapped edge margins. A block brings `margin: var(--mg-space-5) 0`, and a
   container that establishes an independent formatting context -- a grid item,
   or any box with padding -- cannot let that margin collapse out of itself. The
   result is a band of empty space at the top and bottom of every such container
   that nobody wrote and nothing reports.
 *
 * The frame hit this first (it WRAPS the block rather than sharing its element,
 * renderer.py _framed), but every one of these containers has the same shape:
 *   .mg-block-columns__column   grid item
 *   .mg-block-accordion__body   inside a bordered panel
 *   .mg-block-tabs__panel       same
 *   .mg-plate__inner            padded
 *   .mg-shell-sidebar           flex item
 *
 * ⚠ Add a new block container to this list, not a one-off `margin: 0` on
 * whatever block happened to land inside it. */
:where(
  .mg-frame,
  .mg-block-columns__column,
  .mg-block-accordion__body,
  .mg-block-tabs__panel,
  .mg-plate__inner,
  .mg-shell-sidebar
) > :first-child { margin-block-start: 0; }

:where(
  .mg-frame,
  .mg-block-columns__column,
  .mg-block-accordion__body,
  .mg-block-tabs__panel,
  .mg-plate__inner,
  .mg-shell-sidebar
) > :last-child { margin-block-end: 0; }

/* 01 notch -- the default. A diagonal cut instead of a radius.
   Two clip paths 2px apart: the outer box IS the stroke and the inset pseudo
   is the card, so the ink edge keeps an even width around the cut corner.
   isolation + z-index: -1 puts the card under the text but over the stroke;
   without the isolation the pseudo would fall behind the stroke entirely. */
.mg-frame--notch {
  isolation: isolate;
  padding: calc(var(--mg-space-5) + var(--mg-stroke-1));
  background: var(--mg-border-strong);
  clip-path: var(--mg-clip-notch);
}

.mg-frame--notch::before {
  content: "";
  position: absolute;
  inset: var(--mg-stroke-1);
  z-index: -1;
  background: var(--mg-surface);
  clip-path: var(--mg-clip-notch-inner);
}

/* 02 bracket -- four corner anchors, for a block that reads as a target.
   Eight gradients in a pseudo-element, not four child spans as in the design
   system: one pseudo can hold all four corners, and a pseudo is cheaper than
   markup the block templates would each have to print.

   ⛔ The corners CANNOT live on .mg-frame--bracket itself. An inset box-shadow
   is painted over the element's own background, and --mg-frame-ring is a 2px
   inset ring at the very edge -- exactly where the 2px corner bars sit. The
   ink ring covered the gold and the corners looked colourless (2026-08-31
   사람 발견). A positioned descendant paints after the parent's background and
   shadow, so the pseudo puts them back on top. */
.mg-frame--bracket {
  background: var(--mg-surface);
  box-shadow: var(--mg-frame-ring);
}

.mg-frame--bracket::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-repeat: no-repeat;
  background-image:
    linear-gradient(var(--mg-accent), var(--mg-accent)),
    linear-gradient(var(--mg-accent), var(--mg-accent)),
    linear-gradient(var(--mg-accent), var(--mg-accent)),
    linear-gradient(var(--mg-accent), var(--mg-accent)),
    linear-gradient(var(--mg-accent), var(--mg-accent)),
    linear-gradient(var(--mg-accent), var(--mg-accent)),
    linear-gradient(var(--mg-accent), var(--mg-accent)),
    linear-gradient(var(--mg-accent), var(--mg-accent));
  background-size:
    16px var(--mg-stroke-1), var(--mg-stroke-1) 16px,
    16px var(--mg-stroke-1), var(--mg-stroke-1) 16px,
    16px var(--mg-stroke-1), var(--mg-stroke-1) 16px,
    16px var(--mg-stroke-1), var(--mg-stroke-1) 16px;
  background-position:
    left top, left top,
    right top, right top,
    left bottom, left bottom,
    right bottom, right bottom;
}

/* 03 tab -- a document. The tab rides above the box, so the block carries a
   matching top margin or it collides with whatever is above it.
   The tab is a blank stub: the design system fills it from a `label` prop and
   the block schema has no such field yet (docs/SKIN_API.md 프레임 계약). */
.mg-frame--tab {
  margin-top: calc(var(--mg-space-5) + 20px);
  border: var(--mg-stroke-1) solid var(--mg-border-strong);
  background: var(--mg-surface);
}

.mg-frame--tab::before {
  content: "";
  position: absolute;
  top: calc(-20px - var(--mg-stroke-1));
  inset-inline-start: calc(-1 * var(--mg-stroke-1));
  width: 72px;
  height: 20px;
  background: var(--mg-border-strong);
  clip-path: var(--mg-clip-tab);
}

/* 04 rail -- a repeating list row. The 26px rail is the left border itself, so
   the text column needs no extra grid. The two gold ticks sit inside it:
   absolute offsets resolve against the padding box, so a negative inline start
   reaches into the border area. */
.mg-frame--rail {
  border: var(--mg-stroke-1) solid var(--mg-border-strong);
  border-inline-start-width: 26px;
  background: var(--mg-surface);
}

.mg-frame--rail::before,
.mg-frame--rail::after {
  content: "";
  position: absolute;
  inset-inline-start: -18px;
  width: 10px;
  height: var(--mg-stroke-1);
  background: var(--mg-accent);
}

.mg-frame--rail::before { top: var(--mg-space-2); }
.mg-frame--rail::after { bottom: var(--mg-space-2); }

/* 05 hazard -- a real warning state. ⚠ Not decoration: the design system says
   "실제 위험 상태에만. 장식으로 쓰지 않는다". The stripe is danger over ink,
   and it does not travel -- the moving version is the hover state we do not
   ship. */
.mg-frame--hazard {
  padding-top: calc(var(--mg-space-5) + 2px);
  border: var(--mg-stroke-1) solid var(--mg-border-strong);
  background: var(--mg-surface);
}

.mg-frame--hazard::before {
  content: "";
  position: absolute;
  inset-inline: 0;
  top: 0;
  height: 10px;
  border-bottom: var(--mg-stroke-1) solid var(--mg-border-strong);
  background-image: repeating-linear-gradient(
    135deg,
    var(--mg-danger) 0 9px,
    var(--mg-text) 9px 18px
  );
}

/* 06 slot -- a cartridge bay. The notch is cut out of the top edge, so the
   first line of text has to clear it. */
.mg-frame--slot {
  isolation: isolate;
  padding: calc(var(--mg-space-5) + var(--mg-stroke-1));
  padding-top: calc(var(--mg-space-5) + 14px);
  background: var(--mg-border-strong);
  clip-path: var(--mg-clip-slot);
}

.mg-frame--slot::before {
  content: "";
  position: absolute;
  inset: var(--mg-stroke-1);
  z-index: -1;
  background: var(--mg-surface);
  clip-path: var(--mg-clip-slot-inner);
}

/* 07 scan -- a measured field. The 22px grid is what carries the instrument
   reading; the two crosshairs mark the diagonal. */
.mg-frame--scan {
  border: var(--mg-stroke-1) solid var(--mg-border-strong);
  background-color: var(--mg-surface);
  background-image:
    linear-gradient(var(--mg-border) 1px, transparent 1px),
    linear-gradient(90deg, var(--mg-border) 1px, transparent 1px);
  background-size: 100% 22px, 22px 100%;
}

.mg-frame--scan::before,
.mg-frame--scan::after {
  content: "";
  position: absolute;
  width: 9px;
  height: 9px;
  background-repeat: no-repeat;
  background-image:
    linear-gradient(var(--mg-accent), var(--mg-accent)),
    linear-gradient(var(--mg-accent), var(--mg-accent));
  background-size: 9px var(--mg-stroke-1), var(--mg-stroke-1) 9px;
  background-position: 0 4px, 4px 0;
}

.mg-frame--scan::before { top: 6px; inset-inline-start: 6px; }
.mg-frame--scan::after { bottom: 6px; inset-inline-end: 6px; }

/* 08 punch -- an archived record. A punched card column and a dashed fold. */
.mg-frame--punch {
  padding-inline-start: calc(var(--mg-space-5) + 22px);
  border: var(--mg-stroke-1) solid var(--mg-border-strong);
  background: var(--mg-surface);
}

.mg-frame--punch::before {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  width: 22px;
  background-color: var(--mg-surface-tint);
  background-image: radial-gradient(circle, var(--mg-border) 2.5px, transparent 2.6px);
  background-size: 22px 22px;
  background-position: center 6px;
  border-inline-end: var(--mg-stroke-1) dashed var(--mg-border);
}

.mg-block-gallery,
.mg-block-columns {
  display: grid;
  gap: var(--mg-space-6);
}

.mg-block-gallery__item {
  margin: 0;
}

.mg-block-gallery--cols-2,
.mg-block-columns--1-1 {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.mg-block-gallery--cols-3,
.mg-block-columns--1-1-1 {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

.mg-block-gallery--cols-4 {
  grid-template-columns: repeat(4, minmax(0, 1fr));
}

.mg-block-columns--1-1-1-1 {
  grid-template-columns: repeat(4, minmax(0, 1fr));
}

.mg-block-columns--1-2 { grid-template-columns: 1fr 2fr; }
.mg-block-columns--2-1 { grid-template-columns: 2fr 1fr; }
/* Three columns with a wider middle: the character profile puts a certificate
   rail and a contents rail either side of the reading column. */
.mg-block-columns--1-2-1 { grid-template-columns: 1fr 2fr 1fr; }
.mg-block-columns--1-3-1 { grid-template-columns: 1fr 3fr 1fr; }

.mg-block-quote {
  margin-inline: 0;
  padding: var(--mg-space-3) var(--mg-space-5);
  border-inline-start: var(--mg-space-1) solid var(--mg-accent-soft);
  color: var(--mg-text);
}

.mg-block-quote__cite {
  display: block;
  color: var(--mg-text-muted);
  font-size: var(--mg-font-size-sm);
  font-style: normal;
  margin-top: var(--mg-space-2);
}

.mg-block-timeline {
  list-style: none;
  padding-inline-start: 0;
  border-inline-start: var(--mg-border-width) solid var(--mg-border);
}

.mg-block-timeline__item {
  padding: 0 0 var(--mg-space-5) var(--mg-space-5);
}

/* 연도와 제목은 제목 얼굴, 상세 내용만 본문 얼굴이다 (2026-09-01 사람 지시).
   연표에서 읽는 순서는 "언제 → 무엇이 → 어떻게"이고, 앞의 둘은 훑는 말이라
   본문과 같은 얼굴이면 세 줄이 한 덩어리로 뭉쳐 보인다.

   ⛔ 굵기를 700으로 함께 올린다. 제목 사슬(Chakra Petch 600·700 / Gothic A1
   500·700·800·900)에 300단이 없어서, 얼굴만 바꾸고 300을 두면 브라우저가 가까운
   단을 멋대로 고른다 — 파일에 적힌 굵기와 화면의 굵기가 갈리는데 오류는 없다.
   700은 같은 크기(--mg-font-size-md)의 h3와 같은 값이다. */
.mg-block-timeline__label {
  display: block;
  font-family: var(--mg-font-heading);
  color: var(--mg-link);
  font-weight: 700;
  font-size: var(--mg-font-size-sm);
}

.mg-block-timeline__title {
  display: block;
  font-family: var(--mg-font-heading);
  font-weight: 500;
  font-size: var(--mg-font-size-md);
}

.mg-block-toc {
  background: var(--mg-surface-tint);
  border-radius: var(--mg-radius);
  padding: var(--mg-space-4) var(--mg-space-5);
}

.mg-block-toc ol {
  list-style: none;
  padding-inline-start: 0;
  margin: 0;
}

.mg-block-toc__item--l3 { padding-inline-start: var(--mg-space-4); }
.mg-block-toc__item--l4 { padding-inline-start: var(--mg-space-6); }

.mg-block-page-link {
  list-style: none;
  padding-inline-start: 0;
}

.mg-block-page-link--card {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
  gap: var(--mg-space-4);
}

.mg-block-page-link--card li {
  background: var(--mg-surface);
  border: var(--mg-border-width) solid var(--mg-border);
  border-radius: var(--mg-radius);
  box-shadow: var(--mg-shadow);
  padding: var(--mg-space-4);
}

.mg-block-divider {
  border-top: var(--mg-border-width) solid var(--mg-border);
  opacity:0.2;
}

/* Rendered by the engine for perm_level >= 8 viewers only. */
.mg-block-error {
  border: var(--mg-border-width) dashed var(--mg-border-strong);
  border-radius: var(--mg-radius-sm);
  color: var(--mg-text-muted);
  font-size: var(--mg-font-size-sm);
  padding: var(--mg-space-3);
}

/* ------------------------------------------------------------------ *
 * 8. boards
 * ------------------------------------------------------------------ */

.mg-post-subtitle {
  color: var(--mg-text-muted);
  font-size: var(--mg-font-size-md);
  margin-top: calc(var(--mg-space-3) * -1);
}

/* Thumbnail and gallery grids wrap with flex, not with
   repeat(auto-fill, minmax(...)). An auto-fill track whose minimum is an
   intrinsic size is invalid and drops the whole declaration silently, which
   collapsed these to a single column once already (commit 169fe88). Flex
   wrapping has no such cliff, and apps/boards/tests.py holds the mechanism
   in place. */
.mg-post-grid {
  display: flex;
  flex-wrap: wrap;
  gap: var(--mg-space-4);
  list-style: none;
  padding-inline-start: 0;
}

.mg-post-grid__item {
  flex: 0 1 12rem;
  max-width: 100%;
}

/* Artwork rests desaturated and returns to full colour under the cursor —
   ROOT's one image treatment. The wrapper clips so the scale never bleeds past
   the card edge. --mg-radius-sm is the input radius (3px); images sit a step up. */
.mg-post-grid__item,
.mg-post-mixed-list__item {
  overflow: hidden;
  border-radius: var(--mg-radius-md);
}

.mg-post-grid__item img,
.mg-post-mixed-list__item img,
.mg-gallery__item img {
  max-width: 100%;
  height: auto;
  border-radius: var(--mg-radius-md);
  filter: grayscale(0.35);
  transition: filter var(--mg-transition-slow), transform var(--mg-transition-slow);
}

.mg-post-grid__item:hover img,
.mg-post-mixed-list__item:hover img {
  filter: grayscale(0);
  transform: scale(1.05);
}

.mg-post-mixed-list {
  list-style: none;
  padding-inline-start: 0;
}

.mg-post-mixed-list__item {
  border-bottom: var(--mg-border-width) solid var(--mg-border);
  padding: var(--mg-space-3) 0;
}

.mg-post-mixed-list__item a {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--mg-space-4);
}

.mg-gallery {
  display: flex;
  flex-wrap: wrap;
  gap: var(--mg-space-3);
  margin: var(--mg-space-5) 0;
}

/* This class lands on two different elements. A viewer who passes download_level
   gets <button class="mg-gallery__item"><img></button>; everyone else gets a bare
   <img class="mg-gallery__item"> with no lightbox hook (docs/SKIN_API.md). The
   descendant rule above only reaches the first shape, so height must be declared
   here as well — otherwise flex shrinks the width while the intrinsic height
   attribute survives and the image renders as a tall strip. */
.mg-gallery__item {
  flex: 0 1 10rem;
  max-width: 100%;
  height: auto;
  background: var(--mg-surface-tint);
  border: var(--mg-border-width) solid var(--mg-border);
  border-radius: var(--mg-radius-sm);
  padding: 0;
  overflow: hidden;
  cursor: zoom-in;
}

/* No opening hook on the bare image, so it must not advertise one. */
img.mg-gallery__item {
  cursor: default;
}

/* Lightbox. The overlay lives inside #content so an hx-boost swap disposes of
   it (docs/SKIN_API.md). */
.mg-lightbox {
  position: fixed;
  inset: 0;
  /* ⛔ 셸의 어떤 층보다 위다 — 고정 헤더(2)·모바일 드로어(20)·오류 배너보다 높다.
     ⚠ 숫자만으로는 부족했다: 오버레이가 sticky 인 좌열 **안**에 있으면 그 열이
     쌓임 맥락을 만들어 여기 값이 그 안에서만 뜻을 갖는다. 그래서 오버레이를
     블록 밖(페이지)으로 옮겼다 (2026-09-05 사람 신고). */
  z-index: 100;
  display: grid;
  place-items: center;
  gap: var(--mg-space-4);
  padding: var(--mg-space-5);
  background: var(--mg-bg);
  overflow: auto;
}

.mg-lightbox[hidden] {
  display: none;
}

/* ⭐ 열려 있는 동안 문서 스크롤을 멈춘다 — 그래야 오버레이가 스크롤바 자리까지
   덮는다 (2026-09-05 사람 질문).
   ⛔⛔ 상태를 <body> 클래스로 만들지 않는다. 셸은 hx-boost 가 갈아끼우지 않아
   **이동해도 안 풀리고**, 그러면 사이트 전체가 스크롤을 잃는다(모바일 드로어 사건).
   여기서는 오버레이 자신의 `hidden` 을 :has() 로 읽으므로, 오버레이가 #content 와
   함께 스왑되는 순간 조건도 같이 사라진다. ⚠ :has() 를 모르는 브라우저에서는
   아무 일도 안 일어난다 — 오버레이는 그대로 뜨고 스크롤바만 남는다.
   ⚠ scrollbar-gutter 로 자리를 늘 비워 둔다. 없으면 스크롤이 멈추는 순간 본문이
   스크롤바 폭만큼 옆으로 튄다. */
html { scrollbar-gutter: stable; }

html:has(.mg-lightbox:not([hidden])) { overflow: hidden; }

/* ⛔ 폭을 본문 폭에 맞춘다. 원본이 그보다 작으면 **늘리지 않는다** — inline-size 를
   지정하지 않고 max 로만 자르는 이유다 (2026-09-05 사람 지시).
   ⚠ 높이는 안 자른다. 세로로 긴 전신을 80vh 로 자르면 폭이 본문 폭에 한참 못
   미쳐 작게 뜬다 — 넘치는 만큼은 오버레이가 스크롤한다. */
.mg-lightbox__image {
  max-inline-size: min(100%, var(--mg-lightbox-width));
  block-size: auto;
  object-fit: contain;
}

/* ⚠ 확대 버튼도 같은 생김새다 (2026-09-06 사람 지시) — 같은 오버레이 안의 같은
   층인데 혼자 맨몸이면 "아직 안 만든 것" 으로 읽힌다.
   ⛔ 이 묶음은 **세로 위치를 안 정한다**(prev·next 는 플렉스가 가운데로 놓고
   close·zoom 만 제 규칙에서 정한다). 여기에 top 을 적으면 아래쪽에 붙는 zoom 이
   위아래로 늘어난다. */
.mg-lightbox__prev,
.mg-lightbox__next,
.mg-lightbox__close,
.mg-lightbox__zoom {
  position: absolute;
  border: var(--mg-border-width) solid var(--mg-border);
  border-radius: var(--mg-radius-pill);
  background: var(--mg-surface);
  color: var(--mg-text);
  font: inherit;
  padding: var(--mg-space-2) var(--mg-space-4);
  cursor: pointer;
}

.mg-lightbox__prev { inset-inline-start: var(--mg-space-5); }
.mg-lightbox__next { inset-inline-end: var(--mg-space-5); }
.mg-lightbox__close { top: var(--mg-space-5); inset-inline-end: var(--mg-space-5); }

.mg-lightbox__caption { margin: 0; color: var(--mg-text); }
.mg-lightbox__counter { margin-inline-start: var(--mg-space-2); color: var(--mg-text-muted); }
.mg-lightbox__zoom { inset-block-end: var(--mg-space-5); inset-inline-end: var(--mg-space-5); }
.mg-lightbox--zoom { overflow: auto; align-items: start; }
.mg-lightbox--zoom .mg-lightbox__image { max-width: none; max-height: none; width: auto; }
.mg-lightbox--zoom .mg-lightbox__prev,
.mg-lightbox--zoom .mg-lightbox__next { display: none; }

/* Links inside block content. The shell's bare `a` rule only recolours, which
   on a page reads as ordinary text that happens to be gold -- authors kept
   reporting links as "not applied" (2026-08-31). Board posts already state a
   link with weight as well as colour (.root-prose a); block content matches
   that, minus the underline (사람 결정).

   Written against the block classes rather than the plate so a link looks the
   same wherever blocks render: page body, sidebar, popup, profile. */
:where(.mg-block-text, .mg-block-list, .mg-block-quote, .mg-block-table, .mg-block-heading) a {
  color: var(--mg-link);
  font-weight: 600;
  text-decoration: none;
}

:where(.mg-block-text, .mg-block-list, .mg-block-quote, .mg-block-table, .mg-block-heading) a:hover {
  color: var(--mg-link-hover);
  text-decoration: underline;
}

/* Button block (docs/plans/v0.3-e-10-button-block.md).
 *
 * ⛔ This is the ONE exception to the frame section's "STATIC ONLY" rule, and
 * the reason is the same one that made the rule: pointer feedback on a box
 * invites a click. A button is the one block where that click does something,
 * so the design system's interactive state (Frame `interactive={true}`) belongs
 * here and nowhere else. The selectors below never widen past a button.
 *
 * Motion is the design system's, not invented here (guidelines/frame-variants):
 * hover lays a 12% wash and sweeps it left→right ONCE, press answers with a
 * tone flash 40%→0, and the block rises 3px on hover and settles 1px on press.
 *
 * ⚠ Durations come from tokens, never a literal: tokens.css zeroes every
 * duration under prefers-reduced-motion, and a hard-coded 420ms would keep
 * animating for a reader who asked the system to stop. */
/* 정렬은 감싸는 줄이 정한다 — 버튼 자신은 inline-flex 라 글자만큼만 넓다. */
.mg-block-button-wrap { display: block; }
.mg-block-button-wrap--left { text-align: start; }
.mg-block-button-wrap--center { text-align: center; }
.mg-block-button-wrap--right { text-align: end; }

.mg-block-button {
  position: relative;
  isolation: isolate;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-block-size: var(--mg-control-h);
  padding: var(--mg-space-2) var(--mg-space-6);
  border: var(--mg-border-width) solid var(--mg-border-strong);
  border-radius: var(--mg-radius);
  background: var(--mg-surface);
  color: var(--mg-text-strong);
  font-family: var(--mg-font-body);
  font-weight: 700;
  font-size: var(--mg-font-size-sm);
  line-height: 1.6;
  text-decoration: none;
  overflow: hidden;
  transition: transform var(--mg-transition-fast), box-shadow var(--mg-transition-fast);
}

/* The sweep and the flash are separate layers so one can run on hover and the
   other on press without either resetting the other. */
/* ⚠ 절대 배치의 기준 상자는 padding box 다 — `inset: 0` 이면 테두리 두께만큼
   훑고 지나가지 않는다. 테두리 폭만큼 밖으로 물려 border box 를 덮는다. */
/* 칸을 꽉 채우는 선택. ⚠ 기본 규칙(`display: inline-flex`) **뒤**에 있어야
   한다 — 같은 명시도라 순서가 이긴다. */
.mg-block-button--fill {
  display: flex;
  inline-size: 100%;
}

.mg-block-button::before,
.mg-block-button::after {
  content: "";
  position: absolute;
  inset: calc(-1 * var(--mg-border-width));
  z-index: -1;
  opacity: 0;
  pointer-events: none;
}

.mg-block-button::before {
  background-image: var(--mg-wash-gold);
  background-size: 220% 100%;
  background-repeat: no-repeat;
}

.mg-block-button::after { background: var(--mg-accent); }

/* ⛔ 그림자를 변수로 받는다. 투명한 버튼(아이콘·이미지)에서 이 그림자가 그대로
   나오면 **아무것도 없는 사각형이 떠 있는 것처럼** 보이는데, 그렇다고 hover 를
   한 벌 더 만들면 상호작용이 두 갈래가 된다 — 값만 바꾼다. */
.mg-block-button:hover,
.mg-block-button:focus-visible {
  transform: translateY(-3px);
  box-shadow: var(--mg-button-lift-shadow);
}

/* ⛔ One pass. `infinite` here is a shine, and ROOT does not shine. */
.mg-block-button:hover::before,
.mg-block-button:focus-visible::before {
  opacity: 1;
  animation: mg-sweep var(--mg-duration-slow) var(--mg-ease-out) 1;
}

.mg-block-button:active {
  transform: translateY(1px);
  box-shadow: none;
}

.mg-block-button:active::after {
  animation: mg-flash var(--mg-duration-fast) var(--mg-ease-out) 1;
}

/* 아이콘·이미지 버튼 (v0.3-h-3). 글자 버튼과 **같은 상호작용**을 쓴다 — 들어
   올림·스윕·클릭 플래시가 위에서 이미 걸려 있고, 여기서는 담는 그릇만 바꾼다.
   ⛔ hover 를 따로 정의하지 않는다: 두 벌이 되면 덜 보이는 쪽이 먼저 어긋난다
   (블록 프레임 여덟 종에서 배운 것과 같은 규칙). */
/* ⛔ 아이콘·이미지 버튼은 **테두리도 배경도 없다** (2026-09-01 사람 지시).
   기본 규칙이 상자를 갖고 있어서, 테두리를 지정하지 않았는데도 블록 테두리를
   단 것처럼 보였다. 상자가 필요하면 블록 프레임(`--mg-frame-*`)을 쓴다 —
   그것이 테두리를 고르는 자리이고, 버튼이 제멋대로 상자를 갖고 있으면
   프레임을 껐을 때와 켰을 때가 구분되지 않는다.
   ⚠ min-block-size 는 그대로 둔다 — 손가락이 닿을 크기다. */
.mg-block-button--icon,
.mg-block-button--image {
  display: inline-grid;
  place-items: center;
  padding: var(--mg-space-2);
  line-height: 1;
  background: transparent;
  border-color: transparent;
  box-shadow: none;
  --mg-button-lift-shadow: none;
}

/* 아이콘은 글자보다 크게 — 버튼의 뜻을 혼자 짊어진다. */
.mg-block-button--icon .mg-icon {
  font-size: var(--mg-font-size-xl);
}

/* ⛔ 이미지에 크기를 안 주면 원본 픽셀대로 나와 버튼이 화면을 넘긴다.
   높이를 토큰으로 묶고 폭은 비율이 정한다. */
.mg-block-button__image {
  display: block;
  block-size: var(--mg-button-image-size);
  inline-size: auto;
  max-inline-size: 100%;
  object-fit: contain;
}

/* 칸 꽉 채우기에서는 이미지가 버튼 폭을 따라간다. */
.mg-block-button--fill.mg-block-button--image .mg-block-button__image {
  inline-size: 100%;
}

/* 이미지는 자기 여백을 갖지 않는다 — 그림 자체가 버튼의 얼굴이다.
   ⚠ :focus-visible 링은 남는다(전역 규칙). 키보드 사용자가 어디에 있는지
   알 수 없게 되면 안 된다. */
.mg-block-button--image {
  padding: 0;
}

.mg-block-button-notice {
  margin: 0 0 var(--mg-space-2);
  color: var(--mg-text-muted);
  font-size: var(--mg-font-size-sm);
}

/* A framed button keeps the frame's edge and hands the press feedback to the
   frame box, so the lift moves the whole card rather than the label inside it. */
/* ⛔ 테두리를 단 버튼은 **카드가 곧 버튼**이다. inline-block 으로 두면 카드가
   글자만큼 쪼그라들어 "칸 꽉 채우기"가 늘어날 곳이 없고, 정렬도 카드를 못
   옮긴다 (2026-09-01 사람 신고).
   `fit-content` 인 블록이라야 ①기본은 글자에 맞고 ②`--fill` 이 100% 로 늘리고
   ③`margin-inline: auto` 로 가운데·오른쪽 정렬이 먹는다. 정렬 클래스는 프레임
   **안쪽** 래퍼에 있으므로 :has() 로 읽는다. */
.mg-frame:has(.mg-block-button) {
  display: block;
  inline-size: fit-content;
  transition: transform var(--mg-transition-fast), box-shadow var(--mg-transition-fast);
}

.mg-frame:has(.mg-block-button--fill) { inline-size: 100%; }
.mg-frame:has(.mg-block-button-wrap--center) { margin-inline: auto; }
.mg-frame:has(.mg-block-button-wrap--right) { margin-inline-start: auto; }

.mg-frame:has(.mg-block-button):hover { transform: translateY(-3px); box-shadow: var(--mg-shadow-hover); }
.mg-frame:has(.mg-block-button):active { transform: translateY(1px); box-shadow: none; }
/* ⛔ 테두리를 단 버튼은 여백의 주인을 바꾼다.
   프레임은 자기 padding 으로 카드 모양을 만들고 버튼은 그 **안쪽**에만 있으므로,
   스윕을 버튼에 걸면 프레임의 여백 띠가 훑히지 않는다 — 커서를 올려도 글자
   둘레만 반짝인다 (2026-08-31 사람 신고).
   프레임에는 획 두께만 남기고 **여백을 버튼이 들고 간다.** 그러면 버튼이 카드를
   가득 채우고 스윕이 카드 전체를 지나간다.
   ⚠ 프레임 여덟 종은 장식을 저마다 `::before` 로 그리는데, 그 위치는 inset 기준이라
   padding 과 무관하다. 다만 tab·rail 처럼 장식이 자리를 요구하는 종류는 눈으로
   확인이 필요하다 — 기본값인 notch 기준으로 맞췄다. */
.mg-frame:has(.mg-block-button) {
  padding: var(--mg-stroke-1);
}

.mg-frame:has(.mg-block-button) .mg-block-button {
  border: 0;
  background: transparent;
  /* ⛔ 자기 라운드를 버린다. 안 버리면 스윕이 6px 둥근 사각형으로 잘려 노치
     카드 안에서 겉돈다 — 모서리만 색이 안 닿는 그 증상이다. 카드 모양은
     프레임의 clip-path 가 정한다. */
  border-radius: 0;
  inline-size: 100%;
  block-size: 100%;
  padding: var(--mg-space-4) var(--mg-space-6);
}
.mg-frame:has(.mg-block-button) .mg-block-button:hover,
.mg-frame:has(.mg-block-button) .mg-block-button:focus-visible { transform: none; box-shadow: none; }

/* Page font zoom (docs/plans/v0.3-e-1-page-font-zoom.md). The controls sit
   outside .mg-page-body so the buttons keep their own size while the document
   grows, and the whole thing lives inside #content -- never in the shell,
   which is what keeps home, intro and popups out of it.

   ⛔ The type tokens are redeclared HERE, on the element the scale is set on.
   A custom property is substituted where it is declared, so folding the
   multiplier into tokens.css :root would freeze the scale at :root and inherit
   the frozen value down -- the buttons would do nothing. Every
   --mg-font-size-* token has to be listed: one left out is a run of text that
   silently refuses to grow (apps/core/tests.py guards the pair). */
.mg-page-body {
  --mg-font-size-base: calc(var(--mg-font-size-base-raw) * var(--mg-page-font-scale, 1));
  --mg-font-size-xs: calc(var(--mg-font-size-xs-raw) * var(--mg-page-font-scale, 1));
  --mg-font-size-sm: calc(var(--mg-font-size-sm-raw) * var(--mg-page-font-scale, 1));
  --mg-font-size-md: calc(var(--mg-font-size-md-raw) * var(--mg-page-font-scale, 1));
  --mg-font-size-lg: calc(var(--mg-font-size-lg-raw) * var(--mg-page-font-scale, 1));
  --mg-font-size-xl: calc(var(--mg-font-size-xl-raw) * var(--mg-page-font-scale, 1));
  --mg-font-size-2xl: calc(var(--mg-font-size-2xl-raw) * var(--mg-page-font-scale, 1));
  font-size: var(--mg-font-size-base);
}

/* Auto table of contents, drawn as a sticky rail on the LEFT of the reading
   column, after the ROOT design-system reference "문서 뷰어 TOC" (2026-08-31).

   The rail is a ruled margin, not a filled box: no background, no radius, one
   hairline divider on its inner edge. ROOT reserves the hairline for a divider
   inside an already-bordered area, which is exactly what this is.

   ⛔ Hidden is the base state. A narrow screen has nowhere to put a rail, and
   the markup sits AFTER the article in the DOM, so leaving it in the flow would
   park the contents at the foot of the page -- worse than absent.

   ⛔ DOM order stays article-first and the LEFT placement is visual only, set by
   grid-column. Same rule the shell follows for a left sidebar: tab order keeps
   the reading order and no narrow-screen reorder is needed.

   Two widths, because the content box is capped and does not grow with the
   viewport (measured with the 236px left dock and the 960px reading box):

     >= 75rem   the rail takes a column INSIDE the content box. Viewport 1200 ->
                912px of #content -> 680px article beside a 208px rail.
     >= 102.5rem  the page finally has a gutter (viewport 1640 -> 222px each
                side + the 24px shell padding = 246px, against 208 + 24 needed),
                so the rail is pulled OUT of the box and the article gets its
                full measure back.

   ⚠ Both numbers follow --mg-reading-width and --mg-page-toc-width. Re-run the
   arithmetic if either token moves.

   ⚠ With a shell sidebar switched on, #content never exceeds ~600px whatever
   the viewport, so the in-box rail would leave ~370px of article. Those modes
   get the rail only in gutter form. */
.mg-page-toc {
  display: none;
  position: sticky;
  top: var(--mg-space-5);
  align-self: start;
  max-block-size: calc(100vh - var(--mg-space-8));
  overflow-y: auto;
  margin: 0;
  padding-inline-end: var(--mg-space-5);
  border-inline-end: var(--mg-stroke-hair) solid var(--mg-border);
}

/* Same resolution .mg-profile-toc reached: a fixed top header and a sticky rail
   fight over the same offset, so the rail stops sticking rather than hiding
   under it. */
body.mg-headerfix-on.mg-header-top .mg-page-toc { position: static; }

.mg-page-body__main { min-inline-size: 0; }

.mg-page-toc__label {
  margin: 0 0 var(--mg-space-3);
  font: 700 var(--mg-font-size-base) / 1.4 var(--mg-font-body);
  letter-spacing: var(--mg-track-widest);
  color: var(--mg-text-faint);
  text-transform: uppercase;
}

.mg-page-toc__list {
  list-style: none;
  padding-inline-start: 0;
  margin: 0;
}

/* The gold bar marks the entry the reader is on. Three states set the same
   bar: hover, keyboard focus, and the scroll position that
   static/core/js/page-toc.js writes as aria-current="true".

   ⛔ The spy owns no colour -- it only sets the attribute. Giving the current
   entry a look of its own would leave two golds to keep in step, and the one
   the reader sees least would be the one that drifts. */
.mg-page-toc__link {
  position: relative;
  display: flex;
  align-items: baseline;
  gap: var(--mg-space-3);
  padding: var(--mg-space-2) 0 var(--mg-space-2) var(--mg-space-3);
  text-decoration: none;
}

.mg-page-toc__link::before {
  content: "";
  position: absolute;
  inset-inline-start: 0;
  inset-block: 2px;
  inline-size: var(--mg-stroke-1);
  background: transparent;
  transition: background var(--mg-transition);
}

.mg-page-toc__link:hover::before,
.mg-page-toc__link:focus-visible::before,
.mg-page-toc__link[aria-current]::before { background: var(--mg-bar-gold); }

.mg-page-toc__text {
  font-size: var(--mg-font-size-sm);
  color: var(--mg-text-muted);
  transition: color var(--mg-transition);
}

/* The whole entry goes gold under the cursor, matching the bar beside it
   (2026-08-31 사람 결정 — the design reference numbered its entries instead).

   ⛔ Gold as TYPE steps down to --mg-link. --mg-accent (#FAD228) is the bar and
   highlighter gold and reads 1.6:1 on white: pointing at an entry would make it
   unreadable at the moment the reader looks at it. tokens.css rations gold to
   material and sends type to the gold-700 ramp. */
.mg-page-toc__link:hover .mg-page-toc__text,
.mg-page-toc__link:focus-visible .mg-page-toc__text,
.mg-page-toc__link[aria-current] .mg-page-toc__text { color: var(--mg-link); }

/* Depth reads as indent. Margin rather than padding so the gold bar travels
   with the entry instead of drifting away from its text. */
.mg-page-toc__item--l3 { margin-inline-start: var(--mg-space-4); }
.mg-page-toc__item--l4 { margin-inline-start: var(--mg-space-6); }

/* The grid declarations repeat in both queries on purpose: a rule cannot be
   shared across two media conditions, and hoisting them into the base rule
   would lay a two-column grid out on the screens that have no rail. */
@media (min-width: 75rem) {
  body:not(.mg-sidebar-left):not(.mg-sidebar-right) .mg-page-body {
    display: grid;
    grid-template-columns: var(--mg-page-toc-width) minmax(0, 1fr);
    column-gap: var(--mg-space-5);
    align-items: start;
  }

  body:not(.mg-sidebar-left):not(.mg-sidebar-right) .mg-page-body__main { grid-column: 2; }
  body:not(.mg-sidebar-left):not(.mg-sidebar-right) .mg-page-toc { display: block; grid-column: 1; grid-row: 1; }
}

@media (min-width: 102.5rem) {
  .mg-page-body {
    display: grid;
    grid-template-columns: var(--mg-page-toc-width) minmax(0, 1fr);
    column-gap: var(--mg-space-5);
    align-items: start;
    margin-inline-start: calc(-1 * (var(--mg-page-toc-width) + var(--mg-space-5)));
  }

  .mg-page-body__main { grid-column: 2; }
  .mg-page-toc { display: block; grid-column: 1; grid-row: 1; }
}

/* Font size control, after the same design reference. A joined segmented bar
   pushed to the reading side: square corners, one shared 2px edge between
   segments, mono labels, and the reset segment inverted in ink so the way back
   to the default reads as the terminal step rather than a fourth option. */
.mg-page-zoom {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  align-items: center;
  gap: 0;
  margin-block-end: var(--mg-space-5);
}

.mg-page-zoom__label {
  margin-inline-end: var(--mg-space-4);
  font: 700 var(--mg-font-size-xs) / 1.4 var(--mg-font-body);
  color: var(--mg-text-faint);
  text-transform: uppercase;
}

/* One shared edge: every segment drops its inner border and the last one closes
   the bar. A gap here would turn a single control into four buttons. */
.mg-page-zoom__step,
.mg-page-zoom__value,
.mg-page-zoom__reset {
  border: var(--mg-border-width) solid var(--mg-border);
  border-radius: 0;
  padding: var(--mg-space-2) var(--mg-space-4);
  font: 700 var(--mg-font-size-xs) / 1.4 var(--mg-font-body);
  letter-spacing: var(--mg-track-wide);
}

.mg-page-zoom__step,
.mg-page-zoom__value { border-inline-end: none; }

.mg-page-zoom__step {
  background: var(--mg-surface);
  color: var(--mg-text);
  cursor: pointer;
  transition: background var(--mg-transition-fast), color var(--mg-transition-fast);
}

.mg-page-zoom__step:hover:not(:disabled) { background: var(--mg-surface-tint); }

/* Both ends of the scale are reachable, and the segment that would do nothing
   says so instead of looking broken. */
.mg-page-zoom__step:disabled {
  color: var(--mg-text-faint);
  cursor: default;
}

/* The one gold in this control, and it is type -- so it takes --mg-link, not
   --mg-accent. Tabular figures keep the bar from resizing as the number steps. */
.mg-page-zoom__value {
  min-inline-size: 5ch;
  padding-inline: var(--mg-space-3);
  text-align: center;
  background: var(--mg-surface);
  color: var(--mg-link);
  font-variant-numeric: tabular-nums;
}

.mg-page-zoom__reset {
  background: var(--mg-text);
  color: var(--mg-surface);
  cursor: pointer;
  transition: background var(--mg-transition-fast);
}

.mg-page-zoom__reset:hover { background: var(--mg-text-strong); }

/* The plate — ROOT's article box. The title stays OUTSIDE it and the body
   sits inside a notched ink frame, so a document reads as a titled object
   rather than as a run of text. Two elements because the notch has to be cut
   twice: the outer 15px carries the 2px ink edge, the inner 13px carries the
   paper, and the 2px difference is what keeps the edge an even width around
   the cut. A single element with a border would have its corner squared off
   by the clip. */
.mg-plate {
  padding: var(--mg-stroke-1);
  margin-block: var(--mg-space-5);
  background: var(--mg-border-strong);
  clip-path: var(--mg-clip-notch);
  box-shadow: var(--mg-shadow-slab-soft);
}

.mg-plate__inner {
  background: var(--mg-surface);
  clip-path: var(--mg-clip-notch-inner);
  padding: var(--mg-space-6) var(--mg-space-6) var(--mg-space-7);
}

/* Prose inside a plate keeps ROOT's measure. The plate itself may be wider —
   a table or an image should be able to use the full box. */
.mg-plate__inner.root-prose > :where(p, ul, ol, blockquote, h1, h2, h3, h4, h5, h6) {
  /* max-inline-size: 68ch; */
}

/* The same measure for a page built out of blocks. Until now this was the one
   reading surface in the site without it: board posts, profile sections and
   character field values all cap at 68ch, but .mg-plate__inner on a page holds
   mg-block-* markup rather than .root-prose, so running text ran the full box.

   Descendant, not child: a block with a box edge is wrapped in .mg-frame by the
   core renderer, and a block inside a column is deeper still. The cap simply
   does not bind in a narrow column, so reaching them costs nothing.

   Tables, images and galleries are left out on purpose -- they are not running
   text and should be able to use the whole box. */
.mg-plate__inner :where(.mg-block-text, .mg-block-list, .mg-block-quote, .mg-block-heading) {
  /* max-inline-size: 68ch; */
}

/* Page body leading. Same surface the paragraph indent uses, and for the same
   reason: type has two languages here, .root-prose for board posts and
   mg-block-* for pages, so widening one does not touch the other.

   ⛔ Not --mg-line-height. That token is read by `body`, so raising it moves
   the shell, the menu, buttons, board posts and every manage screen along with
   the page -- the whole site gets looser when only the article was meant to.

   ⛔ Headings are left out. They take line-height 1.24 from the h1..h6 rule,
   and running-text leading on a two-line title reads as a gap, not as air.
   Tables, images and galleries are out for the reason the measure rule above
   gives: they are not running text. */
.mg-plate__inner :where(.mg-block-text, .mg-block-list, .mg-block-quote) {
  line-height: var(--mg-page-line-height);
}

/* A capped box would otherwise sit at the start of the plate, so centred text
   would read off-centre and right-aligned text would stop short of the edge. */
.mg-plate__inner :where(.mg-block-text--center, .mg-block-list--center, .mg-block-quote--center, .mg-block-heading--center) {
  margin-inline: auto;
}

.mg-plate__inner :where(.mg-block-text--right, .mg-block-list--right, .mg-block-quote--right, .mg-block-heading--right) {
  margin-inline-start: auto;
}

@media (max-width: 40rem) {
  .mg-plate__inner { padding: var(--mg-space-4); }
}

.mg-page-head, .mg-board-head { display: flex; flex-wrap: wrap; align-items: center; gap: var(--mg-space-3); margin-block-end: var(--mg-space-5); }
.mg-page-head__title, .mg-board-head__title { margin: 0; }
.mg-page-head__meta, .mg-board-head__desc { margin: 0; }
.mg-page-head__actions, .mg-board-head__actions { display: flex; gap: var(--mg-space-2); margin-inline-start: auto; }
.mg-badge { display: inline-block; padding: var(--mg-space-1) var(--mg-space-2); border-radius: var(--mg-radius-pill); font-size: var(--mg-font-size-xs); font-weight: 700; background: var(--mg-accent-soft); color: var(--mg-link-hover); }
.mg-badge--green { background: var(--mg-green-soft); color: var(--mg-green-hover); }
.mg-badge--muted { background: var(--mg-surface-tint); color: var(--mg-text-muted); }
.mg-badge--notice { background: var(--mg-accent-soft); color: var(--mg-link-hover); }
.mg-badge--pending { background: var(--mg-danger-soft); color: var(--mg-danger); }
.mg-avatar { display: block; inline-size: var(--mg-avatar-size); block-size: var(--mg-avatar-size); border: var(--mg-border-width) solid var(--mg-border); border-radius: var(--mg-radius-pill); background: var(--mg-surface-tint); object-fit: cover; }
.mg-avatar--sm { inline-size: var(--mg-avatar-size-sm); block-size: var(--mg-avatar-size-sm); }
.mg-avatar--empty { display: inline-block; }
.mg-character-grid { display: flex; flex-wrap: wrap; gap: var(--mg-space-4); }
.mg-character-card { flex: 1 1 13.75rem; max-width: 100%; border: var(--mg-border-width) solid var(--mg-border); border-radius: var(--mg-radius); background: var(--mg-surface); }
.mg-character-card__link { display: flex; gap: var(--mg-space-3); padding: var(--mg-space-4); color: inherit; text-decoration: none; }
.mg-character-card__body { display: block; min-width: 0; }
.mg-character-card__name { display: block; }
.mg-character-grid--list { flex-direction: column; gap: 0; }
.mg-character-grid--list .mg-character-card { flex: 1 1 auto; border-radius: 0; }

/* ---- 캐릭터 목록 배너 카드 (m-2) ---------------------------------------
   디자인 원본: Claude Design d1cc901f `캐릭터 프로필 리뉴얼` 계열 목록 카드.
   ⛔ 색·크기는 --mg-* 만 쓴다. 기본 스킨이 스킨 제작 레퍼런스다 (CLAUDE.md). */
.mg-character-card--banner {
  flex: 1 1 100%;
  border: 0;
  border-radius: 0;
  background: none;
}

/* ⛔ overflow: hidden 은 이 링크(배너)까지다. 라이트박스는 목록에 없고,
   #content 안 position: fixed 를 자르지 않는다. */
.mg-character-card--banner .mg-character-card__link {
  position: relative;
  display: block;
  block-size: var(--mg-character-banner-height);
  padding: 0;
  overflow: hidden;
  color: inherit;
  text-decoration: none;
}

.mg-character-grid--list .mg-character-card--banner .mg-character-card__link {
  block-size: var(--mg-character-banner-height-sm);
}

/* ⭐ 초점·확대는 인라인 style 로 온다 — 상세의 인물 도시에와 **같은 값**이다
   (m-3 후속). 두 화면이 다른 크롭을 쓰면 목록에서 고른 얼굴과 들어가서 보는
   얼굴이 달라진다. ⛔ 여기에 object-position 을 다시 적지 않는다 — 적으면
   인라인 값이 아니라 이 값이 이기는 자리가 생긴다. */
.mg-character-banner__art {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  /* ⛔ 프로필 인물 도시에와 **같은 처리**다. 한쪽만 흑백이면 같은 그림이 두 화면에서
     달라 보인다 (2026-09-05 사람 신고 — 배경을 인장에서 전신으로 바꾸며 풀렸다).
     ⭐ hover 전까지는 여기에 흐림이 얹힌다 (2026-09-06 사람 지시).
     ⛔⛔ 흐림을 **양(量)으로만** 뒤집는다. hover 규칙에서 filter 를 통째로 다시
        적으면 흑백·대비가 두 곳에 적히고, 나중에 한쪽만 고쳐지면 hover 할 때만
        색이 돌아오는 화면이 되는데 아무도 그걸 의도로 안 읽는다. */
  filter: grayscale(1) contrast(1.05) blur(var(--mg-character-banner-blur));
  /* ⛔⛔ 흐림은 상자 **바깥**을 빨아들여 가장자리를 투명하게 만든다. 링크가
     overflow: hidden 이라 그 흐린 테두리가 배너 안쪽에 밝은 띠로 남는데,
     "그런 디자인" 으로 보여 오류가 안 난다. 상자보다 크게 깔아 잘라 낸다.
     ⛔ `scale` 이 아니라 `transform` 이다 — `scale` 속성은 인라인 style 이 쓰고
        있어서(관리자가 지정한 배너 확대) 여기서 쓰면 **인라인이 이겨 조용히 안
        먹는다.** 둘은 따로 곱해지므로 관리자 값은 그대로 살아 있다.
     ⚠ 두 상태 **모두**에 걸어 둔다 — hover 에서만 빼면 그림이 살짝 줄어든다. */
  transform: scale(var(--mg-character-banner-bleed)) translateX(var(--mg-character-banner-pan));
  /* ⛔ 표류는 흐림과 **다른 속도**다 — 한 토큰으로 묶으면 hover 하는 순간 흐림이
     9초 동안 걷히거나, 사진이 0.3초 만에 튄다. 축마다 시간을 따로 준다. */
  transition: filter var(--mg-transition), transform var(--mg-transition-drift);
}

.mg-character-banner__art--empty {
  display: block;
  background: var(--mg-surface-sunken);
}

/* ⛔⛔ 왼쪽 두 모서리(소속·등급)는 **인장 사진 위**에 얹힌다. 사진은 관리자가
   올리는 것이라 밝을 수도 있고, 그러면 흰 글자와 금색 글자가 통째로 안 읽힌다 —
   화면은 멀쩡히 그려지므로 오류가 안 난다. ROOT 의 원래 교리대로 **바탕을 먼저
   보장하고** 글자를 얹는다. 어두운 사진(디자인 원본)에서는 눈에 띄지 않는다.
   ⚠ 이 층은 링크의 ::before 라 art 다음, wedge 앞에 칠해진다(DOM 순서). */
.mg-character-card--banner .mg-character-card__link::before {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--mg-veil-ink-left);
  pointer-events: none;
}

/* 노란 사다리꼴. 위가 넓고 아래로 갈수록 왼쪽으로 파고든다. */
.mg-character-banner__wedge {
  position: absolute;
  inset-block: 0;
  inset-inline-end: 0;
  inline-size: 58%;
  background: var(--mg-accent);
  clip-path: polygon(14% 0, 100% 0, 100% 100%, 0 100%);
  pointer-events: none;
}

.mg-character-banner__guild,
.mg-character-banner__rank,
.mg-character-banner__name,
.mg-character-banner__en {
  position: absolute;
  /* ⚠ 좁은 화면에서는 이 값도 함께 넓힌다(아래 미디어 쿼리) — 글자만 줄이면
     46% 안에서 계속 잘린다. 두 축이 같이 움직여야 다 보인다. */
  max-inline-size: 46%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  pointer-events: none;
}

.mg-character-banner__guild,
.mg-character-banner__rank,
.mg-character-banner__name {
  font-family: var(--mg-font-heading);
  color: var(--mg-text-oninverse);
}

/* ⭐ 배너 글자는 화면 폭을 따라 줄어든다 (2026-09-06 사람 신고: 모바일에서 이름이
   잘린다). ⛔ 좁은 화면에서 값을 갈아 끼우는 미디어 쿼리가 아니라 `clamp()` 다 —
   구간을 나누면 그 경계 바로 아래에서 여전히 잘린다.
   ⚠ 위 칸(최댓값)이 지금까지의 값이라 넓은 화면은 그대로다.
   ⚠ 가운뎃값의 `vw` 는 크기 리터럴이 아니라 화면 폭 비율이다 — 하드코딩 금지는
      고정 길이(px·rem)에 대한 규칙이다. */
.mg-character-banner__guild {
  inset-block-start: var(--mg-space-3);
  inset-inline-start: var(--mg-space-4);
  font-size: clamp(var(--mg-font-size-xs), 2.2vw, var(--mg-font-size-md));
  letter-spacing: var(--mg-track-wider);
}

.mg-character-banner__rank {
  inset-block-end: var(--mg-space-3);
  inset-inline-start: var(--mg-space-4);
  font-size: clamp(var(--mg-font-size-sm), 3.4vw, var(--mg-font-size-2xl));
  letter-spacing: var(--mg-track-wide);
  color: var(--mg-accent);
  /* ⚠ 단위 없는 수다 — `1.2em` 은 크기 리터럴이라 규칙 위반이고, 자식이 생기면
     부모 크기로 고정돼 겹친다. 이 요소에서 보이는 결과는 같다. */
  line-height: 1.2;
  bottom: 0;
}

.mg-character-banner__name {
  inset-block-start: var(--mg-space-3);
  inset-inline-end: var(--mg-space-4);
  font-size: clamp(var(--mg-font-size-sm), 3.4vw, var(--mg-font-size-2xl));
  color: var(--mg-text-raw);
  top: 0;
  text-align: end;
}

/* ⛔ 글꼴 이름을 여기 적지 않는다 — 관리자가 /manage/fonts 에서 필기체 축에
   배정한 글꼴을 쓴다. 이름을 박으면 그 글꼴을 지우는 날 오류 없이 떨어진다. */
.mg-character-banner__en {
  inset-block-end: var(--mg-space-3);
  inset-inline-end: var(--mg-space-4);
  font-family: var(--mg-font-script);
  font-size: clamp(var(--mg-font-size-xs), 2.4vw, var(--mg-font-size-lg));
  color: var(--mg-text-oninverse);
  text-align: end;
  bottom: 0;
}

/* 오버레이 — 오른쪽에서 왼쪽으로 밀려 들어온다.
   ⛔ 지속시간은 토큰을 지나야 prefers-reduced-motion 에서 꺼진다.
   ⛔ animation 축약형이 아니라 transition 이다: 복합 토큰(--mg-transition)은
      이징이 딸려 있어 animation 축약형에 넣으면 선언 전체가 무효가 된다. */
.mg-character-banner__veil,
.mg-character-banner__intro {
  position: absolute;
  transform: translateX(101%);
  transition: transform var(--mg-transition);
  pointer-events: none;
}

.mg-character-banner__veil {
  inset: 0;
  background: var(--mg-veil-ink);
}

.mg-character-banner__intro {
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding-inline: var(--mg-space-4);
  font-family: var(--mg-font-serif);
  /* ⛔ 한 줄로 잘라내지 않는다 (2026-09-10 사람 신고: 모바일에서 열 글자쯤에
     ...으로 잘렸다). 한 줄 소개는 **문장이라 잘리면 뜻이 사라진다** — 이름표와
     달리 말줄임이 정보를 남기지 못한다.
     ⛔ 배너는 뷰포트 전체 폭(`flex: 1 1 100%`)이라 `vw` 가 카드 폭의 대리값이
     된다. 좁은 화면에서 글자가 줄고, 그래도 모자라면 **줄바꿈**으로 받는다.
     ⚠ 기준은 25자다 — 배너 높이(좁은 화면 5.5rem)가 두 줄까지만 받는다.
     그보다 긴 소개는 여전히 상자를 넘어 잘린다. */
  font-size: clamp(var(--mg-font-size-sm), 3.4vw, var(--mg-font-size-xl));
  line-height: 1.45;
  color: var(--mg-text-oninverse);
  text-align: center;
  overflow: hidden;
}

/* ⛔ :hover 만으로는 안 된다 — 터치 기기에는 hover 가 없고 키보드 이동은
   포커스로 온다. 링크가 카드 전체라 :focus-visible 이 여기 걸린다. */
/* ⭐ hover 하면 흐림만 걷힌다 — 흑백·대비·베일·사다리꼴·오버레이는 그대로다.
   ⛔ :hover 만으로는 안 된다 — 터치 기기에는 hover 가 없고 키보드 이동은
   포커스로 온다 (아래 오버레이 규칙과 같은 이유). */
.mg-character-card--banner .mg-character-card__link:hover .mg-character-banner__art,
.mg-character-card--banner .mg-character-card__link:focus-visible .mg-character-banner__art {
  /* ⛔ 토큰을 이 요소에서만 다시 선언한다 — 중간 변수를 하나 더 만들면 코어에
     기본값이 없는 `--mg-*` 가 생겨, "스킨이 쓰는 토큰은 코어에 기본값이 있다" 는
     계약이 깨진다(가드가 실제로 잡았다). 여기 선언은 이 요소 밖으로 안 샌다. */
  --mg-character-banner-blur: 0px;
  /* ⭐ hover 동안 사진이 **오른쪽으로** 아주 천천히 흐른다 (2026-09-09 사람 지시).
     ⛔ 거리는 bleed 가 만드는 여유(좌우 3%) 안에 둔다 — 넘기면 왼쪽 가장자리에
        바탕이 비치는데, "그런 디자인" 으로 보여 오류가 안 난다.
     ⛔ object-position 을 건드리지 않는다 — 그 값은 관리자가 인라인으로 정한
        크롭이고, 여기서 다시 적으면 그쪽이 조용히 무시된다. */
  --mg-character-banner-pan: 2%;
}

.mg-character-card--banner .mg-character-card__link:hover .mg-character-banner__veil,
.mg-character-card--banner .mg-character-card__link:hover .mg-character-banner__intro,
.mg-character-card--banner .mg-character-card__link:focus-visible .mg-character-banner__veil,
.mg-character-card--banner .mg-character-card__link:focus-visible .mg-character-banner__intro {
  transform: none;
}
.mg-filter-chips { display: flex; gap: var(--mg-space-2); overflow-x: auto; margin-block-end: var(--mg-space-4); }
.mg-filter-chip { white-space: nowrap; padding: var(--mg-space-2) var(--mg-space-3); border: var(--mg-border-width) solid var(--mg-border); border-radius: var(--mg-radius-pill); }
.mg-filter-chip--active { background: var(--mg-accent); border-color: var(--mg-accent); color: var(--mg-accent-text); }
.mg-pagination { display: flex; justify-content: center; align-items: center; gap: var(--mg-space-2); margin-block-start: var(--mg-space-5); }
.mg-pagination__page, .mg-pagination__current, .mg-pagination__step { padding: var(--mg-space-2) var(--mg-space-3); border-radius: var(--mg-radius-sm); }
.mg-pagination__current { background: var(--mg-accent); color: var(--mg-accent-text); }
.mg-pagination__gap { padding: var(--mg-space-2); color: var(--mg-text-muted); }
.mg-view-toggle { font-size: var(--mg-font-size-sm); color: var(--mg-text-muted); }
.mg-empty { color: var(--mg-text-muted); }
.mg-notice { color: var(--mg-text-muted); border: 1px solid var(--mg-border); border-radius: var(--mg-radius-sm); padding: var(--mg-space-2) var(--mg-space-3); }
.mg-profile__header { display: flex; align-items: center; gap: var(--mg-space-4); margin-block-end: var(--mg-space-6); }
.mg-profile__name { margin: 0; }
.mg-profile__body { min-width: 0; }
.mg-profile--aside .mg-profile__body { display: grid; grid-template-columns: var(--mg-profile-aside-width) minmax(0, 1fr); gap: var(--mg-space-6); }
.mg-profile__aside { min-width: 0; }
.mg-profile-info__image { max-width: 100%; height: auto; }
.mg-profile-section { scroll-margin-top: var(--mg-space-8); margin-block: var(--mg-space-5); }
.mg-profile-section__title { display: inline; font-size: var(--mg-font-size-lg); }
/* Character stat gauges (d-3). The fill width is the only inline style the
   markup carries: the percent is computed in apps/characters/stats.py so that
   the bar and the number beside it can never come from different arithmetic. */
.mg-profile-stats { display: grid; gap: var(--mg-space-3); margin: 0; padding: 0; list-style: none; }
.mg-profile-stat { display: grid; grid-template-columns: minmax(0, 1fr) max-content; gap: var(--mg-space-1) var(--mg-space-3); align-items: baseline; }
.mg-profile-stat__label { margin: 0; color: var(--mg-text-muted); font-size: var(--mg-font-size-sm); letter-spacing: var(--mg-track-wide); }
.mg-profile-stat__value { margin: 0; font-family: var(--mg-font-mono); color: var(--mg-text-strong); font-variant-numeric: tabular-nums; }
.mg-profile-stat__gauge { grid-column: 1 / -1; block-size: var(--mg-bar-h); background: var(--mg-surface-sunken); border: var(--mg-stroke-hair) solid var(--mg-border); overflow: hidden; }
.mg-profile-stat__fill { block-size: 100%; background: var(--mg-bar-gold); transition: width var(--mg-transition); }
/* Character stamina (r-2). 스탯 게이지와 같은 재료를 쓰되 한 줄짜리다 —
   퍼센트는 apps/characters/stamina.py 가 낸다. 템플릿이 나눗셈을 하지 않는다. */
.mg-profile-stamina { margin-block: var(--mg-space-4); }
.mg-profile-stamina__body { display: grid; gap: var(--mg-space-2); }
.mg-profile-stamina__value { font-family: var(--mg-font-mono); color: var(--mg-text-strong); font-variant-numeric: tabular-nums; }
.mg-profile-stamina__gauge { block-size: var(--mg-bar-h); background: var(--mg-surface-sunken); border: var(--mg-stroke-hair) solid var(--mg-border); overflow: hidden; }
.mg-profile-stamina__fill { block-size: 100%; background: var(--mg-bar-gold); transition: width var(--mg-transition); }

/* 프로필의 세로 리듬 — 카드·체력·스탯이 한 화면에 들어와야 한다 (2026-09-09 사람 지시).
   ⛔ 카드(밴드·인물 도시에)의 규격은 건드리지 않는다. 줄이는 것은 **바탕 없는 판때기**의
      안쪽 여백과 판때기 사이 간격뿐이다 — 그 판때기는 테두리도 배경도 없어서 넉넉한
      패딩이 아무 경계도 만들지 않는다(있는 판때기는 그대로 둔다).
   ⚠ `#content` 를 붙인다: `#content .mg-profile-plate__bar` 가 (1,0,1) 이라 맨 클래스
      규칙은 명시도로 진다 (m-1 에서 이미 밟은 자리). */
#content .mg-profile-plate--bare > .mg-profile-plate__bar { padding-block: var(--mg-space-1); }
#content .mg-profile-plate--bare > .mg-profile-plate__body { padding: var(--mg-space-3) var(--mg-space-4); }
#content .mg-profile-stamina { margin-block: var(--mg-space-3); }
#content .mg-profile-stats { gap: var(--mg-space-2); }
/* ⚠ gap 을 두지 않는다 (2026-09-06 사람 지시) — 자식(이름 밴드·컬럼)이 이미 제
   아래 여백을 들고 있어 gap 이 겹쳐 쓸데없이 벌어졌다. 여백은 한 곳이 든다. */
.mg-profile-layout { display: flex; flex-direction: column; }
.mg-profile-layout > .mg-profile__header { margin-block-end: 0; }
.mg-profile-layout > .mg-featured-log { margin-top: 0; }
.mg-block-character-field { display: flex; flex-direction: column; gap: var(--mg-space-1); }
.mg-block-character-field__label { color: var(--mg-text-muted); font-size: var(--mg-font-size-sm); }
.mg-block-character-field__value { max-width: 68ch; line-height: 1.8; }
.mg-block-character-field__image { max-width: 100%; height: auto; }
.mg-profile__badge { margin-inline-start: var(--mg-space-2); }

/* ---- 캐릭터 프로필 ROOT 외관 (m-1) --------------------------------------
   ⛔⛔ 선택자에 #content 를 붙인 것은 명시도 때문이다. `#content p`·`#content dl`
   은 (1,0,1) 이라 블록 클래스 (0,1,0) 을 이긴다 — 붙이지 않으면 여기 규칙이 통째로
   지고, CSS 만 읽으면 다 맞게 적혀 있어서 원인을 못 찾는다 (docs/PITFALLS.md).
   ⚠ #content 밖(셸 사이드바)에서 갈리는 문제는 없다: SIDEBAR_SURFACE 의 도구
   목록에 character_* 가 없어서 프로필 블록은 사이드바에 들어갈 수 없다.
   ⛔ 바탕·테두리·그림자는 반드시 --mg-* 를 지난다. 다크모드 팔레트가 그 토큰만
   갈아끼우므로 값을 직접 적으면 다크에서 안 따라온다 (docs/SKIN_API.md). */
.mg-profile-plate {
  border: var(--mg-stroke-1) solid var(--mg-border-strong);
  background: var(--mg-surface);
  box-shadow: var(--mg-shadow-slab-soft);
}

/* ⚠ 바탕과 테두리를 뺀 판때기 (2026-09-06 사람 지시) — 도시에·목차와 같은 결이다.
   머리띠(STATUS 줄)는 그대로 남아 무엇의 값인지 알 수 있게 한다. */
.mg-profile-plate--bare {
  border: 0;
  background: none;
  box-shadow: none;
}

#content .mg-profile-plate__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--mg-space-3);
  margin: 0;
  padding: var(--mg-space-2) var(--mg-space-4);
  border-block-end: var(--mg-stroke-1) solid var(--mg-border-strong);
}

.mg-profile-plate__title {
  font-family: var(--mg-font-mono);
  font-size: var(--mg-font-size-xs);
  font-weight: 700;
  letter-spacing: var(--mg-track-widest);
  color: var(--mg-text-faint);
}

.mg-profile-plate__body {
  padding: var(--mg-space-4);
}

/* 증서만 도트 스파인을 단다. 디자인에서도 이 한 장에만 있다. */
.mg-profile__header.mg-profile-plate {
  position: relative;
  overflow: hidden;
}

.mg-profile__header.mg-profile-plate::before {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  inline-size: var(--mg-profile-spine);
  border-inline-end: var(--mg-stroke-1) dashed var(--mg-border);
  background-color: var(--mg-surface-sunken);
  background-image: radial-gradient(circle, var(--mg-border-strong) 2.5px, transparent 2.6px);
  background-size: var(--mg-profile-spine) var(--mg-profile-spine);
  background-position: center var(--mg-space-1);
}

.mg-profile__header.mg-profile-plate > * {
  padding-inline-start: calc(var(--mg-profile-spine) + var(--mg-space-4));
}

#content .mg-profile__header .mg-profile-plate__body {
  display: flex;
  align-items: flex-start;
  gap: var(--mg-space-4);
  margin: 0;
}

.mg-profile__identity {
  flex: 1;
  min-inline-size: 0;
}

#content .mg-profile__intro {
  margin: var(--mg-space-1) 0 0;
  color: var(--mg-text-muted);
}

#content .mg-profile__mastodon {
  margin: var(--mg-space-1) 0 0;
  font-family: var(--mg-font-mono);
  font-size: var(--mg-font-size-xs);
}

/* ⛔ 태그(header 슬롯 값)는 <h1> **안**에 있어야 한다 — 기존 계약이고
   apps/characters/tests.py 의 두 가드가 그것을 정규식으로 못 박는다.
   디자인처럼 이름 아래 줄로 떨어뜨리는 것은 마크업이 아니라 여기서 한다:
   감싸지는 flex 라 이름과 배지가 각각 항목이 되고 좁으면 다음 줄로 내려간다. */
.mg-profile__name {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--mg-space-2);
}

/* 태그는 ROOT 의 사각 아웃라인이다. .mg-badge 를 전역으로 바꾸지 않는다 —
   공지·등급 배지가 같은 클래스를 쓴다. */
.mg-profile__badge {
  margin-inline-start: 0;
  border: var(--mg-stroke-hair) solid var(--mg-border-strong);
  border-radius: 0;
  background: transparent;
  color: var(--mg-text-strong);
  font-family: var(--mg-font-mono);
  letter-spacing: var(--mg-track-wider);
}

.mg-profile__seal {
  flex: none;
  inline-size: 3.5rem;
  block-size: 3.5rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  border: var(--mg-stroke-2) solid var(--mg-accent);
  background: var(--mg-accent-soft);
  transform: rotate(-4deg);
}

.mg-profile__seal-mark {
  font-family: var(--mg-font-mono);
  font-size: var(--mg-font-size-xs);
  font-weight: 700;
  letter-spacing: var(--mg-track-wider);
  color: var(--mg-link);
}

.mg-profile__seal-word {
  font-weight: 700;
  color: var(--mg-text-strong);
}

/* 기본 정보 — 라벨 위, 값 아래의 2칸 격자. */
#content .mg-profile-info {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0;
  margin: 0;
  padding: 0;
  background: none;
  border-radius: 0;
}

.mg-profile-info__pair {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-inline-size: 0;
  padding: var(--mg-space-2) var(--mg-space-4);
  border-block-end: var(--mg-stroke-hair) solid var(--mg-border);
  border-inline-start: var(--mg-stroke-hair) solid var(--mg-border);
}

#content .mg-profile-info__term {
  font-family: var(--mg-font-mono);
  font-size: var(--mg-font-size-xs);
  font-weight: 700;
  letter-spacing: var(--mg-track-wider);
  text-transform: uppercase;
  color: var(--mg-text-faint);
}

#content .mg-profile-info__desc {
  margin: 0;
  font-weight: 600;
  color: var(--mg-text-strong);
  overflow-wrap: anywhere;
}

/* 섹션 카드. ⛔ 번호는 CSS 카운터다 — 데이터에 없고, 마크업에 박으면 순서가
   바뀔 때 어긋난다. 두 표면 모두에서 세어야 하므로 둘 다에 reset 을 건다. */
.mg-profile-layout,
.mg-profile__main {
  counter-reset: mg-profile-section;
}

.mg-profile-section {
  counter-increment: mg-profile-section;
  margin-block: 0;
}

.mg-profile-section__title {
  font-size: var(--mg-font-size-md);
}

.mg-profile-section__no::before {
  content: counter(mg-profile-section, decimal-leading-zero);
  font-family: var(--mg-font-mono);
  font-size: var(--mg-font-size-xs);
  letter-spacing: var(--mg-track-wider);
  color: var(--mg-text-faint);
}

/* ⭐ 글자 크기 조절은 **여기에만** 걸린다 (2026-09-06 사람 지시) — 제목·기본정보·
   스탯은 그대로다. 배율은 프로필 루트에 실리고(`data-mg-page-zoom`) 여기서 읽는다.
   ⛔ 원값(`-raw`)에 곱한다 — `--mg-font-size-base` 는 이미 페이지 배율이 곱해진
      값이라 다시 곱하면 두 배율이 겹친다.
   ⛔ `:root` 에 재선언하지 않는다: 커스텀 속성은 선언된 요소에서 치환이 끝난 뒤
      상속되므로, 위에서 곱하면 이 칸만 커지는 것이 아니라 화면 전체가 커진다. */
.mg-profile-section__body {
  margin-block-start: 0;
  line-height: var(--mg-page-line-height);
  font-size: calc(var(--mg-font-size-base-raw) * var(--mg-page-font-scale, 1));
}
.mg-profile-section__body p[style*="text-align:justify"] {
  text-indent: 2em;
}

/* 컨트롤은 목차 아래 한 줄. ⚠ 프로필 루트 안에 둔다 — 셸로 올리면 목록·메인까지 딸려간다.
   ⭐ 여기서는 바탕도 테두리도 없다 (2026-09-06 사람 지시) — 목차와 같은 결이고,
      좁은 옆열에서 네 칸짜리 상자는 열 폭을 통째로 먹는다.
   ⛔ `.mg-page-zoom` 자체는 그대로 둔다: 페이지 화면의 조절바는 본문 위에 홀로
      놓인 컨트롤이라 상자가 있어야 "누르는 것" 으로 읽힌다. 여기만 덮는다. */
.mg-profile-zoom {
  /* ⛔ 감싸지 않는다 — 옆열이 좁아도 두 줄로 접히면 "한 줄" 이 아니게 된다. */
  flex-wrap: nowrap;
  justify-content: flex-start;
  gap: var(--mg-space-2);
  margin-block-end: var(--mg-space-4);
}

.mg-profile-zoom .mg-page-zoom__label { margin-inline-end: var(--mg-space-1); }

.mg-profile-zoom .mg-page-zoom__step,
.mg-profile-zoom .mg-page-zoom__value,
.mg-profile-zoom .mg-page-zoom__reset {
  border: 0;
  background: none;
  padding: 0;
  /* ⛔ 칸 안에서 줄이 바뀌면 한 줄이 두 줄이 된다 — nowrap 은 이 안쪽에도 필요하다. */
  white-space: nowrap;
}

.mg-profile-zoom .mg-page-zoom__value {
  min-inline-size: 4ch;
  padding-inline: 0;
}

.mg-profile-zoom .mg-page-zoom__step,
.mg-profile-zoom .mg-page-zoom__reset { color: var(--mg-text-muted); }

/* ⛔ 테두리를 없앤 만큼 "눌린다" 는 신호를 글자로 돌려준다 — 안 그러면 그냥
   설명 글로 보여 아무도 안 누른다. */
.mg-profile-zoom .mg-page-zoom__step:hover:not(:disabled),
.mg-profile-zoom .mg-page-zoom__reset:hover {
  background: none;
  color: var(--mg-text-strong);
  text-decoration: underline;
}

/* ⛔⛔ 끝에 닿은 칸은 **여기서 다시 흐리게 만들어야 한다.** 위의 색 규칙과
   `.mg-page-zoom__step:disabled` 가 명시도가 같아(0,2,0) 나중에 오는 이쪽이
   이긴다 — 안 적으면 더 못 줄이는 칸이 멀쩡해 보이는데 눌러도 아무 일이 없다. */
.mg-profile-zoom .mg-page-zoom__step:disabled {
  color: var(--mg-text-faint);
  text-decoration: none;
}

/* 목차 레일 */
.mg-profile-toc__title {
  display: block;
  padding-block-end: var(--mg-space-1);
  border-block-end: var(--mg-stroke-1) solid var(--mg-border-strong);
  font-family: var(--mg-font-mono);
  font-size: var(--mg-font-size-xs);
  font-weight: 700;
  letter-spacing: var(--mg-track-widest);
  color: var(--mg-text-faint);
}

/* 전신 이미지 — 회색조 크롭, 올리면 원본 색.
   ⛔ filter 는 자손 position: fixed 의 컨테이닝 블록을 만든다. 라이트박스는
   .mg-gallery 의 형제라 이 버튼 안에 없다 — 안으로 옮기면 오버레이가 스크롤을
   따라 흘러내린다 (docs/PITFALLS.md 다크모드 항목과 같은 함정). */
.mg-gallery--profile .mg-gallery__item {
  position: relative;
  overflow: hidden;
  border: var(--mg-stroke-2) solid var(--mg-border-strong);
  filter: grayscale(1) contrast(1.05);
  transition: filter var(--mg-transition);
}

.mg-gallery--profile .mg-gallery__item:hover,
.mg-gallery--profile .mg-gallery__item:focus-visible {
  filter: none;
}

.mg-profile-gallery__caption {
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  padding: var(--mg-space-4) var(--mg-space-2) var(--mg-space-2);
  background: var(--mg-veil-bottom);
  font-family: var(--mg-font-mono);
  font-size: var(--mg-font-size-xs);
  font-weight: 700;
  letter-spacing: var(--mg-track-wider);
  color: var(--mg-text-strong);
  text-align: start;
  pointer-events: none;
}


/* ---- 캐릭터 프로필 도시에 (m-3) ------------------------------------------
   디자인 원본: Claude Design d1cc901f `캐릭터 프로필 수정안.dc.html`.
   ⛔⛔ 선택자에 #content 를 붙이는 이유는 m-1 과 같다 — `#content p`·`#content dl`
   이 (1,0,1) 이라 맨 클래스 규칙 (0,1,0) 을 이긴다. 안 붙이면 규칙이 통째로 지고
   CSS 만 읽으면 다 맞게 적혀 있어서 원인을 못 찾는다 (docs/PITFALLS.md). */

/* 이름 밴드 */
/* ⛔⛔ 밴드는 **높이를 바꾸지 않는다.** 접었다 폈다 하면 문서 높이가 변하고,
   위로 스크롤하는 동안 밴드가 다시 펴지면서 그만큼을 화면 위에 끼워 넣어
   **본문이 아래로 밀린다** — 사람에게는 "버벅이면서 안 올라간다" 로 보인다
   (2026-09-05 사람 신고). 밴드는 그냥 흘러 올라가고, 그 자리를 좌열의 작은
   이름표가 대신한다. JS 는 상태만 찍는다. */
#content .mg-profile-band {
  display: flex;
  flex-direction: column;
  gap: var(--mg-space-2);
}

/* ⛔ 두 이름은 **형제**이고 `align-items: baseline` 으로 밑줄을 맞춘다 — 서로 다른
   상자에 넣으면 baseline 정렬이 성립하지 않는다. ⚠ `flex-wrap` 이 있어 합이 넘치면
   로마자가 저절로 아랫줄로 내려간다 (2026-09-05 사람 지시). */
#content .mg-profile-band__names {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--mg-space-4);
  flex-wrap: wrap;
  min-inline-size: 0;
}

#content .mg-profile-band__name {
  font-family: var(--mg-font-heading);
  font-size: var(--mg-font-size-2xl);
  line-height: 1.05;
  letter-spacing: var(--mg-track-wide);
  color: var(--mg-text-strong);
}

#content .mg-profile-band__intro {
  margin: 0;
  font-size: var(--mg-font-size-lg);
  font-family: var(--mg-font-serif);
  color: var(--mg-text-muted);
}

/* ⛔ 따옴표는 **화면이 붙인다**. 저장값에 넣으면 목록·카드 등 따옴표가 어울리지
   않는 자리까지 따라가고, 관리자가 지울 수도 없다 (2026-09-05 사람 지시). */
#content .mg-profile-band__intro::before { content: "\201C"; }
#content .mg-profile-band__intro::after { content: "\201D"; }

/* 로마자 이름. ⛔ 글꼴 이름을 적지 않는다 — 관리자가 /manage/fonts 의 필기체
   축에 배정한 글꼴을 쓴다 (v0.3-m-2 계약). */
/* ⛔ 시안은 이 글자를 gold-400 으로 그리지만 **흰 종이 위**라 대비가 2:1 도 안 된다.
   ROOT 규칙대로 --mg-link(gold-700) 단계로 내린다 — 도시에의 등급·라벨과 달리
   여기에는 보장된 어두운 바탕이 없어서 예외를 뚫을 근거가 없다. */
.mg-profile-band__echo {
  margin-inline-start: auto;
  font-family: var(--mg-font-script);
  font-size: var(--mg-font-size-2xl);
  line-height: 1.1;
  color: var(--mg-accent); /*사용자 지정, 가드에서 예외*/
  white-space: nowrap;
}

/* 좌열의 작은 이름표. 큰 밴드가 화면 밖으로 밀려 올라가면 나타난다.
   ⛔ 판정은 JS 가 밴드에 찍는 상태 하나이고, 여기서는 :has() 로 그것을 읽는다 —
      #content 는 두 표면(블록/폴백)에 모두 있는 공통 조상이다.
   ⛔ 높이가 아니라 opacity·visibility 만 바꾼다. 높이를 바꾸면 sticky 인 좌열이
      다시 흔들린다. 자리는 늘 잡아 두고 보이기만 한다. */
/* ⚠ 자리를 미리 잡아 두지 않는다 — 안 보일 때도 자리를 차지하면 도시에 위에
   빈칸이 남는다 (2026-09-06 사람 신고).
   ⛔⛔ 높이를 바꾸는 것이 여기서는 안전하다. **문서 높이를 바꾸지 않기** 때문이다:
      이 이름표는 sticky 인 좌열 안에 있고, 문서 높이는 훨씬 긴 가운데 칸이 정한다.
      밴드에서 높이를 못 바꾸는 이유(화면 위쪽에 끼어들어 본문을 밀어낸다)는
      **화면 위로 흘러간 요소**에만 해당한다. 두 자리를 같은 규칙으로 묶지 않는다. */
/* ⛔⛔ 이 이름표는 **좌열이 붙어 있을 때만** 뜻이 있다 (2026-09-06 사람 신고).
   안 붙는 폭에서는 도시에가 본문과 함께 흘러가므로 이름표가 나타날 자리도 이유도
   없는데, 규칙이 살아 있으면 화면 밖에서 높이·투명도가 계속 오가며 **잔상이 남는다.**
   ⛔ 그래서 좌열 sticky 와 **같은 폭**에서만 켠다 — 두 규칙이 갈리면 이름표가
   안 붙는 열 안에서 혼자 여닫힌다. 폭을 재는 것은 CSS 이고 JS 는 계속 상태만 찍는다. */
.mg-profile-portrait__compact { display: none; }

@media (min-width: 75rem) {
  .mg-profile-portrait__compact {
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: var(--mg-space-1);
    max-block-size: 0;
    overflow: hidden;
    opacity: 0;
    transition: max-block-size var(--mg-transition), opacity var(--mg-transition),
      margin-block-end var(--mg-transition);
  }

  #content:has([data-mg-profile-band][data-mg-collapsed="true"]) .mg-profile-portrait__compact {
    max-block-size: 12rem;
    margin-block-end: var(--mg-space-3);
    opacity: 1;
  }
}

.mg-profile-portrait__compact-name {
  font-family: var(--mg-font-heading);
  font-size: var(--mg-font-size-lg);
  line-height: 1.1;
  color: var(--mg-text-strong);
}

.mg-profile-portrait__compact-echo {
  font-family: var(--mg-font-script);
  font-size: var(--mg-font-size-md);
  color: var(--mg-accent);
}

#content .mg-profile-portrait__compact-intro {
  margin: 0;
  font-size: var(--mg-font-size-sm);
  color: var(--mg-text-muted);
}

#content .mg-profile-portrait__compact-intro::before { content: "\201C"; }
#content .mg-profile-portrait__compact-intro::after { content: "\201D"; }

/* 인물 도시에 */
/* ⚠ 테두리·바탕 없이 사진만 남긴다 (2026-09-05 사람 지시).
   ⛔ 다만 **이미지가 없을 때는** 잉크 바탕을 깐다 — 흰 종이에 흰 글자를 두지 않는다.
      금색 예외(등급·라벨)의 근거인 어두운 바탕은 베일이 계속 책임진다. */
.mg-profile-portrait {
  position: relative;
  min-block-size: var(--mg-portrait-min-height);
  display: flex;
  overflow: hidden;
}

/* 이미지가 없을 때. ⛔ 흰 바탕에 흰 글자를 두지 않는다 — 잉크 바탕은
   .mg-profile-portrait 가 이미 깔고, 여기서는 최소 높이만 지킨다. */
/* ⛔ 사진이 없을 때만 잉크 바탕을 깐다 — 흰 종이에 흰 글자를 두지 않는다.
   금색 예외(등급·라벨)의 근거인 어두운 바탕은 사진이 있을 때 베일이 책임진다. */
.mg-profile-portrait--empty {
  min-block-size: var(--mg-portrait-min-height);
  background: var(--mg-surface-inverse);
}

/* 두 번째 이미지(인장)의 진입점. 화면에는 작은 글자 버튼으로 남는다 —
   ⛔ 숨기지 않는다. lightbox.js 는 보이는 형제만 이전/다음으로 잇고,
   숨기면 키보드 사용자가 그 그림에 영영 못 닿는다. */
#content .mg-profile-portrait__more {
  position: absolute;
  inset-block-start: var(--mg-space-3);
  inset-inline-end: var(--mg-space-3);
  max-inline-size: 45%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  padding: var(--mg-space-1) var(--mg-space-3);
  border: var(--mg-stroke-hair) solid var(--mg-accent-hover);
  background: transparent;
  color: var(--mg-accent-hover);
  font-family: var(--mg-font-mono);
  font-size: var(--mg-font-size-xs);
  letter-spacing: var(--mg-track-wider);
  cursor: pointer;
}

#content .mg-profile-portrait__open {
  position: absolute;
  inset: 0;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}

.mg-profile-portrait__art {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  /* ⚠ object-position·scale·transform-origin 은 인라인 style 로 온다 — 값이
     캐릭터마다 다르다. 문자열 조립은 서버가 한 번만 한다(블록 prepare).
     ⛔⛔ scale 은 **이미지 자신에만** 건다. 조상에 걸면 그 요소가 자손
     `position: fixed` 의 컨테이닝 블록이 되어 **라이트박스 오버레이가 스크롤을
     따라 흘러내린다** — 오버레이는 이 도시에 상자 안에 있다 (CLAUDE.md).
     ⚠ transform-origin 을 초점에 맞춰야 당긴 뒤에도 그 지점이 화면에 남는다. */
  filter: grayscale(1) contrast(1.05);
  /* ⛔ 지속시간을 하드코딩하지 않는다 — 토큰을 지나야 prefers-reduced-motion 에서
     함께 꺼진다. ⚠ transition 은 복합 토큰(`시간 이징`)을 그대로 받는다. */
  transition: filter var(--mg-transition);
}

/* ⭐ 가리키면 색이 돌아온다 (2026-09-08 사람 지시). 평소 흑백은 도시에의 재질이고,
   컬러는 "지금 보고 있는 것" 의 표시다.
   ⛔ :hover 만 걸지 않는다 — 키보드로 도는 사람에게도 같은 일이 일어나야 한다.
      확대 버튼이 포커스를 받으므로 :focus-visible 은 그 버튼에 건다.
   ⚠ 터치에는 hover 가 없다. 그쪽은 눌러서 확대 보기로 가는 길이 이미 있다. */
.mg-profile-portrait:hover .mg-profile-portrait__art,
.mg-profile-portrait__open:focus-visible .mg-profile-portrait__art {
  filter: none;
}

/* 베일 → 등급 → 기본정보 순으로 쌓인다. ⛔ 순서가 뒤집히면 등급이 본문을
   가리거나 사진에 묻힌다. 전부 DOM 순서로만 정한다(z-index 를 안 쓴다). */
.mg-profile-portrait__veil {
  position: absolute;
  inset: 0;
  background: var(--mg-veil-ink-bottom);
  pointer-events: none;
}

/* ⭐ 등급을 그림으로 바꾸려면 `static/skins/default/grade/<소문자>.png` 를 두고
   아래 목록에 한 줄을 더한다 (2026-09-06 사람 지시).
   ⛔ 파일이 없는 등급은 **글자 그대로 남는다** — 규칙이 없으면 기본 규칙만 걸린다.
      그래서 그림을 안 올린 등급이 화면에서 사라지지 않는다.
   ⛔ 값을 그대로 선택자에 쓰지 않는다: 서버가 영문 소문자·숫자·하이픈만 남긴
      슬러그를 낸다(`grade_slug`).
   ⚠ 정적 해시가 켜져 있으므로 파일을 넣은 뒤 `scripts/deploy-static.sh` 를 돌린다.
      url() 은 collectstatic 이 해시 이름으로 바꿔 준다. */
.mg-profile-portrait__grade {
  position: absolute;
  translate: 25% 25%;
  inset-inline-end: 0;
  inset-block-end: var(--mg-space-6);
  font-family: var(--mg-font-heading);
  font-size: var(--mg-portrait-grade-size);
  line-height: 0.7;
  color: var(--mg-accent);
  opacity: 0.5;
  pointer-events: none;
}

/* ⭐ 등급을 그림으로 바꾸는 법 (2026-09-06 사람 지시)
     ① `static/skins/default/grade/` 에 **소문자** 파일명으로 둔다 — `s.png`, `a.png` …
        (서버가 내는 슬러그가 소문자다. `A.png` 로 두면 리눅스에서 안 잡힌다)
     ② 아래 목록에 그 등급 한 줄을 더한다
     ③ `scripts/deploy-static.sh` 를 돌린다

   ⛔⛔ **파일이 없는 등급의 줄을 미리 적어 두지 않는다.** 정적 해시가 켜져 있어
      `collectstatic` 이 `url()` 대상 파일을 못 찾으면 **배포가 통째로 죽고**
      (`ValueError: The file … could not be found`) 사이트 전체가 옛 정적 파일로
      남는다. 실측으로 확인했다 — 줄과 파일은 **같은 커밋에서 함께** 들어간다.
   ⛔ 그림을 안 올린 등급은 **글자 그대로 남는다.** 화면에서 사라지지 않는 것이
      비어 보이는 것보다 낫다.

   ⛔⛔ **주석 안에도 그 함수를 적지 않는다.** collectstatic 의 스캐너는 주석을
      건너뛰지 않아서, 예시로 적어 둔 한 줄만으로도 배포가 죽는다 — 이것도 실측으로
      확인했다(2026-09-06). 그래서 아래 안내는 함수 이름을 풀어서 쓴다.

   한 줄의 모양: 선택자 `.mg-profile-portrait__grade--s` 에 background-image 로
   `grade/s.png` 를 가리키고, 그 요소에 `data-mg-grade-image` 속성이 붙도록
   마크업이나 규칙을 맞춘다(아래 공통 규칙이 크기·글자 감춤을 맡는다). */
/* ⚠ 아래 목록에 **파일이 실제로 있는 등급만** 적혀 있다. D 는 이 사이트가 안 쓰는
   등급이라 파일이 없고, 그래서 줄도 없다 — 적어 두면 배포가 죽는다.
   ⛔ 규칙이 없는 등급은 글자 그대로 남는다(그게 안전한 기본값이다). */
.mg-profile-portrait__grade--s,
.mg-profile-portrait__grade--a,
.mg-profile-portrait__grade--b,
.mg-profile-portrait__grade--c,
.mg-profile-portrait__grade--f {
  inline-size: var(--mg-portrait-grade-size);
  block-size: var(--mg-portrait-grade-size);
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
  color: transparent;
}

.mg-profile-portrait__grade--s { background-image: url("grade/s.911734214f0d.png"); }
.mg-profile-portrait__grade--a { background-image: url("grade/a.c7877cf414fb.png"); }
.mg-profile-portrait__grade--b { background-image: url("grade/b.8e95ce4e2f7d.png"); }
.mg-profile-portrait__grade--c { background-image: url("grade/c.974e3eccf8c0.png"); }
.mg-profile-portrait__grade--f { background-image: url("grade/f.5fc3154445aa.png"); }

.mg-profile-portrait__foot {
  position: absolute;
  inset-inline: var(--mg-space-4);
  inset-block-end: var(--mg-space-4);
  display: flex;
  flex-direction: column;
  gap: var(--mg-space-3);
  pointer-events: none;
}

#content .mg-profile-portrait__basics {
  display: grid;
  /* ⛔⛔ 트랙을 반드시 함께 덮는다. `#content dl` 이 minmax(6rem, max-content) 1fr
     을 이미 선언해 두었고, display 만 덮으면 **그 값이 그대로 살아** 기본정보가
     2열로 쪼개진다(2026-09-05 사람 신고). 부분만 덮으면 나머지가 샌다. */
  grid-template-columns: minmax(0, 1fr);
  gap: 2px var(--mg-space-4);
  margin: 0;
  padding: 0;
  background: none;
  border-radius: 0;
}

.mg-profile-portrait__pair {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  column-gap: var(--mg-space-4);
  align-items: baseline;
}

#content .mg-profile-portrait__key {
  font-family: var(--mg-font-heading);
  font-size: var(--mg-font-size-xs);
  font-weight: 100;
  letter-spacing: var(--mg-track-wide);
  color: var(--mg-accent);
}

#content .mg-profile-portrait__value {
  margin: 0;
  font-family: var(--mg-font-heading);
  font-weight: 100;
  font-size: var(--mg-font-size-xs);
  letter-spacing: var(--mg-track-wide);
  color: var(--mg-text-oninverse);
  overflow-wrap: anywhere;
}

.mg-profile-portrait__ability {
  font-family: "Black Ops One", var(--mg-font-heading);
  font-size: var(--mg-font-size-xl);
  line-height: 1;
  color: var(--mg-text-oninverse);
}

/* 다각형(레이더) 스탯. ⛔ 좌표는 서버가 낸다 — 여기서는 색과 두께만 정한다.
   ⚠ 숫자는 그래프 밖 범례에 남긴다: 도형만으로는 값을 못 읽고 낭독기도 못 읽는다. */
#content .mg-profile-radar {
  display: flex;
  flex-direction: column;
  gap: var(--mg-space-3);
  align-items: center;
  margin: 0;
}

/* ⚠ 상자보다 도형이 작다 — 꼭짓점 이름이 바깥에 있어 그만큼 자리를 남긴다.
   반지름은 서버(stats.POLYGON_RADIUS)가 정하고 여기서는 크기만 준다. */
.mg-profile-radar__chart {
  inline-size: 100%;
  max-inline-size: 11rem;
  aspect-ratio: 1;
  /* ⚠ 아래 여백 (2026-09-06 사람 지시) — 다음 칸이 바로 붙으면 답답하다. */
  margin-block-end: var(--mg-space-4);
  overflow: visible;
}

.mg-profile-radar__name {
  font-family: var(--mg-font-mono);
  font-size: 6px;
  fill: var(--mg-text-muted);
}

/* ⛔ 숫자는 툴팁이 띄운다(core/js/tooltip.js 의 [data-mg-tip]). 새 스크립트를 만들지
   않는다. ⚠ <title> 도 함께 둔다 — 스크립트가 못 돌아도 값에 닿을 길이 남는다. */
.mg-profile-radar__dot {
  fill: var(--mg-accent);
  stroke: var(--mg-surface);
  stroke-width: 1;
  cursor: help;
}

.mg-profile-radar__dot:hover,
.mg-profile-radar__dot:focus-visible {
  fill: var(--mg-link);
}

.mg-profile-radar__axis,
.mg-profile-radar__outline {
  fill: none;
  stroke: var(--mg-border);
  stroke-width: 0.6;
}

.mg-profile-radar__fill {
  fill: var(--mg-accent-soft);
  stroke: var(--mg-accent);
  stroke-width: 1.2;
  stroke-linejoin: round;
}

/* 전투 포지션. ⛔ 값 → 색 대응이 여기다 — 값은 관리자가 늘릴 수 있는 select
   선택지라 서버가 표를 들면 선택지를 늘릴 때마다 코드를 고쳐야 한다.
   ⚠ 모르는 값은 --mg-skill-role-common 으로 떨어지되 글자는 그대로 뜬다. */
/* ⚠ 꾸밈용 네모를 글자 **바로 뒤**에 흘려 두면 빠진 글리프(두부)처럼 보인다
   (2026-09-05 사람 신고). 시안대로 글자와 바를 한 칸에 쌓고 네모는 옆 칸에
   떨어뜨린다 — 같은 요소라도 자리가 뜻을 만든다. */
#content .mg-profile-position {
  --mg-position-color: var(--mg-skill-role-common);
  display: grid;
  grid-template-columns: auto auto;
  grid-template-areas:
    "word"
    "bar";
  justify-content: start;
  row-gap: var(--mg-space-2);
  /* ⚠ 바 아래 여백. 다음 칸(스탯 상자)이 바로 붙으면 장식이 아니라 구분선으로 읽힌다.
     ⛔ 값을 여기서 고친다 — 같은 선택자의 규칙을 하나 더 만들면 이 규칙을 찾던
        가드가 **그쪽을 먼저 집는다**(2026-09-09 실제로 그렇게 깨졌다). */
  margin: var(--mg-space-3) 0 var(--mg-space-4);
}

#content .mg-profile-position--tanker { --mg-position-color: var(--mg-skill-role-tank); }
#content .mg-profile-position--dealer { --mg-position-color: var(--mg-skill-role-deal); }
#content .mg-profile-position--healer { --mg-position-color: var(--mg-skill-role-heal); }

.mg-profile-position__word {
  grid-area: word;
  font-family: var(--mg-font-heading);
  font-size: var(--mg-font-size-xl);
  line-height: 1;
  color: var(--mg-position-color);
}

.mg-profile-position__bar {
  grid-area: bar;
  block-size: var(--mg-bar-h);
  inline-size: 3.5rem;
  background: var(--mg-position-color);
}

/* 섹션 — 판때기가 아니라 큰 제목 + 아래 굵은 선 + 번호 */
#content .mg-profile-section__head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--mg-space-4);
  /* ⚠ 글자와 선 사이는 좁게, 선 아래는 넓게 (2026-09-05 사람 지시).
     제목이 선에 붙어야 한 덩어리로 읽히고, 본문은 선에서 떨어져야 시작이 보인다. */
  padding-block-end: var(--mg-space-1);
  margin-block-end: var(--mg-space-4);
  border-block-end: var(--mg-stroke-1) solid var(--mg-border-strong);
}

#content .mg-profile-section__title {
  font-family: "Black Ops One", var(--mg-font-heading);
  font-size: var(--mg-font-size-xl);
  line-height: 1;
  color: var(--mg-text-strong);
}

.mg-profile-section__no::before {
  font-family: var(--mg-font-mono);
  font-size: var(--mg-font-size-lg);
  letter-spacing: var(--mg-track-wide);
  color: var(--mg-border);
}

/* 목차 레일 — 상자 안, 현재 위치는 page-toc.js 가 aria-current 로만 찍는다 */
/* ⚠ 상자를 벗겼다 (2026-09-05 사람 지시) — 배경도 테두리도 없다. */
#content .mg-profile-toc {
  padding: 0;
}

/* ⭐ 페이지 목차와 **같은 결**로 되돌린다 — 왼쪽 금색 막대가 hover·포커스·현재
   위치에서 함께 켜지고, 색은 전환을 지난다. 규칙이 둘이 되면 덜 보이는 쪽이
   먼저 어긋난다 (docs/SKIN_API.md "현재 위치 강조"). */
#content .mg-profile-toc__link {
  position: relative;
  display: block;
  padding: var(--mg-space-2) 0 var(--mg-space-2) var(--mg-space-3);
  color: var(--mg-text-muted);
  text-decoration: none;
  transition: color var(--mg-transition);
}

#content .mg-profile-toc__link::before {
  content: "";
  position: absolute;
  inset-inline-start: 0;
  inset-block: 2px;
  inline-size: var(--mg-stroke-1);
  background: transparent;
  transition: background var(--mg-transition);
}

#content .mg-profile-toc__link:hover,
#content .mg-profile-toc__link:focus-visible,
#content .mg-profile-toc__link[aria-current="true"] {
  color: var(--mg-text-strong);
}

#content .mg-profile-toc__link:hover::before,
#content .mg-profile-toc__link:focus-visible::before,
#content .mg-profile-toc__link[aria-current="true"]::before {
  background: var(--mg-bar-gold);
}

/* ⛔⛔ 목차 <nav> 자신은 붙지 않는다 (2026-09-06 사람 신고). 붙이면 **형제인
   글자 크기 조절바만 흘러내려** 스크롤을 내린 사람에게서 사라지는데, 목차는
   그대로 보이니 고장으로도 안 보인다. 붙는 주인은 그 둘을 함께 담은 **열**이다
   (아래 :last-child 규칙, 좌열과 같은 모양). 폴백 표면에서는 `.mg-profile__aside`
   가 이미 그 주인이라 여기서 뺀 것이 그쪽에는 영향이 없다. */
/* 좌열 고정. ⛔ 미디어 쿼리 안이어야 한다 — 좁은 화면에서는 열이 한 칸으로
   접히므로 sticky 가 본문을 붙잡는다. */
@media (min-width: 75rem) {
  /* ⚠ 옆열은 아래 규칙이 함께 맡는다 — 여기 다시 적으면 같은 일을 하는 규칙이
     두 벌이 되어 한쪽만 고쳐진다(2026-09-06 변이 앵커가 실제로 겹쳤다).
     이 자리는 폴백 표면의 aside 만 든다. */
  .mg-profile--aside .mg-profile__aside {
    position: sticky;
    top: var(--mg-space-5);
    align-self: start;
  }
}

/* 고정 헤더와 sticky 레일이 같은 오프셋을 다투면 레일이 아예 안 붙는다. */
body.mg-headerfix-on.mg-header-top .mg-profile--aside .mg-profile__aside { position: static; }

@media (max-width: 48rem) {
  #content .mg-profile-band { grid-template-columns: minmax(0, 1fr); }
  /* ⛔⛔ `nowrap` 을 여기서 푼다 (2026-09-06 사람 신고: 모바일 가로 스크롤).
     로마자 이름은 한 낱말이 아니라 문장이라("Lothar von der Laue") 좁은 화면에서
     화면 밖으로 나가고, **문서 전체에 가로 스크롤이 생긴다.** 넘치는 것은 이
     요소인데 스크롤은 페이지에 붙어서 원인이 안 보인다.
     ⚠ 넓은 화면의 `nowrap` 은 그대로 둔다 — 거기서는 이름과 밑줄을 맞추는 값이다.
     ⛔ `overflow-wrap: anywhere` 를 함께 준다: 띄어쓰기 없는 긴 이름은 줄바꿈만으로
        안 잘린다. */
  .mg-profile-band__echo {
    justify-self: start;
    white-space: normal;
    overflow-wrap: anywhere;
  }
}

/* 좌열 고정. ⛔ 규칙이 미디어 쿼리 안에 있어야 한다 — 좁은 화면에서는 열이
   한 칸으로 접히므로 sticky 가 본문을 밀어 올린다.
   ⛔ 직계 자식으로 좁힌다: 안쪽 컨테이너까지 걸면 중첩 컬럼이 함께 붙는다. */
@media (min-width: 75rem) {
  .mg-profile-layout > .mg-block-columns > .mg-block-columns__column:first-child,
  .mg-profile-layout > .mg-block-columns > .mg-block-columns__column:last-child {
    position: sticky;
    top: var(--mg-space-5);
    align-self: start;
  }
}

/* 고정 헤더와 sticky 레일이 같은 오프셋을 다투면 레일이 아예 안 붙는다
   (.mg-profile__aside 와 같은 해법). */
body.mg-headerfix-on.mg-header-top .mg-profile-layout > .mg-block-columns > .mg-block-columns__column:first-child,
body.mg-headerfix-on.mg-header-top .mg-profile-layout > .mg-block-columns > .mg-block-columns__column:last-child {
  position: static;
}

@media (max-width: 48rem) {
  .mg-character-banner__guild,
  .mg-character-banner__rank,
  .mg-character-banner__name,
  .mg-character-banner__en { max-inline-size: 52%; }
  #content .mg-profile-info { grid-template-columns: 1fr; }
  #content .mg-profile__header .mg-profile-plate__body { flex-wrap: wrap; }
}

@media (max-width: 64rem) {
  .mg-profile--aside .mg-profile__body { grid-template-columns: 1fr; }
  /* 좁은 화면에서 설정 목차는 가로로 흐른다.
     ⛔⛔ `#content` 를 붙인다 — `#content ul` 이 (1,0,1) 이라 맨 클래스 규칙의
        `padding: 0` 은 **들여쓰기만 졌다**(2026-09-11 사람 신고: *"각 요소는 왼쪽
        정렬인데 전체가 오른쪽으로 밀려 보인다"*). 불릿은 사라지는데 24px 이 남아
        덩어리가 치우친 것처럼 보이고, **어디에도 오류가 안 난다.**
     ⛔ `safe center` 다. 그냥 `center` 면 항목이 화면보다 길어질 때 가운데 정렬이
        **시작 부분을 잘라내** 첫 항목에 영영 못 닿는다 — `safe` 는 넘칠 때만
        시작 정렬로 돌아간다. 지원하지 않는 브라우저는 선언을 버리고 기존 동작
        (왼쪽 시작)으로 남는다. */
  #content .mg-profile-toc__list {
    display: flex;
    justify-content: safe center;
    gap: var(--mg-space-2);
    overflow-x: auto;
    padding: 0;
    list-style: none;
  }
}
/* 좁은 화면에서는 한 칸으로 쌓이고, 순서가 **좌 → 우 → 가운데** 다
   (2026-09-06 사람 지시). 인물과 목차를 먼저 보여 주고 긴 서술을 뒤로 보낸다 —
   가운데 열이 화면 몇 개 분량이라 그것을 먼저 두면 목차에 영영 못 닿는다.
   ⛔ 직계 자식으로 좁힌다: 안쪽에 중첩된 컬럼까지 걸면 그쪽 순서도 뒤집힌다.
   ⚠ `order` 는 **보이는 순서만** 바꾼다 — 읽기·탭 순서는 마크업 그대로(좌·가운데·우)라
      키보드로는 목차가 서술 뒤에 온다. 마크업을 바꾸면 두 표면이 갈리므로 여기서 감수한다. */
@media (max-width: 64rem) {
  .mg-profile-layout .mg-block-columns { grid-template-columns: 1fr; }
  .mg-profile-layout > .mg-block-columns > .mg-block-columns__column:first-child { order: 1; }
  .mg-profile-layout > .mg-block-columns > .mg-block-columns__column:last-child { order: 2; }
  .mg-profile-layout > .mg-block-columns > .mg-block-columns__column { order: 3; }
}

/* Novel reader. The --mg-reader-* variables are inherited from the .mg-reader
   root and consumed by .mg-reader-body, so the title, byline and typography
   controls keep their own size (docs/SKIN_API.md). */
.mg-reader {
  max-width: var(--mg-reader-width);
  margin: 0 auto;
}

.mg-reader-body {
  font-family: var(--mg-reader-font-family);
  font-size: calc(var(--mg-font-size-base) * var(--mg-reader-font-scale));
  line-height: var(--mg-reader-line-height);
  text-align: var(--mg-reader-align);
}

.mg-reader-body p {
  margin: 0 0 1em;
}

.mg-reader-controls,
.mg-series-nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--mg-space-2);
  margin-block: var(--mg-space-5);
}

.mg-reader-controls {
  background: var(--mg-surface-tint);
  border-radius: var(--mg-radius);
  padding: var(--mg-space-3) var(--mg-space-4);
  align-items: center;
}

.mg-reader-controls button {
  font: inherit;
  font-size: var(--mg-font-size-sm);
  border: var(--mg-border-width) solid var(--mg-border);
  border-radius: var(--mg-radius-pill);
  background: var(--mg-surface);
  color: var(--mg-text);
  padding: var(--mg-space-1) var(--mg-space-3);
  cursor: pointer;
  transition: background var(--mg-transition-fast), border-color var(--mg-transition-fast);
}

.mg-reader-controls button:hover {
  border-color: var(--mg-accent);
  background: var(--mg-accent-soft);
}

.mg-series-cover,
.mg-series img {
  max-width: 100%;
  border-radius: var(--mg-radius);
}

.mg-series-posts {
  list-style: none;
  padding-inline-start: 0;
}

.mg-bookmark {
  margin-block: var(--mg-space-5);
}

/* ------------------------------------------------------------------ *
 * 9. characters
 * ------------------------------------------------------------------ */

.mg-featured-characters,
/* 알림 메시지. ⛔ #content 안에서 한 곳이 그린다 (templates/core/base.html). */
.mg-messages {
  list-style: none;
  padding-inline-start: 0;
  margin-block-end: var(--mg-space-4);
  display: grid;
  gap: var(--mg-space-2);
}

.mg-messages__item {
  border: var(--mg-border-width) solid var(--mg-border);
  border-radius: var(--mg-radius);
  padding: var(--mg-space-3);
  background: var(--mg-surface);
}

.mg-messages__item--error {
  border-color: var(--mg-danger);
  color: var(--mg-danger);
}

/* ------------------------------------------------------------------ *
   상점 (계획서 v0.3-p-4)
   ⛔ 색과 크기를 하드코딩하지 않는다 — 전부 --mg-* 다.
 * ------------------------------------------------------------------ */
.mg-shop { display: block; }

.mg-shop__buyer { margin-block-start: var(--mg-space-2); }

.mg-shop__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
  gap: var(--mg-space-4);
  margin-block-start: var(--mg-space-5);
}

.mg-shop-card {
  border: var(--mg-border-width) solid var(--mg-border);
  border-radius: var(--mg-radius);
  padding: var(--mg-space-4);
  background: var(--mg-surface);
  display: flex;
  flex-direction: column;
  gap: var(--mg-space-2);
}

/* ⚠ 못 사는 것을 숨기지 않고 물러나게 한다 — 무엇이 있는지는 보여야 모은다. */
.mg-shop-card--unaffordable { opacity: 0.72; }

.mg-shop-card__art {
  inline-size: 100%;
  block-size: auto;
  border-radius: var(--mg-radius-sm);
  object-fit: cover;
}

.mg-shop-card__name { margin: 0; font-size: var(--mg-font-size-lg); }

.mg-shop-card__desc { margin: 0; color: var(--mg-text-muted); font-size: var(--mg-font-size-sm); }

.mg-shop-card__price { margin: 0; font-weight: 600; font-variant-numeric: tabular-nums; }

.mg-shop-card__stock { margin: 0; color: var(--mg-text-muted); font-size: var(--mg-font-size-sm); }

/* 뽑기 진열. ⚠ 구조는 정가 판매와 같다 — 다른 것은 "무엇이 나올 수 있는가" 목록뿐이다. */
.mg-shop-card--gacha { border-color: var(--mg-accent); }

.mg-shop-odds {
  margin: 0;
  padding-inline-start: 0;
  list-style: none;
  display: grid;
  gap: var(--mg-space-1);
  font-size: var(--mg-font-size-sm);
}

.mg-shop-odds__item {
  display: flex;
  justify-content: space-between;
  gap: var(--mg-space-2);
  color: var(--mg-text-muted);
}

/* ⚠ 아이템 이름은 관리자가 짓는다 — 긴 이름이 비율 칸을 밀어내지 않게 한다. */
.mg-shop-odds__name { overflow-wrap: anywhere; }

.mg-shop-odds__rate { font-variant-numeric: tabular-nums; }

.mg-shop-card__buy { margin-block-start: auto; }

/* 코어가 그리는 폼 (DECISIONS D8-2). ⛔ 스킨은 이 클래스를 꾸미기만 한다. */
.mg-form__field { margin-block: var(--mg-space-3); }

.mg-form__label { display: block; font-size: var(--mg-font-size-sm); }

.mg-form__help { display: block; color: var(--mg-text-muted); font-size: var(--mg-font-size-sm); }

.mg-form__errors,
.mg-form__error { color: var(--mg-danger); font-size: var(--mg-font-size-sm); }

.mg-form__errors { list-style: none; padding-inline-start: 0; }

.mg-form__field--invalid input,
.mg-form__field--invalid select,
.mg-form__field--invalid textarea { border-color: var(--mg-danger); }

/* ------------------------------------------------------------------ *
   캐릭터 보유 — 재화와 소지품 (계획서 v0.3-p-3 §4-3)
   ⛔ 색과 크기를 하드코딩하지 않는다 — 전부 --mg-* 다.
 * ------------------------------------------------------------------ */
.mg-profile-holdings {
  border-top: var(--mg-border-width) solid var(--mg-border);
  padding-top: var(--mg-space-4);
  margin-top: var(--mg-space-5);
}

.mg-profile-holdings__title {
  font-size: var(--mg-font-size-lg);
  margin-block-end: var(--mg-space-4);
}

.mg-profile-holdings__group + .mg-profile-holdings__group {
  margin-block-start: var(--mg-space-4);
}

.mg-profile-holdings__heading {
  font-size: var(--mg-font-size-sm);
  color: var(--mg-text-muted);
  letter-spacing: 0.08em;
  margin-block-end: var(--mg-space-2);
}

.mg-profile-holdings__list {
  list-style: none;
  padding-inline-start: 0;
  margin: 0;
  display: grid;
  gap: var(--mg-space-1);
}

/* 이름과 수를 양 끝으로 — 숫자가 한 줄에 정렬되어야 읽힌다. */
.mg-profile-holdings__row {
  display: flex;
  justify-content: space-between;
  gap: var(--mg-space-3);
  padding: var(--mg-space-2) 0;
  border-bottom: var(--mg-border-width) solid var(--mg-border);
}

.mg-profile-holdings__name { min-inline-size: 0; }

.mg-profile-holdings__count {
  font-variant-numeric: tabular-nums;
  color: var(--mg-text-muted);
}

/* 두 묶음의 결을 나눈다 — 재화는 수가 크고 소지품은 이름이 길다. */
.mg-profile-holdings__group--currency .mg-profile-holdings__count { font-weight: 600; }

.mg-profile-holdings__group--items .mg-profile-holdings__count::after { content: "개"; }

/* ------------------------------------------------------------------ *
   캐릭터 관계 (계획서 v0.3-o-1 §4-4)
   ⭐ 줄 하나 = 카드 하나 — Figma g1ZAYFMIDw5eBxB7dqORqi 18:2 (2026-10-15 사람 지시).
      왼쪽 칸 = 상대의 **흑백** 두상(올리면 컬러, 누르면 상대 프로필) · 오른쪽 =
      한글 이름 뒤에 겹친 필기체 로마자 이름 · 설명 · 오른쪽 위 작성중.
   ⭐ 그림 칸은 300px(`--mg-relation-art-width`) × 멤버란 배너 높이, 그림·초점·확대는
      프로필 인물 칸 그대로다 (2026-10-15 사람 지시·결정 — 96px 는 너무 작았고, 배너
      초점으로는 확대할수록 얼굴에서 멀어졌다).
   ⭐ 배치는 시안, 크기는 **옛 관계 줄과 비슷하게** 줄였다 (2026-10-15 사람 지시 —
      시안 비율 그대로는 카드 하나가 화면 반을 먹었다). 크기는 전부 토큰이다.
   ⭐ 카드 바탕은 **투명**이고(시안의 흰→회색 바탕·그림자를 걷었다) 그림이 오른쪽으로
      투명하게 사라지는 그라데이션만 남는다 (같은 날 사람 지시 — 경계가 안 보이게).
   ⭐ 위계가 둘이다 — 이 인물이 맺은 관계가 주(主), 상대가 이 인물을 향해 건
      관계가 부(副)다 (2026-09-07 사람 지시). 부는 **크기가 아니라 밀도**로
      물러난다: 들여쓰기 · 작은 글자 (옅은 바탕은 카드 바탕이 투명해지며 걷혔다).
   ⛔ 색과 크기를 하드코딩하지 않는다 — 전부 --mg-* 다.
 * ------------------------------------------------------------------ */
.mg-profile-relations {
  border-top: var(--mg-border-width) solid var(--mg-border);
  padding-top: var(--mg-space-4);
  margin-top: var(--mg-space-5);
}

.mg-profile-relations__title {
  font-size: var(--mg-font-size-lg);
  margin-block-end: var(--mg-space-4);
}

.mg-profile-relations__group + .mg-profile-relations__group {
  margin-block-start: var(--mg-space-5);
}

.mg-profile-relations__heading {
  font-size: var(--mg-font-size-sm);
  color: var(--mg-text-muted);
  letter-spacing: 0.08em;
  margin-block-end: var(--mg-space-3);
}

/* ⛔ #content 를 붙인다 — `#content ul` 이 (1,0,1) 이라 들여쓰기를 이긴다. */
#content .mg-profile-relations__list {
  list-style: none;
  padding-inline-start: 0;
  margin: 0;
  display: grid;
  gap: var(--mg-space-3);
}

/* 카드. 바탕이 없다 — 페이지 바탕이 그대로 비친다.
   ⛔ overflow: hidden 이 둘을 자른다 — 넘치는 로마자 이름과 확대한 그림.
   그래서 포커스 테두리는 안쪽에 그린다. */
.mg-profile-relations__item {
  position: relative;
  isolation: isolate;
  display: grid;
  /* ⚠ 좁은 화면에서는 카드의 40% 까지만 — 300px 을 고집하면 글 칸이 사라진다. */
  grid-template-columns: min(var(--mg-relation-art-width), 40%) minmax(0, 1fr);
  min-block-size: var(--mg-character-banner-height);
  overflow: hidden;
}

/* ⭐ 그림 오른쪽 끝 = 불투명 → 투명 (마스크). 바탕이 투명이라 색을 덮어 흐리게 할 수
   없다 — 그림 칸 전체의 알파를 깎는다. 마스크는 알파만 보므로 색 토큰은 불투명이면 된다
   (탐색 지역 카드와 같은 모양).
   ⛔⛔ 그림(img)이 아니라 **링크**에 건다 — 그림은 `scale` 로 확대되고 마스크도 함께
      커져서, 확대할수록 투명한 끝이 칸 밖으로 밀려나 오른쪽이 딱 잘려 보였다(헤드리스로
      확인). 링크는 확대되지 않는다.
   ⛔ 링크에 걸면 링크의 포커스 테두리도 마스크에 깎인다 — 그래서 포커스 표시는 카드가
      대신 그린다(아래).
   그림 칸 = 상대 프로필 링크. 그림은 칸을 채우기만 하고 줄 높이를 정하지 않는다. */
.mg-profile-relations__partner {
  position: relative;
  z-index: 0;
  display: block;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(to right, var(--mg-surface-inverse) 60%, transparent);
  mask-image: linear-gradient(to right, var(--mg-surface-inverse) 60%, transparent);
}

.mg-profile-relations__partner:not(:has(img)) {
  background: var(--mg-surface-sunken);
}

.mg-profile-relations__partner:focus-visible {
  outline: none;
  box-shadow: none;
}

/* 키보드 포커스는 카드 테두리로 — 전역 :focus-visible 과 같은 모양.
   카드 자신의 overflow: hidden 은 자기 outline 을 자르지 않는다. */
.mg-profile-relations__item:has(.mg-profile-relations__partner:focus-visible) {
  outline: var(--mg-stroke-1) solid var(--mg-border-strong);
  outline-offset: 2px;
  box-shadow: 0 0 0 4px var(--mg-accent-soft);
}

/* ⭐ 프로필 인물 칸과 같은 그림·초점이다 (2026-10-15 사람 결정 — 10-13 의 "두상" 을
   뒤집었다). 초점·확대 기준점은 인라인 style 로 온다 — 고르는 곳은
   `relations._partner_dict`. ⛔ 여기에 object-position·transform-origin 을 다시 적지 않는다.
   ⭐ 확대는 프로필 확대값의 **절반**이다 (같은 날 사람 결정 — 그대로 곱하면 얼굴만,
      안 곱하면 상반신이 작았다). 값은 인라인 변수 `--mg-relation-zoom` 으로만 오고 배율은
      토큰 `--mg-relation-art-zoom` 이다. ⛔ 1 아래로는 안 줄인다 — 그림이 칸보다 작아져
      가장자리가 빈다(확대값 기본 100% 인 캐릭터가 그렇게 된다).
   ⭐ 흑백 → 올리거나 포커스하면 컬러 (2026-10-15, Figma 의 Grayscale 필터). 시안의
      셰이더는 Rec.709 휘도 가중이고 CSS grayscale() 과 같은 계산이다.
   ⛔ scale·filter 는 이미지 자신에만 건다: 조상에 걸면 자손 position: fixed 의
      컨테이닝 블록이 되어 오버레이가 스크롤을 따라 흘러내린다. */
.mg-profile-relations__portrait {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  scale: max(1, calc(var(--mg-relation-zoom, 1) * var(--mg-relation-art-zoom)));
  filter: grayscale(1);
  transition: filter var(--mg-transition);
}

.mg-profile-relations__partner:hover .mg-profile-relations__portrait,
.mg-profile-relations__partner:focus-visible .mg-profile-relations__portrait {
  filter: grayscale(0);
}

/* 글 칸은 그림 칸 위에 쌓인다 — 로마자 이름이 그림 끝에 걸쳐 그려진다(시안).
   ⛔ pointer-events 는 로마자 이름만 끈다: 걸친 부분에서도 그림을 누를 수 있다. */
.mg-profile-relations__text {
  position: relative;
  z-index: 1;
  padding: var(--mg-space-2) var(--mg-space-5) var(--mg-space-3) var(--mg-space-4);
}

.mg-profile-relations__names {
  position: relative;
  font-size: var(--mg-font-size-lg);
  line-height: 1.2;
}

.mg-profile-relations__name {
  position: relative;
  z-index: 1;
  font-family: var(--mg-font-heading);
  font-weight: 400;
  color: var(--mg-text-strong);
}

/* 시안: 한글 이름보다 61px 왼쪽 · 13px 아래(64px 글자 기준 0.95em · 0.2em), 이름 **뒤**. */
.mg-profile-relations__echo {
  position: absolute;
  z-index: 0;
  inset-inline-start: -0.95em;
  inset-block-start: 0.2em;
  font-family: var(--mg-font-script);
  color: var(--mg-accent); /*사용자 지정, 가드에서 예외*/
  white-space: nowrap;
  pointer-events: none;
}

.mg-profile-relations__label {
  display: block;
  margin-block-start: var(--mg-space-1);
  font-size: var(--mg-font-size-sm);
  color: var(--mg-text-muted);
}

/* ⛔ #content 를 붙인다 — `.root-prose` 의 font 축약형과 `#content p` 를 이겨야 한다. */
#content .mg-profile-relations__body {
  margin-block: var(--mg-space-2) 0;
  line-height: var(--mg-line-height);
}

#content .mg-profile-relations__label + .mg-profile-relations__body {
  margin-block-start: var(--mg-space-1);
}

/* 시안: 카드 오른쪽 위의 회색 판(#d9d9d9 ≈ 흰 바탕 위 먹 15%).
   ⛔ 반투명 먹이다 — 카드 바탕이 투명이라 바탕색 토큰을 쓰면 다크에서 페이지 바탕과
      같은 색이 되어 판이 사라진다(헤드리스로 확인). */
.mg-profile-relations__draft {
  position: absolute;
  z-index: 2;
  inset-block-start: var(--mg-space-2);
  inset-inline-end: var(--mg-space-2);
  padding: 0 var(--mg-space-3);
  font-size: var(--mg-font-size-xs);
  color: var(--mg-text-strong);
  background: color-mix(in srgb, var(--mg-text-strong) 15%, transparent);
}

/* 주(主) 위계. 부 위계와 짝이 되는 자리라 규칙을 비워 두지 않는다 — 비우면
   "이쪽은 손대지 않은 것" 인지 "규칙을 빠뜨린 것" 인지 구분이 안 된다
   (카드 바탕을 걷으며 실제로 비었고 클래스 커버리지 가드가 잡았다). */
#content .mg-profile-relations__group--out .mg-profile-relations__body {
  font-size: var(--mg-font-size-base);
}

/* 부(副) 위계. ⛔ 숨기는 것이 아니라 물러나게 하는 것이다 — 지우면 상대가 건
   관계를 아무도 못 본다. */
#content .mg-profile-relations__group--in .mg-profile-relations__list {
  padding-inline-start: var(--mg-space-4);
  border-inline-start: var(--mg-border-width) solid var(--mg-border);
}

#content .mg-profile-relations__group--in .mg-profile-relations__body {
  font-size: var(--mg-font-size-sm);
}

/* 소유주 화면의 상대 검색. ⛔ [hidden] 되돌림을 같은 자리에 둔다 —
   display 를 준 요소에서는 HTML hidden 이 진다 (스킬 뷰어에서 밟은 함정). */
.mg-relation-results {
  list-style: none;
  padding-inline-start: 0;
  margin-block-start: var(--mg-space-2);
  display: grid;
  gap: var(--mg-space-1);
  border: var(--mg-border-width) solid var(--mg-border);
  border-radius: var(--mg-radius);
  background: var(--mg-surface);
}

.mg-relation-results[hidden] { display: none; }

.mg-relation-results__item {
  display: block;
  inline-size: 100%;
  text-align: start;
  background: none;
  border: 0;
  padding: var(--mg-space-2) var(--mg-space-3);
  color: var(--mg-text);
  cursor: pointer;
}

.mg-relation-results__item:hover,
.mg-relation-results__item:focus-visible {
  background: var(--mg-surface-tint);
}

.mg-relation-field { margin-block: var(--mg-space-3); }

.mg-relation-picked { margin-inline-start: var(--mg-space-2); font-weight: 600; }

.mg-relation-rows { list-style: none; padding-inline-start: 0; }

.mg-relation-row {
  border-bottom: var(--mg-border-width) solid var(--mg-border);
  padding: var(--mg-space-3) 0;
}

.mg-relation-row__label,
.mg-relation-row__draft {
  margin-inline-start: var(--mg-space-2);
  font-size: var(--mg-font-size-sm);
  color: var(--mg-text-muted);
}

.mg-relation-row__body { margin-block: var(--mg-space-2); }

.mg-relation-row__delete { display: inline; }

.mg-featured-log {
  border-top: var(--mg-border-width) solid var(--mg-border);
  padding-top: var(--mg-space-4);
  margin-top: var(--mg-space-5);
}

.mg-featured-log ul {
  list-style: none;
  padding-inline-start: 0;
}

.mg-featured-log li {
  border-bottom: var(--mg-border-width) solid var(--mg-border);
  padding: var(--mg-space-2) 0;
}

.mg-post-row--notice td { background: var(--mg-surface-tint); }
.mg-mypage__summary { display: flex; align-items: center; gap: var(--mg-space-4); }
.mg-mypage__account { margin-block-start: var(--mg-space-4); }
.mg-auth { max-width: 26rem; margin-inline: auto; }
.mg-form-actions { margin-block-start: var(--mg-space-4); }

/* ------------------------------------------------------------------ *
 * 10. manage
 * ------------------------------------------------------------------ */

.mg-manage nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--mg-space-3);
  background: var(--mg-surface-tint);
  border-radius: var(--mg-radius);
  padding: var(--mg-space-3) var(--mg-space-4);
  margin-bottom: var(--mg-space-5);
  font-size: var(--mg-font-size-sm);
}

/* ------------------------------------------------------------------ *
 * 11. narrow viewports
 * ------------------------------------------------------------------ */

/* ------------------------------------------------------------------ *
 * Dock — expanded (icon + label) and collapsed (icon only)
 *
 * ⛔ 선택자를 :where() 로 쓰지 않는다. :where() 는 명시도가 0이라 이 블록이
 * 독 규칙(body.mg-header-left .mg-shell-nav a = 0,2,2)에 져서 아이콘이 44px
 * 원에 갇힌 채 글자만 한 줄로 붙는다 — 2026-08-31 실측. :is() 로 쓰고, 블록을
 * 독 규칙 **뒤**에 둔다.
 *
 * ⛔ No script. A checkbox in the shell holds the state and CSS reads it with
 * :has(). The shell is the one thing hx-boost never swaps, so the choice
 * survives every boosted move and only returns to the default (expanded) on a
 * full reload — which is the trade for having no storage and no JS.
 *
 * ⛔ Nothing moves in the DOM. The BGM iframe lives inside this header and a
 * re-parent restarts playback (docs/DECISIONS.md D2-1), so expanding is width,
 * alignment and visibility only.
 *
 * ⚠ Above the drawer breakpoint only. Below 48rem the nav is a slide-in drawer
 * and there is no capsule to expand.
 * ------------------------------------------------------------------ */
.mg-shell-collapse { position: absolute; inline-size: 1px; block-size: 1px; clip-path: inset(50%); overflow: hidden; }

/* 접기 버튼은 독일 때만 쓴다 — 가로 헤더에는 접을 것이 없다. */
.mg-shell-collapse-toggle { display: none; }

@media (min-width: 48rem) {
  body.mg-header-left .mg-shell-collapse-toggle,
  body.mg-header-right .mg-shell-collapse-toggle {
    order: -1;
    display: grid;
    place-items: center;
    inline-size: var(--mg-shell-icon);
    block-size: var(--mg-space-6);
    border-radius: var(--mg-radius-sm);
    cursor: pointer;
  }

  body.mg-header-left .mg-shell-collapse-toggle:hover,
  body.mg-header-right .mg-shell-collapse-toggle:hover { background: var(--mg-surface-tint); }

  /* 라벨은 스크린리더용이고 화면에는 갈매기표만 둔다 — 아이콘 폰트를 쓰지
     않는 이유는 슬라이드 화살표와 같다 (CDN이 막히면 리거처가 글자로 나온다). */
  .mg-shell-collapse-toggle__label { position: absolute; inline-size: 1px; block-size: 1px; clip-path: inset(50%); overflow: hidden; }

  .mg-shell-collapse-toggle::before {
    content: "";
    inline-size: var(--mg-space-2);
    block-size: var(--mg-space-2);
    border-block-start: var(--mg-stroke-1) solid var(--mg-text-muted);
    border-inline-start: var(--mg-stroke-1) solid var(--mg-text-muted);
    transform: rotate(-45deg);
  }

  body.mg-header-right .mg-shell-collapse-toggle::before,
  body:has(#mg-shell-collapse:checked).mg-header-left .mg-shell-collapse-toggle::before { transform: rotate(135deg); }

  body:has(#mg-shell-collapse:checked).mg-header-right .mg-shell-collapse-toggle::before { transform: rotate(-45deg); }

  /* --- 펼침(기본): 아이콘 + 문구 --------------------------------- */
  body.mg-header-left:not(:has(#mg-shell-collapse:checked)) .mg-shell,
  body.mg-header-right:not(:has(#mg-shell-collapse:checked)) .mg-shell {
    grid-template-columns: var(--mg-shell-nav-width) 1fr;
  }

  body.mg-header-right:not(:has(#mg-shell-collapse:checked)) .mg-shell {
    grid-template-columns: 1fr var(--mg-shell-nav-width);
  }

  /* 펼치면 캡슐이 아니라 **가장자리를 세로로 덮는 흰 베일**이 된다
     (2026-08-31 사람 지시). ROOT 는 그림 위 요소에 어두운 스크림 대신 흰
     베일을 쓰고, `--mg-veil-*` 가 바로 그 토큰이다 — 상자·라운드·그림자·
     블러를 전부 걷고 그러데이션만 남긴다. */
  body.mg-header-left:not(:has(#mg-shell-collapse:checked)) .mg-shell-header,
  body.mg-header-right:not(:has(#mg-shell-collapse:checked)) .mg-shell-header {
    top: 0;
    inset-block: 0;
    transform: none;
    block-size: 100dvh;
    inline-size: var(--mg-shell-nav-width);
    align-items: stretch;
    justify-content: flex-start;
    gap: var(--mg-space-2);
    padding: var(--mg-space-6) var(--mg-space-5);
    background: var(--mg-veil-left);
    backdrop-filter: none;
    border-radius: 0;
    box-shadow: none;
  }

  body.mg-header-left:not(:has(#mg-shell-collapse:checked)) .mg-shell-header { inset-inline-start: 0; }

  body.mg-header-right:not(:has(#mg-shell-collapse:checked)) .mg-shell-header {
    inset-inline-end: 0;
    background: var(--mg-veil-right);
  }

  /* 브랜드가 마크에서 워드마크로 돌아온다. */
  body.mg-header-left:not(:has(#mg-shell-collapse:checked)) .mg-shell-brand,
  body.mg-header-right:not(:has(#mg-shell-collapse:checked)) .mg-shell-brand {
    inline-size: 100%;
    block-size: auto;
    display: flex;
    align-items: center;
    justify-content: flex-start;
    padding: var(--mg-space-2) var(--mg-space-3);
    font-size: var(--mg-font-size-lg);
    letter-spacing: normal;
    text-align: start;
  }

  /* ⛔⛔ 컨테이너부터 편다. 캡슐 블록이 `.mg-shell-nav` 에 `align-items: center;
     inline-size: auto` 를 걸어 **네비 자체가 아이콘 너비로 쪼그라들고 가운데로
     몰린다** — 그 상태에서는 앵커를 100% 로 만들어도 "쪼그라든 네비의 100%"라
     계속 가운데로 보인다 (2026-08-31 실측).
     ⚠ `.mg-shell-account` 에는 그 규칙이 없어서 혼자 왼쪽으로 붙었고, 그 차이가
     "기본 메뉴만 정렬되고 관리자 메뉴는 안 되는" 증상으로 나타났다. */
  body.mg-header-left:not(:has(#mg-shell-collapse:checked)) .mg-shell-nav,
  body.mg-header-right:not(:has(#mg-shell-collapse:checked)) .mg-shell-nav,
  body.mg-header-left:not(:has(#mg-shell-collapse:checked)) .mg-shell-account,
  body.mg-header-right:not(:has(#mg-shell-collapse:checked)) .mg-shell-account {
    align-items: stretch;
    inline-size: 100%;
  }

  /* ⛔ `.mg-shell-nav` 의 `flex: 1` 을 되돌린다. 가로 헤더에서는 그 값이 메뉴를
     가운데 띠로 만들고 계정·BGM 을 오른쪽 끝으로 미는 **의도된** 배치지만,
     세로로 펼친 사이드바에서는 같은 값이 **남은 높이를 통째로 먹어** 마이페이지·
     BGM·다크모드 스위치를 화면 맨 아래로 밀어낸다 (2026-09-02 사람 지시).
     세로에서는 메뉴 바로 아래에 이어 붙는 것이 맞다.
     ⚠ `0 1 auto` 이지 `none` 이 아니다 — 메뉴가 화면보다 길어지면 줄어들 수 있어야
     한다. 헤더는 `overflow: visible` 이라 안 줄면 그대로 화면 밖으로 나간다. */
  body.mg-header-left:not(:has(#mg-shell-collapse:checked)) .mg-shell-nav,
  body.mg-header-right:not(:has(#mg-shell-collapse:checked)) .mg-shell-nav {
    flex: 0 1 auto;
  }

  /* 하위 메뉴도 같은 들여쓰기를 따른다. */
  body.mg-header-left:not(:has(#mg-shell-collapse:checked)) .mg-shell-nav-item,
  body.mg-header-right:not(:has(#mg-shell-collapse:checked)) .mg-shell-nav-item {
    inline-size: 100%;
  }

  /* ⛔ 접힘 상태의 타깃은 `display: grid; place-items: center` 인 44px 원이다.
     펼칠 때 그것을 **풀지 않으면** 아이콘과 글자가 두 줄로 쌓이고 높이에 잘린다
     (2026-08-31 실측). 그래서 display 와 block-size 까지 되돌린다. */
  body.mg-header-left:not(:has(#mg-shell-collapse:checked)) :is(.mg-shell-nav, .mg-shell-account) :is(a, span[tabindex], .mg-shell-nav-item > span),
  body.mg-header-right:not(:has(#mg-shell-collapse:checked)) :is(.mg-shell-nav, .mg-shell-account) :is(a, span[tabindex], .mg-shell-nav-item > span) {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    gap: var(--mg-space-3);
    inline-size: 100%;
    block-size: auto;
    min-block-size: var(--mg-shell-icon);
    padding: var(--mg-space-2) var(--mg-space-3);
    border-radius: var(--mg-radius-sm);
    text-align: start;
  }

  /* 아이콘이 줄어들거나 늘어나지 않게 자리를 고정한다. */
  body.mg-header-left:not(:has(#mg-shell-collapse:checked)) :is(.mg-shell-nav, .mg-shell-account) .mg-icon,
  body.mg-header-right:not(:has(#mg-shell-collapse:checked)) :is(.mg-shell-nav, .mg-shell-account) .mg-icon {
    flex: none;
  }

  /* ⛔ 툴팁을 풀어 제자리 글자로 되돌린다. 위치·배경·그림자를 전부 되돌리지
     않으면 검은 말풍선이 목록 위에 얹힌 채로 남는다. */
  body.mg-header-left:not(:has(#mg-shell-collapse:checked)) :is(.mg-shell-nav, .mg-shell-account) .mg-shell-nav__label,
  body.mg-header-right:not(:has(#mg-shell-collapse:checked)) :is(.mg-shell-nav, .mg-shell-account) .mg-shell-nav__label {
    position: static;
    inset: auto;
    transform: none;
    opacity: 1;
    /* ⚠ 툴팁일 때는 `inline-size: max-content` 로 글자 길이만큼 벌어졌다. 줄
       안에서는 그 값이 긴 메뉴 이름을 캡슐 밖으로 밀어낸다 — 자동으로 두고
       바탕 규칙의 말줄임에 맡긴다. */
    inline-size: auto;
    min-inline-size: 0;
    max-inline-size: none;
    block-size: auto;
    padding: 0;
    border-radius: 0;
    background: transparent;
    color: inherit;
    box-shadow: none;
    /* ⛔ The dock shrinks an icon-less label and every group header's label to 0.625rem
       (they stay visible there). Unfolded, put them back to the size every other item uses —
       otherwise a group header reads smaller than the links beside it (2026-10-11 사람 신고). */
    font-size: var(--mg-font-size-sm);
    line-height: inherit;
    text-align: start;
    font-weight: 500;
    pointer-events: auto;
  }

  /* BGM — 펼치면 음량이 캡슐 안으로 들어와 재생 버튼과 한 줄에 선다.
     ⭐ 패널에 `display: contents` 를 줘서 상자를 없앤다: 어두운 배경과 그림자가
     함께 사라지고, 곡명·스피커·슬라이더가 .mg-bgm 의 직접 자식처럼 배치된다.
     ⛔ 노드는 하나도 안 옮긴다 — BGM iframe 이 이 안에 있다 (D2-1). */
  body.mg-header-left:not(:has(#mg-shell-collapse:checked)) .mg-bgm,
  body.mg-header-right:not(:has(#mg-shell-collapse:checked)) .mg-bgm {
    inline-size: 100%;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--mg-space-2);
    /* ⚠ 캡슐 여백만으로는 아래가 답답하다 — 위쪽은 접기 버튼이 자리를 벌어
       주는데 아래는 슬라이더가 모서리에 바로 닿는다. 플레이어가 제 숨 쉴
       자리를 들고 있게 해서, BGM 이 꺼진 사이트에서는 이 값이 아예 안 생긴다. */
    margin-block: var(--mg-space-2) var(--mg-space-3);
  }

  /* ⛔⛔ `display: contents` 는 상자를 없애지 뜻을 없애지 않는다. 이 패널은
     접힘일 때 **뒤집힌 잉크 판**이라 `background: var(--mg-text)` 위에
     `color: var(--mg-bg)` 를 쓰는데, 상자가 사라져도 그 글자색은 **자식에게
     계속 상속된다.** 그래서 스피커 아이콘이 흰 사이드바 위에 흰색으로 찍혀
     사라져 있었다 — 곡명은 아래에서 자기 색을 따로 받아 살아남았고 아이콘만
     빠졌다(2026-09-02 사람 신고). 색을 여기서 되돌리면 지금 있는 자식도,
     나중에 더할 자식도 함께 낫는다. */
  body.mg-header-left:not(:has(#mg-shell-collapse:checked)) .mg-bgm__panel,
  body.mg-header-right:not(:has(#mg-shell-collapse:checked)) .mg-bgm__panel {
    display: contents;
    color: var(--mg-text);
  }

  /* ⛔ 재생 버튼은 접힘일 때 44px 캡슐을 채우려고 `inline-size: 100%` 다.
     펼친 줄에서는 그 값이 **한 줄을 통째로 먹어** 스피커와 슬라이더를 다음
     줄로 밀어낸다 — 세로로 쌓여 보이는 원인이 이것이다 (2026-08-31 실측).
     제 크기로 되돌려 같은 줄에 세운다. */
  body.mg-header-left:not(:has(#mg-shell-collapse:checked)) .mg-bgm__toggle,
  body.mg-header-right:not(:has(#mg-shell-collapse:checked)) .mg-bgm__toggle {
    flex: none;
    inline-size: var(--mg-bgm-button-size);
  }

  /* 곡명은 윗줄 전체를 쓰고, 아랫줄에 재생·스피커·슬라이더가 나란히 선다. */
  body.mg-header-left:not(:has(#mg-shell-collapse:checked)) .mg-bgm__title,
  body.mg-header-right:not(:has(#mg-shell-collapse:checked)) .mg-bgm__title {
    order: -1;
    flex: 1 0 100%;
    min-inline-size: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--mg-text-muted);
    font-size: var(--mg-font-size-xs);
  }

  body.mg-header-left:not(:has(#mg-shell-collapse:checked)) .mg-bgm__volume,
  body.mg-header-right:not(:has(#mg-shell-collapse:checked)) .mg-bgm__volume {
    flex: 1;
    min-inline-size: 0;
  }

  /* ⭐ 펼친 목록에서는 hover 가 상자를 그리지 않는다 (2026-08-31 사람 지시).
     44px 원일 때는 테두리와 흰 배경이 "누를 수 있는 표적"을 그렸지만, 글자가
     함께 있는 줄에서는 그 상자가 목록을 어지럽힌다 — 색만 바뀐다.
     ⛔ 글자의 금색은 --mg-link 다. --mg-accent 는 흰 배경에서 안 읽힌다
     (tokens.css: 금색은 재료이고 글자는 gold-700 램프로 내린다).
     아이콘은 재료이므로 기존대로 --mg-accent 를 그대로 쓴다. */
  body.mg-header-left:not(:has(#mg-shell-collapse:checked)) :is(.mg-shell-nav, .mg-shell-account) :is(a, span[tabindex]):hover,
  body.mg-header-left:not(:has(#mg-shell-collapse:checked)) :is(.mg-shell-nav, .mg-shell-account) :is(a, span[tabindex]):focus-visible,
  body.mg-header-right:not(:has(#mg-shell-collapse:checked)) :is(.mg-shell-nav, .mg-shell-account) :is(a, span[tabindex]):hover,
  body.mg-header-right:not(:has(#mg-shell-collapse:checked)) :is(.mg-shell-nav, .mg-shell-account) :is(a, span[tabindex]):focus-visible {
    border-color: transparent;
    background: transparent;
    color: var(--mg-link);
  }
}
@media (max-width: 48rem) {
  .mg-shell-main {
    padding: var(--mg-space-5) var(--mg-space-4);
  }

  body.mg-header-top .mg-shell,
  body.mg-header-left .mg-shell,
  body.mg-header-right .mg-shell {
    grid-template-areas: "header" "content";
    grid-template-columns: 1fr;
    grid-template-rows: auto 1fr;
  }

  body.mg-header-top .mg-shell-header,
  body.mg-header-left .mg-shell-header,
  body.mg-header-right .mg-shell-header {
    flex-direction: row;
    align-items: center;
    gap: var(--mg-space-3);
    padding: var(--mg-space-3) var(--mg-space-4);
  }

  /* On narrow screens the left/right headers are ordinary top bars, not
     sidebars. The desktop sticky/100vh/overflow rules must be undone:
     1. otherwise the header is as tall as the viewport
     2. position: sticky creates a stacking context, which traps the drawer
        (.mg-shell-nav, z-index 3) inside the header and lets the scrim
        (z-index 2, a sibling of the shell) cover it — the menu is visible
        but unclickable

     The floating dock capsule (section 3, "ROOT dock") has to come off with
     them, and undoing position alone does not do it:
     3. inline-size is still the rail width, so the top bar is a 76px column
        and the toggle breaks "메뉴" one glyph per line
     4. transform AND backdrop-filter each make the capsule a containing block
        for fixed descendants - the drawer anchors to the capsule instead of
        the viewport and is clipped to its box - and each also makes a stacking
        context, which is trap (2) again: the drawer sinks under the scrim
     5. pill radius, float shadow and the plate background read as a chip
        floating over the page once the header is a full-width bar */
  body.mg-header-left .mg-shell-header,
  body.mg-header-right .mg-shell-header {
    position: static;
    transform: none;
    inline-size: auto;
    height: auto;
    overflow-y: visible;
    align-self: auto;
    background: var(--mg-surface);
    backdrop-filter: none;
    box-shadow: none;
    border-radius: 0;
    border-inline: 0;
    border-bottom: var(--mg-border-width) solid var(--mg-border);
  }

  /* The brand is a wordmark again: a top bar has room for the site name, and
     the rail rule clips it to a 3.25rem box. */
  body.mg-header-left .mg-shell-brand,
  body.mg-header-right .mg-shell-brand {
    display: inline-block;
    inline-size: auto;
    block-size: auto;
    overflow: visible;
    font-size: var(--mg-font-size-lg);
    letter-spacing: normal;
  }

  /* Lift the header above the scrim while the drawer is open. A sticky or
     fixed header (headerfix-on, bottom) makes its own stacking context, so
     the drawer inside it would otherwise sit under the scrim.

     The placement class is repeated to reach specificity (0,3,1).
     body.mg-nav-open .mg-shell-header alone is (0,2,1) and loses to
     body.mg-headerfix-on.mg-header-top .mg-shell-header. */
  body.mg-nav-open.mg-header-top .mg-shell-header,
  body.mg-nav-open.mg-header-left .mg-shell-header,
  body.mg-nav-open.mg-header-right .mg-shell-header,
  body.mg-nav-open.mg-header-bottom .mg-shell-header {
    z-index: 4;
  }

  /* With the drawer open the back-to-top button would float over the scrim. */
  body.mg-nav-open .mg-shell-totop {
    display: none;
  }

  /* flex: none and nowrap together. The toggle is the only flex item in the
     bar that can lose width, so the row squeezed it until "메뉴" broke one
     glyph per line and the header grew to two rows (2026-08-30, reported from a
     phone). */
  /* ⛔ 아이콘과 글자를 flex 로 세운다 (2026-09-01 사람 신고: 접힌 메뉴에서 정렬이
     안 맞음). `display: block` 이면 둘이 **베이스라인**으로 맞는데, `.mg-icon` 은
     아이콘 폰트에 `line-height: 1` 이고 글자 크기도 달라서 기준선이 서로 다르다 —
     아이콘만 글자보다 내려앉는다. 오류는 없고 화면으로만 드러난다.
     ⚠ 간격은 모바일 메뉴 항목과 같은 값을 쓴다(그 항목들도 flex + gap 이다). */
  .mg-shell-nav-toggle {
    display: flex;
    align-items: center;
    gap: var(--mg-space-2);
    flex: none;
    white-space: nowrap;
  }

  .mg-shell-nav {
    display: block;
    position: fixed;
    inset-block: 0;
    inset-inline-start: 0;
    width: var(--mg-shell-nav-width);
    background: var(--mg-surface);
    border-inline-end: var(--mg-border-width) solid var(--mg-border);
    padding: var(--mg-space-4);
    transform: translateX(-100%);
    transition: transform var(--mg-transition);
    z-index: 3;
    overflow-y: auto;
  }

  /* The rail sizes the nav to its icons with inline-size: auto, and that rule
     is (0,2,0) while the drawer above is (0,1,0) - the drawer would open as a
     shrink-to-fit sliver. inline-size and width are the same property, so
     restating width here would not win; the placement has to be named. */
  body.mg-header-left .mg-shell-nav,
  body.mg-header-right .mg-shell-nav {
    inline-size: var(--mg-shell-nav-width);
    align-items: stretch;
  }

  body.mg-header-right .mg-shell-nav {
    inset-inline-start: auto;
    inset-inline-end: 0;
    border-inline-end: 0;
    border-inline-start: var(--mg-border-width) solid var(--mg-border);
    transform: translateX(100%);
  }

  body.mg-nav-open .mg-shell-nav {
    transform: translateX(0);
  }

  .mg-shell-nav-item:hover > .mg-shell-subnav,
  .mg-shell-nav-item:focus-within > .mg-shell-subnav {
    position: static;
  }

  .mg-shell-subnav {
    display: block;
    border: 0;
    box-shadow: none;
    background: transparent;
    padding: 0;
    margin-inline-start: var(--mg-space-3);
  }

  /* ⛔ 세로 독의 기하학을 여기서 되돌린다. 좁은 화면에서 left/right는 사이드바가
     아니라 서랍 달린 top bar인데(위 블록), 레일 규칙에는 media 가드가 없어서
     서랍 안에 3.25rem 원형 아이콘이 그려지고 라벨은 opacity:0 툴팁으로 남는다 —
     항목이 아이콘만 남아 뭉개져 보인다. 오류는 하나도 안 난다. */
  body.mg-header-left .mg-shell-nav a,
  body.mg-header-left .mg-shell-nav > .mg-shell-nav-item > span,
  body.mg-header-right .mg-shell-nav a,
  body.mg-header-right .mg-shell-nav > .mg-shell-nav-item > span {
    display: flex;
    align-items: center;
    gap: var(--mg-space-2);
    inline-size: auto;
    block-size: auto;
    padding: var(--mg-space-2) var(--mg-space-3);
    border-color: transparent;
    border-radius: var(--mg-radius-sm);
  }

  /* ⛔⛔ 호버 상태를 이 목록에 **함께** 적는다 (2026-09-01 사람 신고: 모바일에서
     메뉴를 누르면 글자가 아이콘보다 위로 튄다).

     되돌리는 이 규칙은 `:where()`(명시도 0) 때문에 (0,2,1)인데, 레일의 툴팁을
     띄우는 규칙 `… :where(…) a:hover .mg-shell-nav__label` 은 `a:hover` 가 붙어
     (0,3,2)다 — **되돌리기가 진다.** 터치 기기는 탭에 `:hover` 가 붙으므로,
     누르는 순간 `transform: translateY(-50%)` 가 살아나 라벨이 자기 높이의 절반만큼
     떠오른다. 접힘(바)·펼침(서랍) 양쪽 모두 이 규칙이 덮으므로 두 상태에서 다 났다.
     ⚠ `:where()` → `:is()` 로 바꾸는 것만으로는 부족하다((0,3,1) vs (0,3,2)).
     상태 변형을 명시적으로 같은 목록에 넣어 명시도를 맞추고, 이 블록이 뒤에 있으므로
     순서로 이긴다. `docs/PITFALLS.md` "되돌리는 규칙은 `:where()` 를 쓰지 않는다". */
  body.mg-header-left :where(.mg-shell-nav, .mg-shell-account) .mg-shell-nav__label,
  body.mg-header-right :where(.mg-shell-nav, .mg-shell-account) .mg-shell-nav__label,
  body.mg-header-left :where(.mg-shell-nav, .mg-shell-account) a:hover .mg-shell-nav__label,
  body.mg-header-right :where(.mg-shell-nav, .mg-shell-account) a:hover .mg-shell-nav__label,
  body.mg-header-left :where(.mg-shell-nav, .mg-shell-account) a:focus-visible .mg-shell-nav__label,
  body.mg-header-right :where(.mg-shell-nav, .mg-shell-account) a:focus-visible .mg-shell-nav__label {
    position: static;
    inline-size: auto;
    max-inline-size: none;
    block-size: auto;
    padding: 0;
    background: transparent;
    color: inherit;
    box-shadow: none;
    font-weight: inherit;
    opacity: 1;
    transform: none;
  }

  /* The account links are a row of icon targets in the bar, not a labelled
     column in a rail (2026-08-30 human decision: the words are not needed here).
     Sized to match the BGM button so the right end of the bar reads as one
     row of targets. The drawer keeps its labels - only the bar loses them.

     ⛔ The label is clipped, not display:none. The icon span is aria-hidden,
     so hiding the label leaves the link with NO accessible name: a screen
     reader announces "링크" and nothing else, and nothing on screen changes. */
  body.mg-header-left .mg-shell-account,
  body.mg-header-right .mg-shell-account {
    flex-direction: row;
  }

  body.mg-header-left .mg-shell-account a,
  body.mg-header-right .mg-shell-account a {
    inline-size: var(--mg-bgm-button-size);
    block-size: var(--mg-bgm-button-size);
  }

  body.mg-header-left .mg-shell-account .mg-shell-nav__label,
  body.mg-header-right .mg-shell-account .mg-shell-nav__label {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  /* The volume panel drops from the bar; sideways would leave the viewport. */
  body.mg-header-left .mg-bgm__panel,
  body.mg-header-right .mg-bgm__panel {
    top: 100%;
    bottom: auto;
    inset-inline: auto 0;
    transform: translateY(-6px);
  }

  body.mg-header-left :where(.mg-bgm:hover, .mg-bgm:focus-within) .mg-bgm__panel,
  body.mg-header-right :where(.mg-bgm:hover, .mg-bgm:focus-within) .mg-bgm__panel {
    transform: translateY(0);
  }

  body.mg-nav-open .mg-shell-scrim {
    display: block;
    position: fixed;
    inset: 0;
    background: color-mix(in srgb, var(--mg-text) 25%, transparent);
    z-index: 2;
  }

  /* The bottom placement uses a bottom sheet on mobile, not a tab bar. A tab
     bar only works when labels are short, but MenuItem.title is free text the
     admin writes, so the core cannot bound its length — names like
     "자유게시판" were clipped in every slot. The dock keeps the brand, the
     toggle and the player; the menu moves into a sheet that rises over it. */
  body.mg-header-bottom .mg-shell-header {
    gap: var(--mg-space-3);
  }

  body.mg-header-bottom .mg-shell-nav {
    inset-block: auto;
    inset-inline: 0;
    bottom: var(--mg-shell-dock-height);
    width: auto;
    max-height: 60vh;
    overflow-y: auto;
    border: var(--mg-border-width) solid var(--mg-border);
    border-block-end: 0;
    border-radius: var(--mg-radius) var(--mg-radius) 0 0;
    /* Travelling only its own height (100%) leaves it short by the bottom
       offset, so the sheet still covers the dock: the menu shows instead of
       the dock and a stacked list reads as "the dock turned vertical". */
    transform: translateY(calc(100% + var(--mg-shell-dock-height)));
  }

  body.mg-nav-open.mg-header-bottom .mg-shell-nav {
    transform: translateY(0);
  }

  body.mg-header-bottom .mg-shell-account {
    display: none;
  }

  #content dl {
    grid-template-columns: 1fr;
    gap: 0;
  }

  #content dd {
    margin-bottom: var(--mg-space-3);
  }

  .mg-block-gallery,
  .mg-block-columns,
  .mg-block-gallery--cols-2,
  .mg-block-gallery--cols-3,
  .mg-block-gallery--cols-4,
  .mg-block-columns--1-1,
  .mg-block-columns--1-1-1,
  .mg-block-columns--1-1-1-1,
  .mg-block-columns--1-2,
  .mg-block-columns--2-1 {
    grid-template-columns: 1fr;
  }

  .mg-comment--reply {
    margin-left: var(--mg-space-4);
  }
}

/* Users who ask for reduced motion get none of the transitions above.
 *
 * ⛔ animation-delay has to be zeroed too. The entrance stagger hides an
 * element until its delay elapses (fill-mode backwards), so zeroing only the
 * duration would leave content invisible for the length of the ladder -- and
 * only for the people who asked for less motion.
 *
 * ⛔ The universal selector does NOT reach ::view-transition-*: those live in
 * a separate pseudo-element tree on the document root. They need their own
 * rule or the page transition keeps running with no error anywhere. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
  }

  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

.mg-block-accordion {
  display: flex;
  flex-direction: column;
  gap: var(--mg-space-2);
  margin: var(--mg-space-5) 0;
}

.mg-block-accordion__panel {
  border: var(--mg-border-width) solid var(--mg-border);
  border-radius: var(--mg-radius-sm);
  background: var(--mg-surface);
  scroll-margin-top: var(--mg-space-8);
}

.mg-block-accordion__summary {
  cursor: pointer;
  padding: var(--mg-space-3) var(--mg-space-4);
  font-family: var(--mg-font-heading);
}

.mg-block-accordion__body {
  padding: 0 var(--mg-space-4) var(--mg-space-4);
}

.mg-block-tabs {
  margin: var(--mg-space-5) 0;
}

.mg-block-tabs__list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--mg-space-2);
  border-bottom: var(--mg-border-width) solid var(--mg-border);
  margin-bottom: var(--mg-space-4);
}

.mg-block-tabs__list[hidden] { display: none; }

/* ⚠ 알약 모서리를 뺐다 (2026-09-01 사람 결정). 선택된 탭이 금색으로 꽉 차면서
   각진 모서리가 아래 패널의 직선과 이어진다 — ROOT 는 둥근 조각을 안 쓴다. */
.mg-block-tabs__tab {
  border: var(--mg-border-width) solid transparent;
  background: none;
  color: var(--mg-text-muted);
  cursor: pointer;
  padding: var(--mg-space-2) var(--mg-space-4);
  font-family: var(--mg-font-heading);
  transition: background var(--mg-transition-fast), color var(--mg-transition-fast),
    border-color var(--mg-transition-fast);
}

.mg-block-tabs__tab:hover {
  color: var(--mg-text);
  background: var(--mg-surface-tint);
}

/* 선택된 탭은 금색으로 꽉 찬다 (2026-09-01 사람 결정. 전에는 accent-soft 였다).
   ⚠ 금색 위 --mg-text 대비는 7.86:1 로 AAA 다 — 금색을 배경으로 쓸 때는 늘
   재고 넘어간다(이 저장소는 금색을 **글자색**으로 쓰는 것만 금지한다). */
.mg-block-tabs__tab[aria-selected="true"] {
  background: var(--mg-accent);
  border-color: var(--mg-accent);
  color: var(--mg-text);
}

.mg-block-tabs__panel { display: block; }
.mg-block-tabs__panel[hidden] { display: none; }

.mg-block-tabs__title {
  font-size: var(--mg-font-size-lg);
  margin: 0 0 var(--mg-space-3);
}

.mg-block-tabs__title[hidden] { display: none; }
/* Main widgets */
/* Slideshow controls sit ON the picture, not in a row beneath it (2026-08-31
   사람 지시). The whole block is the frame; the image is the content.
 *
 * ⛔ The control labels stay in the markup and are hidden visually, never
   replaced by a glyph. A screen reader still says 이전/다음, and the arrows are
   drawn with borders rather than an icon font -- an icon font that fails to
   load exposes its ligature text as literal letters (docs/OPEN-ISSUES.md), and
   an installed site cannot assume the CDN is reachable. */
.mg-block-slideshow { position: relative; }
.mg-block-slideshow__viewport { overflow: hidden; }
.mg-block-slideshow__slide { display: block; margin: 0; }
.mg-block-slideshow__slide[hidden] { display: none; }
.mg-block-slideshow__link { display: block; }
.mg-block-slideshow__slide img { display: block; inline-size: 100%; }

.mg-block-slideshow__nav {
  position: absolute;
  inset-block-start: 50%;
  transform: translateY(-50%);
  z-index: 1;
  display: grid;
  place-items: center;
  inline-size: var(--mg-space-7);
  block-size: var(--mg-control-h);
  padding: 0;
  border: 0;
  background: transparent;
  color: transparent;
  font-size: 0;
  cursor: pointer;
  transition: opacity var(--mg-transition-fast);
}

.mg-block-slideshow__nav:hover { opacity: 0.7; }
.mg-block-slideshow__nav[hidden] { display: none; }
.mg-block-slideshow__nav--prev { inset-inline-start: var(--mg-space-4); }
.mg-block-slideshow__nav--next { inset-inline-end: var(--mg-space-4); }

/* The chevron is two borders on a rotated box — no glyph, no font. */
.mg-block-slideshow__nav::before {
  content: "";
  inline-size: var(--mg-space-4);
  block-size: var(--mg-space-4);
  border-block-start: var(--mg-stroke-2) solid var(--mg-surface);
  border-inline-start: var(--mg-stroke-2) solid var(--mg-surface);
  /* 상자가 없으니 화살표가 스스로 읽혀야 한다. ROOT 그림자는 흐림 없는 하드
     오프셋이라 밝은 사진 위에서도 흰 획의 윤곽이 살아난다. */
  filter: drop-shadow(var(--mg-shadow-slab-soft));
}

.mg-block-slideshow__nav--prev::before { transform: translateX(2px) rotate(-45deg); }
.mg-block-slideshow__nav--next::before { transform: translateX(-2px) rotate(135deg); }

/* Which of how many, along the bottom edge of the picture. The veil is the
   system's answer to text over artwork — a white wash, never a dark scrim. */
.mg-block-slideshow__dots {
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  z-index: 1;
  display: flex;
  justify-content: center;
  gap: var(--mg-space-2);
  padding-block: var(--mg-space-4) var(--mg-space-3);
}

.mg-block-slideshow__dots[hidden] { display: none; }

.mg-block-slideshow__dot {
  inline-size: var(--mg-space-3);
  block-size: var(--mg-space-3);
  padding: 0;
  border: var(--mg-border-width) solid var(--mg-border-strong);
  border-radius: var(--mg-radius-pill);
  background: transparent;
  cursor: pointer;
  transition: background var(--mg-transition-fast);
}

.mg-block-slideshow__dot[aria-current="true"] { background: var(--mg-accent); }

.mg-block-dday p{ margin: 0 !important; padding: 0 !important;}
.mg-block-dday--today { border-color: var(--mg-accent); }
.mg-block-dday--past { color: var(--mg-text-muted); }
.mg-block-dday__label, .mg-block-dday__note { color: var(--mg-text-muted); }
.mg-block-dday__count { font-size: var(--mg-font-size-xl); font-family: var(--mg-font-heading); }
.mg-block-dday__date { margin: 0; }
/* The month links carry a #mg-cal-<id> anchor so the reader lands back on the
   calendar instead of the top of the page. A bare anchor jump puts the section's
   top edge flush against the viewport top, scrolling the header out of view, so
   keep the same breathing room the other anchor targets use. */
.mg-block-calendar { container-type: inline-size; container-name: mg-calendar; overflow-x: auto; scroll-margin-top: var(--mg-space-8);}
.mg-block-calendar__head { display: flex; align-items: center; justify-content: space-between; gap: var(--mg-space-3); margin-block-end: var(--mg-space-3); }
.mg-block-calendar__step { padding: var(--mg-space-1) var(--mg-space-2); border-radius: var(--mg-radius-sm); color: var(--mg-link); font-weight: 700; }
.mg-block-calendar__step:hover { background: var(--mg-surface-tint); }
.mg-block-calendar__heading { text-align: center; }
.mg-block-calendar__title { margin: 0; font-family: var(--mg-font-mono); font-size: var(--mg-font-size-xs); font-weight: 700; letter-spacing: var(--mg-track-widest); text-transform: uppercase; color: var(--mg-link); }
.mg-block-calendar__month { margin: 0; font-family: var(--mg-font-heading); font-size: var(--mg-font-size-lg); font-weight: 800; letter-spacing: var(--mg-track-tight); }
.mg-block-calendar__grid { inline-size: 100%; table-layout: fixed; border-collapse: collapse; border-block-start: var(--mg-stroke-1) solid var(--mg-border-strong); border-inline-start: var(--mg-stroke-1) solid var(--mg-border-strong); }
.mg-block-calendar__grid caption { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip: rect(0 0 0 0); }
.mg-block-calendar__grid th { padding: var(--mg-space-1) var(--mg-space-2); border-inline-end: var(--mg-stroke-1) solid var(--mg-border-strong); border-block-end: var(--mg-stroke-1) solid var(--mg-border-strong); background: var(--mg-surface-tint); font-family: var(--mg-font-mono); font-size: var(--mg-font-size-xs); font-weight: 700; letter-spacing: var(--mg-track-wider); color: var(--mg-text-muted); }
.mg-block-calendar__grid th:first-child { color: var(--mg-danger); }
.mg-block-calendar__grid th:last-child { color: var(--mg-link); }
/* ⛔ 칸 높이는 고정이다 (2026-09-01 사람 지시). min- 이면 일정 수에 따라
   칸마다 높이가 달라져 격자가 울퉁불퉁해진다. 기준은 "일정 1개" 높이이고,
   그 값이 원래의 min-block-size 와 같다 — 0~1개인 칸은 이미 이 높이였다.
   ⛔ 넘치는 일정은 __events 가 잘라내고 +N 토글이 대신 알려 준다.
   잘라내기만 하고 알림이 없으면 일정이 조용히 사라진다. */
.mg-block-calendar__cell { position: relative; vertical-align: top; text-align: start; block-size: 3.875rem; padding: 5px 0; border-inline-end: var(--mg-stroke-1) solid var(--mg-border-strong); border-block-end: var(--mg-stroke-1) solid var(--mg-border-strong); }
.mg-block-calendar__cell--out { background: var(--mg-surface-tint); }
.mg-block-calendar__cell--out .mg-block-calendar__day { color: var(--mg-text-muted); font-weight: 400; }
.mg-block-calendar__cell--today { background: var(--mg-accent-soft); }
.mg-block-calendar__cell--today::after { content: ""; position: absolute; inset-inline-end: 5px; top: 5px; inline-size: 7px; block-size: 7px; background: var(--mg-accent); border: var(--mg-stroke-1) solid var(--mg-border-strong); }
.mg-block-calendar__cell--today .mg-block-calendar__day { color: var(--mg-text); }
/* table-layout: fixed makes every column an equal share of the block's own
   width (18rem sidebar included), so the grid never needs the outer overflow-x
   scrollbar - only the event chip text truncates via its own ellipsis. */
.mg-block-calendar__daybar { display: flex; align-items: center; gap: var(--mg-space-1); padding-inline: 7px; }
.mg-block-calendar__day { display: inline-block; font-family: var(--mg-font-mono); font-size: var(--mg-font-size-xs); font-weight: 700; letter-spacing: var(--mg-track-wider); font-variant-numeric: tabular-nums; }
/* Capped to two chip rows regardless of how many events land on the day, so
   switching months never reflows row heights wildly - the rest live behind
   the __more toggle instead of growing the cell (and, with it, the page). */
.mg-block-calendar__events { display: flex; flex-direction: column; gap: var(--mg-space-1); margin-block-start: var(--mg-space-1); max-block-size: calc(var(--mg-font-size-xs) * 1.5); overflow: hidden; }
/* block-size is explicit, not min-block-size: a continuation tile's label is
   empty (only the first visible tile of an event carries text), and an empty
   block-level child gives the chip no content height to stand on otherwise -
   the compact dot/line mode below overrides this with its own explicit size. */
/* ⭐ 카테고리 색은 `color` 가 아니라 전용 변수를 탄다 (2026-09-02 사람 지시:
   "글씨색을 고정, 배경을 가변으로"). 글씨는 기본 잉크로 고정되고 칸을 칠하는
   것이 카테고리다. ⛔ 출처가 하나여야 한다 — 배경과 좁은 칸의 점이 같은 변수를
   읽는다. 예전처럼 `currentColor` 로 나르면 글씨색을 고정하는 순간 점이 전부
   같은 색이 되는데 화면은 멀쩡해 보인다.
   ⛔⛔ **단색으로 못 채운다.** 30% 는 두 테마가 모두 AA 를 넘는 최대치다
   (라이트 최저 7.16:1, 다크 최저 4.67:1). 일정 8색은 원래 **글씨색으로** 설계된
   램프라(DS: "muted so they sit under the gold") 라이트에서는 색이 어둡고
   다크에서는 밝다 — 단색으로 깔면 라이트는 흰 글씨도 잉크도 넷씩 미달이고,
   다크는 `--mg-text` 가 배경과 같이 밝아 1.1~1.8:1 까지 떨어진다. */
.mg-block-calendar__event { display: block; overflow: hidden; block-size: calc(var(--mg-font-size-xs) * 1.5);
  padding-inline: var(--mg-space-1); border-radius: 0; border: none;
  background: color-mix(in srgb, var(--mg-event-color) 80%, var(--mg-surface)); color: var(--mg-text); text-align: start;
  font-family: var(--mg-font-mono); font-size: var(--mg-font-size-xs); line-height: 1.3; }
.mg-block-calendar__event-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block; }
.mg-block-calendar__event--cat-1 { --mg-event-color: var(--mg-schedule-1); }
.mg-block-calendar__event--cat-2 { --mg-event-color: var(--mg-schedule-2); }
.mg-block-calendar__event--cat-3 { --mg-event-color: var(--mg-schedule-3); }
.mg-block-calendar__event--cat-4 { --mg-event-color: var(--mg-schedule-4); }
.mg-block-calendar__event--cat-5 { --mg-event-color: var(--mg-schedule-5); }
.mg-block-calendar__event--cat-6 { --mg-event-color: var(--mg-schedule-6); }
.mg-block-calendar__event--cat-7 { --mg-event-color: var(--mg-schedule-7); }
.mg-block-calendar__event--cat-8 { --mg-event-color: var(--mg-schedule-8); }
/* start/mid/end square off the edge that continues into the next day's cell so
   a multi-day event at least hints at continuity without colspan (out of scope
   for v0.3-b-11 — see docs/plans/v0.3-b-11-schedule-calendar.md §2). */
/* A multi-day event is ONE bar. The tile that opens it keeps its leading edge,
   the tile that closes it keeps its trailing edge, and everything between has
   neither — otherwise each day draws a closed box and the run looks broken. */
.mg-block-calendar__event--only { margin-inline: 7px; }
.mg-block-calendar__event--start { margin-inline-start: 7px; border-inline-end: 0; }
.mg-block-calendar__event--mid { border-inline: 0; }
.mg-block-calendar__event--end { margin-inline-end: 7px; border-inline-start: 0; }
/* "+N" overflow toggle. The panel is positioned absolute so opening it never
   changes the table's row heights (that reflow was itself pushing the page
   around under the anchor-jump from month navigation). */
/* 날짜와 같은 줄에 있으므로 위 여백을 주지 않는다 — 주면 칸이 높아진다. */
.mg-block-calendar__more { margin-block-start: 0; line-height: 1; }
.mg-block-calendar__more-toggle { display: inline-block; padding: 0 var(--mg-space-1); border-radius: var(--mg-radius-sm); background: var(--mg-surface-tint); color: var(--mg-text-muted); font-size: var(--mg-font-size-xs); font-weight: 700; cursor: pointer; list-style: none; }
.mg-block-calendar__more-toggle::-webkit-details-marker { display: none; }
.mg-block-calendar__more[open] .mg-block-calendar__more-toggle { background: var(--mg-accent); color: var(--mg-accent-text); }
.mg-block-calendar__more-panel { position: absolute; z-index: 1; inset-inline-start: var(--mg-space-1); top: 100%; margin-block-start: var(--mg-space-1); min-inline-size: 10rem; max-inline-size: 16rem; padding: var(--mg-space-2); border: var(--mg-border-width) solid var(--mg-border); border-radius: var(--mg-radius-sm); background: var(--mg-surface); box-shadow: var(--mg-shadow-lift); }
.mg-block-calendar__more-item { display: flex; align-items: center; gap: var(--mg-space-1); margin: 0; font-size: var(--mg-font-size-xs); }
.mg-block-calendar__more-item + .mg-block-calendar__more-item { margin-block-start: var(--mg-space-1); }
/* Below this the column is too narrow for readable event text (the 18rem
   sidebar included) - collapse each chip to a color dot, or a thin bar for
   the days a multi-day event continues through. The full title still reaches
   the reader through the chip's title="" tooltip. */
@container mg-calendar (max-width: 26rem) {
  .mg-block-calendar__event-label { display: none; }
    /* ⛔ 점은 카테고리 **단색**이다 — 글자가 없으니 대비 제약이 없고, 이 크기에서 옅은 틴트는 서로 구분이 안 된다. `currentColor` 가 아니라 위와 같은 변수를 읽는다. */
  .mg-block-calendar__event { inline-size: var(--mg-space-2); block-size: var(--mg-space-2); margin-inline: auto; padding: 0;
    background: var(--mg-event-color); border-radius: var(--mg-radius-pill); }
  .mg-block-calendar__event--start, .mg-block-calendar__event--mid, .mg-block-calendar__event--end {
    inline-size: 100%; block-size: var(--mg-space-1); margin-inline: 0; border-radius: 0;
  }
}
.mg-block-calendar__legend { display: flex; flex-wrap: wrap; gap: var(--mg-space-3); margin-block-start: var(--mg-space-3); padding: 0; list-style: none; font-size: var(--mg-font-size-sm); color: var(--mg-text-muted); }
.mg-block-calendar__legend-item { display: flex; align-items: center; gap: var(--mg-space-1); }
/* ⛔ 범례와 "+N" 패널의 견본은 칸의 칩과 **같은 `--cat-N` 클래스**를 재사용한다.
   카테고리 색이 `color` 를 타던 시절에는 `currentColor` 로 됐지만, 이제는 같은
   변수를 읽어야 한다 — 안 그러면 견본이 전부 부모의 글자색 하나로 칠해져
   **범례가 통째로 회색이 되는데 오류가 안 난다** (2026-09-02).
   ⚠ 견본은 글자가 없으므로 틴트가 아니라 카테고리 단색이다. */
.mg-block-calendar__swatch { flex: none; inline-size: 9px; block-size: 9px; border-radius: 0; border: var(--mg-stroke-1) solid var(--mg-border-strong); background: var(--mg-event-color, var(--mg-text-muted)); }
/* Tooltip — rendered on <body> by static/core/js/tooltip.js so it escapes the
   scroll container and clip-path frame that would otherwise cut it off. The
   z-index clears the dock (20) and the lightbox scrim's neighbours. */
.mg-tooltip {
  position: fixed;
  z-index: 60;
  max-inline-size: 20rem;
  padding: var(--mg-space-2) var(--mg-space-3);
  border-radius: var(--mg-radius-sm);
  background: var(--mg-text);
  color: var(--mg-bg);
  font-family: var(--mg-font-body);
  font-size: var(--mg-font-size-sm);
  font-weight: 600;
  line-height: 1.4;
  box-shadow: var(--mg-shadow-float);
  pointer-events: none;
}

/* ROOT calendar frame. The panel is a 2px ink edge wrapped around a
   backdrop-blurred plate with notched corners, so the page texture stays
   faintly visible through it — that is what makes it read as paper rather
   than as a filled box. The outer and inner notches differ by 2px so the ink
   edge keeps an even width all the way around the cut. */
.mg-block-calendar__frame {
  padding: var(--mg-stroke-1);
  background: var(--mg-border-strong);
  clip-path: var(--mg-clip-notch);
  box-shadow: var(--mg-shadow-slab-soft);
}

.mg-block-calendar__body {
  display: grid;
  grid-template-columns: minmax(0, 1.55fr) minmax(0, 0.75fr);
  background: var(--mg-surface-plate-strong);
  backdrop-filter: var(--mg-frame-blur);
  clip-path: var(--mg-clip-notch-inner);
}

.mg-block-calendar__main {
  min-inline-size: 0;
  padding: var(--mg-space-5);
}

/* The upcoming rail. It reads from TODAY, not from the month being browsed,
   so paging back to March does not hide what is next (apps/blocks/types/
   widgets.py builds it from its own forward window). */
.mg-block-calendar__upcoming {
  min-inline-size: 0;
  display: flex;
  flex-direction: column;
  border-inline-start: var(--mg-stroke-1) solid var(--mg-border-strong);
  background: var(--mg-surface);
}

.mg-block-calendar__upcoming-head {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--mg-space-3);
  padding: var(--mg-space-2) var(--mg-space-4);
  border-block-end: var(--mg-stroke-1) solid var(--mg-border-strong);
  background: var(--mg-surface-tint);
}

.mg-block-calendar__upcoming-label {
  font-family: var(--mg-font-mono);
  font-size: var(--mg-font-size-xs);
  font-weight: 700;
  letter-spacing: var(--mg-track-widest);
  color: var(--mg-text);
}

.mg-block-calendar__upcoming-today {
  font-family: var(--mg-font-mono);
  font-size: var(--mg-font-size-xs);
  letter-spacing: var(--mg-track-wider);
  color: var(--mg-text-faint);
  font-variant-numeric: tabular-nums;
}

.mg-block-calendar__upcoming-list {
  flex: 1;
  margin: 0;
  padding: var(--mg-space-2) var(--mg-space-4) var(--mg-space-4);
  list-style: none;
}

.mg-block-calendar__upcoming-item {
  display: flex;
  align-items: baseline;
  gap: var(--mg-space-3);
  padding-block: var(--mg-space-2);
  border-block-end: var(--mg-stroke-hair) solid var(--mg-border);
}

.mg-block-calendar__upcoming-date {
  flex: none;
  inline-size: 2.75rem;
  font-family: var(--mg-font-mono);
  font-size: var(--mg-font-size-xs);
  letter-spacing: var(--mg-track-wider);
  color: var(--mg-text-muted);
  font-variant-numeric: tabular-nums;
}

/* Today is the one place the rail spends gold, and it spends it on the date
   rather than the title — the title still has to read as body text. */
.mg-block-calendar__upcoming-item--today .mg-block-calendar__upcoming-date {
  color: var(--mg-link);
  font-weight: 700;
}

.mg-block-calendar__upcoming-text {
  min-inline-size: 0;
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.mg-block-calendar__upcoming-title {
  font-size: var(--mg-font-size-sm);
  font-weight: 600;
  color: var(--mg-text);
}

.mg-block-calendar__upcoming-meta {
  font-family: var(--mg-font-mono);
  font-size: var(--mg-font-size-xs);
  letter-spacing: var(--mg-track-wider);
  color: var(--mg-text-faint);
}

/* One column below the width where the rail stops being readable. The rail
   goes UNDER the grid rather than away: it is the part a reader actually
   acts on. Same container as the chip-collapse query above. */
@container mg-calendar (max-width: 34rem) {
  .mg-block-calendar__body { grid-template-columns: minmax(0, 1fr); }
  .mg-block-calendar__upcoming {
    border-inline-start: 0;
    border-block-start: var(--mg-stroke-1) solid var(--mg-border-strong);
  }
}

.mg-block-recent-posts { margin-block: var(--mg-space-5); }
.mg-block-recent-posts__title { margin-block-end: var(--mg-space-3); }
/* Both halves of this block are THREE rows and end at the same height, so the
   rail does not jump when a reader signs in or out:
     signed out   아이디 / 비밀번호 / [로그인 회원가입]
     signed in    닉네임 / 내 캐릭터 / [마이페이지 로그아웃]
   The heights are summed from the tokens the rows are built from and the
   difference is parked on the character row -- the one row with slack to give,
   and real content rather than padding.
   ⚠ `line-height: normal` on <input> is a font metric, not a number CSS can
   sum, so the control's leading and block padding are pinned below and the sum
   reads the SAME variables the control does. They cannot drift apart.
   ⛔ Add or remove a row in blocks/login.html and this goes stale in silence:
   the two halves just stop matching, with no error anywhere. */
.mg-block-login {
  --mg-login-leading: 1.2;
  --mg-login-control-pad: 6px;
  --mg-login-btn-leading: 1.6; /* tracks .mg-btn */
  --mg-login-line: calc(var(--mg-font-size-base) * var(--mg-line-height));
  --mg-login-control: calc(
    var(--mg-font-size-base) * var(--mg-login-leading)
    + var(--mg-login-control-pad) * 2 + var(--mg-border-width) * 2
  );
  --mg-login-btn: calc(
    var(--mg-font-size-sm) * var(--mg-login-btn-leading)
    + var(--mg-space-2) * 2 + var(--mg-border-width) * 2
  );
  /* The label sits beside the input, so the row is the taller of the two. */
  --mg-login-field: max(var(--mg-login-line), var(--mg-login-control));
  --mg-login-form: calc(
    var(--mg-login-field) * 2 + var(--mg-space-3) * 2 + var(--mg-login-btn)
  );
  --mg-login-character: calc(
    var(--mg-login-form) - var(--mg-login-line) - var(--mg-login-btn)
    - var(--mg-space-3) * 2
  );
  display: grid;
  gap: var(--mg-space-3);
}

.mg-block-login__title, .mg-block-login__me { margin: 0; }

/* The label sits BESIDE its input, not above it: stacked labels made the form
   six rows deep against the signed-in state's two.
   ⛔ The label column is one shared subgrid track, not `auto` per field.
   "아이디" and "비밀번호" are different widths, so per-field auto columns would
   start the two inputs at different x -- it reads as a broken form, not as an
   error. Where subgrid is missing the declaration drops and the labels stack
   again, which is the old layout: degraded, not broken. */
.mg-block-login__form {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--mg-space-3) var(--mg-space-2);
}

.mg-block-login__field {
  display: grid;
  grid-column: 1 / -1;
  grid-template-columns: subgrid;
  align-items: center;
}

.mg-block-login__field :where(input) {
  padding-block: var(--mg-login-control-pad);
  line-height: var(--mg-login-leading);
}

/* The reader's own default character, with a count when they have more. This
   is the row that carries the height difference between the two states, hence
   min-block-size rather than letting the chip size itself. */
.mg-block-login__character {
  display: flex;
  min-block-size: var(--mg-login-character);
  align-items: center;
  gap: var(--mg-space-2);
  padding-inline: var(--mg-space-2);
  border-radius: var(--mg-radius-sm);
  background: var(--mg-surface-tint);
  color: var(--mg-text);
  text-decoration: none;
}

.mg-block-login__character:hover { text-decoration: none; }
.mg-block-login__character--none { color: var(--mg-link); font-size: var(--mg-font-size-sm); }

/* Sized off the row, not off --mg-avatar-size-sm (52px): that token is for a
   card, and it is taller than this row. */
.mg-block-login__avatar {
  flex: none;
  inline-size: calc(var(--mg-login-character) - var(--mg-space-2) * 2);
  block-size: calc(var(--mg-login-character) - var(--mg-space-2) * 2);
  border: var(--mg-border-width) solid var(--mg-border);
  border-radius: var(--mg-radius-pill);
  background: var(--mg-surface-tint);
  object-fit: cover;
}

/* No avatar yet. The circle still has to hold its place or the row collapses
   and the signed-in half stops matching the signed-out half's height. */
.mg-block-login__avatar--empty {
  border-style: dashed;
  background: var(--mg-surface-sunken);
}

.mg-block-login__character-name {
  min-inline-size: 0;
  overflow: hidden;
  font-size: var(--mg-font-size-sm);
  font-weight: 700;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.mg-block-login__character-more {
  flex: none;
  margin-inline-start: auto;
  font-family: var(--mg-font-mono);
  font-size: var(--mg-font-size-xs);
  color: var(--mg-text-faint);
}

/* Both states end on this row. In the signed-out branch it is INSIDE the
   <form> (blocks/login.html) because the submit button has to be -- 회원가입
   moved in with it rather than the button moving out. */
.mg-block-login__links {
  display: flex;
  grid-column: 1 / -1;
  gap: var(--mg-space-2);
  align-items: center;
}

.mg-intro-page {
  display: grid;
  min-block-size: 100vh;
  place-items: center;
  padding: var(--mg-space-5);
}

.mg-intro {
  inline-size: min(100%, var(--mg-content-width));
  padding: var(--mg-space-6);
}

.mg-intro__title { margin-block-start: 0; }
.mg-intro__body { margin-block: var(--mg-space-5); }
.mg-intro__enter { display: flex; justify-content: center; }

.mg-popup-layer {
  position: fixed;
  inset: 0;
  z-index: 3;
  pointer-events: none;
}

.mg-popup {
  position: absolute;
  pointer-events: auto;
  inline-size: min(var(--mg-popup-w), calc(100vw - var(--mg-space-6)));
  block-size: var(--mg-popup-h, auto);
  max-block-size: calc(100vh - var(--mg-space-6));
  overflow: auto;
  padding: var(--mg-space-5);
  border: var(--mg-border-width) solid var(--mg-border);
  border-radius: var(--mg-radius);
  background: var(--mg-surface);
  box-shadow: var(--mg-shadow);
}

.mg-popup--center {
  inset-block-start: 50%;
  inset-inline-start: 50%;
  translate: calc(-50% + var(--mg-popup-x)) calc(-50% + var(--mg-popup-y));
}

.mg-popup--top-left {
  inset-block-start: calc(var(--mg-space-5) + var(--mg-popup-y));
  inset-inline-start: calc(var(--mg-space-5) + var(--mg-popup-x));
}

.mg-popup--top-right {
  inset-block-start: calc(var(--mg-space-5) + var(--mg-popup-y));
  inset-inline-end: calc(var(--mg-space-5) + var(--mg-popup-x));
}

.mg-popup--bottom-left {
  inset-block-end: calc(var(--mg-space-5) + var(--mg-popup-y));
  inset-inline-start: calc(var(--mg-space-5) + var(--mg-popup-x));
}

.mg-popup--bottom-right {
  inset-block-end: calc(var(--mg-space-5) + var(--mg-popup-y));
  inset-inline-end: calc(var(--mg-space-5) + var(--mg-popup-x));
}

.mg-popup__actions { display: flex; flex-wrap: wrap; gap: var(--mg-space-2); margin-block-start: var(--mg-space-4); }
.mg-popup__body > :first-child { margin-block-start: 0; }

/* The gallery block wraps every thumbnail in <a href="원본 파일">, which is fine on a
   page but wrong inside a popup: clicking an announcement image would navigate the
   visitor away to a raw image file. Neutralise the link here only — the block markup
   is shared with pages and the sidebar and is not this stage's to change. Giving the
   gallery block a real lightbox (as character_gallery has) is the proper fix and is
   its own step; see the plan's "닫지 않는 것". */
.mg-popup .mg-block-gallery__item a { pointer-events: none; cursor: default; }
.mg-popup__form { margin: 0; }
.mg-popup__close { font: inherit; }

body:has(#content .mg-manage) .mg-popup-layer { display: none; }

@media (max-width: 48rem) {
  .mg-popup-layer {
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    gap: var(--mg-space-3);
    padding: var(--mg-space-4);
  }

  .mg-popup { position: static; translate: none; inline-size: 100%; }
}

/* ------------------------------------------------------------------ *
 * Skill viewer (docs/SKIN_API.md "스킬 뷰어 계약")
 *
 * ⛔ Nothing here hides a panel or a row at rest. The server ships the
 * selected panel visible and the rest with [hidden]; core JS moves that
 * attribute around. A `display: none` default in this file would turn a
 * failed script into an empty box with nothing in the console.
 * ------------------------------------------------------------------ */
.mg-block-skill-viewer {
  margin: var(--mg-space-5) 0;
  display: flex;
  flex-direction: column;
  gap: var(--mg-space-4);
}

.mg-block-skill-viewer__notice {
  margin: 0;
  padding: var(--mg-space-2) var(--mg-space-3);
  border: var(--mg-stroke-hair) solid var(--mg-border);
  background: var(--mg-surface-tint);
  color: var(--mg-text-muted);
  font-size: var(--mg-font-size-sm);
}

.mg-block-skill-viewer__head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--mg-space-4);
  padding-block-end: var(--mg-space-2);
  border-block-end: var(--mg-stroke-2) solid var(--mg-border-strong);
}

.mg-block-skill-viewer__title { margin: 0; }

.mg-block-skill-viewer__filters { display: flex; flex-wrap: wrap; gap: var(--mg-space-2); }

.mg-block-skill-viewer__filter {
  border: var(--mg-stroke-hair) solid var(--mg-border);
  background: var(--mg-surface);
  color: var(--mg-text);
  padding: var(--mg-space-2) var(--mg-space-3);
  font-family: var(--mg-font-mono);
  font-size: var(--mg-font-size-sm);
  letter-spacing: var(--mg-track-wide);
  cursor: pointer;
  transition: background var(--mg-transition-fast), color var(--mg-transition-fast);
}

/* ⛔ The pressed look hangs off aria-pressed, which the JS already has to keep
   correct for screen readers. A separate `.is-active` class would be a second
   source of truth and the two would drift. */
.mg-block-skill-viewer__filter[aria-pressed="true"] {
  background: var(--mg-text-strong);
  border-color: var(--mg-text-strong);
  color: var(--mg-surface);
}

/* ⛔ align-items 는 기본값 stretch 로 둔다 (2026-09-01 사람 지시: 두 칸의 높이가
   늘 같아야 한다). start 로 두면 목록은 자기 내용 높이, 카드는 자기 내용 높이라
   둘이 제각각이 된다.
 *
 * 행 높이를 정하는 것은 **오른쪽 카드**다. 목록은 min-block-size: 0 이라
 * 자기 내용으로 행을 밀지 않고, 남는 만큼 안에서 스크롤한다. 그래서 카드가
 * 짧은 스킬을 고르면 목록도 함께 짧아진다 — 그것이 "늘 같은 높이"의 뜻이다. */
.mg-block-skill-viewer__body {
  display: grid;
  grid-template-columns: minmax(0, 22rem) minmax(0, 1fr);
  gap: var(--mg-space-5);
}

.mg-block-skill-viewer__list {
  display: flex;
  flex-direction: column;
  /* ⛔⛔ 행 높이는 **카드가** 정하고 목록은 그 안에서 스크롤한다
     (docs/SKIN_API.md "스킬 뷰어 계약").
     ⚠ `min-block-size: 0` 만으로는 안 된다 — 그것은 그리드 아이템의 **자동 최소
     크기**(줄어들 수 있는 한계)만 없앨 뿐이고, 행 트랙은 여전히 `auto` 라
     **두 칸 중 큰 쪽의 max-content 에 맞춰 자란다.** 스킬이 몇 개만 넘어도
     목록이 카드보다 길어져 계약이 조용히 깨져 있었다 (2026-09-02 사람 신고).
     ⭐ 두 줄이 짝이다: `block-size: 0` 으로 **행 높이 계산에서 빠지고**(그래서
     행은 카드만 보고 정해진다), `min-block-size: 100%` 로 **정해진 행 높이만큼
     다시 늘어난다**. 퍼센트는 그리드 영역 기준이라 트랙이 정해진 뒤 풀린다.
     ⛔ 한 칸으로 접히는 좁은 화면에서는 둘 다 되돌린다 — 맞출 상대가 없는데
     `block-size: 0` 이 남으면 목록이 통째로 사라진다(아래 미디어 쿼리). */
  block-size: 0;
  min-block-size: 100%;
  overflow-y: auto;
  border-inline-end: var(--mg-stroke-hair) solid var(--mg-border);
  padding-inline-end: var(--mg-space-3);
}

.mg-block-skill-viewer__row {
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: var(--mg-space-3);
  text-align: start;
  border: 0;
  border-block-end: var(--mg-stroke-hair) solid var(--mg-border);
  background: transparent;
  padding: var(--mg-space-2) var(--mg-space-3);
  cursor: pointer;
  color: var(--mg-text);
}

.mg-block-skill-viewer__row[aria-current="true"] { background: var(--mg-accent-soft); }

/* The selected row carries the gold bar the design uses as its marker. */
.mg-block-skill-viewer__row[aria-current="true"]::before {
  content: "";
  position: absolute;
  inset-inline-start: 0;
  inset-block: var(--mg-space-1);
  inline-size: var(--mg-stroke-2);
  background: var(--mg-bar-gold);
}

.mg-block-skill-viewer__tag {
  flex: none;
  inline-size: 2.75rem;
  padding-block: var(--mg-space-1);
  text-align: center;
  border: var(--mg-stroke-hair) solid currentColor;
  font-family: var(--mg-font-mono);
  font-size: var(--mg-font-size-xs);
  letter-spacing: var(--mg-track-wide);
}

.mg-block-skill-viewer__rowtext { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.mg-block-skill-viewer__name { font-weight: 700; }
.mg-block-skill-viewer__brief {
  font-size: var(--mg-font-size-sm);
  color: var(--mg-text-faint);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Role hue. ⛔ The template only emits the slug; the colour lives here so a
   skin can retheme it, and so Python never decides an appearance. */
[data-mg-skill-role-of="tank"] { color: var(--mg-skill-role-tank); }
[data-mg-skill-role-of="deal"] { color: var(--mg-skill-role-deal); }
[data-mg-skill-role-of="heal"] { color: var(--mg-skill-role-heal); }
[data-mg-skill-role-of="common"] { color: var(--mg-skill-role-common); }

.mg-block-skill-viewer__panel {
  display: flex;
  flex-direction: column;
  gap: var(--mg-space-2);
  border: var(--mg-stroke-1) solid var(--mg-border-strong);
  background: var(--mg-surface);
  box-shadow: var(--mg-shadow-slab-soft);
  padding: var(--mg-space-4);
}

/* ⛔⛔ `display` 를 준 순간 HTML 의 hidden 속성이 진다 — UA 스타일시트의
   `[hidden] { display: none }` 은 명시도 (0,0,1) 이라 클래스 규칙에 밀린다.
   서버가 hidden 을 찍고 JS 가 그것을 옮기는데도 **전부 펼쳐진 채로 보였다**
   (2026-09-01 사람 신고). 행 필터가 안 먹던 것도 같은 원인이다.
   ⚠ 이 저장소의 "쉬는 상태에서 숨기지 않는다" 규칙과 어긋나지 않는다 —
   숨기는 조건이 CSS 가 아니라 **서버와 JS 가 붙이는 속성**이기 때문이다. */
.mg-block-skill-viewer__panel[hidden],
.mg-block-skill-viewer__row[hidden] {
  display: none;
}

.mg-block-skill-viewer__panelhead {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--mg-space-4);
}

/* ⛔ 이 패널 안은 p·dl 을 쓰지 않는다. `#content p` 와 `#content dl` 이 id
   명시도(1,0,1)로 블록의 클래스 규칙(0,1,0)을 이겨서, 라벨마다 16px 아래 여백이
   붙고 메타 줄이 2칸 그리드 회색 상자가 됐다 (2026-09-01 사람 신고).
   ⚠ 이 저장소에서 명시도로 진 것이 이번이 다섯 번째다 — 새 규칙을 넣기 전에
   `#content` 로 시작하는 일반 규칙과 대조한다. */
.mg-block-skill-viewer__role {
  display: block;
  margin-block-end: var(--mg-space-1);
  font-family: var(--mg-font-mono);
  font-size: var(--mg-font-size-sm);
  letter-spacing: var(--mg-track-widest);
  font-weight: 700;
}

.mg-block-skill-viewer__panelname { margin: 0; font-size: var(--mg-font-size-xl); }

.mg-block-skill-viewer__ap { text-align: end; display: flex; flex-direction: column; flex: none; }
.mg-block-skill-viewer__aplabel {
  font-family: var(--mg-font-mono);
  font-size: var(--mg-font-size-sm);
  letter-spacing: var(--mg-track-widest);
  color: var(--mg-text-faint);
}
.mg-block-skill-viewer__apvalue {
  font-family: var(--mg-font-mono);
  font-size: var(--mg-font-size-xl);
  font-weight: 700;
  color: var(--mg-link);
}

.mg-block-skill-viewer__label {
  display: block;
  margin-block-end: var(--mg-space-1);
  font-family: var(--mg-font-mono);
  font-size: var(--mg-font-size-sm);
  letter-spacing: var(--mg-track-widest);
  color: var(--mg-text-faint);
}

/* 7x7, sized from the cell so the board never disagrees with the grid. */
/* 오른쪽 칸. min-inline-size 가 없으면 긴 효과 문장이 그리드 칸을 밀어내
   목록 쪽이 찌그러진다 — 그리드 아이템의 기본 최소 폭이 auto 라서다. */
.mg-block-skill-viewer__detail { min-inline-size: 0; }

.mg-block-skill-viewer__range { display: flex; flex-direction: column; }

/* 디자인이 효과 앞에 굵은 구분선을 둔다 — 위의 메타 줄과 성격이 달라서다. */
.mg-block-skill-viewer__effect {
  border-block-start: var(--mg-stroke-1) solid var(--mg-border);
  padding-block-start: var(--mg-space-2);
}

.mg-block-skill-viewer__grid {
  display: grid;
  grid-template-columns: repeat(7, var(--mg-skill-cell));
  grid-template-rows: repeat(7, var(--mg-skill-cell));
  inline-size: max-content;
  border: var(--mg-stroke-1) solid var(--mg-border-strong);
  background: var(--mg-surface);
}

.mg-block-skill-viewer__cell {
  border: var(--mg-stroke-hair) solid var(--mg-border);
  background: var(--mg-surface);
}

.mg-block-skill-viewer__cell--in { background: var(--mg-bar-gold); }
/* The caster tile wins over the range fill: it is where the character stands,
   and the range is drawn around it. Declared after --in for that reason. */
.mg-block-skill-viewer__cell--caster { background: var(--mg-text-strong); }

/* 거리·대상·AP 는 한 줄이다. 좁아지면 줄바꿈은 허용하되, 라벨의 넓은 자간
   때문에 쉽게 두 줄이 되므로 가로 간격을 좁게 잡는다. */
.mg-block-skill-viewer__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--mg-space-1) var(--mg-space-4);
  padding-block: var(--mg-space-2);
  border-block: var(--mg-stroke-hair) solid var(--mg-border);
}
.mg-block-skill-viewer__metaitem { display: inline-flex; align-items: baseline; gap: var(--mg-space-2); }
.mg-block-skill-viewer__metalabel {
  font-family: var(--mg-font-mono);
  font-size: var(--mg-font-size-sm);
  letter-spacing: var(--mg-track-widest);
  color: var(--mg-text-faint);
  white-space: nowrap;
}
.mg-block-skill-viewer__metavalue { color: var(--mg-text-strong); white-space: nowrap; }

.mg-block-skill-viewer__effecttext { display: block; white-space: pre-line; color: var(--mg-text); }

/* One column below the two-pane breakpoint. The list stops scrolling on its
   own so the page scroll carries it -- a nested scroller inside a page that
   also scrolls is the thing touch users cannot get out of. */
@media (max-width: 48rem) {
  .mg-block-skill-viewer__body { grid-template-columns: minmax(0, 1fr); }
  /* ⛔ 좁은 화면에서는 카드가 목록 **위**로 온다 (2026-09-01 사람 지시).
     한 칸으로 접히면 DOM 순서대로 목록이 먼저 오는데, 스킬을 고르면 결과가
     화면 밖 아래에 나타나 무엇이 바뀌었는지 안 보인다.
     ⛔ 마크업을 재배치하지 않고 order 로만 바꾼다 — DOM 순서는 목록이 먼저인
     것이 읽기·탭 순서로 맞다 (목차 레일과 같은 판단). */
  .mg-block-skill-viewer__detail { order: -1; }

  /* 한 칸으로 접히면 두 칸이 아니므로 높이를 맞출 상대가 없다. 목록이 자기
     높이를 갖고 페이지 스크롤을 탄다 — 안에 또 스크롤 상자를 두면 터치에서
     빠져나오기 어렵다. */
  .mg-block-skill-viewer__list {
    /* ⛔ `block-size` 를 함께 되돌린다. 위 규칙의 `0` 이 남으면 맞출 상대가 없는
       한 칸 배치에서 행 높이가 0 이 되고 목록이 통째로 사라진다. */
    block-size: auto;
    min-block-size: auto;
    overflow-y: visible;
    border-inline-end: 0;
    padding-inline-end: 0;
  }
}
