/* Core-owned CSS custom property defaults (docs/DECISIONS.md D9-1).
 *
 * The shell loads this file BEFORE the active skin's stylesheet, so a skin that
 * declares no --mg-* values at all still renders a readable site and a working
 * /manage/ area. Skins override by redefining the same names in their own
 * :root block: same specificity, later in the cascade, so the skin wins.
 *
 * Cascade order the shell builds (templates/core/base.html):
 *   1. this file                    core defaults
 *   2. skins/{active_skin}/style.css   site skin
 *   3. #content <link>              board skin (docs/SPEC-v0.3-a.md 2)
 *   4. <style id="mango-theme">     SiteSetting.theme_values
 *
 * RULES FOR THIS FILE
 * - Declarations only. No layout, no component rules, no selectors other than
 *   :root. A skin must never have to undo something the core painted here.
 * - Every name is prefixed --mg-. Unprefixed names would collide with CSS that
 *   admins paste into `html` blocks and with skin-private variables. The ROOT
 *   design system names its own tokens without a prefix (--gold-400, --sp-6,
 *   --type-h1); every one of them is renamed on the way in for that reason.
 * - The TEXT webfont import lives here, next to the font-family defaults it
 *   feeds. Splitting them would mean a token-less skin inherits a font stack
 *   whose files nobody loaded. Every stack still ends in system-ui, so a
 *   blocked or unreachable CDN degrades instead of breaking.
 * - ⛔ The ICON font is the exception and is loaded by <link> in the shell
 *   documents instead. It is not a fallback chain — a late icon font shows the
 *   ligature source as words — so discovery speed matters more than keeping the
 *   declaration next to its token (2026-09-05 사람 결정, docs/PITFALLS.md).
 *
 * SOURCE OF RECORD
 * Values come from the ROOT design system project
 * d7baae82-a86e-4cd6-895b-d4e8542157e3, files tokens/*.css. Its readme.md
 * paraphrases colours in prose and has been wrong before in an earlier design
 * system; tokens/*.css is the only source (docs/PITFALLS.md).
 */

/* ROOT display voice: Chakra Petch sets Latin, Gothic A1 carries Korean at a
   matching weight. Isamanru speaks (UI), JetBrains Mono logs (numerals and
   labels), Gowun Batang survives for lyrical long-form prose only. */
@import url("https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@600;700&family=Gothic+A1:wght@500;700;800;900&family=IBM+Plex+Sans+KR:wght@400;500;600;700&family=Gowun+Batang:wght@400;700&family=JetBrains+Mono:wght@400;500;700;800&display=swap");

/* Icons — Material Symbols Outlined, addressed by NAME: the glyph is chosen by
   the ligature text inside the element, so a menu can use any of ~3,700 icons
   without anyone vendoring a file for it first.
   ⛔⛔ The @font-face for it is NOT imported here. It is a <link> in
      templates/core/base.html and intro.html (2026-09-05 사람 결정), because an
      @import is a second-level request: the browser has to fetch and parse this
      file before it even learns the URL, and the preload scanner never sees it.
      Under load that pushed the icon font past its font-display block period and
      the ligature source appeared on screen as words (docs/PITFALLS.md).
   ⚠ Unlike the text stacks above, this one does NOT degrade gracefully: with
      the CDN unreachable an icon renders as its own name ("calendar_month"
      shows the word). Visible rather than invisible, but still wrong. That is
      the accepted cost of not shipping the font (2026-08-30 사람 결정). */

/* Body face. Not on Google Fonts, so it comes from the same jsDelivr host the
   rest of this file already uses (2026-08-31 사람 지시).
 *
 * ⚠ Three static weights, not a variable font: 300 / 400 / 700. The ROOT type
 * scale asks for 500 on body and 800 on display, and neither exists here — the
 * browser picks the nearest cut (500 renders as 400, 800 as 700), so copy sits
 * a shade lighter than the design system draws it. That is the trade the face
 * comes with, not a bug to hunt later.
 *
 * ⚠ The family is named for the design, the files are GongGothic. Keep the two
 * together in one place so nobody "fixes" the mismatch by editing one of them.
 *
 * IBM Plex Sans KR further down --mg-font-body is the fallback when jsDelivr is
 * unreachable, which for an installed site is somebody else's network. */
@font-face {
  font-family: "Isamanru";
  font-weight: 300;
  font-style: normal;
  font-display: swap;
  src: url("https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_20-10@1.0/GongGothicLight.woff") format("woff");
}

@font-face {
  font-family: "Isamanru";
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  src: url("https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_20-10@1.0/GongGothicMedium.woff") format("woff");
}

@font-face {
  font-family: "Isamanru";
  font-weight: 700;
  font-style: normal;
  font-display: swap;
  src: url("https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_20-10@1.0/GongGothicBold.woff") format("woff");
}

