/* 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(255 255 255 / var(--mg-bg-texture-lighten)) 0 100%),
    url("bg-paper-texture.0394904f85b7.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);
  /* ⚠ 본문 얼굴의 기본 굵기다 (2026-08-31 사람 지시). Isamanru는 300/400/700
     세 단뿐이라 300은 실제 Light 컷이고, 굵기를 따로 적은 규칙은 그대로
     자기 값을 쓴다. */
  font-weight: 300;
  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: 700; 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; }
}

/* ------------------------------------------------------------------ *
 * 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-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;
}

/* 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;
}

.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: border-color var(--mg-transition-fast), background var(--mg-transition-fast);
}

.mg-bgm__toggle:hover,
.mg-bgm__toggle:focus-visible {
  border-color: var(--mg-border-strong);
  background: var(--mg-surface);
}

.mg-bgm[data-bgm-state="playing"] .mg-bgm__toggle {
  border-color: var(--mg-accent);
}

.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);
}

/* 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: 600;
  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; }

.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);
}

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

.mg-table th {
  color: var(--mg-text-muted);
  font-weight: 700;
  font-size: var(--mg-font-size-xs);
  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 {
  font-size: 16px;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  margin: 0;
}

/* 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. */
:where(#content) :where(form p) {
  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"]) {
  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;
  z-index: 20;
  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;
}

.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);
}

.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);
}

/* 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);
}

/* 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; }

/* 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-4);
}

.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-2 { grid-template-columns: 1fr 2fr; }
.mg-block-columns--2-1 { grid-template-columns: 2fr 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);
}

.mg-block-timeline__label {
  display: block;
  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-body);
  font-weight: 300;
  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;
  z-index: 50;
  display: grid;
  place-items: center;
  gap: var(--mg-space-4);
  padding: var(--mg-space-5);
  background: var(--mg-bg);
}

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

.mg-lightbox__image {
  max-width: min(100%, var(--mg-lightbox-width));
  max-height: 80vh;
  object-fit: contain;
}

.mg-lightbox__prev,
.mg-lightbox__next,
.mg-lightbox__close {
  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 { position: absolute; 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--fill { display: flex; inline-size: 100%; }

.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 를 덮는다. */
.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); }

.mg-block-button:hover,
.mg-block-button:focus-visible {
  transform: translateY(-3px);
  box-shadow: var(--mg-shadow-hover);
}

/* ⛔ 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-transition-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-transition-fast) var(--mg-ease-out) 1;
}

/* 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. */
.mg-frame:has(.mg-block-button) {
  display: inline-block;
  transition: transform var(--mg-transition-fast), box-shadow var(--mg-transition-fast);
}

.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;
  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-xs) / 1.4 var(--mg-font-mono);
  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. Nothing tracks scroll position
   yet (that needs an observer, deferred), so it answers to hover and keyboard
   focus; a future scroll spy only has to set the same bar. */
.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 { 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 { 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-mono);
  letter-spacing: var(--mg-track-widest);
  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-mono);
  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; */
}

/* 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, .mg-character-card__intro { display: block; }
.mg-character-card__badges { display: flex; flex-wrap: wrap; gap: var(--mg-space-1); margin-block: var(--mg-space-1); }
.mg-character-grid--list { flex-direction: column; gap: 0; }
.mg-character-grid--list .mg-character-card { flex: 1 1 auto; border-radius: 0; }
.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__avatar { inline-size: var(--mg-avatar-size); block-size: var(--mg-avatar-size); }
.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 { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: var(--mg-space-2) var(--mg-space-4); }
.mg-profile-info__term, .mg-profile-info__desc { margin: 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__head { cursor: pointer; }
.mg-profile-section__title { display: inline; font-size: var(--mg-font-size-lg); }
.mg-profile-section__more { margin-inline-start: var(--mg-space-2); color: var(--mg-text-muted); }
.mg-profile-section__body { max-width: 68ch; line-height: 1.8; margin-block-start: var(--mg-space-3); }
/* 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); }
.mg-profile-layout { display: flex; flex-direction: column; gap: var(--mg-space-5); }
.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); }
@media (min-width: 75rem) { .mg-profile-toc { position: sticky; top: var(--mg-space-5); } }
body.mg-headerfix-on.mg-header-top .mg-profile-toc { position: static; }
@media (max-width: 64rem) { .mg-profile--aside .mg-profile__body { grid-template-columns: 1fr; } .mg-profile-toc__list { display: flex; gap: var(--mg-space-2); overflow-x: auto; padding: 0; list-style: none; } }
@media (max-width: 64rem) { .mg-profile-layout .mg-block-columns { grid-template-columns: 1fr; } }
@media (max-width: 48rem) { .mg-profile__avatar { inline-size: var(--mg-avatar-size-sm); block-size: var(--mg-avatar-size-sm); } .mg-profile-info { grid-template-columns: 1fr; } }

/* 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,
.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%;
  }

  /* 하위 메뉴도 같은 들여쓰기를 따른다. */
  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;
    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);
  }

  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;
  }

  /* ⛔ 재생 버튼은 접힘일 때 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). */
  .mg-shell-nav-toggle {
    display: block;
    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);
  }

  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: 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-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. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !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; }

.mg-block-tabs__tab {
  border: var(--mg-border-width) solid transparent;
  border-radius: var(--mg-radius-pill);
  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);
}

.mg-block-tabs__tab[aria-selected="true"] {
  background: var(--mg-accent-soft);
  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); }
.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); }
.mg-block-calendar__cell { position: relative; vertical-align: top; text-align: start; min-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__day { display: inline-block; margin-inline: 7px; 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(2 * var(--mg-font-size-xs) * 1.5 + var(--mg-space-1)); 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. */
.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: var(--mg-stroke-1) solid var(--mg-border-strong); background: color-mix(in srgb, currentColor 38%, 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.5; }
.mg-block-calendar__event-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block; }
.mg-block-calendar__event--cat-1 { color: var(--mg-schedule-1); }
.mg-block-calendar__event--cat-2 { color: var(--mg-schedule-2); }
.mg-block-calendar__event--cat-3 { color: var(--mg-schedule-3); }
.mg-block-calendar__event--cat-4 { color: var(--mg-schedule-4); }
.mg-block-calendar__event--cat-5 { color: var(--mg-schedule-5); }
.mg-block-calendar__event--cat-6 { color: var(--mg-schedule-6); }
.mg-block-calendar__event--cat-7 { color: var(--mg-schedule-7); }
.mg-block-calendar__event--cat-8 { 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: var(--mg-space-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; }
  .mg-block-calendar__event { inline-size: var(--mg-space-2); block-size: var(--mg-space-2); margin-inline: auto; padding: 0; background: currentColor; 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); }
.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: currentColor; }
/* 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);
  border: var(--mg-border-width) solid var(--mg-border);
  border-radius: var(--mg-radius);
  background: var(--mg-surface);
}

.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%; }
}
