/* garamnoh design system - entry point.
   Consumers link ONLY this file. Order matters. */
/* fonts - Pretendard for body copy, JetBrains Mono for labels and code.
   Self-hosted: no request leaves this origin. Both are SIL OFL 1.1;
   the licences ship next to the files in ../fonts/.

   Pretendard is subset to Latin-1 + the 2350 KS X 1001 Hangul syllables
   (2.1MB -> 459KB). A syllable outside that set falls back to the stack in
   typography.css. Rebuild the subset if the copy ever needs one. */
@font-face {
  font-family: "Pretendard Variable";
  font-style: normal;
  font-weight: 45 920;
  font-display: swap;
  src: url("/system/fonts/PretendardVariable.subset.woff2") format("woff2");
}
@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url("/system/fonts/JetBrainsMono-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url("/system/fonts/JetBrainsMono-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* JetBrains Mono Nerd Font - 글리프(아이콘 문자)가 필요한 화면에서만 쓴다.
   v2.304 · Nerd Fonts 3.5.0 패치본. 본문·라벨은 위의 JetBrains Mono 를 쓰고,
   이 패밀리는 글리프를 직접 찍을 때만 부른다:  font-family: "JetBrainsMonoNF", monospace
   라이선스는 원본과 같은 SIL OFL 1.1 - ../fonts/OFL-JetBrainsMono.txt */
@font-face {
  font-family: "JetBrainsMonoNF";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/system/fonts/JetBrainsMonoNerdFont-Regular.ttf") format("truetype");
}

/* colors - two themes, one set of names. Terminal DNA only.
   Paper (warm cream / terracotta) was dropped 2026.09.02. Do not reintroduce. */
:root, [data-theme="dark"] {
  --bg: #08090A; --bg-2: #131917; --surface: #0B0E0D;
  --text: #E6EBE6; --text-2: #8B968E; --text-3: #6B776F;
  --line: #1E2422; --line-soft: #161B19;
  --line-strong: #5C6760;   /* 흰(채움 없는) 버튼 · 입력칸 테두리. --line(1.26:1)은 가장자리가 안 보이고 --text-3 은 진함 - 바탕 3.38 · 면 3.29 · 칸색 3.02:1 (task 3956) */
  /* --sep is the SEPARATOR between two things on one line - the hairline in a
     header, the rule in a lockup. It is not a border. A border encloses a
     surface and can sit at --line; a separator has to survive alone on the
     ground, and on this theme a 1px light line on near-black disappears at
     --line (1.26:1 on --bg, the same number that reads fine as a dark line on
     the light theme). It steps up to --text-3 here - 4.26:1, measured - and
     stays at --line on the light theme, where nothing is wrong. Same name,
     two values, one job. */
  --sep: var(--text-3);
  --accent: #3ED49C; --accent-text: #3ED49C;
  --accent-weak: #12281F; --accent-deep: #7CE7BD; --on-accent: #08090A;
  --ok: #3ED49C; --warn: #E0B64A; --err: #E0685F; --info: #7FB8D9;
  --ok-weak: #12281F; --warn-weak: #2C2413; --err-weak: #2C1917; --info-weak: #14232C;
  --chart-1: #3ED49C; --chart-2: #2F6B52; --chart-3: #E0B64A; --chart-4: #7FB8D9; --chart-5: #E0685F;
  /* 선택 · 활성(목록형) - 한 규칙: 칸 배경 --sel-bg + 칸 벽 쪽 --sel-bar-w 민트 띠. 띠 자리는 모든 줄에 투명으로 비워 글자 시작선이 같다.
     토큰은 두 테마 블록에 다 두어 테마가 섞인 페이지에서도 그 자리 테마 값으로 계산되게 (guidelines/selection.html) */
  --sel-bg: var(--bg-2); --sel-bar: var(--accent); --sel-bar-w: 3px;
}
[data-theme="light"] {
  --bg: #F4F6F4; --bg-2: #E8EBE8; --surface: #FFFFFF;
  --text: #101413; --text-2: #59635C; --text-3: #6E7A72;
  --line: #D3DAD4; --line-soft: #E4E9E5;
  --line-strong: #7E8982;   /* 바탕 3.34 · 면 3.63 · 칸색 3.02:1 */
  --sep: var(--line);            /* 1.31:1 on --bg - a dark line on light ground holds */
  /* one accent, both themes. --accent-text used to be a darkened pair so the
     green would clear 3:1 as text on white; it now resolves to the same
     #3ED49C, because a mark that changes colour between themes stops being
     one mark. The token stays as a name so every call site keeps working and
     a future exception has one place to live.

     The consequence, decided 2026-09-17: mint as TEXT on this theme's ground
     is 1.74:1, under the 4.5:1 body minimum. So on a light ground mint is a
     FILL, not a letter - a label goes on an --accent field with --on-accent
     ink, emphasis inside a sentence is weight or --accent-weak behind it, and
     position is a 3px --accent bar or a 1px line. Print is the one exception
     that steps the token down (tokens/print.css, #0B6B4A, 6.53:1); the screen
     keeps one colour. See guidelines/colors-accent-on-light.html. */
  --accent: #3ED49C;
  --accent-text: #3ED49C;
  --accent-weak: #DCF2E8; --accent-deep: #2BC189; --on-accent: #08090A;
  --ok: #0B6B4A; --warn: #8A6410; --err: #B03A30; --info: #2A5F7D;
  --ok-weak: #DCF2E8; --warn-weak: #F5E9CC; --err-weak: #F6DCD8; --info-weak: #DDE9F0;
  --chart-1: #12805A; --chart-2: #0B6B4A; --chart-3: #8A6410; --chart-4: #2A5F7D; --chart-5: #B03A30;
  --sel-bg: var(--bg-2); --sel-bar: var(--accent); --sel-bar-w: 3px;
}
/* shape, weight and type no longer vary by theme */
:root {
  --bw: 1px;
  --shadow: none; --shadow-sm: none;
  --font: var(--font-sans); --font-mono: var(--font-code);
  --w-strong: 600;
}

/* typography - one scale for all four themes. No intermediate sizes. */
:root {
  --font-sans: "Pretendard Variable", Pretendard, system-ui, sans-serif;
  --font-code: "JetBrains Mono", ui-monospace, Menlo, monospace;

  --fs-micro: 10px;                     /* one step under the label: compact
                                           chips and header controls only */
  --fs-label: 11px;  --lh-label: 1.3;   --ls-label: 0.14em;
  --fs-small: 13px;  --lh-small: 1.55;
  --fs-body: 15px;   --lh-body: 1.62;
  --fs-h2: 22px;     --lh-h2: 1.3;      --ls-h2: -0.02em;
  --fs-h1: 34px;     --lh-h1: 1.18;     --ls-h1: -0.03em;
  --fs-display: 66px;
  /* landing only - a panel that fills the viewport needs type that scales with
     it. Used by home and the system entry; documentation pages stay on
     --fs-display. */
  --fs-hero: clamp(52px, 8.4vw, 128px);--lh-display: 1.04;--ls-display: -0.045em; --ls-hero: -0.05em;

  --w-regular: 400;
  --numeric-tabular: "tnum" 1;
  --measure: 660px;
  /* --measure-lead is the OTHER reading width, and it is narrower on purpose.
     --measure is for a document: paragraph after paragraph, read straight
     through. A lead is the one paragraph under a heading that has to be taken
     in at a glance, so it stops earlier. 54ch is what the product sites had
     already settled on by hand in 16 places; naming it changed nothing on
     screen. At the 15px body size it measures 482px, against --measure's 660 -
     the two are not interchangeable, and a lead set to --measure reads as a
     block of copy rather than a subtitle. */
  --measure-lead: 54ch;
}

/* spacing - multiples of 4 only. */
:root {
  --sp-1: 4px;  --sp-2: 8px;  --sp-3: 12px; --sp-4: 16px;
  --sp-6: 24px; --sp-8: 32px; --sp-12: 48px; --sp-19: 76px;

  --container: 1120px;
  --gutter: 28px;
  --section-gap: 76px;

  --h-sm: 32px; --h-md: 38px; --h-lg: 44px;
  --touch-min: 44px;

  --sidebar-w: 268px;
  --topbar-h: 54px;
  --hairline: 1px;

  --bp-sm: 480px; --bp-md: 768px; --bp-lg: 1120px;
}

/* radius - none. Square corners are the system; curvature carries no meaning.
   Kept as tokens so a future exception has one place to live. */
:root {
  --r-none: 0px;
  --r-sm: 0px; --r-md: 0px; --r-lg: 0px; --r-pill: 0px;
  --r-control: 0px; --r-field: 0px; --r-card: 0px; --r-overlay: 0px;
}

/* elevation - three levels. Paper uses hard shadows, terminal uses none. */
:root {
  --elev-0: none;
  --elev-1: var(--shadow-sm);
  --elev-2: var(--shadow);
  --elev-inset: inset 0 0 0 var(--bw) var(--line-soft);
  --focus-ring: 2px solid var(--accent);
  --focus-offset: 2px;
}

/* motion - three durations, one easing. */
:root {
  --dur-color: 120ms;
  --dur-move: 180ms;
  --dur-enter: 260ms;
  --dur-blink: 550ms;
  --ease: cubic-bezier(.2, .8, .2, 1);
  --transition-control: color var(--dur-color) var(--ease),
                        background-color var(--dur-color) var(--ease),
                        border-color var(--dur-color) var(--ease),
                        box-shadow var(--dur-move) var(--ease);
  --move-max: 4px;
}
@media (prefers-reduced-motion: reduce) {
  :root {
    --dur-color: 0ms; --dur-move: 0ms; --dur-enter: 0ms;
  }
  * { animation: none !important; transition: none !important; }
}

/* base - element resets built on the tokens above.
   Korean line breaking (keep-all) is part of the system, not an override. */
* { box-sizing: border-box; }

html { scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
}

body, div, p, h1, h2, h3, h4, span, label, td, th, li, figcaption, button, input, textarea, select {
  word-break: keep-all;
  overflow-wrap: break-word;
}
p, h1, h2, h3, h4, li { text-wrap: pretty; }
pre, code, kbd, samp { font-family: var(--font-mono); word-break: normal; }

h1, h2, h3, h4 { margin: 0; font-weight: var(--w-strong); }
h1 { font-size: var(--fs-h1); line-height: var(--lh-h1); letter-spacing: var(--ls-h1); }
h2 { font-size: var(--fs-h2); line-height: var(--lh-h2); letter-spacing: var(--ls-h2); }
p  { margin: 0; }

strong, b { font-weight: var(--w-strong); }

/* Links are NEVER underlined - not in body copy, not on inverted faces, not on
   hover. Affordance comes from colour, and from the shape a link sits in
   (a row, a card, a chip). Underline is reserved for nothing. */
a { color: var(--accent-text); text-decoration: none; }
a:hover { color: var(--accent); }

:focus-visible { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
:focus:not(:focus-visible) { outline: none; }

hr { height: var(--bw); margin: var(--sp-6) 0; border: 0; background: var(--line); }

table { border-collapse: collapse; font-variant-numeric: tabular-nums; }

/* inline mono run - numbers, paths, tokens, IDs inside a sentence */
.gn-mono {
  font-family: var(--font-code);
  font-variant-numeric: tabular-nums;
  /* One unit, never split. A path like garamnoh-system/tokens/ carries a
     hyphen, which is a break opportunity that word-break: keep-all does not
     suppress - so a wrap can cut a path in half. Code blocks are unaffected:
     pre and .gn-code-body declare their own whitespace. */
  white-space: nowrap;
}
/* nowrap above is for LABELS - a version, a key, a path in a cell, which must
   not break mid-token. A paragraph is never a label: set as mono it would run
   off the page instead of wrapping, and nothing in the layout would say so.
   Found on two docs pages before this rule existed. */
p.gn-mono { white-space: normal; }

.gn-label {
  font-family: var(--font-mono);
  font-size: var(--fs-label);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--text-3);
}
.gn-measure { max-width: var(--measure); }
.gn-container { max-width: var(--container); margin-inline: auto; padding-inline: var(--gutter); }


/* --------------------------------------------------------------- LANGUAGE
   Bilingual documents. English renders by default; set data-lang="ko" on
   <html> to swap. display:contents keeps the wrapper out of layout, so a
   pair can sit inside a flex row, a heading, or a sentence alike. */
.t-en { display: contents; }
.t-ko { display: none; }
[data-lang="ko"] .t-en { display: none; }
[data-lang="ko"] .t-ko { display: contents; }

/* z-index - one ladder. Never write a raw z-index in a component. */
:root {
  --z-base: 0; /* @kind other */
  --z-raise: 10; /* @kind other */      /* sticky table header, focused row */
  --z-sticky: 60; /* @kind other */     /* topbar, toolbar */
  --z-chrome: 80; /* @kind other */     /* app shell chrome, deck chrome layer */
  --z-drawer: 150; /* @kind other */    /* drawer / sheet */
  --z-overlay: 200; /* @kind other */   /* dialog + its scrim */
  --z-popover: 250; /* @kind other */   /* menu, popover, combobox list, command palette */
  --z-toast: 300; /* @kind other */
  --z-tooltip: 350; /* @kind other */
}

/* density - two steps, same tokens. Site is roomy, console is tight.
   data-density="console" on any subtree switches it; default is site. */
:root {
  --row-h: 44px;
  --row-pad-x: var(--sp-4);
  --row-pad-y: var(--sp-3);
  --stack-gap: var(--sp-4);
  --block-gap: var(--sp-8);
  --density-font: var(--fs-body); /* @kind font */
}
[data-density="console"] {
  --row-h: 34px;
  --row-pad-x: var(--sp-3);
  --row-pad-y: var(--sp-2);
  --stack-gap: var(--sp-3);
  --block-gap: var(--sp-4);
  --density-font: var(--fs-small); /* @kind font */
}
[data-density="console"] .gn-table td,
[data-density="console"] .gn-table th { padding: 5px var(--sp-3); }
[data-density="console"] .gn-sidenav-item { padding: 5px var(--sp-4); }

/* deck - slide-only aliases and FRAME COORDINATES, scoped to .deck-scope.
   1920x1080. Every slide puts title, subtitle and body at the same y.
   Chrome (the four corner items) is a separate layer, never body content. */
:root, .deck-scope {
  --canvas-w: 1920px;
  --canvas-h: 1080px;

  /* frame */
  --deck-margin: 108px;      /* left / right / bottom safe area */
  --deck-cols: 8;
  --deck-gutter: 40px;

  /* chrome layer - z 5, mono uppercase, --text-3 */
  --chrome-top: 56px;        /* corner items, top pair */
  --chrome-bottom: 52px;     /* corner items, bottom pair */
  --rule-top: 96px;          /* hairline separating chrome from body */
  --rule-bottom: 96px;

  /* body regions - identical on every slide */
  --title-y: 132px;
  --meta-y: 226px;           /* label + subtitle, one line, side by side */
  --body-y: 282px;
  --body-bottom: 132px;

  /* aliases */
  --paper: var(--surface);
  --field: var(--accent);
  --field-ink: var(--on-accent);
  --invert: var(--text);
  --invert-ink: var(--bg);

  /* type - 24px is the floor on a 1920 canvas */
  --fs-slide-display: 132px;
  --fs-slide-title: 60px;
  --fs-slide-h3: 44px;
  --fs-slide-body: 32px;
  --fs-slide-caption: 24px;
  --fs-slide-label: 24px;        /* content labels - the 24px floor */
  --fs-slide-micro: 20px;        /* secondary label INSIDE a box: footer strips,
                                    sub-bands, stat captions. Content, but one
                                    rank below the box's own label. */
  --fs-slide-chrome: 20px;       /* frame furniture ONLY: corner items, table
                                    headers, placeholders. Never body content. */

  /* box padding inside a slide */
  --slide-box-pad: 36px;
  --slide-gap: 40px;

  --colorfield-max: 0.2;     /* colour-field slides: 1/5 of the deck at most */
}

/* Chrome exceptions: cover, section field, full bleed, closing invert keep
   the page number ONLY - they have no body safe area to align to. */
.deck-scope .slide-bare > .gn-chrome-tl,
.deck-scope .slide-bare > .gn-chrome-tr,
.deck-scope .slide-bare > .gn-chrome-bl,
.deck-scope .slide-bare > .gn-rule { display: none; }

/* layout - the grammar. Boxes, density, alignment, state chips, figures.
   These are the rules that make two screens look like one system.
   Everything here is theme-agnostic: values come from tokens only. */

/* ---------------------------------------------------------------- BOX
   Every content element is a box: surface + 1px line. No radius, no shadow.
   Padding comes from ONE of three steps - never an ad-hoc value. */
.gn-box { position: relative; background: var(--surface); border: var(--bw) solid var(--line); min-width: 0; }
.gn-box-pad     { padding: var(--sp-4); }              /* 16 - dense / console */
.gn-box-pad-md  { padding: var(--sp-6); }              /* 24 - default */
.gn-box-pad-lg  { padding: var(--sp-8); }              /* 32 - page-level */
.gn-box-flat    { background: none; }                  /* line only */
.gn-box-deep    { background: var(--bg-2); }           /* one step in */
/* one emphasis box per view. Accent border only - never a fill + border + bar. */
.gn-box-lead    { border-color: var(--accent); }
/* not yet built / not yet decided. Dashed = future, always. */
.gn-box-future  { background: none; border-style: dashed; border-color: var(--text-3); }

/* ---------------------------------------------------------------- ROWS
   A list inside a box divides that box's height equally. Never stack with
   fixed padding - that is what leaves dead space at the bottom. */
/* NOTE: .gn-rows carries flex:1, which only acts if its PARENT is a flex
   column. A block parent silently ignores it and the surplus collects as one
   slab at the bottom - the most common way this grammar gets broken. */
.gn-rows { display: flex; flex-direction: column; flex: 1; min-height: 0; }
/* Vertical padding, not a container gap: in a box with a resolved height the
   flex:1 children already distribute and the padding is absorbed, while in an
   AUTO-height box flex:1 collapses to content - the padding is then the only
   thing separating the rows. One rule covers both cases. */
.gn-rows > * { flex: 1; min-height: 0; align-items: center; gap: var(--sp-4);
               padding: var(--sp-3) 0;
               border-top: var(--hairline) solid var(--line-soft); }
.gn-rows > *:first-child { padding-top: 0; }
.gn-rows > *:where(:not([style*="grid"]):not([style*="block"])) { display: flex; }
.gn-rows > *:first-child { border-top: 0; }
.gn-rows-tight > * { gap: var(--sp-2); padding: var(--sp-2) 0; }
.gn-rows-tight > *:first-child { padding-top: 0; }
/* rows that must NOT stretch (a short stat pair pinned to the bottom) */
.gn-rows-fixed { flex: none; }

/* ------------------------------------------------------------ BOX SHAPES
   Two ways a box uses its height. Pick one; mixing them is what looks amateur.
   .gn-fill  - one stretching child (table, rows, nested box, figure)
   .gn-card  - label / value / caption distributed, last child is a footer */
.gn-fill { display: flex; flex-direction: column; }
.gn-fill > table, .gn-fill > .gn-rows, .gn-fill > .gn-box, .gn-fill > .gn-figure-slot { flex: 1; min-height: 0; }
.gn-card-dist { display: flex; flex-direction: column; justify-content: space-between; }
.gn-card-dist > :last-child { padding-top: var(--sp-6); border-top: var(--hairline) solid var(--line-soft); }
.gn-box-lead.gn-card-dist > :last-child { border-top-color: var(--accent); }

/* --------------------------------------------------------------- BANDS
   A full-width strip that closes a view: label column + one sentence.
   Conclusion = solid. Next-up = dashed accent. */
.gn-band { display: flex; align-items: center; gap: var(--sp-6);
           padding: var(--sp-4) var(--sp-6);
           background: var(--surface); border: var(--bw) solid var(--line); }
.gn-band > :first-child { flex: 0 1 200px; min-width: 0; max-width: 32%; }
.gn-band-future { background: none; border-style: dashed; border-color: var(--accent); }

/* --------------------------------------------------------------- CHIPS
   Status on a TIME axis, five steps. Anything with a before/after ordering
   is a chip - never grey body text. One solid .gn-chip-now per view.
     done  past, settled      → neutral solid
     now   current / shipping → accent solid
     near  next up            → accent outline
     mid   classified, undated→ hairline outline
     far   not yet            → dashed outline */
.gn-chip { display: inline-flex; align-items: center; gap: var(--sp-2);
           padding: 4px var(--sp-3); white-space: nowrap;
           font-family: var(--font-mono); font-size: var(--fs-label);
           letter-spacing: 0.08em; text-transform: uppercase; line-height: 1.4;
           border: var(--bw) solid transparent; }
.gn-chip-done { background: var(--text-2); color: var(--bg); }
.gn-chip-now  { background: var(--accent);  color: var(--on-accent); }
.gn-chip-near { border-color: var(--accent); color: var(--accent-text); }
.gn-chip-mid  { border-color: var(--line);   color: var(--text-3); }
.gn-chip-far  { border-style: dashed; border-color: var(--text-3); color: var(--text-3); }
/* semantic status variants - same base, coloured by meaning rather than time.
   Use these when the label names a machine state; use the five steps above
   when it names a position on the roadmap. */
.gn-chip-running { border-color: var(--warn); color: var(--warn); }
.gn-chip-failed  { border-color: var(--err);  color: var(--err); }
.gn-chip-queued  { border-color: var(--line); color: var(--text-3); }
/* opt-in leading dot. Only .gn-chip-running animates it. */
.gn-chip-dot::before { content: ""; width: 6px; height: 6px; flex: none;
                       background: currentColor; display: block; }
.gn-chip-running.gn-chip-dot::before { animation: gn-pulse 1.1s steps(1, end) infinite; }
@keyframes gn-pulse { 0%, 100% { opacity: 1 } 50% { opacity: 0.25 } }
@media (prefers-reduced-motion: reduce) { .gn-chip-dot::before { animation: none } }

/* -xs: the compact size. A chip that sits inside a metadata line - a date
   column, a page kicker - must not outweigh the text it annotates, so the box
   tightens and the label drops to --fs-micro. The five steps and the semantic
   variants all take it. */
.gn-chip-xs { padding: 1px 6px; font-size: var(--fs-micro); letter-spacing: 0.06em; }
.gn-chip-xs.gn-chip-dot::before { width: 5px; height: 5px; }

/* a value that does not exist yet, inline in running text */
.gn-tbu { display: inline-block; padding: 2px var(--sp-2);
          border: var(--hairline) dashed var(--text-3);
          font-family: var(--font-mono); font-size: var(--fs-label);
          letter-spacing: 0.08em; color: var(--text-3); }

/* ------------------------------------------------------------ ALIGNMENT
   Sibling groups are laid out with gap, never with margins or source
   whitespace. Grid columns are fixed counts - auto-fit leaves orphan cells. */
.gn-grid   { display: grid; gap: var(--sp-4); grid-template-columns: repeat(var(--cols, 3), minmax(0, 1fr)); }
.gn-grid-2 { --cols: 2; } .gn-grid-3 { --cols: 3; } .gn-grid-4 { --cols: 4; }
.gn-grid-tight { gap: var(--hairline); background: var(--line); }
.gn-grid-tight > * { background: var(--surface); }
/* cells divided by lines instead of gaps: last cell drops its rule */
.gn-split > * { border-right: var(--hairline) solid var(--line-soft); }
.gn-split > :last-child { border-right: 0; }
/* label + value pair, baseline aligned, value right */
.gn-pair { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-4); }
.gn-pair > :last-child { font-variant-numeric: tabular-nums; }
/* a spec strip: equal columns between two rules, one row always */
.gn-strip { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr);
            border-top: var(--bw) solid var(--line); border-bottom: var(--bw) solid var(--line); }