:root {
  /* Color — ROOT is white-dominant, ink-drawn and gold-punctuated. Gold
     (--mg-accent) is the ONLY saturated hue in the system and is rationed to
     one element per view: the single primary button, the active nav underline,
     one rule, one chip. Gold type fails contrast on white, so --mg-link steps
     down to the gold-700 ramp and --mg-accent-text is INK, never white. */
  /* ⛔⛔ The nine names the admin can set (apps/core/theme.py COLOR_KEYS) are
     declared as -raw and read through an alias, exactly like the type scale
     further down and for the same reason: dark mode redefines the alias as the
     raw colour with its lightness flipped, and a custom property cannot be
     written in terms of itself -- a var() cycle makes the declaration invalid.
     ⚠ Everything on the site keeps reading the plain name. Only the DECLARATION
     moved, so a skin or a board stylesheet that already uses --mg-bg is
     unaffected. A skin that REDECLARES --mg-bg still wins in light mode but
     stops being derived in dark -- documented in docs/SKIN_API.md. */
  --mg-accent-raw: #fad228;
  --mg-accent-text-raw: #333a40;
  --mg-bg-raw: #f8fafb;
  --mg-surface-raw: #fafafa;
  --mg-text-raw: #333a40;
  --mg-text-muted-raw: #6b7278;
  --mg-border-raw: #c6d2de;
  --mg-link-raw: #9a7605;
  --mg-danger-raw: #d8452f;
  --mg-accent: var(--mg-accent-raw);
  --mg-accent-text: var(--mg-accent-text-raw);
  --mg-bg: var(--mg-bg-raw);
  --mg-surface: var(--mg-surface-raw);
  --mg-text: var(--mg-text-raw);
  --mg-text-muted: var(--mg-text-muted-raw);
  --mg-border: var(--mg-border-raw);
  --mg-link: var(--mg-link-raw);
  --mg-danger: var(--mg-danger-raw);

  --mg-accent-hover: #efc20e;
  --mg-accent-soft: #fffbea;
  --mg-green: #1fa97f;
  --mg-green-hover: #188566;
  --mg-green-soft: #e4f5ef;
  --mg-surface-tint: #f0f3f5;
  --mg-border-strong: #33445a;
  --mg-link-hover: #333a40;
  --mg-danger-soft: #fbe9e6;
  /* Status colours are deliberately desaturated so they never compete with the
     brand gold. Used by the prose callouts (docs/SKIN_API.md 프로즈 계약). */
  --mg-info: #3a6ea5;
  --mg-info-soft: #e8eff7;
  --mg-warn: #e08a16;
  --mg-warn-soft: #fdf1dc;
  /* Deeper and fainter ink than --mg-text / --mg-text-muted. ROOT's neutral
     ramp runs further at both ends than the two-step scale allowed for. */
  --mg-text-strong: #272d33;
  --mg-text-faint: #8e959b;
  /* One step below --mg-surface-tint: a plate inside an already tinted panel. */
  --mg-surface-sunken: #e5eaed;
  /* Gold used as material rather than as type: a highlighter wash and an
     underline. Neither is ever a text colour — gold type steps to --mg-link. */
  --mg-accent-mark: #fde68a;

  /* Skill roles (docs/SKIN_API.md "스킬 뷰어 계약"). Two of the four already
     have a token; the other two come from the ROOT DS rarity ramp, which is
     where every colour in this file is sourced from. ⛔ The role colour is NOT
     decided in Python -- a hue chosen server-side cannot be re-themed, and a
     skin owning appearance is the whole point of the project. */
  --mg-skill-role-tank: #4371c5;
  --mg-skill-role-deal: var(--mg-danger);
  --mg-skill-role-heal: #5e973d;
  --mg-skill-role-common: var(--mg-text-muted);
  /* 범위판 한 칸. 7칸 × 이 값이 판의 폭이라, 스킨이 판을 키우려면 여기만 만진다. */
  /* 버튼을 들어 올릴 때의 그림자. 투명한 버튼은 이 값을 none 으로 덮어
     상자 없는 그림자가 뜨는 것을 막는다 — hover 를 두 벌로 만들지 않기 위해서다. */
  --mg-button-lift-shadow: var(--mg-shadow-hover);
  /* 이미지 버튼의 높이. 폭은 이미지 비율이 정한다. */
  --mg-button-image-size: 3rem;
  --mg-skill-cell: 1.75rem;
  --mg-accent-line: #fbdd5c;
  /* The scan sweep's material: a gold wash that crosses a pressable surface
     once on hover. Never a looping shine (docs/SKIN_API.md 프레임 계약). */
  --mg-wash-gold: linear-gradient(120deg, rgba(250, 210, 40, 0.22) 0%, rgba(250, 210, 40, 0) 62%);

  /* Schedule categories. ROOT rations saturated colour to one element per view,
     but a calendar shows every category at once, so these are borrowed whole
     from ROOT's status and rarity ramps rather than invented. ROOT only offers
     six usable hues; 3 and 7 are the same blue family split by lightness. */
  /* 일정 칩 하나가 자기 카테고리 색을 나르는 자리. 칩의 배경과 좁은 칸의 점,
     범례 견본이 **이 한 이름**을 읽는다 — 예전에는 `color` 를 타서 글자색을
     고정하는 순간 전부 같은 색이 됐다 (2026-09-02). 카테고리가 없는 칩이
     기댈 곳이 있어야 하므로 기본값은 코어가 갖는다. */
  --mg-event-color: var(--mg-text-muted);
  --mg-schedule-1: #fbdd5c;
  --mg-schedule-2: #00e29e;
  --mg-schedule-3: #1787ff;
  --mg-schedule-4: #ffae00;
  --mg-schedule-5: #cd88ff;
  --mg-schedule-6: #d8452f;
  --mg-schedule-7: #7fa3c4;
  --mg-schedule-8: #4e555b;

  /* Background — the image is set from an Attachment id by apps/core/theme.py.
     A raw URL is never accepted from the admin (D9-1). ROOT ships a paper grain
     texture, but it stays an opt-in asset: baking it in here would fight the
     admin's own background choice in /manage/theme. */
  --mg-bg-image: none;
  /* 종이 질감을 얼마나 밝힐지 (0 = 원본, 1 = 흰색). 관리자가 배경 이미지를
     올리면 그 이미지가 위 층이라 이 값은 질감에만 걸린다. */
  --mg-bg-texture-lighten: 0.3;
  /* ⛔ 무엇으로 덮을지. bg-paper-texture.webp 는 밝은 종이 이미지라 다크에서는
     CSS 로 뒤집을 방법이 없다 -- 대신 이 베일을 검게 해서 눌러 앉힌다. 세기와
     색을 따로 둔 이유는 다크가 세기를 그대로 쓰면서 색만 바꾸기 때문이다. */
  --mg-bg-texture-veil: 255 255 255;
  --mg-bg-repeat: no-repeat;
  --mg-bg-size: cover;
  --mg-bg-position: center;
  /* ROOT lays its paper texture fixed so the grain stays put while the page
     scrolls. The admin can still override this like any other token. */
  --mg-bg-attachment: fixed;

  /* Shape — ROOT corners are soft-but-square and the ladder is tighter than the
     old one: cards 6px, inputs 3px. ROOT draws with THICK line, so the default
     container edge is 2px and shadows are hard offsets with ZERO blur, tuned to
     the frame's ink blue so shadow and stroke read as one material. */
  --mg-radius: 6px;
  --mg-radius-sm: 3px;
  /* Images sit between cards and chips on ROOT's radius ladder. */
  --mg-radius-md: 10px;
  --mg-radius-pill: 999px;
  /* Control height. ROOT sizes a pressable thing by height, not by padding, so
     a button and an input line up without either knowing the other's spacing.
     44px is the tap minimum; 46 is the design system's default control. */
  --mg-control-h: 46px;
  --mg-border-width: 2px;
  --mg-shadow: 0 1px 0 rgba(51, 58, 64, 0.1);
  /* The middle rung of ROOT's elevation ladder (rest 0 / hover 3 / press 6).
     Only a pressable thing climbs it -- see the button rules in the skin. */
  --mg-shadow-hover: 0 3px 0 rgba(51, 58, 64, 0.13);
  --mg-shadow-lift: 0 10px 0 rgba(51, 58, 64, 0.18);
  /* The one dramatic depth in the system: a printed slab, never a glow. */
  --mg-shadow-slab: 4px 4px 0 #232f3d;
  --mg-shadow-slab-soft: 3px 3px 0 rgba(51, 58, 64, 0.2);
  /* Emphasis and block edges. --mg-border-width is the container default.
     A true hairline exists only for a divider INSIDE an already bordered
     panel — ROOT has no whisper-grey edges anywhere else. */
  --mg-stroke-hair: 1px;
  --mg-stroke-1: 2px;
  --mg-stroke-2: 3px;
  --mg-stroke-3: 4px;

  /* Motion — enter slow and eased out, exit fast. Nothing bounces.
   *
   * ⛔⛔ Two shapes here and they are NOT interchangeable.
   *   --mg-duration-*  : a bare <time>. The only thing `animation` (and
   *                      animation-duration / animation-delay) will take.
   *   --mg-transition-*: that duration bundled WITH the easing, for the
   *                      `transition` shorthand.
   * Handing a bundled one to `animation` puts two easing functions in the
   * shorthand, which makes the whole declaration invalid -- the browser drops
   * it and nothing moves, with no error and no warning. That mistake shipped
   * twice (the button sweep and flash, then the page transition) before it was
   * noticed, because both look exactly like a site with no animation. */
  --mg-ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --mg-duration-fast: 200ms;
  --mg-duration: 300ms;
  /* Panels and artwork travel slower than a hover: ROOT's long step. */
  --mg-duration-slow: 600ms;
  --mg-transition-fast: var(--mg-duration-fast) var(--mg-ease-out);
  --mg-transition: var(--mg-duration) var(--mg-ease-out);
  --mg-transition-slow: var(--mg-duration-slow) var(--mg-ease-out);
  /* 아주 느린 표류(drift). 배너 사진이 hover 동안 천천히 흐르는 연출용이라
     초 단위이고, 가속이 붙으면 "움직이다 멈춘" 것처럼 보여 linear 다.
     ⛔ 다른 상호작용에 쓰지 않는다 — 누른 뒤 9초 뒤에 반응하는 화면이 된다. */
  --mg-duration-drift: 9s;
  --mg-transition-drift: var(--mg-duration-drift) linear;
  /* Delay between two neighbouring elements entering after a navigation. One
     step only -- the skin caps how many steps accumulate so a page full of
     blocks does not take seconds to finish arriving. */
  --mg-enter-stagger: 40ms;

  /* Typography — three voices. The display face hits (heavy squared geometric,
     flat terminals), the sans speaks and starts at 500 rather than 400, and the
     mono logs: kickers, dates, ids and counts, always uppercase and tracked. */
  --mg-font-heading: "Chakra Petch", "Gothic A1", "Apple SD Gothic Neo", "Malgun Gothic", system-ui, sans-serif;
  --mg-font-body: "Isamanru", "IBM Plex Sans KR", "Apple SD Gothic Neo", system-ui, sans-serif;
  --mg-font-mono: "JetBrains Mono", ui-monospace, Menlo, monospace;
  /* Lives here rather than in the skin so the @import above and the family
     that consumes it stay in one file (docs/PITFALLS.md 폰트 규칙). */
  --mg-font-icon: "Material Symbols Outlined";
  /* Lyrical long-form paragraphs only. Never a title face. */
  --mg-font-serif: "Gowun Batang", "Apple SD Gothic Neo", serif;
  /* 필기체 축. 관리자가 /manage/fonts 에서 배정하고, 배정이 없으면 이 기본값이다.
     ⛔ 스킨이 글꼴 이름을 직접 적지 않는 이유는 docs/SKIN_API.md "웹폰트 계약" 참조. */
  --mg-font-script: cursive;
  /* Type scale, declared twice on purpose. The -raw pair holds the measurement;
     the plain name is an alias every consumer already uses.
     A surface that scales its own type (the page font zoom, static/core/js/
     page-zoom.js) redeclares the plain names on ITS OWN element as
     calc(-raw * scale). It cannot fold the multiplier in here: a custom
     property is substituted where it is declared, so a :root declaration that
     read a scale variable would freeze that scale at :root and inherit the
     frozen value down. Nor can it multiply the plain name by itself -- a var()
     cycle makes the declaration invalid. Hence the raw pair. */
  --mg-font-size-base-raw: 16px;
  --mg-font-size-xs-raw: 12px;
  --mg-font-size-sm-raw: 14px;
  --mg-font-size-md-raw: 18px;
  --mg-font-size-lg-raw: 26px;
  --mg-font-size-xl-raw: 34px;
  --mg-font-size-2xl-raw: clamp(30px, 3.1vw, 46px);
  --mg-font-size-base: var(--mg-font-size-base-raw);
  --mg-font-size-xs: var(--mg-font-size-xs-raw);
  --mg-font-size-sm: var(--mg-font-size-sm-raw);
  --mg-font-size-md: var(--mg-font-size-md-raw);
  --mg-font-size-lg: var(--mg-font-size-lg-raw);
  --mg-font-size-xl: var(--mg-font-size-xl-raw);
  --mg-font-size-2xl: var(--mg-font-size-2xl-raw);
  /* Reader-chosen page type scale. The default belongs here so a skin that
     draws the zoom controls never reads an undeclared name; the multiplication
     does NOT -- see the comment above the -raw pair. */
  --mg-page-font-scale: 1;
  --mg-emoticon-size: 1.5em;
  --mg-line-height: 1.68;
  /* ⛔ 페이지 본문(플레이트 안의 블록)만 쓰는 줄 간격이다. --mg-line-height 는
     body 가 읽어 셸·메뉴·버튼·게시판·관리 화면까지 전부 따라오므로, "페이지
     본문만 넓게" 를 그 토큰으로 하면 사이트 전체가 움직인다.
     쓰는 곳은 skins/default/style.css 의 .mg-plate__inner 규칙 하나다. */
  --mg-page-line-height: 2.2;
  /* Tracking. Mono labels are set ALL CAPS at --mg-track-widest; Korean has no
     case, so the Latin does the work of signalling a system label. */
  --mg-track-tight: -0.03em;
  --mg-track-wide: 0.06em;
  --mg-track-wider: 0.14em;
  --mg-track-widest: 0.28em;

  /* Rules — a ROOT rule is a bar, not a line. */
  --mg-bar-h: 4px;
  --mg-bar-ink: #333a40;
  --mg-bar-gold: #fad228;

  /* Translucent plates. ROOT frames a panel by putting a 2px ink edge around a
     backdrop-blurred plate rather than by filling it — the page texture stays
     faintly visible through the panel, which is what makes it read as paper. */
  --mg-surface-plate: rgba(248, 250, 251, 0.72);
  --mg-surface-plate-strong: rgba(240, 243, 245, 0.88);
  --mg-frame-blur: saturate(130%) blur(22px);
  --mg-shadow-float: 0 12px 32px rgba(51, 58, 64, 0.18);
  /* An edge drawn INSIDE the box, so a plate carries a stroke without spending
     layout on a border. No hairline variant — a faint inner line reads as fake
     material texture, which ROOT does not do. */
  --mg-frame-ring: inset 0 0 0 var(--mg-stroke-1) var(--mg-border-strong);
  /* Notched corners. The outer frame and the inner plate use different radii so
     the 2px ink edge stays an even width around the whole notch. */
  --mg-clip-notch: polygon(15px 0, 100% 0, 100% calc(100% - 15px), calc(100% - 15px) 100%, 0 100%, 0 15px);
  --mg-clip-notch-inner: polygon(13px 0, 100% 0, 100% calc(100% - 13px), calc(100% - 13px) 100%, 0 100%, 0 13px);
  /* A label tab that sits above a box: the trailing edge is cut away. */
  --mg-clip-tab: polygon(0 0, 100% 0, calc(100% - 8px) 100%, 0 100%);
  /* A cartridge bay: the notch is cut OUT of the top edge rather than off a
     corner. Same two-path trick as the notch -- outer is the stroke, inner is
     the card 2px in. */
  --mg-clip-slot: polygon(0 0, 26% 0, 26% 12px, 50% 12px, 50% 0, 100% 0, 100% 100%, 0 100%);
  --mg-clip-slot-inner: polygon(0 0, 26% 0, 26% 10px, 50% 10px, 50% 0, 100% 0, 100% 100%, 0 100%);

  /* Protection veils — artwork is veiled, never scrimmed: ROOT lays WHITE
     gradients over images so text stays black, the opposite of the usual
     dark-overlay habit. */
  --mg-veil-bottom: linear-gradient(to top, rgba(255, 255, 255, 0.96) 0%, rgba(255, 255, 255, 0.82) 38%, rgba(255, 255, 255, 0) 100%);
  --mg-veil-left: linear-gradient(to right, rgba(255, 255, 255, 0.94) 0%, rgba(255, 255, 255, 0) 62%);
  /* 오른쪽 독의 짝. 같은 베일을 반대 방향으로 흘린다. */
  --mg-veil-right: linear-gradient(to left, rgba(255, 255, 255, 0.94) 0%, rgba(255, 255, 255, 0) 62%);

  /* ⛔⛔ 위의 흰 베일과 정반대인 평면 스크림. ROOT DS 의 --veil-ink 를 옮긴 것이고
     (우리가 여태 안 옮겼을 뿐이다), 아트 **위에 겹쳐 덮는** 한 가지 용도만 있다:
     캐릭터 목록 배너의 hover 오버레이. 그 위 글자는 --mg-text-oninverse 다.
     ⛔ 이 둘은 다크모드에서 **뒤집지 않는다.** 어두운 스크림은 어느 테마에서도
     어둡고 그 위 글자는 어느 테마에서도 밝다 — 뒤집으면 밝은 층에 흰 글자가 된다.
     흰 베일(--mg-veil-*)이 뒤집히는 것과 반대라는 점이 이 자리의 함정이다. */
  --mg-veil-ink: rgba(0, 0, 0, 0.5);
  /* 같은 잉크의 아래→위 그라데이션. 인물 사진 아래쪽에 글자를 얹는 자리다. */
  --mg-veil-ink-bottom: linear-gradient(to top, rgba(10, 10, 10, 0.94) 0%, rgba(10, 10, 10, 0.86) 26%, rgba(10, 10, 10, 0.42) 48%, rgba(10, 10, 10, 0) 70%);
  /* 같은 잉크의 왼쪽 그라데이션. 사진 위에 얹는 글자의 바탕을 보장한다. */
  --mg-veil-ink-left: linear-gradient(to right, rgba(0, 0, 0, 0.55) 0%, rgba(0, 0, 0, 0.18) 38%, rgba(0, 0, 0, 0) 62%);
  --mg-text-oninverse: #f8fafb;
  /* 잉크 바탕. DS 의 --surface-inverse 를 옮긴 것이다. --mg-text-oninverse 와 한 쌍이고,
     둘 다 다크모드에서 **뒤집지 않는다** — 잉크 바탕은 어느 테마에서도 어둡다. */
  --mg-surface-inverse: #333a40;

  /* Spacing — 4px rhythm. Before these existed the skin used --mg-radius as a
     gap unit, which coupled corner rounding to layout density. */
  --mg-space-1: 4px;
  --mg-space-2: 8px;
  --mg-space-3: 12px;
  --mg-space-4: 16px;
  --mg-space-5: 24px;
  --mg-space-6: 32px;
  --mg-space-7: 48px;
  --mg-space-8: 64px;

  /* Layout */
  --mg-content-width: 1160px;
  /* Narrower box for the screens a visitor reads straight through -- a page and
     a novel chapter. Reading width is a measure, not real estate: a Korean
     character is about one em at body size, so 1160px ran ~65 characters per
     line against the 40 CJK characters accessibility guidance asks for. Lists,
     the home screen and profile layouts keep --mg-content-width; they are
     scanned, not read (2026-08-31 사람 결정). */
  --mg-reading-width: 960px;
  /* Raid control screen (u-8a): the tallest a scrolling panel grows before it scrolls inside itself. */
  --mg-raid-panel-height: 70vh;
  /* ⭐ 캐릭터 프로필만 더 넓다 (2026-09-06 사람 지시). 세 칸을 나란히 두는 화면이라
     훑는 폭(1160)에서는 가운데 읽는 칸이 700px 도 안 된다 — 1:3:1 로 나누면
     232/696/232 다. 1520 이면 304/912/304 이 되어 가운데가 읽는 폭(960)에 가깝고
     양옆 레일도 도시에가 들어갈 만큼 넓어진다. 디자인 원본(Claude Design d1cc901f)의
     max-width 도 1520 이다.
     ⛔ --mg-content-width 를 넓혀서 해결하지 않는다 — 그것은 목록·메인·게시판이
        함께 쓰는 폭이고, 그 화면들은 지금 폭이 맞다. */
  --mg-profile-width: 1520px;
  /* The lightbox is a viewer, not a column. It kept the wider value on purpose
     when the content box narrowed -- shrinking "view the original" is a loss on
     a gallery site, and it has no line length to protect. */
  --mg-lightbox-width: 1160px;
  --mg-shell-nav-width: 236px;
  /* The floating rail: a short capsule detached from the edge, not a
     full-height column. --rail-width is the column the shell reserves;
     the capsule itself is narrower and sits inset within it. */
  --mg-shell-rail-width: 6.25rem;
  --mg-shell-rail-inset: 0.75rem;
  --mg-shell-icon: 3.25rem;
  /* ROOT sets icons at 18-24px and never thinner than 2 stroke. The
     glyph size is absolute, not an em: the rail's own font-size is the
     small label scale, which would shrink every icon to ~18px. */
  --mg-icon-size: 20px;
  --mg-shell-dock-height: 64px;
  /* BGM widget (docs/plans/v0.3-c-1-bgm-widget.md §4-5). One round icon the size
     of a dock target; the YouTube frame fills that same box behind it and is
     hidden with opacity. The frame is never display:none or zero-sized — a
     frame the browser stops rendering can stop playing. */
  --mg-bgm-button-size: 2.75rem;
  --mg-bgm-panel-width: 14rem;
  --mg-shell-sidebar-width: 18rem;
  /* Page contents rail. Sized so it still clears the page gutter beside the
     1160px content box once the viewport is wide enough to have one. */
  --mg-page-toc-width: 13rem;
  --mg-avatar-size: 4.75rem;
  --mg-avatar-size-sm: 3.25rem;
  --mg-profile-aside-width: 13rem;
  /* Certificate spine: the dotted binding strip on the profile identity plate. */
  --mg-profile-spine: 1.375rem;
  /* 캐릭터 목록 배너. 인장이 배경이므로 높이가 곧 크롭 비율이다. */
  /* 목록 배너의 인물은 hover 전까지 흐리다 (2026-09-06 사람 지시). */
  --mg-character-banner-blur: 2px;
  /* ⛔ blur 가 상자 바깥을 빨아들여 가장자리를 투명하게 만든 만큼 크게 깐다.
     흐림 반경보다 넉넉해야 한다 — 모자라면 배너 안쪽에 밝은 띠가 남는다. */
  --mg-character-banner-bleed: 1.06;
  /* 배너 사진이 hover 동안 흐르는 거리. ⛔ 기본은 0 이고 hover 규칙만 올린다.
     ⚠ 상한은 bleed 가 만드는 여유다 — bleed 1.06 은 좌우로 3% 씩 넘치므로
     그보다 크게 밀면 반대쪽 가장자리에 바탕이 비친다(오류는 안 난다). */
  --mg-character-banner-pan: 0%;
  --mg-character-banner-height: 8rem;
  --mg-character-banner-height-sm: 5.5rem;
  /* The picture column of a profile relation card (2026-10-15). Its height is the
     banner height above; the picture and its focus are the profile portrait's. */
  --mg-relation-art-width: 300px;
  /* How much of the profile portrait zoom the relation card applies (2026-10-15:
     half — full zoom showed only the face, none showed a small upper body). */
  --mg-relation-art-zoom: 0.5;
  /* 프로필 인물 도시에. 높이가 곧 크롭 비율이라 초점 좌표와 한 쌍이다. */
  --mg-portrait-min-height: 24rem;
  --mg-portrait-grade-size: 10rem;
  /* 전투 포지션 색. 스킨이 값별 규칙에서 덮어쓰고, 모르는 값은 이 기본값이다. */
  --mg-position-color: var(--mg-skill-role-common);

  /* Novel reader (docs/SKIN_API.md). Long-form copy runs looser than the rest
     of the site, so the reader keeps its own line height rather than
     inheriting --mg-line-height. */
  --mg-reader-font-scale: 1;
  --mg-reader-line-height: 1.8;
  --mg-reader-align: left;
  --mg-reader-font-family: var(--mg-font-body);
  --mg-reader-font-serif: "Gowun Batang", Georgia, "Times New Roman", serif;
  --mg-reader-width: var(--mg-reading-width);
}