.gn-strip > * { padding: var(--sp-4) var(--sp-6); border-left: var(--bw) solid var(--line-soft); }
.gn-strip > :first-child { padding-left: 0; border-left: 0; }
@media (max-width: 760px) {
  .gn-strip { grid-auto-flow: row; grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .gn-strip > * { border-left: 0; padding: var(--sp-3) var(--sp-4) var(--sp-3) 0; }
}

/* ---------------------------------------------------------- SECTION HEAD
   Title + optional aside. Default: the aside sits on the title's line, so a
   sibling without one is the same height. -below puts it under the title and
   reserves that line even when it is empty (min-height), so siblings still
   match. When an inline aside wraps on a narrow screen only the row can know:
   .gn-grid-sub makes every cell a 2-row subgrid (head, body) and the head row
   takes the tallest head in the row. Rule: guidelines/layout-align.html */
.gn-sechead { display: flex; flex-wrap: wrap; align-items: baseline; column-gap: var(--sp-3); row-gap: var(--sp-1);
              min-height: calc(var(--fs-h2) * var(--lh-h2)); }
.gn-sechead-title { margin: 0; font-size: var(--fs-h2); line-height: var(--lh-h2); letter-spacing: var(--ls-h2);
                    font-weight: var(--w-strong); color: var(--text); }
.gn-sechead-sub { margin: 0; font-size: var(--fs-small); line-height: var(--lh-small); color: var(--text-2); }
.gn-sechead-below { flex-direction: column; align-items: stretch; flex-wrap: nowrap;
                    min-height: calc(var(--fs-h2) * var(--lh-h2) + var(--sp-1) + var(--fs-small) * var(--lh-small)); }
.gn-grid-sub { row-gap: var(--sp-12); align-items: start; }
.gn-grid-sub > * { display: grid; grid-row: span 2; grid-template-rows: subgrid; row-gap: var(--sp-3); min-width: 0; }

/* --------------------------------------------------------- IMAGE PLACEMENT
   Three ratios only: 16/9 screenshots, 4/3 process, 1/1 portrait (b&w).
   Never set text directly on an image - either put it beside the image,
   or lay a fade under it (.gn-bleed-fade) and put the text on the fade. */
.gn-ratio-wide     { aspect-ratio: 16 / 9; }
.gn-ratio-standard { aspect-ratio: 4 / 3; }
.gn-ratio-square   { aspect-ratio: 1 / 1; }
.gn-portrait { aspect-ratio: 1 / 1; filter: grayscale(1); }
.gn-bleed { position: relative; overflow: hidden; }
.gn-bleed > img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: 0; }
/* the fade is the only surface text may sit on over an image */
.gn-bleed-fade { position: absolute; left: 0; right: 0; bottom: 0; z-index: 1;
  height: var(--fade-h, 58%);
  background: linear-gradient(to top,
    rgb(from var(--bg) r g b / 0.94) 0%,
    rgb(from var(--bg) r g b / 0.82) 34%,
    rgb(from var(--bg) r g b / 0.42) 68%,
    rgb(from var(--bg) r g b / 0) 100%); }
.gn-bleed-text { position: relative; z-index: 2; }

/* ------------------------------------------------------- THEMED SCREENSHOT
   A product shot carries the theme it was captured in, so a single file
   contradicts one of the two themes. Ship both and let the attribute choose.
   Both images are in the DOM; only one is displayed, so there is no layout
   shift when the theme flips. */
.gn-shot { display: block; width: 100%; border: var(--bw) solid var(--line); }
[data-theme="dark"] .gn-shot-light,
[data-theme="light"] .gn-shot-dark { display: none; }

/* ---------------------------------------------------------- TABLE IN A BOX
   A table that owns its box divides the box height across its rows. */
.gn-table-fill { height: 100%; }
.gn-table-fill td { vertical-align: middle; }

/* ------------------------------------------------------------- PAGE SHELL
   Header carries identity and controls only. Section navigation belongs to
   the body, where it can show scroll position and does not compete with the
   path. Every documentation page uses this, so it lives here rather than in
   any one page's <style> - a page cannot forget to include it. */
/* Three widths, three names. They were one before, so a reading limit meant
   for long prose was also clamping intros and whole boxes.
     --container  the page shell, shared by every page
     the content column, whatever the grid hands the main area
     --measure    a READING limit, applied per element and only to long prose
   A table, a code block, a folder tree, a callout or a figure shares the outer
   alignment of its content column; it is not narrowed to --measure. */
.gn-page { max-width: var(--container); margin: 0 auto; padding: 0 var(--gutter) 120px; }
/* Opt in on the paragraph that needs it, never on a wrapper. */
.gn-read { max-width: var(--measure); }
/* The lead paragraph under a page or section heading. Narrower than .gn-read;
   see tokens/typography.css for why the two widths are different jobs. */
.gn-lead { max-width: var(--measure-lead); }
.gn-read-wide { max-width: 78ch; }
/* Everything that is not prose keeps the column's own edges. */
.gn-page-main > table, .gn-page-main > pre,
.gn-page-main > .gn-box, .gn-page-main > .gn-band,
.gn-page-main > .gn-code, .gn-page-main > figure { max-width: none; }
.gn-page-body { --pg-nav: 156px; --pg-gap: var(--sp-8);
                /* --pg-top is the real sticky-bar height. A page whose top bar wraps
                   onto two lines overrides it; the token is only the default. */
                --pg-top: var(--topbar-h);
                display: grid; grid-template-columns: var(--pg-nav) minmax(0, 1fr);
                gap: var(--pg-gap); align-items: start; }
/* An anchor target stops below the sticky bar instead of behind it. .gn-panel
   has had this since the landing page; the document shell was missing it, so
   every #section jump hid its own heading. */
.gn-page-body [id] { scroll-margin-top: calc(var(--pg-top) + var(--sp-6)); }
/* Pinned below the bar AND bounded by what is left of the screen, so a long
   list scrolls itself. Without max-height the last entries sat below the fold
   and could only be reached by scrolling the article - which moved the reader
   away from the thing they were trying to navigate to. */
.gn-page-nav { position: sticky; top: calc(var(--pg-top) + var(--sp-8));
               max-height: calc(100vh - var(--pg-top) - var(--sp-12));
               overflow-y: auto; overscroll-behavior: contain;
               scrollbar-width: thin;
               display: flex; flex-direction: column;
               border-left: var(--bw) solid var(--line); }
.gn-page-nav a { padding: 7px 0 7px 14px; margin-left: calc(var(--bw) * -1);
                 border-left: var(--sel-bar-w) solid transparent;
                 font-family: var(--font-code); font-size: var(--fs-label);
                 letter-spacing: 0.08em; text-transform: uppercase;
                 color: var(--text-3); text-decoration: none;
                 transition: color var(--dur-color) var(--ease); }
.gn-page-nav a:hover { color: var(--text); }
/* colour alone is too quiet at 11px: the weight step carries the state, the
   left bar keeps it from being colour-only */
.gn-page-nav a[aria-current="true"] { color: var(--text); border-left-color: var(--sel-bar); background: var(--sel-bg);
                                      font-weight: var(--w-strong); }
@media (max-width: 900px) {
  .gn-page-body { --pg-nav: 0px; --pg-gap: 0px; grid-template-columns: minmax(0, 1fr); }
  /* One scrolling row rather than a wrapped block: a 30-entry list would
     otherwise push the article off the first screen. */
  .gn-page-nav { position: sticky; top: var(--pg-top); z-index: 20;
                 max-height: none; flex-direction: row; flex-wrap: nowrap;
                 overflow-x: auto; overflow-y: hidden;
                 background: var(--bg);
                 border-left: 0; border-bottom: var(--bw) solid var(--line);
                 margin-bottom: var(--sp-2); }
  .gn-page-nav a { flex: none; white-space: nowrap; }
  .gn-page-nav a { border-left: 0; border-bottom: var(--sel-bar-w) solid transparent;
                   margin: 0 var(--sp-3) 0 0; padding: 7px 0; }
  .gn-page-nav a[aria-current="true"] { border-left-color: transparent;
                   border-bottom-color: var(--accent); }
}

/* ----------------------------------------------------------------- PANELS
   Landing furniture. A documentation page is a column you read top to bottom;
   a landing is a sequence of full-height statements. Same tokens, different
   rhythm - the impact comes from scale, colour fields and inversion, never
   from gradients, shadows or imagery.

   Snap is 'proximity', not 'mandatory': a panel that outgrows the viewport
   must still scroll normally instead of trapping the reader. */
/* the scroller is <html>, not the wrapper - a snap declared on a
   non-scrolling element is silently inert */
html:has(.gn-panels) { scroll-snap-type: y mandatory; }
/* A panel scales with the viewport (min-height 100vh minus the bar, padding on
   7vh), so it cannot outgrow the screen and the snap never needs relaxing.
   Only a viewport too short for one line of hero type gets the loose fallback. */
@media (max-height: 380px) { html:has(.gn-panels) { scroll-snap-type: y proximity; } }
.gn-panel {
  position: relative;
  /* the panel rests at the very top, under the bar, and pads its own content
     clear of it. Snapping to below the bar instead would leave the bar sitting
     on the PREVIOUS panel's tail, so at rest the bar could not carry this
     panel's ground. */
  min-height: 100vh; min-height: 100svh;
  scroll-snap-align: start;
  display: flex; flex-direction: column; justify-content: center;
  /* clamped to viewport height: a fixed 76px is what pushed a list panel past
     a short screen, and an overflowing panel disables the magnetic snap */
  padding: calc(var(--topbar-h) + clamp(var(--sp-6), 7vh, var(--sp-12))) 0
           clamp(var(--sp-6), 7vh, var(--sp-12));
  border-top: var(--bw) solid var(--line);
}
.gn-panel:first-of-type { border-top: 0; }
/* content-height panel - for a closing block that should not be padded to a
   full screen */
.gn-panel-auto { min-height: 0; }
/* accent field: the deck's colour-field slide, on the web. Ink is always
   --on-accent, and the panel counts against the same 1-in-5 budget. */
.gn-panel-field { background: var(--accent); color: var(--on-accent);
                  border-top-color: var(--accent); }
.gn-panel-field .gn-label,
.gn-panel-field .gn-panel-num { color: rgb(from var(--on-accent) r g b / 0.62); }
/* invert: the theme's ink becomes the surface */
.gn-panel-invert { background: var(--text); color: var(--bg);
                   border-top-color: var(--text); }
.gn-panel-invert .gn-label,
.gn-panel-invert .gn-panel-num { color: rgb(from var(--bg) r g b / 0.6); }
/* the invert face is light in both themes, so accent cannot be legible as
   text on it. The link takes the face's own ink and gains an accent fill on
   hover - a shape, never an underline. */
.gn-panel-invert a { color: currentColor; }
.gn-panel-invert a:hover { background: var(--accent); color: var(--on-accent);
                           box-shadow: 0 0 0 3px var(--accent); }
/* one step of surface, not a new face (2C). Only one panel takes it: --bg and
   --bg-2 are close enough that the ink does not change. */
.gn-panel-deep { background: var(--bg-2); }

/* the bar belongs to the ground it sits on (1B). The deck already works this
   way - a colour-field slide's chrome takes that field's ink. panels.js writes
   the face of whatever panel is under the bar onto .gn-panels[data-tone];
   there is no selector that reaches from a sticky bar to the section passing
   beneath it. background: inherit does not work here - it resolves against the
   page wrapper, which never sees a panel. */
/* The bar does not move: the panel passing under it does. So the bar paints the
   panel's ground itself - two stops with the panel boundary between them, and
   panels.js moves that boundary as the incoming panel rises through the bar.
   A transparent bar would let the body TEXT show through; painting the same
   grounds keeps only the colour. --gn-edge is the boundary's y inside the bar. */
.gn-panels {
  --tone-ink: var(--bg); --tone-deep: var(--bg-2);
  --tone-field: var(--accent); --tone-invert: var(--text);
  --gn-edge: 0px; --gn-bar-from: var(--bg); --gn-bar-to: var(--bg);
}
.gn-panels .gn-topbar {
  background: linear-gradient(to bottom,
    var(--gn-bar-from) calc(var(--gn-edge) - 1px),
    var(--gn-bar-to)   calc(var(--gn-edge) + 1px));
  transition: color var(--dur-color) linear;
}
/* ink follows the ground that holds most of the bar; panels.js flips [data-tone]
   as the boundary passes the bar's middle. */
.gn-panels[data-tone="field"] .gn-topbar {
  color: var(--on-accent);
  border-bottom-color: rgb(from var(--on-accent) r g b / 0.28); }
.gn-panels[data-tone="invert"] .gn-topbar {
  color: var(--bg);
  border-bottom-color: rgb(from var(--bg) r g b / 0.28); }
/* .gn-wordmark sets its own colour, so on a colour face it keeps the page ink
   and disappears. Hand it back the bar's ink before the cursor reads it. */
.gn-panels[data-tone="field"] .gn-topbar .gn-wordmark,
.gn-panels[data-tone="invert"] .gn-topbar .gn-wordmark { color: inherit; }
/* the cursor stays accent on every ground - it is the mark. The one exception
   is the accent field itself, where accent on accent is invisible and the
   cursor takes the face's ink. */
.gn-panels[data-tone="field"] .gn-topbar .gn-wordmark-cursor { background: currentColor; }
.gn-panels[data-tone="invert"] .gn-topbar .gn-wordmark-cursor { background: var(--accent); }
/* secondary text in the bar dims with opacity, never with a colour built from
   currentColor: inside `color`, currentColor is resolved against the inherited
   value and never re-reads the ancestor's [data-tone], which made the crumbs
   disappear on the accent and invert faces. */
.gn-panels[data-tone="field"] .gn-topbar .gn-crumbs,
.gn-panels[data-tone="invert"] .gn-topbar .gn-crumbs { color: inherit; opacity: 0.62; }
/* .gn-crumbs > :last-child carries its own --text, which is the page ink, not
   this face's. */
.gn-panels[data-tone="field"] .gn-topbar .gn-crumbs > *,
.gn-panels[data-tone="invert"] .gn-topbar .gn-crumbs > * { color: inherit; }
.gn-panels[data-tone="field"] .gn-topbar .gn-seg,
.gn-panels[data-tone="invert"] .gn-topbar .gn-seg { border-color: currentColor; }
.gn-panels[data-tone="field"] .gn-topbar .gn-seg-opt,
.gn-panels[data-tone="invert"] .gn-topbar .gn-seg-opt { color: currentColor; opacity: 0.56; }
.gn-panels[data-tone="field"] .gn-topbar .gn-seg-opt[aria-selected="true"],
.gn-panels[data-tone="invert"] .gn-topbar .gn-seg-opt[aria-selected="true"] { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .gn-panels .gn-topbar { transition: none; } }

/* position indicator - a fixed stack of marks beside the panels. Each mark is
   a link, so it doubles as navigation. */
.gn-panel-dots { position: fixed; z-index: 40; right: var(--sp-6); top: 50%;
                 transform: translateY(-50%);
                 display: flex; flex-direction: column; gap: var(--sp-2); }
.gn-panel-dots a { width: 10px; height: 10px; display: block;
                   border: var(--bw) solid var(--text-3); background: none;
                   transition: background var(--dur-color) var(--ease),
                               border-color var(--dur-color) var(--ease); }
.gn-panel-dots a:hover { border-color: var(--accent); }
.gn-panel-dots a[aria-current="true"] { background: var(--accent);
                                        border-color: var(--accent); }
/* on an accent or inverted panel the marks take that face's ink */
.gn-panel-dots[data-on="field"] a { border-color: rgb(from var(--on-accent) r g b / 0.5); }
.gn-panel-dots[data-on="field"] a[aria-current="true"] { background: var(--on-accent);
                                        border-color: var(--on-accent); }
.gn-panel-dots[data-on="invert"] a { border-color: rgb(from var(--bg) r g b / 0.5); }
.gn-panel-dots[data-on="invert"] a[aria-current="true"] { background: var(--bg);
                                        border-color: var(--bg); }
/* trailing band: a thin mark, not a square - the shape says "this one is a
   strip, not a screen" */
.gn-panel-dots a[data-bar] { height: 3px; }
@media (max-width: 900px) { .gn-panel-dots { display: none; } }
/* the one oversized statement a panel is built around */
.gn-hero { margin: 0; font-size: var(--fs-hero); line-height: 0.98;
           font-weight: var(--w-strong); letter-spacing: var(--ls-hero); }
.gn-hero-sub { max-width: var(--measure); margin: var(--sp-6) 0 0;
               font-size: 19px; line-height: 1.5; }
@media (prefers-reduced-motion: reduce) { html:has(.gn-panels) { scroll-snap-type: none; } }

/* -------------------------------------------------------------- PAGE HEAD
   Every document page opens with the same four slots, so a reader can tell
   which page they are on without reading a word. Level shows in the title
   size ONLY - never by adding or dropping a slot.
     kicker - the page's SCALE, as a figure. Never the title again.
     title  - the page's NAME.
     lead   - what this page decides. One paragraph.
     strip  - four cells of hard numbers (.gn-strip).
   .gn-pagehead-hub raises the title one step for the section index. The
   landing page is not a document page: it uses .gn-hero inside a panel. */
.gn-pagehead { padding: var(--sp-12) 0 var(--sp-8); }
.gn-pagehead-kicker { display: flex; align-items: center; gap: var(--sp-3);
                      font-family: var(--font-code); font-size: var(--fs-label);
                      letter-spacing: var(--ls-label); text-transform: uppercase;
                      color: var(--text-3); }
.gn-pagehead-title { margin: var(--sp-4) 0 0; font-size: var(--fs-h1);
                     line-height: 1.06; font-weight: var(--w-strong);
                     letter-spacing: var(--ls-h1); }
.gn-pagehead-hub .gn-pagehead-title { font-size: clamp(34px, 5vw, 56px);
                     line-height: 1.04; letter-spacing: -0.04em; }
.gn-pagehead-lead { max-width: var(--measure); margin: var(--sp-4) 0 0;
                    font-size: 17px; line-height: 1.5; color: var(--text-2); }
.gn-pagehead .gn-strip { margin: var(--sp-6) 0 0; }

/* ------------------------------------------------------------------ FOOTER
   Every page closed with its own hand-built row, so they drifted - different
   inks, a missing link, an inverted wordmark left on a plain surface. One
   pattern instead: mark and year on the left, links on the right, a rule
   above. Put it last inside .gn-page or a .gn-panel-auto band. */