/* ============================================================================
 * DARK MODE (docs/SKIN_API.md "다크모드 계약")
 *
 * SOURCE OF RECORD — ROOT DS project d7baae82, `tokens/colors-dark.css`.
 *
 * ⛔⛔ That project ships TWO dark files and they DISAGREE: `colors-dark.css`
 * (--dk-* ramp) and `dark.css` (--night-* ramp, different neutrals, and a slab
 * that goes DARKER instead of pale). `styles.css` imports colors-dark first and
 * dark second -- but colors-dark's selector is `:root[data-theme="dark"]`
 * (0,2,0) against dark.css's `[data-theme="dark"]` (0,1,0), so the EARLIER file
 * wins on specificity for every token both declare. `guidelines/
 * color-dark.card.html` documents the --dk-* values, which confirms it
 * independently. This file follows colors-dark.css. (2026-09-02)
 *
 * ⛔ ONE selector, not two. Putting the palette on both [data-mg-theme="dark"]
 * and @media (prefers-color-scheme: dark) would mean writing it twice, because
 * a media query cannot be joined to a selector with a comma. Instead the inline
 * script in the document head resolves the system preference itself and always
 * writes an explicit data-mg-theme, so CSS only ever knows one selector. The
 * person's CHOICE lives in localStorage; the SCREEN's state lives in the
 * attribute. They are deliberately different things -- with no stored choice
 * the script keeps following the system.
 *
 * ⚠ :root[data-mg-theme="dark"] is specificity (0,2,0), which beats the
 * :root (0,1,0) block that apps/core/theme.py renders LAST in the cascade. That
 * is the only reason the admin's own colours do not win here.
 * ============================================================================ */
:root[data-mg-theme="dark"] {
  /* Scrollbars, <select> popups and form widgets are drawn by the browser and
     read this, not our tokens. Without it they stay light on a dark page. */
  color-scheme: dark;

  /* -- Ground and ink: ROOT's --dk-* ramp, recomputed from the light palette
        rather than inverted. ⛔ The floor is #0E1216 and ROOT never reaches
        #000 -- a plain lightness flip of --mg-bg would land on #040606, which
        is why the fixed palette is authored and not computed. -- */
  --mg-bg: #131a20;             /* dk-50  · bg-page */
  --mg-surface: #192229;        /* dk-100 · surface-card */
  --mg-surface-tint: #222d36;   /* dk-200 · surface-raised */
  --mg-surface-sunken: #0e1216; /* dk-0   · surface-sunken */
  --mg-text: #b8c3cc;           /* dk-700 · text-body */
  --mg-text-strong: #eef2f5;    /* dk-800 · text-title */
  --mg-text-muted: #93a1ac;     /* dk-600 */
  --mg-text-faint: #6b7a86;     /* dk-500 */
  --mg-border: #2e3b46;         /* dk-300 · line-hairline */
  /* ⛔ ROOT rule 2, flipped: the 2px ink stroke goes PALE on night. An ink
     border is invisible against the ground. */
  --mg-border-strong: #5b6268;  /* dk-700 · line-strong */

  /* -- Gold ---------------------------------------------------------------
     ⛔ Gold STAYS gold-400 -- on a dark ground it is the brightest pixel on
     screen, so ROOT keeps the ONE gold and makes everything gold-ADJACENT
     recede instead (soft, dim). --mg-accent-text stays ink for the same reason:
     the accent is still a light gold, so its label is still dark.
     ⛔ Rule 3 flips: light steps the link ramp DOWN (gold-700) because gold type
     fails contrast on white; dark steps it UP to gold-300, and hover goes to
     PAPER, never to ink. */
  --mg-accent: #fad228;         /* gold-400 · brand — unchanged from light */
  --mg-accent-hover: #fbdd5c;   /* gold-300 · brand-strong — brighter, not deeper */
  --mg-accent-soft: #2b2711;    /* brand-soft */
  --mg-accent-text: #272d33;    /* ink-950  · text-onbrand */
  --mg-accent-mark: #5a4a12;    /* brand-dim · recessed gold, never a bar */
  --mg-accent-line: #fbdd5c;    /* gold-300 · line-focus */
  --mg-link: #fbdd5c;           /* gold-300 · text-link */
  --mg-link-hover: #eef2f5;     /* dk-800 — paper, never ink */

  /* -- Status: lifted ~1 step so they hold on dark; soft = deep tinted plate -- */
  --mg-green: #34c596;
  /* ⚠ ROOT has no state-live-strong token. Light goes DEEPER on hover, so dark
     goes brighter by the same one step. The only invented value in this block. */
  --mg-green-hover: #5ed4af;
  --mg-green-soft: #12312a;
  --mg-danger: #ef6a55;
  --mg-danger-soft: #3a1a17;
  --mg-info: #6fa3dc;
  --mg-info-soft: #172638;
  --mg-warn: #f0a23a;
  --mg-warn-soft: #362712;

  /* -- Categories: a calendar shows every one at once, so all eight move.
        ROOT lifted them to clear dk-100 at 4.5:1. -- */
  --mg-schedule-1: #d9718a;
  --mg-schedule-2: #e39a4a;
  --mg-schedule-3: #c4bc48;
  --mg-schedule-4: #46b892;
  --mg-schedule-5: #4faad0;
  --mg-schedule-6: #7a8be0;
  --mg-schedule-7: #a982da;
  --mg-schedule-8: #93a1ac;
  /* deal and common are var() aliases and follow on their own -- and they land
     where ROOT puts them: role-atk == state-danger, rarity-n == text-muted. */
  --mg-skill-role-tank: #4b89fd; /* rarity-r */
  --mg-skill-role-heal: #78ca48; /* rarity-sr */

  /* -- Material -----------------------------------------------------------
     ⚠ A ROOT rule is a bar, and the ink bar becomes light ink here. */
  --mg-bar-ink: #eef2f5;
  --mg-bar-gold: #fad228;
  --mg-shadow: 0 1px 0 rgba(0, 0, 0, 0.4);
  --mg-shadow-hover: 0 3px 0 rgba(0, 0, 0, 0.45);
  --mg-shadow-lift: 0 10px 0 rgba(0, 0, 0, 0.55);
  /* ⛔⛔ The slab INVERTS its material. Still 4px 4px 0 with zero blur -- the
     geometry does not change -- but on night it is PALE, the same material as
     the stroke, so depth reads as a lit rim rather than a dropped shadow. A
     dark slab here is invisible against a dark ground; that was the provisional
     value and it was backwards. */
  --mg-shadow-slab: 4px 4px 0 #eef2f5;
  --mg-shadow-slab-soft: 3px 3px 0 rgba(238, 242, 245, 0.26);
  --mg-shadow-float: 0 12px 32px rgba(0, 0, 0, 0.6);
  --mg-surface-plate: rgba(25, 34, 41, 0.72);
  --mg-surface-plate-strong: rgba(34, 45, 54, 0.9);
  --mg-wash-gold: linear-gradient(120deg, rgba(250, 210, 40, 0.14) 0%, rgba(250, 210, 40, 0) 62%);

  /* ⛔ ROOT veils artwork with WHITE so text stays black. On a dark ground that
     rule flips whole: the veil is the ground colour, so text stays light. A
     white veil left here would blind the one screen it covers. */
  --mg-veil-bottom: linear-gradient(to top, rgba(14, 18, 22, 0.96) 0%, rgba(14, 18, 22, 0.82) 38%, rgba(14, 18, 22, 0) 100%);
  --mg-veil-left: linear-gradient(to right, rgba(14, 18, 22, 0.94) 0%, rgba(14, 18, 22, 0) 62%);
  /* ⚠ ROOT has no right-hand veil (we have a 4-way dock). Mirror of the left. */
  --mg-veil-right: linear-gradient(to left, rgba(14, 18, 22, 0.94) 0%, rgba(14, 18, 22, 0) 62%);

  /* The paper grain is a light image and CSS cannot invert it, so the veil over
     it turns to the ramp floor and takes most of it away. ⚠ ROOT's own dark.css
     says `body{background-image:none}`; we do NOT do that, because the same
     background layer carries the ADMIN's uploaded image. The strength token is
     named "lighten" from the light-mode job; here it is how much veil to lay. */
  --mg-bg-texture-veil: 14 18 22;
  --mg-bg-texture-lighten: 0.82;
}