.gn-footer {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-4);
  margin-top: var(--sp-12); padding-top: var(--sp-6);
  border-top: var(--bw) solid var(--line);
  font-family: var(--font-code); font-size: var(--fs-label);
  letter-spacing: 0.06em; color: var(--text-3);
}
.gn-footer-links { margin-left: auto; display: flex; gap: var(--sp-4); }
.gn-footer a { color: var(--text-3); text-decoration: none; }
.gn-footer a:hover { color: var(--accent-text); }
/* Exception, and the only one: a fixed-height shell - the deck, with its stage
   and thumbnail rail - has no page end to close, so it carries no footer. Its
   crumb and language control sit in the top bar instead. Any page that scrolls
   takes the footer. */
/* On an inverted or accent plate the footer takes that plate's ink. */
.gn-panel-invert .gn-footer, .gn-panel-field .gn-footer {
  border-top-color: rgb(from currentColor r g b / 0.28);
  color: rgb(from currentColor r g b / 0.62);
}

/* components.css - gn-* class layer (P3: core + forms).
   Built entirely on tokens; no literal colours, no radii, no shadows.
   Load AFTER tokens: styles.css already imports both in order. */

/* ---------------------------------------------------------------- BUTTON */
.gn-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2);
  height: var(--h-md); padding: 0 var(--sp-4);
  /* 테두리는 --line-strong: 바탕·면·칸색 위 3:1 이상(task 3956 에서 --text-3 보다 한 단계 연하게). --line(1.3:1)이면 흰 버튼 가장자리가 안 보여
     같은 높이의 채움 버튼 옆에서 작아 보임(task 3949). 채움 버튼은 테두리 = 채움색이라 박스가 같다 */
  border: var(--bw) solid var(--line-strong); background: var(--surface); color: var(--text);
  font-family: var(--font); font-size: var(--fs-small); font-weight: var(--w-strong);
  line-height: 1; white-space: nowrap; cursor: pointer;
  transition: var(--transition-control);
}
.gn-btn:hover { border-color: var(--accent); color: var(--accent-text); }
.gn-btn:active { transform: translateY(1px); }
.gn-btn[disabled], .gn-btn[aria-disabled="true"] {
  border-color: var(--line-soft); color: var(--text-3); cursor: not-allowed; transform: none;
}
.gn-btn-brand { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.gn-btn-brand:hover { background: var(--accent-deep); border-color: var(--accent-deep); color: var(--on-accent); }
.gn-btn-ghost { background: transparent; border-color: transparent; color: var(--text-2); font-weight: var(--w-regular); }
.gn-btn-ghost:hover { background: transparent; border-color: transparent; color: var(--text); }
.gn-btn-danger { background: transparent; border-color: var(--err); color: var(--err); }
.gn-btn-danger:hover { background: var(--err); border-color: var(--err); color: var(--bg); }
.gn-btn-sm { height: var(--h-sm); padding: 0 var(--sp-3); font-size: 12.5px; }
.gn-btn-lg { height: var(--h-lg); padding: 0 var(--sp-6); font-size: var(--fs-body); }
.gn-btn-block { width: 100%; }

/* ------------------------------------------------------------ ICONBUTTON */
.gn-iconbtn {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--h-md); height: var(--h-md); flex: none;
  border: var(--bw) solid var(--line-strong);   /* ⑯ 버튼과 같은 가장자리 */ background: var(--surface); color: var(--text);
  font-family: var(--font-mono); font-size: 15px; line-height: 1; cursor: pointer;
  transition: var(--transition-control);
}
.gn-iconbtn:hover { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.gn-iconbtn-sm { width: var(--h-sm); height: var(--h-sm); font-size: 13px; }
.gn-iconbtn[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }

/* ------------------------------------------------------------------ ICON */
.gn-icon { display: inline-block; width: 1em; height: 1em; font-family: var(--font-mono); font-style: normal; line-height: 1; }
.gn-icon-accent { color: var(--accent-text); }

/* ------------------------------------------------------------------ CARD */
.gn-card { background: var(--surface); border: var(--bw) solid var(--line); }
.gn-card-interactive { transition: border-color var(--dur-color) var(--ease); cursor: pointer; }
.gn-card-interactive:hover { border-color: var(--accent); }
.gn-card-accent { border-left: 3px solid var(--accent); }
.gn-card-head { padding: var(--sp-4); border-bottom: var(--bw) solid var(--line-soft); }
.gn-card-title { font-size: var(--fs-h2); font-weight: var(--w-strong); letter-spacing: var(--ls-h2); }
.gn-card-desc { margin-top: var(--sp-1); font-size: var(--fs-small); color: var(--text-2); }
.gn-card-body { padding: var(--sp-4); }
.gn-card-foot { padding: var(--sp-3) var(--sp-4); border-top: var(--bw) solid var(--line-soft); display: flex; gap: var(--sp-2); justify-content: flex-end; }
.gn-card-sep { height: var(--bw); margin: var(--sp-4) 0; background: var(--line-soft); }

/* ----------------------------------------------------------------- BADGE */
.gn-badge {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  padding: 3px var(--sp-2);
  border: var(--bw) solid var(--line-soft); background: transparent; color: var(--text-2);
  font-family: var(--font-mono); font-size: var(--fs-label); letter-spacing: 0.06em; line-height: 1.4;
}
.gn-badge-brand   { background: var(--accent); border-color: var(--accent); color: var(--on-accent); text-transform: uppercase; }
.gn-badge-success { background: var(--ok-weak);   border-color: var(--ok);   color: var(--ok); }
.gn-badge-warning { background: var(--warn-weak); border-color: var(--warn); color: var(--warn); }
.gn-badge-danger  { background: var(--err-weak);  border-color: var(--err);  color: var(--err); }
.gn-badge-info    { background: var(--info-weak); border-color: var(--info); color: var(--info); }
.gn-badge-dot::before { content: ""; width: 6px; height: 6px; background: currentColor; display: block; }

/* ------------------------------------------------------------------- TAG */
.gn-tag {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  padding: 5px var(--sp-3);
  border: var(--bw) solid var(--line-soft); background: transparent; color: var(--text-2);
  font-size: 12.5px; cursor: pointer; transition: var(--transition-control);
}
.gn-tag:hover { border-color: var(--line); color: var(--text); }
.gn-tag[aria-selected="true"] { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }   /* 토글 = 민트 채움(선택 규칙) */
.gn-tag-x { color: var(--text-3); font-family: var(--font-mono); }
.gn-tag-x:hover { color: var(--err); }

/* ----------------------------------------------------------------- TABLE */
.gn-table { width: 100%; border-collapse: collapse; font-size: var(--fs-small); }
.gn-table th {
  text-align: left; padding: var(--sp-2) var(--sp-3);
  border-bottom: var(--bw) solid var(--line);
  font-family: var(--font-mono); font-size: 10.5px; font-weight: var(--w-regular);
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-3);
}
.gn-table td { padding: var(--sp-2) var(--sp-3); border-bottom: var(--hairline) solid var(--line-soft); }
.gn-table tr:last-child td { border-bottom: 0; }
.gn-table-num { text-align: right; font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.gn-table-empty { padding: var(--sp-8) var(--sp-4); text-align: center; color: var(--text-3); }

/* --------------------------------------------------------------- DIVIDER */
.gn-divider { height: var(--bw); margin: var(--sp-6) 0; background: var(--line); border: 0; }
.gn-divider-tight { margin: var(--sp-3) 0; background: var(--line-soft); }
.gn-divider-label { display: flex; align-items: center; gap: var(--sp-3); margin: var(--sp-6) 0; }
.gn-divider-label::before, .gn-divider-label::after { content: ""; flex: 1; height: var(--hairline); background: var(--line-soft); }
.gn-divider-label > span {
  font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--text-3);
}

/* ---------------------------------------------------------------- FIGURE */
.gn-figure { margin: 0; }
.gn-figure img { display: block; width: 100%; border: var(--bw) solid var(--line); }
.gn-figure-slot {
  display: flex; align-items: center; justify-content: center; aspect-ratio: 16 / 9;
  background: var(--bg-2); border: var(--bw) solid var(--line);
  font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--text-3);
}
.gn-figure-cap { margin-top: var(--sp-2); font-size: 12.5px; color: var(--text-2); }

/* ----------------------------------------------------------------- FIELD */
.gn-field { display: block; }
.gn-field-label {
  display: block; font-family: var(--font-mono); font-size: var(--fs-label);
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--text-3);
}
.gn-field-req { color: var(--accent-text); }
.gn-field-hint { margin-top: var(--sp-2); font-size: 12.5px; color: var(--text-3); }
.gn-field-error { margin-top: var(--sp-2); font-size: 12.5px; color: var(--err); }
.gn-field-error::before { content: "\00d7  "; font-family: var(--font-mono); }

/* --------------------------------------------------- INPUT / TEXTAREA / SELECT */
.gn-input, .gn-textarea, .gn-select {
  width: 100%; margin-top: 0; padding: 0 var(--sp-3);
  background: var(--bg); color: var(--text);
  border: var(--bw) solid var(--line-strong);   /* 버튼과 같은 가장자리 - 나란히 두면 같은 박스로 보이게 */
  font-family: var(--font); font-size: var(--fs-small);
  transition: var(--transition-control);
}
.gn-input, .gn-select { height: var(--h-md); }
.gn-textarea { min-height: 88px; padding: 10px var(--sp-3); resize: vertical; line-height: var(--lh-body); }
.gn-input::placeholder, .gn-textarea::placeholder { color: var(--text-3); }
.gn-input:hover, .gn-textarea:hover, .gn-select:hover { border-color: var(--text-2); }
/* 라벨 아래 간격은 라벨 쪽 관계로만 - 입력칸 자체에 margin-top 을 두면 버튼과 같은 줄에서 위끝이 7px 내려갔다(task 3949) */
.gn-field-label + :is(.gn-input, .gn-textarea, .gn-select) { margin-top: var(--sp-2); }
.gn-input[aria-invalid="true"], .gn-textarea[aria-invalid="true"], .gn-select[aria-invalid="true"] {
  border-color: var(--err); background: var(--err-weak);
}
.gn-input-mono { font-family: var(--font-mono); }
.gn-input-sm { height: var(--h-sm); font-size: 12.5px; }