/* Admin-chosen colours, carried into dark by flipping HSL lightness and keeping
 * hue and saturation (docs/DECISIONS.md D9-1). This is the whole reason the nine
 * COLOR_KEYS are declared as -raw.
 *
 * ⛔⛔ SIX of the nine, not all nine. bg / surface / text / text-muted / border /
 * link are a ground-and-ink axis, where flipping lightness is exactly right.
 * --mg-accent, --mg-accent-text and --mg-danger are NOT: they are marks whose
 * job is to stand out by hue, and flipping the accent turns ROOT's gold into
 * dark brown. Leaving the accent pair untouched also keeps the two in contrast
 * with each other, which is the property that actually matters -- the label sits
 * ON the accent. ⭐ ROOT's own dark palette confirms the split: --brand stays
 * gold-400 (identical to light) and --text-onbrand stays ink, while --text-link
 * moves gold-700 -> gold-300 -- and a lightness flip of our gold-700 link lands
 * on #fad866 against ROOT's #FBDD5C, i.e. the derivation reproduces the
 * designer's own step. Border checks out the same way (#222e3a vs #2E3B46).
 *
 * ⚠ The derivation does NOT reproduce ROOT at the extremes, and cannot. ROOT's
 * ramp bottoms out at #0E1216 and "never reaches #000", but flipping our light
 * --mg-bg (#f8fafb, L98) gives #040606. The authored palette above is the
 * designer's; this block is only for a colour the admin picked, where there is
 * no designer to ask. ⭐ So the derived lightness is floored at ROOT's bottom
 * (2026-09-13 사람 결정): max(7.06, 100 - l), where 7.06 is the HSL lightness of
 * #0E1216 (--mg-surface-sunken in the dark block). A near-white admin ground now
 * lands on the ramp floor instead of near-black. The number is written out
 * because a colour token cannot be read as a lightness here; a guard recomputes
 * it from that token, so moving the floor without moving this fails a test.
 *
 * ⛔ The @supports guard is load-bearing, and not only for old browsers. If the
 * relative-colour syntax below is ever written wrong (`calc(100% - l)` instead
 * of `calc(100 - l)`, say) the declaration is invalid at computed-value time and
 * the custom property falls back to its inherited value -- dark text on a dark
 * ground, with no error and no warning anywhere. The guard sends both cases to
 * the fixed palette above instead. */
@supports (color: hsl(from red h s l)) {
  :root[data-mg-theme="dark"] {
    --mg-bg: hsl(from var(--mg-bg-raw) h s max(7.06, calc(100 - l)));
    --mg-surface: #212121;
    --mg-text: #fafafa;
    --mg-text-muted: #9e9e9e;
    --mg-border:  #efc20e;
    --mg-link: hsl(from var(--mg-link-raw) h s max(7.06, calc(100 - l)));
  }
}

/* ROOT zeroes every duration token at the :root level rather than asking each
   component to opt out. Declarations only, still no new selectors. */
@media (prefers-reduced-motion: reduce) {
  :root {
    /* ⛔ Only the durations are zeroed: --mg-transition-* are derived from
       these, so they follow. Restating them here would create a second source
       for the same number, and the one that stopped being derived would keep
       animating for exactly the people who asked it not to. */
    --mg-duration-fast: 0ms;
    --mg-duration: 0ms;
    --mg-duration-slow: 0ms;
    --mg-duration-drift: 0ms;
    --mg-enter-stagger: 0ms;
  }
}

/* Cross-document view transitions, so a plain navigation looks the same as an
   hx-boost swap. Both the outgoing and the incoming document have to opt in,
   which scopes this exactly right without an exclusion list: the shell and the
   intro screen load this file, while Django admin and the standalone error
   document do not and simply navigate as before.

   ⛔ The transition itself is disabled for prefers-reduced-motion in the skin,
   not here -- ::view-transition-* are pseudo-elements on the document root and
   are NOT matched by the `*` reset that zeroes every other animation. */