/* ------------------------------------------------------ CHECKBOX / RADIO */
.gn-check { display: flex; align-items: center; gap: var(--sp-3); font-size: var(--fs-small); cursor: pointer; }
.gn-check-box, .gn-check-radio {
  width: 18px; height: 18px; flex: none;
  display: flex; align-items: center; justify-content: center;
  background: var(--bg); border: var(--bw) solid var(--line);
  font-family: var(--font-mono); font-size: 11px; color: transparent;
  transition: var(--transition-control);
}
.gn-check input { position: absolute; opacity: 0; width: 0; height: 0; }
.gn-check input:checked + .gn-check-box { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.gn-check input:checked + .gn-check-radio::after { content: ""; width: 8px; height: 8px; background: var(--accent); display: block; }
.gn-check input:focus-visible + .gn-check-box,
.gn-check input:focus-visible + .gn-check-radio { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
.gn-check-desc { margin-top: 2px; font-size: 12.5px; color: var(--text-3); }

/* ---------------------------------------------------------------- SWITCH */
.gn-switch { display: flex; align-items: center; gap: var(--sp-3); font-size: var(--fs-small); cursor: pointer; }
.gn-switch-track {
  width: 38px; height: 22px; flex: none; padding: 2px;
  display: flex; align-items: center;
  background: var(--bg-2); border: var(--bw) solid var(--line);
  transition: var(--transition-control);
}
.gn-switch-knob { width: 14px; height: 14px; background: var(--text-3); transition: var(--transition-control); }
.gn-switch input { position: absolute; opacity: 0; width: 0; height: 0; }
.gn-switch input:checked + .gn-switch-track { background: var(--accent); border-color: var(--accent); justify-content: flex-end; }
.gn-switch input:checked + .gn-switch-track .gn-switch-knob { background: var(--on-accent); }

/* ------------------------------------------------------ SEGMENTEDCONTROL */
.gn-seg { display: inline-flex; border: var(--bw) solid var(--line); }
.gn-seg-opt {
  padding: var(--sp-2) var(--sp-4); border: 0; border-left: var(--bw) solid var(--line);
  background: transparent; color: var(--text-2);
  font-family: var(--font); font-size: var(--fs-small); cursor: pointer;
  transition: var(--transition-control);
}
.gn-seg-opt:first-child { border-left: 0; }
.gn-seg-opt:hover { color: var(--text); }
.gn-seg-opt[aria-selected="true"] { background: var(--accent); color: var(--on-accent); }
.gn-seg-block { display: flex; width: 100%; }
.gn-seg-block .gn-seg-opt { flex: 1; }
/* -sm: the header control size. Mono, uppercase, 10px - two of these sit
   beside the nav without competing with it. */
.gn-seg-sm .gn-seg-opt {
  padding: 5px 9px; font-family: var(--font-code); font-size: var(--fs-micro);
  letter-spacing: 0.1em; text-transform: uppercase;
}

/* -------------------------------------------------------------- WORDMARK */
/* see components/wordmark.css */

/* ============================================================ P4 LAYER */

/* ---------------------------------------------------- DIALOG / OVERLAY */
.gn-overlay {
  position: fixed; inset: 0; z-index: 200;
  display: flex; align-items: center; justify-content: center; padding: var(--sp-6);
  background: color-mix(in oklab, var(--bg) 78%, transparent);
  backdrop-filter: blur(3px);
}
.gn-dialog {
  width: 100%; max-width: 460px;
  background: var(--surface); border: var(--bw) solid var(--line);
}
.gn-dialog-head { padding: var(--sp-4); border-bottom: var(--bw) solid var(--line-soft); display: flex; align-items: flex-start; gap: var(--sp-3); }
.gn-dialog-title { font-size: 17px; font-weight: var(--w-strong); letter-spacing: -0.01em; }
.gn-dialog-desc { margin-top: var(--sp-2); font-size: var(--fs-small); color: var(--text-2); }
.gn-dialog-body { padding: var(--sp-4); font-size: var(--fs-small); color: var(--text-2); }
.gn-dialog-foot { padding: var(--sp-3) var(--sp-4); border-top: var(--bw) solid var(--line-soft); display: flex; gap: var(--sp-2); justify-content: flex-end; }
.gn-dialog-danger .gn-dialog-title { color: var(--err); }

/* ------------------------------------------------------------- TOASTER */
.gn-toaster {
  position: fixed; z-index: 300; right: var(--sp-6); bottom: var(--sp-6);
  display: flex; flex-direction: column; gap: var(--sp-2); width: min(360px, calc(100vw - var(--sp-8)));
}
.gn-toast {
  display: flex; align-items: flex-start; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  background: var(--surface); border: var(--bw) solid var(--line);
  border-left: 3px solid var(--text-3);
  font-size: var(--fs-small);
  animation: gn-toast-in var(--dur-enter) var(--ease);
}
@keyframes gn-toast-in { from { opacity: 0; transform: translateY(6px) } to { opacity: 1; transform: none } }
.gn-toast-success { border-left-color: var(--ok); }
.gn-toast-danger  { border-left-color: var(--err); }
.gn-toast-info    { border-left-color: var(--info); }
.gn-toast-icon { font-family: var(--font-mono); line-height: 1.5; }
.gn-toast-title { font-weight: var(--w-strong); }
.gn-toast-desc { margin-top: 2px; color: var(--text-2); font-size: 12.5px; }

/* ------------------------------------------------------------- TOOLTIP */
.gn-tip-wrap { position: relative; display: inline-flex; }
.gn-tip {
  position: absolute; left: 50%; bottom: calc(100% + 8px); transform: translateX(-50%);
  padding: 5px var(--sp-2);
  background: var(--text); color: var(--bg);
  font-family: var(--font-mono); font-size: 11.5px; white-space: nowrap;
  opacity: 0; pointer-events: none;
  transition: opacity var(--dur-color) var(--ease) 400ms;
}
.gn-tip-wrap:hover .gn-tip, .gn-tip-wrap:focus-within .gn-tip { opacity: 1; }
.gn-tip-below { bottom: auto; top: calc(100% + 8px); }

/* -------------------------------------------------------------- TOPBAR */
/* the mark and the page path are ONE line of text, so they share a line box
   and therefore a baseline. Centring their single box then centres both. */
/* the path starts on the body's content line; the mark is pinned to the bar's
   own left edge, so it keeps its place while the path moves to the line. */
.gn-topbar-brand { display: contents; }
/* same size as the mark and the nav, so all three boxes centre to one baseline */
.gn-topbar .gn-crumbs { font-size: var(--fs-small); white-space: nowrap; }
/* a fixed slot for the mark, so the path's minimum position is known in CSS
   rather than depending on how wide the name happens to render. */
/* the mark's own minimum width (wordmark.css) - the two must not drift apart */
.gn-topbar { --brand-slot: 88px; }

/* The path starts on the body's content line. It stays in the flow and is
   nudged there by a margin, clamped at 0 so it can never move left of the
   mark - self-correcting at every width, with no breakpoint and no jump.
   The 50% resolves against the bar's content box, so it excludes the gutters
   and the scrollbar alike. */
.gn-topbar .gn-crumbs {
  margin-left: max(0px, calc(50% + var(--gutter) - var(--container) / 2
                             - var(--brand-slot) - var(--sp-6)));
}
.gn-topbar-brand > * { vertical-align: baseline; }
.gn-topbar-brand > * + * { margin-left: var(--sp-3); }
/* The mark is a text run with the same box as any other text run here, so
   centring boxes centres the text with it. */
.gn-topbar {
  display: flex; align-items: center; gap: var(--sp-6);
  height: var(--topbar-h); line-height: 1;
  /* the rule spans the viewport; the contents sit on the body's measure, so the
     first header item starts on the same line as the first content item */
  position: relative; padding-inline: var(--gutter);
  background: var(--bg); border-bottom: var(--bw) solid var(--line);
}
.gn-topbar-brand, .gn-topbar-nav, .gn-topbar-nav a, .gn-crumbs { line-height: 1; }
.gn-topbar-sticky { position: sticky; top: 0; z-index: 60; }
/* one mark size for the bar. The 0.5em clear space is a standalone-placement
   rule; in a row it only inflates the box. */
.gn-topbar .gn-wordmark { font-size: 13px; padding: 0; width: var(--brand-slot); min-width: var(--brand-slot); flex: none; }

.gn-topbar-nav { display: flex; gap: var(--sp-4); margin-left: auto; font-size: var(--fs-small); }
.gn-topbar-nav a { color: var(--text-2); padding: 4px 0 2px; border-bottom: var(--sel-bar-w) solid transparent; }
.gn-topbar-nav a:hover { color: var(--text); }
.gn-topbar-nav a[aria-current="page"] { color: var(--text); border-bottom-color: var(--sel-bar); }   /* 가로 = 탭 규칙: 아래 3px 띠 */
.gn-topbar-actions { margin-left: auto; display: flex; gap: var(--sp-2); }

/* ------------------------------------------------------------- SIDENAV */
.gn-sidenav {
  width: var(--sidebar-w); flex: none; padding: var(--sp-4) 0;
  background: var(--bg-2); border-right: var(--bw) solid var(--line);
  font-size: var(--fs-small);
}
.gn-sidenav-group {
  padding: var(--sp-4) var(--sp-4) var(--sp-2);
  font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--text-3);
}
.gn-sidenav-group:first-child { padding-top: 0; }
.gn-sidenav-item {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-4);
  border-left: 3px solid transparent; color: var(--text-2); cursor: pointer;
  transition: var(--transition-control);
}
.gn-sidenav-item:hover { color: var(--text); }
.gn-sidenav-item[aria-current="page"] { background: var(--sel-bg); border-left-color: var(--sel-bar); color: var(--text); }
.gn-sidenav-meta { margin-left: auto; font-family: var(--font-mono); font-size: 11px; color: var(--text-3); }

/* ---------------------------------------------------------------- TABS */
.gn-tabs { display: flex; border-bottom: var(--bw) solid var(--line-soft); font-size: var(--fs-small); }
.gn-tab {
  padding: var(--sp-2) var(--sp-4); margin-bottom: calc(var(--bw) * -1);
  border: 0; border-bottom: var(--sel-bar-w) solid transparent; background: transparent;
  color: var(--text-2); font-family: var(--font); font-size: var(--fs-small); cursor: pointer;
  transition: var(--transition-control);
}
.gn-tab:hover { color: var(--text); }
.gn-tab[aria-selected="true"] { color: var(--text); border-bottom-color: var(--sel-bar); }

/* --------------------------------------------------------- BREADCRUMB */
/* a text run like the wordmark: a flex box would take its baseline from a
   flex item and sit off the line of whatever text it stands beside. */
/* 11px standalone; the topbar overrides to 13px so its three text runs share
   one box height and therefore one baseline (see .gn-topbar .gn-crumbs). */
.gn-crumbs { display: inline; font-family: var(--font-mono); font-size: 11px; color: var(--text-3); }
.gn-crumbs > * { vertical-align: baseline; }
.gn-crumbs > * + * { margin-left: var(--sp-2); }
.gn-crumbs > :last-child { color: var(--text); }

/* STATUS CHIP - moved to components/layout.css (one chip family, 5 steps).
   .gn-chip-running / -failed / -queued live there as semantic variants. */

/* -------------------------------------------------------- SLIDE FRAME */
.deck-scope .gn-slide {
  position: relative; width: 100%; aspect-ratio: 16 / 9;
  padding: 4.4%; background: var(--surface); border: var(--bw) solid var(--line);
  container-type: inline-size;
}
.deck-scope .gn-slide-field { background: var(--accent); color: var(--on-accent); }
.deck-scope .gn-slide-invert { background: var(--text); color: var(--bg); }
.deck-scope .gn-slide-title { font-size: clamp(22px, 4cqw, 76px); line-height: 1.06; font-weight: var(--w-strong); letter-spacing: -0.04em; }
.deck-scope .gn-slide-body { font-size: clamp(13px, 1.7cqw, 32px); line-height: 1.5; color: var(--text-2); }
.deck-scope .gn-slide-label {
  font-family: var(--font-mono); font-size: clamp(11px, 1.25cqw, 24px); letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--text-3);
}
.deck-scope .gn-slide-field .gn-slide-label,
.deck-scope .gn-slide-field .gn-slide-body { color: inherit; opacity: 0.72; }
.deck-scope .gn-slide-num {
  position: absolute; right: 4.4%; bottom: 4.4%;
  font-family: var(--font-mono); font-size: clamp(11px, 1.25cqw, 24px); color: var(--text-3);
}

/* 남의 브랜드 마크. 우리 아이콘 규칙('자체 세트 없음 · 유니코드 글리프')의 예외다 -
   GitHub 같은 곳으로 나가는 링크는 그쪽 공식 마크를 원형 그대로 쓴다. 고치거나 다시 그리지 않는다.
   출처: GitHub Octicons(mark-github, MIT). 링크를 가리키는 용도로만 쓰고, 색은 currentColor 로 지면을 따른다. */
.gn-brandmark { display: inline-flex; align-items: center; gap: 6px; }
.gn-brandmark svg { fill: currentColor; display: block; }

/* ------------------------------------------------------------- separator
   A vertical hairline between two groups on one line. Uses --sep, not --line:
   see tokens/colors.css for why a separator and a border are not the same
   colour on the dark theme. */
.gn-sep { width: var(--bw); height: 14px; background: var(--sep); flex: 0 0 auto; }

/* --------------------------------------------------------------- navlink
   A text link in a header or footer: quiet at rest, accent on hover. The
   colour is the whole affordance - there is no underline anywhere in this
   system (tokens/base.css).

   EXCEPTION, recorded 2026-09-22: on the light theme --accent-text is 1.74:1
   on --bg, under the 4.5:1 body minimum. It is allowed HERE and only here
   because this is a hover state - a moment, not a reading state. The resting
   colour is --text-2 (5.75:1 light, 6.50:1 dark) and carries the reading. No
   sentence, label or body run may take this exception. */
.gn-navlink { color: var(--text-2); }
.gn-navlink:hover { color: var(--accent-text); }

/* Breadcrumb link - the quiet half of .gn-crumbs. Same hover as .gn-navlink,
   one step quieter at rest because a crumb is where you came from, not where
   you are. */
.gn-crumb-link { color: var(--text-3); }
.gn-crumb-link:hover { color: var(--accent-text); }

/* wordmark - typeset, never an image: the name in mono followed by a
   block cursor (4b). The cursor IS the mark - no symbol, no SVG logo. */
/* a text run, not a flex box: the mark must share the baseline of any text it
   sits beside. An inline-flex derives its baseline from a flex item instead,
   which put it a few px off every nav link it was placed next to. */
.gn-wordmark {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: 21px;
  font-weight: var(--w-regular, 400);
  letter-spacing: -0.02em;
  line-height: 1;
  color: var(--text);
  white-space: nowrap;
  min-width: 88px;           /* 최소 폭 */
  padding: 0.5em 0;          /* 좌우 여백은 글자 높이의 1배 */
}
.gn-wordmark-cursor {
  display: inline-block;
  width: 0.52em;
  height: 0.92em;
  margin-left: 0.16em;
  background: var(--accent);
  /* sits ON the baseline: its bottom edge lines up with the bottom of h and n,
     the way a terminal caret does. Not below it - a dropped caret reads as a
     misaligned block rather than part of the type. */
  vertical-align: baseline;
  animation: gn-blink 1.06s steps(1, end) infinite;
}
@keyframes gn-blink { 0%, 49% { opacity: 1 } 50%, 100% { opacity: 0 } }
@media (prefers-reduced-motion: reduce) { .gn-wordmark-cursor { animation: none; } }
.gn-wordmark-static .gn-wordmark-cursor { animation: none; }
.gn-wordmark-sub   { color: var(--text-2); }

/* A prompt caret, which is NOT the mark. §11 makes the cursor the mark, so a
   terminal caret that happens to look the same needs its own class - reusing
   .gn-wordmark-cursor outside a .gn-wordmark makes one class mean both. Same
   geometry, static by default: §7 allows one blinking cursor per page and the
   header mark is it. A page that has no mark in view may opt in with -live. */
.gn-caret {
  display: inline-block;
  width: 0.52em;
  height: 0.92em;
  margin-left: 0.16em;
  background: var(--accent);
  vertical-align: baseline;
}
.gn-caret-live { animation: gn-blink 1.06s steps(1, end) infinite; }
@media (prefers-reduced-motion: reduce) { .gn-caret-live { animation: none; } }

/* invert - the mark on the theme's ink face. The cursor STAYS accent: mint on
   an ink ground reads fine, and the cursor is the mark, so muting it to the
   face's own colour erases the one part §11 calls the mark itself.
   An accent face is the case that needs the swap - use -on-accent there. */
.gn-wordmark-invert { color: var(--bg); }
.gn-wordmark-on-accent { color: var(--on-accent); }
.gn-wordmark-on-accent .gn-wordmark-cursor { background: currentColor; }

.gn-wordmark-sm { font-size: 15px; min-width: 66px; }
.gn-wordmark-lg { font-size: 34px; min-width: 140px; }

/* lockup: mark + name, gap = 0.5 * cap height */
.gn-lockup { display: inline-flex; align-items: center; gap: 0.5em; }
.gn-lockup .gn-mark { width: 1.15em; height: 1.15em; flex: none; }

/* extended.css - the second gn-* layer (added 2026.09.03).
   Same rules as components.css: tokens only, no literal colours, no radii,
   no shadows, no circles. Load AFTER components.css. */

/* -------------------------------------------------------------- A11Y */
.gn-sr { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.gn-skip { position: fixed; top: var(--sp-2); left: var(--sp-2); z-index: var(--z-tooltip); padding: var(--sp-2) var(--sp-3); background: var(--accent); color: var(--on-accent); font-family: var(--font-mono); font-size: var(--fs-label); letter-spacing: var(--ls-label); text-transform: uppercase; transform: translateY(-200%); }
.gn-skip:focus { transform: none; color: var(--on-accent); }

/* --------------------------------------------------------- ACCORDION */
.gn-acc { border: var(--bw) solid var(--line); background: var(--surface); }
.gn-acc-item + .gn-acc-item { border-top: var(--bw) solid var(--line-soft); }
.gn-acc-trigger { display: flex; align-items: center; gap: var(--sp-3); width: 100%; padding: var(--row-pad-y) var(--sp-4); border: 0; background: transparent; color: var(--text); font-family: var(--font); font-size: var(--fs-small); font-weight: var(--w-strong); text-align: left; cursor: pointer; transition: var(--transition-control); }
.gn-acc-trigger:hover { color: var(--accent-text); }
.gn-acc-mark { font-family: var(--font-mono); color: var(--text-3); }
.gn-acc-trigger[aria-expanded="true"] .gn-acc-mark { color: var(--accent-text); }
.gn-acc-meta { margin-left: auto; font-family: var(--font-mono); font-size: var(--fs-label); letter-spacing: 0.06em; color: var(--text-3); }
.gn-acc-panel { padding: 0 var(--sp-4) var(--sp-4) var(--sp-8); font-size: var(--fs-small); color: var(--text-2); }

/* --------------------------------------------------- COMMAND PALETTE */
.gn-cmdk-overlay { position: fixed; inset: 0; z-index: var(--z-popover); display: flex; justify-content: center; padding-top: 12vh; background: color-mix(in oklab, var(--bg) 78%, transparent); backdrop-filter: blur(3px); }
.gn-cmdk { width: min(620px, calc(100vw - var(--sp-8))); max-height: 60vh; display: flex; flex-direction: column; background: var(--surface); border: var(--bw) solid var(--line); border-left: 3px solid var(--accent); }
.gn-cmdk-row { display: flex; align-items: center; gap: var(--sp-3); padding: 0 var(--sp-4); height: var(--h-lg); border-bottom: var(--bw) solid var(--line-soft); }
.gn-cmdk-prompt { font-family: var(--font-mono); color: var(--accent-text); }
.gn-cmdk-input { flex: 1; height: 100%; border: 0; background: transparent; color: var(--text); font-family: var(--font-mono); font-size: var(--fs-small); }
.gn-cmdk-input:focus { outline: none; }
.gn-cmdk-list { overflow: auto; padding: var(--sp-2) 0; }
.gn-cmdk-group { padding: var(--sp-3) var(--sp-4) var(--sp-1); font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--text-3); }
.gn-cmdk-item { display: flex; align-items: center; gap: var(--sp-3); height: 34px; padding: 0 var(--sp-4); border-left: 3px solid transparent; color: var(--text-2); font-size: var(--fs-small); cursor: pointer; }
.gn-cmdk-item-icon { font-family: var(--font-mono); color: var(--text-3); width: 1em; }
.gn-cmdk-item[aria-selected="true"] { background: var(--bg-2); border-left-color: var(--accent); color: var(--text); }
.gn-cmdk-item[aria-selected="true"] .gn-cmdk-item-icon { color: var(--accent-text); }
.gn-cmdk-item-meta { margin-left: auto; font-family: var(--font-mono); font-size: var(--fs-label); letter-spacing: 0.06em; color: var(--text-3); }
.gn-cmdk-foot { display: flex; align-items: center; gap: var(--sp-4); padding: var(--sp-2) var(--sp-4); border-top: var(--bw) solid var(--line-soft); font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-3); }
.gn-cmdk-empty { padding: var(--sp-8) var(--sp-4); text-align: center; font-family: var(--font-mono); font-size: var(--fs-label); letter-spacing: var(--ls-label); text-transform: uppercase; color: var(--text-3); }

/* ---------------------------------------------- POPOVER / MENU */
.gn-pop-wrap { position: relative; display: inline-flex; }
.gn-pop { position: absolute; top: calc(100% + 4px); left: 0; z-index: var(--z-popover); min-width: 200px; background: var(--surface); border: var(--bw) solid var(--line); }
.gn-pop-right { left: auto; right: 0; }
.gn-pop-pad { padding: var(--sp-4); font-size: var(--fs-small); color: var(--text-2); }
.gn-menu { padding: var(--sp-1) 0; }
.gn-menu-label { padding: var(--sp-2) var(--sp-3) var(--sp-1); font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--text-3); }
.gn-menu-item { display: flex; align-items: center; gap: var(--sp-3); width: 100%; height: 32px; padding: 0 var(--sp-3); border: 0; background: transparent; color: var(--text-2); font-family: var(--font); font-size: var(--fs-small); text-align: left; cursor: pointer; transition: var(--transition-control); }
.gn-menu-item:hover { background: var(--bg-2); color: var(--text); }
.gn-menu-item[aria-disabled="true"] { color: var(--text-3); cursor: not-allowed; }
.gn-menu-item-danger:hover { color: var(--err); }
.gn-menu-key { margin-left: auto; font-family: var(--font-mono); font-size: var(--fs-label); color: var(--text-3); }
.gn-menu-sep { height: var(--bw); margin: var(--sp-1) 0; background: var(--line-soft); }

/* ------------------------------------------------------- PAGINATION */
.gn-pager { display: flex; align-items: center; gap: var(--sp-1); font-family: var(--font-mono); font-size: var(--fs-small); }
.gn-pager-item { min-width: var(--h-sm); height: var(--h-sm); padding: 0 var(--sp-2); display: inline-flex; align-items: center; justify-content: center; border: var(--bw) solid transparent; background: transparent; color: var(--text-2); font-family: var(--font-mono); font-size: 12.5px; font-variant-numeric: tabular-nums; cursor: pointer; transition: var(--transition-control); }
.gn-pager-item:hover { color: var(--text); border-color: var(--line); }
.gn-pager-item[aria-current="page"] { border-color: var(--accent); color: var(--accent-text); }
.gn-pager-item[disabled] { color: var(--line); cursor: not-allowed; }
.gn-pager-gap { color: var(--text-3); padding: 0 var(--sp-1); }
.gn-pager-total { margin-left: var(--sp-3); font-size: var(--fs-label); letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-3); }

/* --------------------------------------------- SKELETON / SPINNER */
.gn-skel { background: var(--bg-2); animation: gn-skel-pulse 1.1s steps(2, end) infinite; }
.gn-skel-line { height: 11px; }
.gn-skel-text { height: 13px; }
.gn-skel-block { display: block; height: 100%; min-height: 64px; }
@keyframes gn-skel-pulse { 0%, 100% { opacity: 1 } 50% { opacity: 0.45 } }
@media (prefers-reduced-motion: reduce) { .gn-skel { animation: none } }
.gn-spin { display: inline-block; font-family: var(--font-mono); color: var(--accent-text); }
.gn-spin::after { content: "|"; animation: gn-spin-frames var(--dur-blink) steps(1, end) infinite; }
@keyframes gn-spin-frames { 0% { content: "|" } 25% { content: "/" } 50% { content: "\2014" } 75% { content: "\\" } }
@media (prefers-reduced-motion: reduce) { .gn-spin::after { animation: none } }

/* ---------------------------------------------------------- PROGRESS */
.gn-prog { display: block; }
.gn-prog-track { height: 6px; background: var(--bg-2); border: var(--bw) solid var(--line-soft); }
.gn-prog-bar { display: block; height: 100%; background: var(--accent); transition: width var(--dur-move) var(--ease); }
.gn-prog-bar-warn { background: var(--warn); }
.gn-prog-bar-err { background: var(--err); }
.gn-prog-meta { display: flex; justify-content: space-between; margin-bottom: var(--sp-2); font-family: var(--font-mono); font-size: var(--fs-label); letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-3); }
.gn-prog-indeterminate .gn-prog-bar { width: 30%; animation: gn-prog-slide 1.4s var(--ease) infinite; }
@keyframes gn-prog-slide { 0% { margin-left: 0 } 100% { margin-left: 70% } }
@media (prefers-reduced-motion: reduce) { .gn-prog-indeterminate .gn-prog-bar { animation: none; width: 100% } }

/* ------------------------------------------------------------ AVATAR */
.gn-avatar { width: var(--h-md); height: var(--h-md); flex: none; display: inline-flex; align-items: center; justify-content: center; overflow: hidden; background: var(--bg-2); border: var(--bw) solid var(--line); color: var(--text-2); font-family: var(--font-mono); font-size: 12.5px; letter-spacing: 0.04em; text-transform: uppercase; }
.gn-avatar img { width: 100%; height: 100%; object-fit: cover; filter: grayscale(1); }
.gn-avatar-sm { width: 24px; height: 24px; font-size: 10.5px; }
.gn-avatar-lg { width: var(--h-lg); height: var(--h-lg); font-size: var(--fs-small); }
.gn-avatar-accent { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.gn-avatar-group { display: flex; gap: var(--sp-1); }

/* -------------------------------------------------------- CODE BLOCK */
.gn-code { border: var(--bw) solid var(--line); background: var(--surface); }
/* --h-md, not --h-sm: the head holds a 32px control, and a 32px head leaves it
   touching both rules. The height scale is 32/38/44 with nothing between, so
   the head grows rather than the button shrinking. */
.gn-code-head { display: flex; align-items: center; gap: var(--sp-3); height: var(--h-md); padding: 0 var(--sp-3); border-bottom: var(--bw) solid var(--line-soft); background: var(--bg-2); }
.gn-code-name { font-family: var(--font-mono); font-size: var(--fs-label); letter-spacing: 0.08em; color: var(--text-3); }
.gn-code-actions { margin-left: auto; display: flex; gap: var(--sp-2); }
.gn-code-body { margin: 0; padding: var(--sp-3) 0; overflow: auto; font-family: var(--font-mono); font-size: 12.5px; line-height: 1.75; color: var(--text); }
/* The body's horizontal padding is zero because .gn-code-line brings its own,
   so a line can span the full width when highlighted. A plain <pre> child has
   no such rule and would sit against the border - give it the inset. */
.gn-code-body > pre { padding: 0 var(--sp-3); }
.gn-code-line { display: flex; gap: var(--sp-3); padding: 0 var(--sp-3); border-left: 3px solid transparent; white-space: pre; }
.gn-code-line-hl { background: var(--accent-weak); border-left-color: var(--accent); }
.gn-code-ln { width: 2ch; flex: none; text-align: right; color: var(--text-3); user-select: none; font-variant-numeric: tabular-nums; }
.gn-t-kw { color: var(--accent-text); }
.gn-t-str { color: var(--info); }
.gn-t-num { color: var(--warn); }
.gn-t-com { color: var(--text-3); }

/* --------------------------------------------------------------- KBD */
.gn-kbd { display: inline-flex; align-items: center; justify-content: center; min-width: 20px; height: 20px; padding: 0 5px; border: var(--bw) solid var(--line); background: var(--bg-2); color: var(--text-2); font-family: var(--font-mono); font-size: 11.5px; line-height: 1; }
.gn-kbd-row { display: inline-flex; align-items: center; gap: var(--sp-1); }

/* ----------------------------------------------------------- CALLOUT */
.gn-callout { display: flex; gap: var(--sp-3); padding: var(--sp-3) var(--sp-4); background: var(--surface); border: var(--bw) solid var(--line); border-left: 3px solid var(--text-3); font-size: var(--fs-small); color: var(--text-2); }
.gn-callout-icon { font-family: var(--font-mono); color: var(--text-3); line-height: 1.55; }
.gn-callout-title { margin-bottom: 2px; color: var(--text); font-weight: var(--w-strong); }
.gn-callout-note { border-left-color: var(--info); }
.gn-callout-note .gn-callout-icon { color: var(--info); }
.gn-callout-warning { border-left-color: var(--warn); }
.gn-callout-warning .gn-callout-icon { color: var(--warn); }
.gn-callout-danger { border-left-color: var(--err); }
.gn-callout-danger .gn-callout-icon { color: var(--err); }
.gn-callout-accent { border-left-color: var(--accent); }
.gn-callout-accent .gn-callout-icon { color: var(--accent-text); }

/* --------------------------------------------------------- TREE VIEW */
.gn-tree { font-family: var(--font-mono); font-size: 12.5px; line-height: 1.9; color: var(--text-2); }
.gn-tree-row { display: flex; align-items: center; gap: var(--sp-2); padding: 0 var(--sp-2); border-left: 3px solid transparent; cursor: pointer; transition: var(--transition-control); white-space: nowrap; }
.gn-tree-row:hover { color: var(--text); }
.gn-tree-row[aria-selected="true"] { background: var(--bg-2); border-left-color: var(--accent); color: var(--text); }
.gn-tree-guide { color: var(--line); user-select: none; }
.gn-tree-mark { color: var(--text-3); }
.gn-tree-row[aria-expanded="true"] .gn-tree-mark { color: var(--accent-text); }
.gn-tree-meta { margin-left: auto; padding-left: var(--sp-4); font-size: var(--fs-label); color: var(--text-3); }

/* --------------------------------------------------------- FILE LIST */
.gn-files { border: var(--bw) solid var(--line); background: var(--surface); }
.gn-file-row { display: flex; align-items: center; gap: var(--sp-3); height: var(--row-h); padding: 0 var(--row-pad-x); border-bottom: var(--hairline) solid var(--line-soft); font-family: var(--font-mono); font-size: 12.5px; color: var(--text-2); }
.gn-file-row:last-child { border-bottom: 0; }
.gn-file-row:hover { color: var(--text); }
.gn-file-name { color: var(--text); }
.gn-file-meta { margin-left: auto; display: flex; gap: var(--sp-3); font-size: var(--fs-label); color: var(--text-3); font-variant-numeric: tabular-nums; }
.gn-file-add { color: var(--ok); }
.gn-file-del { color: var(--err); }

/* -------------------------------------------------------- LOG VIEWER */
.gn-log { display: flex; flex-direction: column; border: var(--bw) solid var(--line); background: var(--surface); min-height: 0; }
.gn-log-head { display: flex; align-items: center; gap: var(--sp-2); height: var(--h-sm); padding: 0 var(--sp-3); border-bottom: var(--bw) solid var(--line-soft); background: var(--bg-2); }
.gn-log-title { font-family: var(--font-mono); font-size: var(--fs-label); letter-spacing: 0.12em; text-transform: uppercase; color: var(--text-3); }
.gn-log-body { flex: 1; overflow: auto; padding: var(--sp-2) 0; font-family: var(--font-mono); font-size: 12px; line-height: 1.8; }
.gn-log-row { display: flex; gap: var(--sp-3); padding: 0 var(--sp-3); white-space: pre-wrap; }
.gn-log-row:hover { background: var(--bg-2); }
.gn-log-ts { flex: none; color: var(--text-3); font-variant-numeric: tabular-nums; }
.gn-log-lvl { flex: none; width: 5ch; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-3); }
.gn-log-lvl-ok { color: var(--ok); }
.gn-log-lvl-warn { color: var(--warn); }
.gn-log-lvl-err { color: var(--err); }
.gn-log-msg { color: var(--text-2); }
.gn-log-cursor { display: inline-block; align-self: center; width: 0.52em; height: 0.92em; background: var(--accent); vertical-align: -0.11em; animation: gn-blink 1.06s steps(1, end) infinite; }