@view-transition {
  navigation: auto;
}

/* ── 복사 금지 (docs/SKIN_API.md "복사 금지 계약") ──────────────────────────
   The whole rule lives in this core file, not in a skin: a skin that forgot to
   copy it would silently turn the setting off for the site that switched to
   it, and an admin who enabled "복사 금지" would never see an error.

   ⛔ The switch is the server-rendered <body class="mg-no-copy">. The manage
      screens are NOT excluded by a second body class -- hx-boost only replaces
      #content, so a per-view body class goes stale the moment a boosted link
      moves between /manage/ and a public screen. The exception is .mg-manage,
      which travels with #content.
   ⛔ Inputs have to opt back in or the site becomes unwritable: a textarea the
      visitor cannot select text in still accepts typing, so this failure looks
      like "글이 잘 안 써진다" rather than an error.
   ⚠ user-select is inherited, so putting `text` back on the wrapper is enough
     for everything inside it. */
body.mg-no-copy {
  -webkit-user-select: none;
  user-select: none;
}

/* Dragging an image out of the page is a copy too. The dragstart handler in
   core/js/no-copy.js is what actually blocks it everywhere; this only stops the
   drag ghost from appearing under the cursor in WebKit. */
body.mg-no-copy img,
body.mg-no-copy a {
  -webkit-user-drag: none;
}