/* ----------------------------------------------------------- TOOLBAR */
.gn-toolbar { display: flex; align-items: center; gap: var(--sp-2); height: var(--h-lg); padding: 0 var(--sp-3); background: var(--bg-2); border-bottom: var(--bw) solid var(--line); }
.gn-toolbar-sep { width: var(--bw); height: 18px; background: var(--line); }
.gn-toolbar-spacer { margin-left: auto; }
.gn-toolbar-label { font-family: var(--font-mono); font-size: var(--fs-label); letter-spacing: var(--ls-label); text-transform: uppercase; color: var(--text-3); }

/* -------------------------------------------------------- SPLIT PANE
   .gn-pane belongs to layout.css (line-divided cells); the pane is .gn-pane* */
.gn-pane { display: grid; min-height: 0; }
.gn-pane-h { grid-template-columns: 1fr var(--bw) 1fr; }
.gn-pane-v { grid-template-rows: 1fr var(--bw) 1fr; }
.gn-pane-gutter { background: var(--line); cursor: col-resize; transition: background-color var(--dur-color) var(--ease); }
.gn-pane-v > .gn-pane-gutter { cursor: row-resize; }
.gn-pane-gutter:hover { background: var(--accent); }
.gn-pane-pane { min-width: 0; min-height: 0; overflow: auto; }

/* ------------------------------------------------------ DRAWER/SHEET */
.gn-drawer-overlay { position: fixed; inset: 0; z-index: var(--z-drawer); background: color-mix(in oklab, var(--bg) 72%, transparent); }
.gn-drawer { position: fixed; z-index: var(--z-drawer); top: 0; bottom: 0; right: 0; width: min(420px, 92vw); display: flex; flex-direction: column; background: var(--surface); border-left: var(--bw) solid var(--line); animation: gn-drawer-in var(--dur-enter) var(--ease); }
.gn-drawer-left { right: auto; left: 0; border-left: 0; border-right: var(--bw) solid var(--line); }
.gn-drawer-bottom { top: auto; left: 0; right: 0; width: auto; height: min(60vh, 520px); border-left: 0; border-top: var(--bw) solid var(--line); }
@keyframes gn-drawer-in { from { opacity: 0 } to { opacity: 1 } }
.gn-drawer-head { display: flex; align-items: center; gap: var(--sp-3); height: var(--h-lg); padding: 0 var(--sp-4); border-bottom: var(--bw) solid var(--line-soft); }
.gn-drawer-title { font-size: 17px; font-weight: var(--w-strong); }
.gn-drawer-body { flex: 1; overflow: auto; padding: var(--sp-4); font-size: var(--fs-small); color: var(--text-2); }
.gn-drawer-foot { display: flex; gap: var(--sp-2); justify-content: flex-end; padding: var(--sp-3) var(--sp-4); border-top: var(--bw) solid var(--line-soft); }

/* --------------------------------------- EMPTY / ERROR / LOADING SET */
.gn-state { display: flex; flex-direction: column; align-items: center; gap: var(--sp-3); padding: var(--sp-12) var(--sp-6); border: var(--bw) dashed var(--line); background: var(--surface); text-align: center; }
.gn-state-mark { font-family: var(--font-mono); font-size: 22px; color: var(--text-3); }
.gn-state-title { font-size: var(--fs-body); font-weight: var(--w-strong); color: var(--text); }
.gn-state-desc { max-width: 42ch; font-size: var(--fs-small); color: var(--text-2); }
.gn-state-actions { display: flex; gap: var(--sp-2); margin-top: var(--sp-1); }
.gn-state-error { border-style: solid; border-left: 3px solid var(--err); }
.gn-state-error .gn-state-mark { color: var(--err); }
.gn-state-code { font-family: var(--font-mono); font-size: var(--fs-label); letter-spacing: var(--ls-label); text-transform: uppercase; color: var(--text-3); }

/* -------------------------------------------------------- STAT/METRIC */
.gn-stat { padding: var(--sp-4); background: var(--surface); border: var(--bw) solid var(--line); }
.gn-stat-accent { border-left: 3px solid var(--accent); }
/* The label row holds one line, always. A two-word label that wraps pushes its
   figure down and the band stops sharing a baseline - which is the whole point
   of a band. Ellipsis rather than wrap: a clipped label is legible, a broken
   row is not. */
.gn-stat-label { font-family: var(--font-mono); font-size: var(--fs-label); letter-spacing: var(--ls-label); text-transform: uppercase; color: var(--text-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.gn-stat-value { margin-top: var(--sp-2); font-family: var(--font-mono); font-size: var(--fs-h1); line-height: 1.1; letter-spacing: var(--ls-h1); color: var(--text); font-variant-numeric: tabular-nums; }
.gn-stat-unit { margin-left: 4px; font-size: var(--fs-small); color: var(--text-3); letter-spacing: 0; }
.gn-stat-delta { margin-top: var(--sp-2); font-family: var(--font-mono); font-size: var(--fs-label); letter-spacing: 0.06em; color: var(--text-3); }
.gn-stat-delta-up { color: var(--ok); }
.gn-stat-delta-down { color: var(--err); }
.gn-stat-band { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; border: var(--bw) solid var(--line); }
.gn-stat-band > .gn-stat { border: 0; border-left: var(--bw) solid var(--line); }
.gn-stat-band > .gn-stat:first-child { border-left: 0; }

/* ---------------------------------------------------------- TIMELINE */
.gn-timeline { display: flex; flex-direction: column; }
.gn-tl-item { display: grid; grid-template-columns: 76px 9px 1fr; gap: var(--sp-3); padding-bottom: var(--sp-4); }
.gn-tl-time { font-family: var(--font-mono); font-size: var(--fs-label); letter-spacing: 0.06em; color: var(--text-3); text-align: right; padding-top: 3px; font-variant-numeric: tabular-nums; }
.gn-tl-rail { position: relative; }
.gn-tl-rail::before { content: ""; position: absolute; left: 4px; top: 12px; bottom: -12px; width: var(--bw); background: var(--line); }
.gn-tl-item:last-child .gn-tl-rail::before { display: none; }
.gn-tl-mark { width: 9px; height: 9px; margin-top: 5px; background: var(--line); }
.gn-tl-item-now .gn-tl-mark { background: var(--accent); }
.gn-tl-title { font-size: var(--fs-small); color: var(--text); }
.gn-tl-desc { margin-top: 2px; font-size: 12.5px; color: var(--text-2); }

/* ------------------------------------------------------ DIFF VIEWER */
.gn-diff { border: var(--bw) solid var(--line); background: var(--surface); font-family: var(--font-mono); font-size: 12.5px; line-height: 1.8; overflow: auto; }
.gn-diff-head { display: flex; align-items: center; gap: var(--sp-3); height: var(--h-sm); padding: 0 var(--sp-3); border-bottom: var(--bw) solid var(--line-soft); background: var(--bg-2); font-size: var(--fs-label); letter-spacing: 0.08em; color: var(--text-3); }
.gn-diff-row { display: flex; gap: var(--sp-3); padding: 0 var(--sp-3); white-space: pre; color: var(--text-2); }
.gn-diff-ln { width: 3ch; flex: none; text-align: right; color: var(--text-3); font-variant-numeric: tabular-nums; }
.gn-diff-sign { width: 1ch; flex: none; }
.gn-diff-add { background: var(--ok-weak); color: var(--text); }
.gn-diff-add .gn-diff-sign { color: var(--ok); }
.gn-diff-del { background: var(--err-weak); color: var(--text); }
.gn-diff-del .gn-diff-sign { color: var(--err); }
.gn-diff-hunk { padding: 0 var(--sp-3); color: var(--text-3); background: var(--bg-2); }

/* --------------------------------------------------------- BAR CHART */
.gn-bars { display: flex; flex-direction: column; gap: var(--sp-2); }
.gn-bar-row { display: grid; grid-template-columns: 96px 1fr 56px; gap: var(--sp-3); align-items: center; }
.gn-bar-label { font-family: var(--font-mono); font-size: var(--fs-label); letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-3); }
/* Both are <span> in the documented markup, and width/height do not apply to a
   non-replaced inline box - so display must be explicit. The track only
   resolved before because .gn-bar-row blockifies its children; the fill sits
   inside the track and had nothing to blockify it. */
.gn-bar-track { display: block; height: 10px; background: var(--bg-2); }
.gn-bar-fill { display: block; height: 100%; background: var(--line); }
.gn-bar-fill-accent { background: var(--accent); }
.gn-bar-value { font-family: var(--font-mono); font-size: 12.5px; text-align: right; color: var(--text-2); font-variant-numeric: tabular-nums; }

/* ------------------------------------------------------------ SLIDER */
/* The track and the thumb are ordinary boxes, and the real <input> lies over
   them at opacity 0. Every other component in this system is styled with
   .gn-* rules; the slider was the one that reached into UA shadow internals
   (::-webkit-slider-thumb), and on a renderer that does not match those the
   native control shows through - a round, platform-blue thumb on the very page
   that documents "radius 0, no circles". Authored boxes cannot regress that
   way, and --gn-fill drives both the fill width and the thumb position. */
.gn-slider-wrap { position: relative; height: 18px; }
.gn-slider-track {
  position: absolute; left: 0; right: 0; top: 50%; height: 6px; margin-top: -3px;
  background-color: var(--bg-2);
  border: var(--bw) solid var(--line-soft);
}
.gn-slider-fill {
  position: absolute; left: 0; top: 0; bottom: 0;
  width: var(--gn-fill, 0%);
  background-color: var(--accent);
}
/* Accent marks the amount, ink marks the position: on the filled side an
   accent thumb would be invisible. */
.gn-slider-thumb {
  position: absolute; top: 0; height: 18px; width: 10px;
  left: var(--gn-fill, 0%); transform: translateX(-50%);
  background-color: var(--text);
  pointer-events: none;
}
/* The input keeps every native affordance - drag, click, arrow keys, Home/End,
   the accessibility tree - and is fully invisible. opacity: 0 rather than
   transparent pseudo-parts, because the parts are what this renderer ignores:
   hiding the whole control cannot depend on them matching. */
.gn-slider {
  position: absolute; inset: 0; width: 100%; height: 100%; margin: 0;
  -webkit-appearance: none; appearance: none;
  opacity: 0; cursor: pointer;
}
/* The ring belongs on the boxes the reader can see. */
.gn-slider-wrap:has(.gn-slider:focus-visible) {
  outline: var(--focus-ring); outline-offset: 2px;
}
.gn-slider-row { display: grid; grid-template-columns: 1fr 56px; gap: var(--sp-3); align-items: center; }
.gn-slider-value { font-family: var(--font-mono); font-size: 12.5px; text-align: right; color: var(--text-2); font-variant-numeric: tabular-nums; }

/* ----------------------------------------------------------- STEPPER */
.gn-stepper { display: inline-flex; border: var(--bw) solid var(--line); background: var(--bg); }
.gn-stepper-btn { width: var(--h-md); height: var(--h-md); border: 0; background: transparent; color: var(--text-2); font-family: var(--font-mono); font-size: 14px; cursor: pointer; transition: var(--transition-control); }
.gn-stepper-btn:hover { background: var(--accent); color: var(--on-accent); }
.gn-stepper-btn[disabled] { color: var(--line); cursor: not-allowed; background: transparent; }
/* This is an <input> - behaviour.js's data-gn-stepper reads q("input", root) -
   so the UA field styling has to be reset explicitly. Without appearance,
   background and border-block, a native input keeps a white field and a full
   border: in dark mode that is near-white text in a white box. text-align
   centres it, since inline-flex does nothing to an input's own text. */
.gn-stepper-value {
  width: 56px; height: var(--h-md);
  appearance: none; -webkit-appearance: none;
  background: transparent;
  border: 0; border-inline: var(--bw) solid var(--line);
  text-align: center;
  font-family: var(--font-mono); font-size: var(--fs-small);
  color: var(--text); font-variant-numeric: tabular-nums;
}
.gn-stepper-value:focus-visible { outline: var(--focus-ring); outline-offset: -2px; }
/* type="number" brings UA spinners; the − + buttons are the stepper */
.gn-stepper-value::-webkit-outer-spin-button,
.gn-stepper-value::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.gn-stepper-value { -moz-appearance: textfield; }

/* ---------------------------------------------------------- COMBOBOX */
.gn-combo { position: relative; }
.gn-combo-list { position: absolute; z-index: var(--z-popover); top: calc(100% + 4px); left: 0; right: 0; max-height: 220px; overflow: auto; background: var(--surface); border: var(--bw) solid var(--line); }
.gn-combo-opt { display: flex; align-items: center; gap: var(--sp-2); height: 32px; padding: 0 var(--sp-3); border-left: 3px solid transparent; color: var(--text-2); font-size: var(--fs-small); cursor: pointer; }
.gn-combo-opt[aria-selected="true"] { background: var(--bg-2); border-left-color: var(--accent); color: var(--text); }
.gn-combo-opt-mark { margin-left: auto; font-family: var(--font-mono); color: var(--accent-text); }
.gn-combo-empty { padding: var(--sp-4); font-family: var(--font-mono); font-size: var(--fs-label); letter-spacing: var(--ls-label); text-transform: uppercase; color: var(--text-3); }
.gn-chips { display: flex; flex-wrap: wrap; gap: var(--sp-1); margin-top: 7px; }

/* ------------------------------------------------ SHELL / RESPONSIVE */
/* SideNav becomes a column so a footer slot can sit at the bottom. */
.gn-sidenav { display: flex; flex-direction: column; }
.gn-sidenav-foot { margin-top: auto; padding: var(--sp-4); display: grid; gap: var(--sp-3); }
@media (max-width: 1119px) {
  .gn-container { max-width: 100%; }
}
@media (max-width: 767px) {
  .gn-sidenav { width: 100%; border-right: 0; border-bottom: var(--bw) solid var(--line); }
  .gn-topbar { gap: var(--sp-3); }
  .gn-topbar-nav { gap: var(--sp-3); }
  .gn-stat-band { grid-auto-flow: row; }
  .gn-stat-band > .gn-stat { border-left: 0; border-top: var(--bw) solid var(--line); }
  .gn-stat-band > .gn-stat:first-child { border-top: 0; }
}

/* -------------------------------------------------- DECK CHROME LAYER */
.deck-scope .gn-chrome { position: absolute; inset: 0; z-index: var(--z-chrome); pointer-events: none; font-family: var(--font-mono); font-size: clamp(9px, 1.05cqw, 20px); letter-spacing: 0.12em; text-transform: uppercase; color: var(--text-3); }
.deck-scope .gn-chrome-tl { position: absolute; left: 5.625%; top: 5.19%; }
.deck-scope .gn-chrome-tr { position: absolute; right: 5.625%; top: 5.19%; display: flex; align-items: center; gap: 0.6em; }
.deck-scope .gn-chrome-tr::before { content: ""; width: 0.3em; height: 0.3em; background: var(--chart-2); }
.deck-scope .gn-chrome-bl { position: absolute; left: 5.625%; bottom: 4.81%; }
.deck-scope .gn-chrome-br { position: absolute; right: 5.625%; bottom: 4.81%; font-variant-numeric: tabular-nums; }
.deck-scope .gn-chrome-br b { color: var(--text-2); font-weight: var(--w-regular); }
.deck-scope .gn-chrome-rule { position: absolute; left: 0; right: 0; height: 1px; background: var(--line); }
.deck-scope .gn-chrome-rule-top { top: 8.89%; }
.deck-scope .gn-chrome-rule-bottom { top: 91.11%; }
.deck-scope .gn-slide-box { background: var(--surface); border: var(--bw) solid var(--line); padding: 1.875%; }
/* one lead box per slide: accent border only - no fill, no bar, no corner marks */
.deck-scope .gn-slide-box-accent { border-color: var(--accent); }
.deck-scope .gn-slide-h3 { font-size: clamp(13px, 2.3cqw, 44px); font-weight: var(--w-strong); letter-spacing: -0.02em; }
.deck-scope .gn-slide-display { font-size: clamp(30px, 6.9cqw, 132px); line-height: 1; font-weight: var(--w-strong); letter-spacing: -0.045em; }
.deck-scope .gn-slide-caption { font-size: clamp(9px, 1.25cqw, 24px); color: var(--text-3); }

/* ------------------------------------------- BEHAVIOUR CONTRACT
   What behaviour.js needs from the CSS. Keep these two in step: the script
   toggles [hidden], and writes --gn-fill onto a slider. */

/* .gn-cmdk-overlay and .gn-drawer are display:flex, which outranks the hidden
   attribute's UA rule - without this they never close. */
[hidden] { display: none !important; }

/* the track fills to the value; the script supplies the stop */

/* external link mark (2026-10-13, task 3420) - the glyph set's ↗ as a line icon: assets/icons/external.svg, 20 grid, 1.5 stroke, square ends.
   For a link whose text is the address itself. The mark takes the signal colour; the text keeps its ink (mint is never a text colour on light).
   Sized by the text (1em), so it sits on any size step; the address and its mark never part (nowrap).
   The mask is the asset inlined - a file:// page cannot load a mask from another file, and résumé PDFs are made from file://. Change the svg, then this line. */
.gn-ext { white-space: nowrap; }
.gn-ext::after { content: ""; display: inline-block; width: 1em; height: 1em; margin-left: 4px; vertical-align: -0.16em; background: var(--accent);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 20 20%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%221.5%22 stroke-linecap=%22square%22 stroke-linejoin=%22miter%22%3E%3Cpath d=%22M7 5.75h7.25V13M14 6 5.75 14.25%22/%3E%3C/svg%3E") center / contain no-repeat; mask: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 20 20%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%221.5%22 stroke-linecap=%22square%22 stroke-linejoin=%22miter%22%3E%3Cpath d=%22M7 5.75h7.25V13M14 6 5.75 14.25%22/%3E%3C/svg%3E") center / contain no-repeat; }

/* product - the slots a product fills, and nothing else.
   This file never grows when a product is added: a product ships its own
   products/<name>/brand.css that sets these, and the system stays fixed.

   The rule the slots enforce: a product is distinguished by its MARK, not by
   its colour. --product-accent defaults to --accent so the discipline holds
   until a product has a real reason to break it - then it is one line, in the
   product's own file, and visible in a diff. */
/* The aliases are declared on the SAME selector list the theme blocks use, not
   on :root alone. A custom property substitutes its var() at the element that
   declares it, so an alias declared only at :root resolves against <html>'s
   theme and inherits that resolved value down - a section or page that
   redeclares the theme for itself could never be followed. --product-accent
   escaped this only because --accent happens to be identical in both themes;
   --product-accent-text did not, and a dark scene inside a light document got
   the light-mode chevron. */
:root, [data-theme="dark"], [data-theme="light"] {
  --product-accent: var(--accent);
  --product-accent-text: var(--accent-text);
}
:root {
  --product-name: "";
  /* the inline floor for a typeset product mark; a product may raise it */
  --product-mark-min: 15px;
}

/* A product mark is the one thing a product draws itself: the system's
   .gn-wordmark is fixed to "garamnoh". Products follow its grammar - a name
   typeset in mono plus one square accent glyph, never an image, never a
   symbol - and declare their own geometry in their brand.css. */
.gn-product-mark {
  font-family: var(--font-code);
  font-weight: var(--w-regular);
  line-height: 1;
  color: inherit;
  position: relative;
  display: inline-block;
}
.gn-product-mark > i {
  position: absolute;
  background: var(--product-accent);
}
/* One blinking glyph per page - the same budget as the garamnoh cursor. */
@media (prefers-reduced-motion: reduce) {
  .gn-product-mark--live > i { animation: none; }
}

/* 문서·대시보드 페이지 셸 (2026-10-08, #3082)
   보고서·실행 문서·대시보드처럼 혼자 서는 페이지의 공통 틀. 기준은 익산 수행 대시보드
   (Projects/iksan-homepage-renewal/site) - 넓은 폭, 기본 light, 오른쪽 위 테마 버튼.
   페이지는 이 클래스와 docpage.js 만 쓴다. 폭·여백·버튼을 페이지 <style> 에 다시 적지 않는다.

     <html lang="ko" data-theme="light">
     <script src=".../docpage.js"></script>             ← <head> 안, 첫 그림 전에 테마 결정
     <main class="gn-doc">
       <div class="gn-doc-top"><nav class="gn-doc-links">…</nav>
         <button class="gn-theme-btn" type="button" data-gn-theme-toggle>DARK</button></div>
       <header class="gn-doc-head"><p class="gn-doc-kicker">…</p><h1 class="gn-doc-title">…</h1>
         <p class="gn-doc-lead">…</p><div class="gn-doc-meta">…</div></header>
       …본문…
     </main>                                                                  */
:root { --doc-max: 1280px; --doc-gutter: 28px; }
@media (max-width: 900px) { :root { --doc-gutter: 16px; } }

.gn-doc { max-width: var(--doc-max); margin: 0 auto; padding: 0 var(--doc-gutter) 96px; box-sizing: border-box; }
.gn-doc-top { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; padding-top: 24px; min-height: 56px; }
.gn-doc-links { display: flex; gap: 16px; flex-wrap: wrap; font-size: 13px; }
.gn-doc-links a { color: var(--accent-text); text-decoration: none; }
.gn-doc-links a:hover { color: var(--accent); }
/* 링크가 없어도 버튼은 오른쪽 끝 - 빈 nav 를 남기지 않아도 되게 */
.gn-doc-top > [data-gn-theme-toggle] { margin-left: auto; }
.gn-theme-btn { display: inline-flex; align-items: center; height: 32px; padding: 0 12px;
  border: var(--bw, 1px) solid var(--line); background: var(--surface); color: var(--text); cursor: pointer;
  font: 400 11px/1 var(--font-code); letter-spacing: .1em; text-transform: uppercase; white-space: nowrap; }
.gn-theme-btn:hover { border-color: var(--accent); color: var(--accent-text); }

.gn-doc-head { padding: 24px 0; }
.gn-doc-kicker { margin: 0; font: 400 11px/1.4 var(--font-code); letter-spacing: .14em; text-transform: uppercase; color: var(--text-3); }
.gn-doc-head > :first-child { margin-top: 0; }
.gn-doc-title { margin: 12px 0 0; font-size: 32px; line-height: 1.1; letter-spacing: -.03em; font-weight: 600; }
.gn-doc-lead { margin: 10px 0 0; max-width: 760px; color: var(--text-2); }
.gn-doc-meta { display: flex; flex-wrap: wrap; gap: 6px 18px; margin-top: 14px; font: 12px/1.5 var(--font-code); color: var(--text-3); }
@media print { .gn-doc-top { display: none; } }

/* 본문 리듬 - 익산 대시보드와 같게(본문 15/1.62, 섹션 위 44px, 구분선 없음).
   섹션 머리: <div class="gn-doc-sec"><h2>제목</h2><p>보조 문구</p></div> - 보조 문구는 같은 줄 오른쪽, 좁으면 아래로 */
.gn-doc { font-size: 15px; line-height: 1.62; }
.gn-doc > section { margin: 0; padding-top: 44px; border: 0; scroll-margin-top: 12px; }
.gn-doc-sec { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.gn-doc-sec h2 { margin: 0; font-size: 22px; line-height: 1.25; letter-spacing: -.02em; font-weight: 600; }
.gn-doc-sec p { margin: 0; color: var(--text-2); font-size: 13px; max-width: 820px; }

/* 보기 전환 컨트롤 (2026-10-10, #3097) - 시안·모음 페이지의 상황·테마·옵션 같은 전환 버튼.
   묶음마다 가로 한 줄(세그먼트), 세로로 쌓지 않는다. 묶음 이름은 왼쪽 한 열 → 모든 묶음의 버튼 시작선이 같다.
   좁으면 버튼이 다음 줄로 넘어감(가로 스크롤 없음), 560 이하에서는 이름이 버튼 위로.
     <div class="gn-ctl"><span class="gn-ctl-label">상황</span><div class="gn-ctl-btns"><button aria-pressed="true">평상</button>…</div> … </div> */
.gn-ctl { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 8px 16px; align-items: center; }
.gn-ctl-label { font: 400 11px/1.4 var(--font-code); letter-spacing: .12em; text-transform: uppercase; color: var(--text-3); }
.gn-ctl-btns { display: flex; flex-wrap: wrap; gap: 6px; min-width: 0; }
.gn-ctl-btns button { font: 13px/1 var(--font-sans); height: 30px; padding: 0 10px; border: var(--bw, 1px) solid var(--line-strong); background: var(--surface); color: var(--text); cursor: pointer; white-space: nowrap; }
.gn-ctl-btns button:hover { border-color: var(--accent); }
.gn-ctl-btns button[aria-pressed="true"] { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }   /* 토글 = 민트 채움(선택 규칙) */
@media (max-width: 560px) { .gn-ctl { grid-template-columns: minmax(0, 1fr); gap: 4px; } .gn-ctl-label:not(:first-child) { margin-top: 8px; } }

/* last: print overrides every theme */
/* print - dark never prints. Ink is the light theme, chrome is dropped. */
@media print {
  :root, [data-theme="dark"], [data-theme="light"] {
    color-scheme: light;
  --bg: #F4F6F4; --bg-2: #E8EBE8; --surface: #FFFFFF;
  --text: #101413; --text-2: #59635C; --text-3: #6E7A72;
  --line: #D3DAD4; --line-soft: #E4E9E5;
  --accent: #3ED49C;        /* fill only, identical in both themes */
  --accent-text: #0B6B4A;   /* text and hairlines step down */
  --accent-weak: #DCF2E8; --accent-deep: #2BC189; --on-accent: #08090A;
  --ok: #0B6B4A; --warn: #8A6410; --err: #B03A30; --info: #2A5F7D;
  --ok-weak: #DCF2E8; --warn-weak: #F5E9CC; --err-weak: #F6DCD8; --info-weak: #DDE9F0;
  --chart-1: #12805A; --chart-2: #0B6B4A; --chart-3: #8A6410; --chart-4: #2A5F7D; --chart-5: #B03A30;
    --accent: #FFFFFF;      /* an accent fill prints as white + a 1px line */
    --on-accent: #101413;
  }
  body { background: #FFFFFF; }
  /* Brand marks keep the brand green in print (2026-10-12, #3149). The white-accent rule above is for UI fills;
     a mark's dot, cursor or bar is part of the mark, and a white one is a different mark. */
  :root, [data-product], .gn-wordmark { --product-accent: #3ED49C; --product-accent-text: #3ED49C; }
  .gn-wordmark-cursor { background: #3ED49C !important; }
  .gn-topbar, .gn-sidenav, .gn-toaster, .gn-overlay, .gn-drawer, .gn-cmdk-overlay,
  .gn-tip, .gn-pop, .gn-skip, [data-print="hide"] { display: none !important; }
  .gn-card, .gn-table, .gn-code, .gn-callout, .gn-log { break-inside: avoid; }
  .gn-btn-brand { border-color: #101413 !important; }
  a[href]::after { content: " (" attr(href) ")"; font-family: var(--font-mono); font-size: 10px; color: #59635C; }
  a[href^="#"]::after { content: ""; }
  @page { margin: 16mm; }
}