body.mg-no-copy input,
body.mg-no-copy textarea,
body.mg-no-copy select,
body.mg-no-copy [contenteditable="true"],
body.mg-no-copy [contenteditable=""],
body.mg-no-copy .mg-manage {
  -webkit-user-select: text;
  user-select: text;
}

/* ------------------------------------------------------------------ *
 * 관리 화면 메뉴 (2026-09-08)
 *
 * ⛔⛔ 규칙을 스킨이 아니라 **코어가 소유한다.** 관리 화면은 코어의 것이라,
 * 스킨에 두면 스킨을 갈아탄 날 메뉴가 조용히 모양을 잃는다 — 복사 금지 계약에서
 * 이미 밟은 함정이고 같은 이유로 여기 둔다. ⚠ tokens.css 가 스킨 시트보다 **먼저**
 * 실리므로 스킨이 덮고 싶으면 덮을 수 있다.
 *
 * ⛔ JS 가 없다. <details> 가 열고 닫으며, 상태는 #content 안이라 hx-boost 이동마다
 * 새로 그려진다(셸 상태였다면 이동해도 안 닫힌다 — 모바일 드로어에서 밟은 함정).
 * ------------------------------------------------------------------ */

.mg-manage-nav {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--mg-space-2);
}

.mg-manage-nav__home { font-weight: 600; padding: var(--mg-space-1) var(--mg-space-2); }

.mg-manage-nav__group { position: relative; }

.mg-manage-nav__summary {
  cursor: pointer;
  padding: var(--mg-space-1) var(--mg-space-2);
  border-radius: var(--mg-radius-sm);
  list-style: none;
  white-space: nowrap;
}

/* ⚠ 사파리는 목록 표시를 이 유사요소로 그린다 — list-style 만으로는 안 사라진다. */
.mg-manage-nav__summary::-webkit-details-marker { display: none; }

.mg-manage-nav__summary::after { content: " ▾"; }

.mg-manage-nav__summary:hover,
.mg-manage-nav__summary:focus-visible,
.mg-manage-nav__group[open] .mg-manage-nav__summary {
  background: var(--mg-surface);
}

/* ⚠ 좁은 화면에서는 겹쳐 띄우지 않고 아래로 밀어 낸다 — 손가락으로 닫기 어렵다. */
.mg-manage-nav__links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--mg-space-2) var(--mg-space-3);
  padding: var(--mg-space-2);
}

@media (min-width: 48rem) {
  .mg-manage-nav__links {
    position: absolute;
    z-index: 2;
    inset-inline-start: 0;
    min-inline-size: max-content;
    flex-direction: column;
    flex-wrap: nowrap;
    background: var(--mg-surface);
    border: var(--mg-border-width) solid var(--mg-border);
    border-radius: var(--mg-radius);
    box-shadow: var(--mg-shadow);
  }
}

.mg-manage-menu {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: var(--mg-space-4);
  margin-block-start: var(--mg-space-4);
}

.mg-manage-menu__title {
  margin: 0 0 var(--mg-space-2);
  font-size: var(--mg-font-size-md);
}

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