/*
 * components.css — layered on top of base.css (the desktop-first legacy copy,
 * kept unforked per ADR 0012). This file makes the shared kit FLUID BY
 * CONSTRUCTION: a screen built on these classes is responsive with no extra
 * per-screen work (ADR 0013 / 0020). TOKEN-BASED ONLY (spacing/type/colour from
 * tokens.css; no one-off hex or off-scale px).
 *
 * The #1 responsive bug is a flex/grid child with the default `min-width: auto`,
 * which refuses to shrink below its content and pushes the layout past the
 * viewport → horizontal PAGE scroll. So every child that holds text or a table
 * gets `min-width: 0`, and wide tables scroll inside `.table-scroll`, never the
 * <body>. We deliberately do NOT `overflow-x: hidden` the body (that masks bugs).
 */

/* ── App frame: nothing sets a fixed width; the body never scrolls sideways ── */
.console__main,
.masterdetail__aside,
.masterdetail__main,
.table-scroll {
  min-width: 0; /* load-bearing: lets these flex/grid children shrink */
}

/* ── Full-height shell (ADR 0013/0020) — the sidebar reaches the bottom at ANY
 * content height. The console is a flex row that is min-height:100vh AND grows
 * with content; the navy sidebar is a STRETCHING child (align-items:stretch), so
 * its background reaches the true bottom. The version footer lives INSIDE the
 * content column (console__main flex-column, footer margin-top:auto) so no
 * non-navy strip appears below the sidebar. NEVER put a fixed height /
 * align-self / height:100vh on the sidebar column. */
.console {
  min-height: 100vh;
  align-items: stretch;
}
.console__main {
  display: flex;
  flex-direction: column;
  /* Shared cap for a record view's header + form so the deactivate button (in the
   * header, top-right) lines up exactly with the form's right edge below it. */
  --record-width: 1120px;
}
.console__footer {
  margin-top: auto;
  padding: var(--space-8) 0 0;
  text-align: left;
}

/* ── Fixed app-shell (desktop console) — the viewport is the frame (like the legacy
 * desktop app / Linear / Gmail). On ≥769px (sidebar is a side column) the console
 * fills 100dvh and the DOCUMENT does not scroll: the navy sidebar (its own scroll),
 * the breadcrumb, and the page header stay fixed; ONLY `.console__scroll` scrolls.
 * Panes/grids inside size to that region (flex:1; min-height:0) instead of fighting a
 * scrolling document. Mobile (≤768, stacked nav) keeps the normal document scroll. */
@media (min-width: 769px) {
  .has-console {
    height: 100dvh;
    overflow: hidden;
  }
  .has-console main.page {
    height: 100dvh;
    overflow: hidden;
  }
  .has-console .console {
    height: 100dvh;
    min-height: 0;
    overflow: hidden;
  }
  .has-console .console__nav {
    overflow-y: auto; /* sidebar scrolls itself if the menu is long */
  }
  .has-console .console__main {
    flex: 1 1 auto;
    min-width: 0;
    min-height: 0;
    overflow: hidden; /* becomes the scroll frame: fixed crumb/header + one scroll region */
    /* .console__scroll adds --space-2 clip-protection inline padding; keep the NET
     * inline gutter at 16px (--space-2 here + --space-2 there = --space-4). */
    padding-inline: var(--space-2);
  }
  .has-console .console__scroll {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto; /* the ONLY scrolling region */
    /* overflow-y:auto forces overflow-x to `auto` too, which CLIPS anything flush at the content
     * edge — the leftmost filter input's left border/focus-ring got shaved. A small inline gutter
     * keeps content off the clip edge (and off the sidebar/scrollbar) so borders show in full. */
    padding-inline: var(--space-2);
    display: flex;
    flex-direction: column;
  }
  /* The merged toolbar (breadcrumb + title + actions, ADR 0045) stays put while the content below
   * scrolls. A hairline + tight block padding read it as a bar and separate it from the rows that
   * scroll under it; the reduced margin below reclaims vertical space for data. */
  .has-console .console__scroll .page-header {
    position: sticky;
    top: 0;
    z-index: 5;
    background: var(--color-bg-base);
    padding-block: var(--space-3);
    margin-bottom: var(--space-4);
    border-bottom: 1px solid var(--color-border-subtle);
  }
}

/* Fixed app-shell for the MAIN ERP layout (top-nav + page) — the same "viewport is the frame" model
 * as the console above, for the pages that carry the sticky top-nav (dashboard, etc.). On ≥769px the
 * body fills 100dvh and the DOCUMENT does not scroll: the top-nav stays fixed at the top and ONLY the
 * content region (main.page) scrolls; the footer sits at the bottom. Mobile (≤768) keeps normal scroll. */
@media (min-width: 769px) {
  .app-shell {
    height: 100dvh;
    overflow: hidden;
    display: flex;
    flex-direction: column;
  }
  .app-shell .topnav {
    flex: 0 0 auto; /* stays at the top, outside the scroll region */
  }
  .app-shell main.page {
    flex: 1 1 auto;
    min-height: 0; /* override .page's min-height so it shrinks + scrolls inside the frame */
    overflow-y: auto; /* the ONLY scrolling region */
  }
  .app-shell .site-footer {
    flex: 0 0 auto;
  }

  /* Slim, fade-when-idle scrollbars on the app-shell scroll regions (content + sidebar) — the same
   * treatment as the grids (Linear/Notion): a 10px track with an inset content-box thumb that is
   * transparent at rest and fades in on hover. Firefox uses scrollbar-width/color. Content regions get
   * a muted (--color-border) thumb; the navy sidebar gets a translucent light thumb. */
  .has-console .console__scroll,
  .app-shell main.page {
    scrollbar-width: thin;
    scrollbar-color: transparent transparent; /* idle: invisible (Firefox) */
  }
  .has-console .console__scroll:hover,
  .has-console .console__scroll:focus-within,
  .app-shell main.page:hover,
  .app-shell main.page:focus-within {
    scrollbar-color: var(--color-border) transparent;
  }
  .has-console .console__scroll::-webkit-scrollbar,
  .app-shell main.page::-webkit-scrollbar {
    width: 10px;
    height: 10px;
  }
  .has-console .console__scroll::-webkit-scrollbar-track,
  .app-shell main.page::-webkit-scrollbar-track {
    background: transparent;
  }
  .has-console .console__scroll::-webkit-scrollbar-thumb,
  .app-shell main.page::-webkit-scrollbar-thumb {
    border-radius: 5px;
    border: 2px solid transparent; /* inset → a slim rounded bar */
    background-color: transparent; /* idle: invisible */
    background-clip: content-box;
    transition: background-color 0.2s ease;
  }
  .has-console .console__scroll:hover::-webkit-scrollbar-thumb,
  .has-console .console__scroll:focus-within::-webkit-scrollbar-thumb,
  .app-shell main.page:hover::-webkit-scrollbar-thumb,
  .app-shell main.page:focus-within::-webkit-scrollbar-thumb {
    background-color: var(--color-border); /* fades in on hover */
  }

  /* Navy sidebar — same slim treatment, translucent LIGHT thumb (it's on a dark surface). */
  .has-console .console__nav {
    scrollbar-width: thin;
    scrollbar-color: transparent transparent;
  }
  .has-console .console__nav:hover,
  .has-console .console__nav:focus-within {
    scrollbar-color: rgba(255, 255, 255, 0.3) transparent;
  }
  .has-console .console__nav::-webkit-scrollbar {
    width: 10px;
  }
  .has-console .console__nav::-webkit-scrollbar-track {
    background: transparent;
  }
  .has-console .console__nav::-webkit-scrollbar-thumb {
    border-radius: 5px;
    border: 2px solid transparent;
    background-color: transparent;
    background-clip: content-box;
    transition: background-color 0.2s ease;
  }
  .has-console .console__nav:hover::-webkit-scrollbar-thumb,
  .has-console .console__nav:focus-within::-webkit-scrollbar-thumb {
    background-color: rgba(255, 255, 255, 0.3);
  }

  /* BOUNDED grids that FLEX-FILL the remaining console height (wide + can-be-tall data grids), so
   * the grid scrolls its rows internally + keeps its h-scrollbar in view instead of an autoHeight
   * card that runs it below the fold. */
  .has-console .console__scroll .closing-detail,
  .has-console .console__scroll .bom-fill {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
  }
  /* 소요량 BOM — pure fill: shrinks freely to whatever height is left. */
  .has-console .console__scroll .bom-fill {
    min-height: 0;
  }
  .has-console .console__scroll .bom-fill #bom-grid {
    flex: 1 1 auto;
    min-height: 0;
  }
  /* 마감 정산 상세 — fill that GROWS on a big screen (both tables fit, no scroll) but never starves
   * the 미마감 grid below a usable ~6-row FLOOR. The section keeps min-height:auto (won't shrink past
   * its content), so on a short 13" the content region nudges just enough to reveal the full detail —
   * the top nav + sidebar stay fixed (app-shell untouched). */
  .has-console .console__scroll .closing-detail #unclosed-grid {
    flex: 1 1 auto;
    min-height: 18rem; /* ~6 rows (36px rows + header + h-scrollbar) */
  }
}
/* Mobile / no fixed-shell fallback: these bounded grids keep a sensible fixed height. */
#unclosed-grid {
  min-height: 18rem;
}
#bom-grid {
  min-height: 240px;
}

/* ── 자재 picker modal (BOM 소요량) — native <dialog>, renders in the top layer (escapes the grid
 * overflow + clipboard handler). Search + filter chips (구분/품목/자재처) + a scrollable results table. */
.matpick {
  flex-direction: column;
  padding: 0;
  border: 0;
  border-radius: var(--radius-md);
  background: var(--color-bg-surface);
  color: var(--color-text-primary);
  width: min(1040px, 96vw);
  height: min(760px, 90vh);
  max-width: 96vw;
  max-height: 90vh;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
  overflow: hidden;
}
/* only the OPEN dialog lays out as a flex column — keep the UA `dialog:not([open]){display:none}`
 * so a closed dialog doesn't render full-screen and swallow clicks. */
.matpick[open] { display: flex; }
.matpick::backdrop { background: rgba(0, 0, 0, 0.5); }
.matpick__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--color-border);
}
.matpick__title { margin: 0; font-size: var(--fs-h3); font-weight: var(--fw-medium); }
.matpick__close {
  width: 30px;
  height: 30px;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--color-text-secondary);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
}
.matpick__close:hover { background: var(--color-bg-base); color: var(--color-text-primary); }
.matpick__body {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-4);
}
.matpick__q { width: 100%; }

/* active-filter pills — the current 품목/자재처 selections, removable */
.matpick__active {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}
.matpick__pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: 2px var(--space-1) 2px var(--space-2);
  border-radius: 999px;
  background: color-mix(in srgb, var(--color-accent) 12%, transparent);
  color: var(--color-accent);
  font-size: var(--fs-caption);
  font-weight: var(--fw-medium);
}
.matpick__pill-x {
  border: 0;
  background: transparent;
  color: inherit;
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  padding: 0 2px;
}
.matpick__pill-x:hover { color: var(--color-text-primary); }

/* compact filter controls (segmented 구분 · 품목 select · 자재처 searchable combo) */
.matpick__controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
}
.matpick__control {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}
.matpick__control--grow { flex: 1 1 220px; }
.matpick__control-label {
  flex: 0 0 auto;
  font-size: var(--fs-caption);
  font-weight: var(--fw-medium);
  color: var(--color-text-secondary);
}
.matpick__control--grow .combo { flex: 1 1 auto; }

/* segmented toggle (구분) */
.seg {
  display: inline-flex;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  overflow: hidden;
}
.seg__btn {
  padding: var(--space-1) var(--space-3);
  border: 0;
  border-left: 1px solid var(--color-border);
  background: var(--color-bg-surface);
  color: var(--color-text-secondary);
  font-size: var(--fs-caption);
  cursor: pointer;
  text-decoration: none; /* the shared status filter renders .seg__btn as <a> links */
  transition: background 0.12s ease, color 0.12s ease;
}
.seg__btn:first-child { border-left: 0; }
.seg__btn:hover { background: var(--color-bg-base); }
.seg__btn.is-on { background: var(--color-accent); color: #fff; }
.seg__btn:focus-visible { outline: none; box-shadow: var(--focus-ring); }

/* searchable combo (자재처) — an input + a floating filtered menu */
.combo { position: relative; min-width: 0; }
.combo__input { width: 100%; }
.combo__menu {
  position: absolute;
  z-index: 5;
  top: calc(100% + 2px);
  left: 0;
  right: 0;
  max-height: 220px;
  overflow-y: auto;
  background: var(--color-bg-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-md);
}
.combo__item {
  padding: var(--space-2) var(--space-3);
  font-size: var(--fs-body);
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.combo__item:hover { background: var(--color-bg-base); }
.combo__item.is-on {
  background: color-mix(in srgb, var(--color-accent) 12%, transparent);
  color: var(--color-accent);
  font-weight: var(--fw-medium);
}

.matpick__results {
  flex: 1 1 auto;
  min-height: 12rem;
  overflow: auto;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}
.matpick__table { width: 100%; border-collapse: collapse; font-size: var(--fs-body); }
.matpick__table th {
  position: sticky;
  top: 0;
  z-index: 1;
  padding: var(--space-2) var(--space-3);
  text-align: left;
  background: var(--color-bg-base);
  color: var(--color-text-secondary);
  font-size: var(--fs-caption);
  font-weight: var(--fw-medium);
  border-bottom: 1px solid var(--color-border);
}
.matpick__row { cursor: pointer; }
.matpick__row td {
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--color-border-subtle);
}
.matpick__row:hover td,
.matpick__row:focus td { background: var(--color-bg-base); }
.matpick__row:focus { outline: none; }
/* select-then-apply: a clicked row is highlighted until 적용 commits it */
.matpick__row.is-selected td {
  background: color-mix(in srgb, var(--color-accent) 14%, transparent);
}
.matpick__empty { padding: var(--space-4); text-align: center; }
/* footer — 취소 / 적용 (bottom-right); 적용 disabled until a row is selected */
.matpick__foot {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  border-top: 1px solid var(--color-border);
}
/* 자재명 cell → clickable (opens the picker); empty state gets an obvious "pick" affordance */
.bom-mat-cell { cursor: pointer; }
.bom-mat-pick { color: var(--color-accent); cursor: pointer; }

/* ── Collapsible console sidebar (button-only) ──────────────────────────────
 * Collapse/expand ONLY via the explicit toggle button (no hover-peek). Collapsing slides
 * .console__nav off-canvas via a negative margin — which BOTH hides it AND reflows .console__main to
 * full width in one animated property. Desktop fixed-shell only (≥769px). */
/* Floating collapse toggle — vertically centred on the sidebar's right border (the standard place).
 * position:fixed → viewport-relative, so the sidebar's overflow never clips it; it slides with the
 * border (left: nav-w → 0) and, when collapsed, stays pinned at the screen's left edge to reopen. */
.console__nav-toggle {
  display: none; /* desktop-only; shown in the ≥769px block below */
  align-items: center;
  justify-content: center;
  padding: 0;
  width: 24px;
  height: 24px;
  border: 1px solid var(--color-border);
  border-radius: 50%;
  background: var(--color-bg-surface);
  color: var(--color-text-secondary);
  cursor: pointer;
  box-shadow: 0 1px 5px rgba(0, 0, 0, 0.14);
  transition: left 0.22s ease, background 0.12s ease, color 0.12s ease;
}
.console__nav-toggle:hover {
  background: var(--color-bg-base);
  color: var(--color-primary);
}
.console__nav-toggle:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}
.console__nav-toggle-closed { display: none; }
.console__nav-toggle[aria-expanded="false"] .console__nav-toggle-open { display: none; }
.console__nav-toggle[aria-expanded="false"] .console__nav-toggle-closed { display: inline-flex; }

@media (min-width: 769px) {
  .console__nav-toggle {
    display: inline-flex;
    position: fixed;
    top: 50%;
    left: calc(var(--console-nav-w) - 12px); /* straddles the sidebar's right border */
    transform: translateY(-50%);
    z-index: 25;
  }
  /* Collapsed → pinned at the screen's left edge, fully visible, as the reopen affordance. */
  .nav-collapsed .console__nav-toggle { left: 0; }

  .console__nav {
    position: relative;
    z-index: 20;
    transition: margin-left 0.22s ease;
  }
  /* Collapsed: slide fully off-canvas (negative margin = hidden AND main reclaims the width). */
  .nav-collapsed .console__nav {
    margin-left: calc(-1 * var(--console-nav-w));
  }
}

/* Respect reduced-motion — no slide, just an instant show/hide. */
@media (prefers-reduced-motion: reduce) {
  .console__nav,
  .console__nav-toggle { transition: none; }
}

/* Mobile: the sidebar is a stacked top bar — collapse is desktop-only, so keep it visible and drop
 * the toggle. */
@media (max-width: 768px) {
  .nav-collapsed .console__nav { margin-left: 0; }
  .console__nav-toggle { display: none; }
}

/* Settings shell — a left sub-nav (.section-nav) + a content pane. Built to grow (more sections slot
 * into the sub-nav). Stacks on mobile. */
.settings-shell {
  display: flex;
  gap: var(--space-6);
  align-items: flex-start;
}
.settings-shell__nav {
  flex: 0 0 200px;
}
.settings-shell__main {
  flex: 1 1 auto;
  min-width: 0;
}
@media (max-width: 768px) {
  .settings-shell {
    flex-direction: column;
  }
  .settings-shell__nav {
    flex: none;
    width: 100%;
  }
}

/* Cap content width for readability, but ALWAYS 100% under the cap. */
.content-narrow {
  width: 100%;
  max-width: 720px;
}

/* ── Typeahead (searchable picker — e.g. the BOM 자재 picker; reusable) ────────
 * A text input with a floating results menu; never loads the full option set. */
.typeahead {
  position: relative;
}
.typeahead__menu {
  position: absolute;
  z-index: 30;
  top: 100%;
  left: 0;
  right: 0;
  max-height: 260px;
  overflow-y: auto;
  background: var(--color-bg-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
}
.typeahead__menu--above {
  top: auto;
  bottom: 100%;
}
.typeahead__item {
  padding: var(--space-2) var(--space-3);
  cursor: pointer;
  white-space: nowrap;
}
.typeahead__item:hover,
.typeahead__item.is-active {
  background: var(--color-bg-base);
}
.typeahead__empty {
  padding: var(--space-2) var(--space-3);
  color: var(--color-text-muted);
}

/* ── Numeric / total columns (ADR 0016) — right-aligned + tabular figures, on BOTH
 * the header and the cells so a 계/합계 header sits over its digits. One class works
 * for a real th/td (text-align) and for our flex .admin-table cells (justify-content). */
.col-num {
  text-align: right;
  justify-content: flex-end;
  font-variant-numeric: tabular-nums;
}

/* ── Uniform field rhythm (ADR 0016) — fields align row-for-row across side-by-side
 * .form-section panes: a fixed label-row height + the standard control, and long helper
 * text lives in an ℹ tooltip (below) instead of a block that pushes its pane down. */
.form-label {
  min-height: 20px;
  display: flex;
  align-items: center;
  gap: var(--space-1);
  /* Muted label so the DATA (the value in the control) reads first; label recedes
   * (Carbon/Atlassian record-page hierarchy). */
  color: var(--color-text-secondary);
  font-weight: var(--fw-regular);
}
/* ── Stretched link (ADR 0016) — a whole row is clickable via a primary link's overlay,
 * while a secondary action stays independently clickable. Put `.stretched-link` on the
 * primary <a>, and `.stretched-above` on any control that must sit over it. */
.admin-table__row--link {
  position: relative;
  /* The whole row is an <a>, but its TEXT is data, not a link — keep it ink (the accent is
   * reserved for actual link/actions), so cells don't render accent-coloured. */
  color: var(--color-text-primary);
}
/* The overlay IS the anchor (position:absolute) — so in a CSS-grid row it does NOT occupy a
 * column track (an empty in-flow anchor would shift every cell over). */
.stretched-link {
  position: absolute;
  inset: 0;
  z-index: 1;
}
.stretched-above {
  position: relative;
  z-index: 2;
}

/* ══════════════════════════════════════════════════════════════════════════
 * DENSITY + STATE — one control height, one focus ring, one disabled/hover
 * treatment for EVERY control (kit-level, ADR 0016). Tokenised over base.css
 * so no screen sets its own heights/states.
 * Height 32px = Carbon `sm` · Ant `middle` · Primer `medium`. Focus-visible
 * ring + disabled treatment follow Carbon / Material a11y guidance.
 * ══════════════════════════════════════════════════════════════════════════ */

/* One height across text inputs, selects, and buttons. */
.form-input,
select.form-input,
.btn {
  height: var(--control-height);
}
/* textarea (비고/note): grows vertically only — never horizontal (breaks the grid).
 * Kit control styling + comfortable padding; min two lines. */
textarea.form-input {
  height: auto;
  min-height: calc(var(--control-height) * 2);
  padding: var(--space-2) var(--control-pad-x);
  line-height: var(--lh-body);
  resize: vertical;
}
.form-input {
  padding: 0 var(--control-pad-x);
}
/* No native number spinners — keep type=number/inputmode for the numeric keypad, drop the
 * up/down steppers (they crowd the dense grid and render differently per browser). */
input.form-input[type="number"] {
  -moz-appearance: textfield;
  appearance: textfield;
}
input.form-input[type="number"]::-webkit-outer-spin-button,
input.form-input[type="number"]::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
.btn {
  padding: 0 var(--space-4);
}

/* Dense tier — one small height (28px = Carbon `sm` / Primer `small`) for the
 * explicit --sm variants. Declared AFTER the base-height rule so these keep
 * their compact size instead of inheriting the 32px default. */
.btn-ghost--sm,
.btn-secondary--sm,
.form-input--sm {
  height: var(--control-height-sm);
}
/* Read-only field matches an editable input's height so mixed rows align. */
.form-readonly {
  height: var(--control-height);
}

/* One focus-visible ring for every interactive control (keyboard + text focus).
 * Text fields render focus-visible on any focus; buttons/links only on keyboard. */
.form-input:focus-visible,
select.form-input:focus-visible,
textarea.form-input:focus-visible,
.btn:focus-visible,
a.btn:focus-visible,
.admin-table__row--link:focus-visible,
.pagination__btn:focus-visible,
input[type="checkbox"]:focus-visible,
.stretched-link:focus-visible {
  outline: none;
  border-color: var(--color-focus);
  box-shadow: var(--focus-ring);
}

/* One disabled treatment. */
.btn:disabled,
.btn[aria-disabled="true"],
.form-input:disabled,
select.form-input:disabled {
  opacity: var(--disabled-opacity);
  cursor: not-allowed;
}

/* A field switched off by another field's value (e.g. 비용구분 when 자재구분 isn't 부자재): dim the
   LABEL too, so the whole field reads as inactive rather than an enabled label over a greyed input.
   The ℹ hint stays at full strength — it is what explains why the field is off. */
.form-field--disabled .form-label { opacity: var(--disabled-opacity); }
.form-field--disabled .form-label .hint-ico { opacity: 1; }

/* Shared hover tint for secondary / ghost controls and linked rows. */
.btn-secondary:hover,
.btn-ghost:hover,
.admin-table__row--link:hover {
  background: var(--color-bg-hover);
}

/* ── Form rhythm (Atlassian spacing) ───────────────────────────────────────
 * label→control gap on the field; BETWEEN-field spacing comes from the container's
 * gap (.form-row / .form-stack), NEVER a sibling `margin-top` hack (that legacy rule
 * is deleted — the token-lint guard fails if it returns). */
.form-field {
  gap: var(--space-label-gap);
}

/* Heading line-heights come from the ramp (no ad-hoc values). */
.heading-h1 {
  line-height: var(--lh-heading);
}
.heading-h2,
.form-section__title {
  line-height: var(--lh-heading);
}
.form-field--dirty .form-input {
  background: color-mix(in srgb, var(--color-warning) 12%, var(--color-bg-surface));
  border-color: var(--color-warning);
}

/* Account creation password controls - compact feedback, using the shared form rhythm. */
.password-setup { margin-top: var(--space-4); }
.password-setup__field { max-width: 640px; }
.password-control { position: relative; }
.password-control .form-input { width: 100%; padding-right: calc(var(--space-8) + var(--space-4)); }
.password-control__toggle {
  position: absolute;
  top: 50%;
  right: var(--space-2);
  transform: translateY(-50%);
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-text-secondary);
  font: inherit;
  font-size: var(--fs-meta);
  font-weight: var(--fw-bold);
  cursor: pointer;
  padding: var(--space-1) var(--space-2);
}
.password-control__toggle:hover { color: var(--color-primary); background: var(--color-bg-subtle); }
.password-control__toggle:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 1px; }
.password-strength { display: flex; align-items: center; gap: var(--space-2); color: var(--color-text-muted); font-size: var(--fs-meta); }
.password-strength__meter { width: 72px; height: 4px; overflow: hidden; border-radius: var(--radius-pill); background: var(--color-border-subtle); }
.password-strength__meter > span { display: block; height: 100%; background: var(--color-danger); transition: width 0.15s ease, background-color 0.15s ease; }
.password-strength[data-level="2"] .password-strength__meter > span { background: var(--color-warning); }
.password-strength[data-level="3"] .password-strength__meter > span { background: var(--color-success); }
.password-match { min-height: 1.2em; margin: 0; color: var(--color-text-muted); font-size: var(--fs-meta); }
.password-match[data-state="valid"] { color: var(--color-success); }
.password-match[data-state="invalid"] { color: var(--color-danger); }

/* ── Wide tables scroll HERE, not the page ────────────────────────────────── */
.table-scroll {
  overflow-x: auto;
  max-width: 100%;
}

/* ── Master-detail two-pane ───────────────────────────────────────────────
 * minmax(0, …) so BOTH tracks shrink below their content (without the 0-floor
 * the 1fr track's min-content pins the grid wide → page overflow). Stacks to
 * one column ≤768px; a wide detail table then scrolls inside its .table-scroll. */
.masterdetail {
  display: grid;
  grid-template-columns: minmax(0, 300px) minmax(0, 1fr);
  gap: var(--space-6);
  align-items: start;
  /* No own top margin — the .page-header above owns the header→content gap
   * (--space-block-gap), same as every other page. An extra margin-top here
   * double-gapped 기초코드 관리 + 사이즈 관리 vs the rest of the app. */
}

/* Detail-pane header (right pane): a back link (mobile drill-down only), the
 * selected item's name, and the "+ 추가" action at the top-right — so the add
 * button sits at the TOP of what it adds to, and is reachable in the drilled-down
 * mobile view. */
.masterdetail__detail-head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  /* Fixed height on BOTH panes' heads so the left master-list header row and the
   * right grid header row line up (the head is the only thing above each). */
  min-height: 2.25rem;
  margin-bottom: var(--space-4);
}
.masterdetail__detail-title {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--fs-h3);
  font-weight: var(--fw-medium);
  color: var(--color-text-primary);
}
/* Left (master) pane caption — labels the list; shares the head height so the two
 * column-header rows align. Muted + body-sized (it's a caption, not a page title). */
.masterdetail__aside-title {
  flex: 1;
  min-width: 0;
  font-size: var(--fs-body);
  font-weight: var(--fw-medium);
  color: var(--color-text-secondary);
}
/* A grouped run of controls in a detail head (a lifecycle group + a grid-actions
 * group), laid out as one tidy toolbar rather than a corner cluster. */
.md-toolbar {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: 0 0 auto;
}
.md-toolbar__sep {
  width: 1px;
  align-self: stretch;
  background: var(--color-border);
  margin: var(--space-1) var(--space-1);
}
.md-toolbar__pending {
  font-size: var(--fs-caption);
  color: var(--color-warning);
}
/* Back link belongs to the mobile drill-down; hidden while both panes show. */
.masterdetail__back { display: none; }

/* ── Resizable split panes (splitter.js drives a CSS var; ADR 0044 kit) ──────
 * ONE gutter style, reused for a vertical column-resize and a horizontal row-resize. ~6–8px
 * hit area, invisible at rest → hairline on hover → accent while dragging. */
.split-gutter {
  align-self: stretch;
  background: transparent;
  border: 0;
  padding: 0;
  transition: background-color 0.15s ease;
  touch-action: none; /* the pointer drag owns the gesture; no scroll/zoom interference */
}
.split-gutter--v:hover { background: var(--color-border); }
.split-gutter--v.is-dragging,
.split-gutter:focus-visible { background: var(--color-accent); outline: none; }
.split-gutter--v { width: 8px; cursor: col-resize; }
/* Horizontal (마감) resize bar: sits BELOW the table with a clear gap (never touching its data), with
 * a faint always-visible grabber so it reads as a distinct, draggable handle. */
.split-gutter--h {
  width: 100%;
  height: 16px;
  margin-top: var(--space-1);
  cursor: row-resize;
}
.split-gutter--h::before {
  content: "";
  display: block;
  width: 44px;
  height: 3px;
  margin: 6px auto 0;
  border-radius: 999px;
  background: var(--color-border);
  transition: background-color 0.15s ease;
}
.split-gutter--h:hover::before { background: var(--color-text-secondary); }
.split-gutter--h.is-dragging::before { background: var(--color-accent); }
/* While dragging, don't let a stray text-selection follow the pointer. */
body.split-dragging { user-select: none; }

/* Resizable master-detail: the aside width becomes a draggable track (var), with the gutter as the
 * middle grid column between aside and main. Gap shrinks (the gutter is the separator now). */
.masterdetail--resizable {
  gap: var(--space-2);
  grid-template-columns: var(--md-aside-w, 300px) 8px minmax(0, 1fr);
}

/* 마감 관리 vertical split: the 거래처 master (collapsible, resizable height) over the 정산 상세,
 * which FILLS the rest. On desktop the fixed shell (ADR 0013/0020) gives .console__scroll a definite
 * height, so the panes flex to fill it and the gutter between them drags the split (--dg-top-h);
 * collapsing the top hands its space to the detail. On mobile (normal document scroll) the panes
 * stack, the gutter hides, and collapse still hides the master body. */
.closing-split {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-top: var(--space-2);
  min-width: 0;
}
.closing-split__pane {
  margin: 0;
  padding: var(--space-4);
  min-width: 0;
}
.closing-split__pane-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--color-border);
}
.closing-split__pane-head .form-section__title {
  margin: 0;
  padding: 0;
  border-bottom: 0;
}
.closing-split__pane-head .record-form__actions { margin: 0; }
.closing-split__pane-body {
  margin-top: var(--space-3);
  min-width: 0;
}
.closing-split__gutter { margin: 0; }
/* Collapse toggle - a quiet icon button; the chevron flips when the master is collapsed. */
.closing-split__toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: var(--control-height-sm);
  height: var(--control-height-sm);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-text-secondary);
  cursor: pointer;
  transition: background-color 0.15s ease, color 0.15s ease;
}
.closing-split__toggle:hover {
  background: var(--color-bg-hover);
  color: var(--color-text-primary);
}
.closing-split__toggle svg {
  transition: transform 0.15s ease;
}
.closing-split.is-collapsed .closing-split__toggle svg {
  transform: rotate(180deg);
}

/* 마감 처리 action bar (ADR 0050) - sits under the 미마감 grid: 저장 on the left, then a summary that
   tracks the ticked rows and falls back to the whole-tab total. Wraps on narrow widths like any
   .form-row; the hidden line_ids live in a display:none box so they never affect the layout. */
.closing-actionbar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: var(--space-2);
}
.closing-actionbar__ids {
  display: none;
}
.closing-actionbar__summary {
  margin-left: auto;
  font-weight: var(--fw-medium);
  font-variant-numeric: tabular-nums;
}
.closing-header-action {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-left: var(--space-3);
}
.closing-header-action__date {
  color: var(--color-text-secondary);
  font-size: var(--fs-caption);
  white-space: nowrap;
}
.closing-header-action .form-input { width: 142px; }
.closing-detail__tabs { margin-left: auto !important; }
@media (max-width: 768px) {
  .closing-split__pane-head--detail { align-items: flex-start; flex-wrap: wrap; }
  .closing-header-action { order: 3; width: 100%; margin-left: 0; }
  .closing-detail__tabs { margin-left: 0 !important; }
}

@media (min-width: 769px) {
  /* Fill the console viewport: the split is the flexing region under the toolbar + filter row. */
  .closing-split {
    flex: 1 1 auto;
    min-height: 0;
  }
  .closing-split__pane {
    display: flex;
    flex-direction: column;
    min-height: 0;
  }
  .closing-split__top {
    flex: 0 0 var(--dg-top-h, 220px);
  } /* resizable height (the gutter drags this) */
  .closing-split__bottom {
    flex: 1 1 auto;
    /* min-content, NOT the 0 inherited from .closing-split__pane. The rule above already SAYS the
     * section "won't shrink past its content" — it did not, because min-height:0 let the pane
     * collapse to the leftover space while #unclosed-grid held its own 18rem floor, and the
     * difference painted BELOW the card (43px at 1280x700). Same defect as the 일괄 등록 line card;
     * the fix is the same: a fill-height card may shrink, but never past its contents. The TOP pane
     * deliberately keeps min-height:0 — it is user-resizable and is meant to give way. */
    min-height: min-content;
  } /* fills the remaining height */
  .closing-split--top-only .closing-split__top {
    flex: 1 1 auto;
  } /* no detail → master fills */
  .closing-split__pane-head {
    flex: 0 0 auto;
  }
  .closing-split__pane-body {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
  }
  /* The bounded list + AG grid FILL the pane body (no fixed cap on desktop). .dg-split is a flex
   * ROW (align-items:flex-start), so to make its .table-scroll fill the pane HEIGHT it has to become
   * a stretch column here; otherwise the table keeps its content height and, uncapped, overflows
   * the pane and overlaps the pane below it. */
  .closing-split__pane-body .dg-split--viewport {
    flex: 1 1 auto;
    min-height: 0;
    flex-direction: column;
    align-items: stretch;
  }
  .closing-split__pane-body .dg-split--viewport .table-scroll {
    flex: 1 1 auto;
    min-height: 0;
    max-height: none;
    overflow-y: auto;
  }
  .closing-split__pane-body [data-grid] {
    flex: 1 1 auto;
    min-height: 0;
    max-height: none;
  }
  /* collapsed: the top pane shrinks to just its header; the gutter hides (nothing to resize) */
  .closing-split.is-collapsed .closing-split__top {
    flex: 0 0 auto;
  }
  .closing-split.is-collapsed .closing-split__top .closing-split__pane-body {
    display: none;
  }
  .closing-split.is-collapsed .closing-split__gutter {
    display: none;
  }
}

@media (max-width: 768px) {
  /* No fixed shell → stack naturally; the gutter is meaningless (no viewport to divide) and the
   * scroll grid needs an explicit height (autoHeight would render every row). */
  .closing-split__gutter {
    display: none;
  }
  .closing-split__pane-body .dg-split--viewport .table-scroll {
    max-height: 320px;
  }
  .closing-split__pane-body [data-grid] {
    height: 60vh;
    min-height: 320px;
  }
  .closing-split.is-collapsed .closing-split__pane-body {
    display: none;
  }
}

/* Selected row highlight (e.g. the picked code group in the left pane). */
.admin-table__row--selected { background: var(--color-bg-base); }

/* Highlight the inline "add" row in an editable admin-table. */
.admin-table__row--add { background: var(--color-bg-base); }

/* Right-aligned numeric cell. `.admin-table__cell` is display:flex, so
 * text-align has no effect — use justify-content to push the value to the edge,
 * keeping it under a right-aligned header. */
.admin-table__cell--num { justify-content: flex-end; }

/* ── Form controls: fluid, never a fixed px that overflows; rows wrap ──────── */
.form-input,
.form-input--sm {
  max-width: 100%;
}
.form-input--md { max-width: 320px; }
.form-input--full {
  width: 100%;
  max-width: 100%;
}
/* Fields sit on a CONSISTENT 2-column grid — every row on the same two grid lines (SAP Fiori
 * dense form): row-gap = the field rhythm token, column-gap = the block-gap token. No irregular
 * 2-and-3-field rows. A field spans one column; `--full` (Desc/비고/주소) spans both. Labels stay
 * on top. Collapses to one column on narrow viewports (below). */
.form-row {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  row-gap: var(--space-field-gap);
  column-gap: var(--space-6);
  align-items: start;
}
.form-row > .form-field--full {
  grid-column: 1 / -1;
}
.record-form > .form-row--full {
  grid-column: 1 / -1;
}

/* Dense document-header band (발주/입고/출고/마감 detail) — compact meta above a prominent line grid
 * (SAP/NetSuite/Xero pattern). Lives in a `.form-section--full` so it spans the whole record width
 * (same as the line-grid card below). auto-fill spreads the fields into as many ~200px columns as
 * fit (≈5 on a wide band, 1 on mobile) so it's SHORT vertically; `--full` (비고) spans the row. */
.doc-meta {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  row-gap: var(--space-3);
  column-gap: var(--space-4);
  align-items: start;
}
.doc-meta > .form-field--full {
  grid-column: 1 / -1;
}
/* 사이즈구분 inline header edit (size console) — one tidy row: 사이즈구분명 · 바이어 · 사용유무 · 저장,
 * controls bottom-aligned so the fields line up regardless of label height. */
.size-run-form { margin-bottom: var(--space-4); }
.size-run-form__fields {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--space-3);
}
.size-run-form__fields > .form-field { flex: 0 0 auto; }
.size-run-form__fields .form-input { width: auto; min-width: 220px; }
.size-run-form__save { flex: 0 0 auto; }
.check-inline {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  height: var(--control-height); /* align the checkbox with the inputs' bottom edge */
  cursor: pointer;
}
/* 비고 sits inline as a compact field in the band (not a tall full-width block): single-line
 * footprint matching the other inputs, still resize:vertical if the user wants more room. */
.doc-meta textarea.form-input {
  min-height: var(--control-height);
}

/* Shared "why is this locked?" hint (hint.js) — one instant, position:fixed tooltip used by BOTH
 * read-only form-field labels (a ℹ with data-hint) AND AG Grid read-only column headers (the
 * SheensHint header). Replaces the laggy native title=. */
.hint-ico {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 14px;
  height: 14px;
  border: 1px solid var(--color-border);
  border-radius: 50%;
  font-size: 10px;
  line-height: 1;
  color: var(--color-text-muted);
  cursor: help;
}
.hint-head {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  width: 100%;
  /* FULL height too, so the whole header cell is the sort target. At auto height a click on the
   * cell's padding lands on the AG Grid cell rather than this component and no sort fires — the
   * column then sorts when you hit the text and ignores you when you hit beside it. */
  height: 100%;
  min-width: 0;
}
.hint-head--right {
  justify-content: flex-end;
}
.hint-head .ag-header-cell-text {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.hint-tip {
  position: fixed;
  z-index: 1000;
  max-width: 280px;
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-sm);
  background: var(--color-text-secondary);
  color: #fff;
  font-size: var(--fs-caption);
  line-height: 1.35;
  box-shadow: var(--shadow-md);
  pointer-events: none;
  white-space: normal;
}

/* "방금 업로드한 N건" banner (post bulk-upload landing) — the message on the left, an obvious
 * "전체 목록 보기" button on the right that clears the ?batch filter. */
.recently-added-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
}

/* Slim LEFT-aligned action bar above a document 정보 card — approval controls (발주/마감 결재). Separate
 * from the back nav (top-right in the page header) and from 저장 (bottom of the form). */
.doc-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}
/* A single-column stack (e.g. auth) — same tokenised row-gap, no sibling margins. */
.form-stack {
  display: flex;
  flex-direction: column;
  gap: var(--space-field-gap);
}

/* ── Record-form pattern (ADR 0022): capped, evenly-spaced create/edit form ──
 * All spacing from the 4/8 token scale — no one-off margins. Sections read as
 * distinct blocks with a uniform heading + subtle divider. */
.record-header__title {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
/* The record header (title + status badge + activate/deactivate button) is capped to
 * the SAME width as the form below it and left-aligned, so the toggle sits at the
 * form's top-right corner — directly above the container it acts on — instead of
 * floating at the far page edge. (`_record_header` adds `.record-header`; list
 * headers keep the full-width `.page-header`.) */
.record-header {
  max-width: var(--record-width);
}
/* Sections lay out in a responsive 2-column grid (side by side on desktop, less
 * scrolling) and collapse to 1 column ≤768px. Each section is a clean card. */
.record-form {
  width: 100%;
  max-width: var(--record-width); /* matches .record-header so the toggle lines up */
  margin-top: var(--space-6);
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-6);
  align-items: start;
}
/* A form with a SINGLE section (e.g. 색상, 코드값) fills the full width so its card
 * reaches the same right edge as the header's toggle — no lonely narrow card with the
 * button stranded far to its right. (`:only-of-type` ignores the actions/alert divs.) */
.record-form > .form-section:only-of-type {
  grid-column: 1 / -1;
}
.form-section {
  background: var(--color-bg-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-6);
  min-width: 0;
}
.form-section__title {
  font-size: var(--fs-h3);
  font-weight: var(--fw-semibold);
  color: var(--color-text-primary);
  padding-bottom: var(--space-2);
  margin-bottom: var(--space-4);
  border-bottom: 1px solid var(--color-border);
}
/* Section header with a right-aligned action (the ADD-AT-TOP rule): an "add" button
 * for what a section CONTAINS sits in the section's header, never below its rows —
 * e.g. "+ 행 추가" above a child-row table (parent+child pattern). The divider moves
 * from the title onto the head so it spans title + action. */
.form-section__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding-bottom: var(--space-2);
  margin-bottom: var(--space-4);
  border-bottom: 1px solid var(--color-border);
}
.form-section__head .form-section__title {
  padding-bottom: 0;
  margin-bottom: 0;
  border-bottom: 0;
}
/* Grey heading bar for a card that sits among data tables — full-bleed to the card edges so it reads
 * like the tables' .admin-table__head (grey strip at the top). */
.form-section__head--grey {
  margin: calc(-1 * var(--space-6)) calc(-1 * var(--space-6)) var(--space-4);
  padding: var(--space-3) var(--space-6);
  background: var(--table-header-bg);
  border-bottom: 0; /* the grey fill separates the head — no extra hairline */
  border-radius: var(--radius-md) var(--radius-md) 0 0;
}
/* Full-width rows inside the section grid (errors, the save/cancel bar, and any
 * section that holds a wide child-row table — the parent+child pattern). */
.record-form > .alert,
.record-form__actions,
.form-section--full {
  grid-column: 1 / -1;
}
/* An order detail owns a tab strip and its panels as separate direct children of the two-column
 * record grid. They are one composite view, so both must span the grid rather than becoming left
 * and right columns of that grid. */
.record-form > .tabs,
.record-form > .order-detail-tabs {
  grid-column: 1 / -1;
  min-width: 0;
}
/* 기본 정보 tab is a compact record overview: image/basic info, then production/price in two
 * predictable desktop rows. The color-size matrix remains the only full-width section. */
.order-detail-tabs #panel-basic {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-6);
  align-items: start;
}
.order-detail-tabs #panel-basic[hidden] {
  display: none;
}
@media (max-width: 768px) {
  .order-detail-tabs #panel-basic { grid-template-columns: 1fr; }
}
/* ── Document OBJECT PAGE (발주/입고/출고) ─────────────────────────────────────────────────────
 * These screens are a LINE GRID with some header facts attached, not a form with a table under it.
 * Measured before this existed, at 1440x900: 750 of 900px went to chrome and THREE rows of line data
 * were visible. The grid was `domLayout:autoHeight` — it sized to its content and could never use the
 * window — while seven stacked bands (page header, 결재 bar, header card, section head, grid header,
 * totals bar, actions bar) and the gaps between them ate the rest.
 *
 * So: one flex COLUMN that owns the height the shell gives it (.console__scroll is already a
 * 100dvh flex frame), the grid takes everything left over, and every band that was only holding
 * buttons folds into the sticky page header. ~14 rows at the same viewport.
 *
 * It also opts out of the --record-width cap: that cap keeps prose and field columns from stretching
 * edge-to-edge, which is right for a master record (거래처, 자재) and wrong for a 12-column grid.
 * Opt-in, so every other record form keeps both the cap and the 2-column card grid. Pair it with a
 * page header that does NOT pass `capped`, or the header stops lining up with the form's edge. */
.record-form--wide {
  max-width: none;
}
/* FILLING only works where there is a definite height to fill: the fixed 100dvh shell, i.e. ≥769px.
 * Scoped to the same breakpoint that creates it. */
@media (min-width: 769px) {
  .record-form--wide {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    flex: 1 1 auto;
    min-height: 0; /* the #1 flex bug — without it the grid refuses to shrink and the page scrolls */
    margin-top: 0; /* the sticky page header already owns the gap above */
    /* .record-form sets align-items:start for its 2-column CARD grid, where it stops a short card
     * stretching to a tall neighbour's height. Inherited into a COLUMN flex container it means the
     * cross axis — width — so every card shrank to its content and the line grid rendered as a
     * ~300px stub. Stretch is what a column layout wants. */
    align-items: stretch;
  }
  /* The lines card is the one that grows; everything else keeps its natural height. */
  .record-form--wide > .form-section--lines {
    flex: 1 1 auto;
    /* min-content, NOT 0. At 0 the card shrinks to whatever is left over while the grid inside holds
     * its own floor, and the difference PAINTS OUTSIDE the card — the table crossing its own border,
     * with no page scroll to hint at it. Seen on 일괄 등록, which carries two header cards instead of
     * one and so leaves ~50px less: 27px of grid hanging below the card edge. This makes the card
     * refuse to shrink below its contents, so the overflow becomes a shell scroll (visible, normal)
     * instead of paint. */
    min-height: min-content;
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
  }
  /* DESCENDANT, not child: on a tabbed section the grid sits one level down inside its panel, and on
   * the create screens it is still a direct child. One rule has to reach both. */
  .record-form--wide > .form-section--lines .tabs__panels,
  .record-form--wide > .form-section--lines .tabs__panel {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
  }
  /* ...but NOT the inactive one. `.tabs__panel[hidden]` is (0,2,0) and the rule above is (0,3,0), so
   * the layout rule wins and the hidden panel renders anyway — the 연결 문서 table showed up under
   * the grid and took 200px off it (11 visible rows -> 6). Re-assert `hidden` at this specificity.
   * The kit uses no `!important`, so the fix is to match the selector, not to escalate. */
  .record-form--wide > .form-section--lines .tabs__panel[hidden] {
    display: none;
  }
  .record-form--wide > .form-section--lines .ag-theme-sheens {
    flex: 1 1 auto;
    /* A FLOOR, and a floor in flex cannot be shrunk past — so it is the number that decides whether
     * the card can hold its contents. 220 was too tall for a screen with two header cards; 140 still
     * shows three rows plus the header, and below it the shell scrolls rather than the grid escaping
     * (see min-content above). */
    min-height: 140px;
  }
}
/* ≤768 the document scrolls normally — there is no leftover height to take, and a MIN-height is not
 * enough on its own: AG Grid's internal wrapper is `height: 100%`, which resolves against the
 * parent's HEIGHT, not its min-height. With `height: auto` the whole grid collapsed to 0 and never
 * rendered at all — responsive_check caught it as `mounted=False` at 375px. A definite height fixes
 * it; viewport-relative so a phone still shows a useful number of rows. */
@media (max-width: 768px) {
  .record-form--wide > .form-section--lines .ag-theme-sheens {
    height: 60vh;
  }
}

/* ── Tabs (kit) ───────────────────────────────────────────────────────────────
 * CLIENT-SIDE by design: both panels render, JS toggles `hidden`. A query-param tab would be a page
 * load, and on 발주 detail the panel it leaves behind is an editable grid — switching tabs would throw
 * away unsaved line edits with no warning. Nothing here is worth that.
 * `hidden` (not a class) is what the browser and assistive tech already understand for "not the
 * current panel"; the display rule below only has to beat the flex layout above. */
.tabs {
  display: flex;
  align-items: stretch;
  gap: var(--space-1);
  border-bottom: 1px solid var(--color-border);
  min-width: 0;
  overflow-x: auto; /* many tabs on a phone scroll the strip, never the page */
}
.tabs__tab {
  flex: 0 0 auto;
  appearance: none;
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px; /* the active underline sits ON the strip's border, not under it */
  padding: var(--space-2) var(--space-3);
  font: inherit;
  font-size: var(--fs-body);
  font-weight: var(--fw-medium);
  color: var(--color-text-secondary);
  cursor: pointer;
  white-space: nowrap;
}
.tabs__tab:hover {
  color: var(--color-text-primary);
  background: var(--color-bg-hover);
}
.tabs__tab[aria-selected="true"] {
  color: var(--color-text-primary);
  border-bottom-color: var(--color-accent);
}
.tabs__tab:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
  border-radius: var(--radius-xs);
}
.tabs__count {
  display: inline-block;
  margin-left: var(--space-1);
  padding: 0 6px;
  border-radius: 999px;
  background: var(--color-bg-active);
  color: var(--color-text-secondary);
  font-size: var(--fs-label);
  font-variant-numeric: tabular-nums;
}
.tabs__panel[hidden] {
  display: none;
}
/* A section head that holds a tab strip: the strip carries the baseline, so the head must not draw a
 * second one 1px below it. Actions sit on the same baseline as the tab underline. */
.form-section__head--tabs {
  align-items: flex-end;
  padding-bottom: 0;
  border-bottom: 0;
}

/* DENSE document header — one band that shows AND edits. A document header is reference data you
 * glance at, not a form you fill in field by field, so it runs at the kit's small control height with
 * 12px labels and tight rows. The earlier attempt split it into a read-only strip plus a folded
 * editor; that read as two headers and said everything twice. ~190px -> ~120px, still fully editable. */
/* The section head holds two line actions now (행 삭제 · 행 추가). They are ONE cluster on the right —
 * loose in a space-between row they drift apart, stranding 행 삭제 in the middle of the header. */
.form-section__head-actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: 0 0 auto;
}
/* The dense header's CARD tightens too — at --space-6 the padding alone was 48px of the band. */
.form-section--dense { padding: var(--space-4); }
/* LABEL BESIDE THE VALUE, not above it (ADR 0059). A document header is reference data you glance
 * at, and stacking label-over-input spent two rows on every one of them. Beside, each field is ONE
 * row: the 발주 header drops from ~190px to ~110px with nothing hidden.
 *
 * The label column is a FIXED width inside each field, and the outer track list gives every field
 * cell the same width — so every input's left edge lines up down the whole card and the values can
 * be scanned vertically. That alignment is the reason for a fixed column rather than letting each
 * label size itself.
 *
 * 84px fits the longest label in these headers (입고처명 · 입반구분 · 입고 창고) plus its ℹ icon. A
 * longer one would be clipped by the nowrap, so measure before adding one. */
.doc-meta--dense {
  row-gap: var(--space-2);
  column-gap: var(--space-4);
  /* wider than the label-on-top version was: each cell now carries 84px of label + the control.
   * 236 is the smallest value that still leaves a date input its natural width (~130px) — going
   * wider costs a whole extra ROW on a 1440 band, which is worth more than the slack. */
  grid-template-columns: repeat(auto-fill, minmax(236px, 1fr));
}
.doc-meta--dense .form-field {
  display: grid;
  grid-template-columns: 84px minmax(0, 1fr);
  align-items: center;
  column-gap: var(--space-2);
  row-gap: 2px;
  gap: 2px var(--space-2); /* overrides .form-field's label-above gap */
}
/* Force a field to start a new grid row — the contact block (ATTN·HP·TEL·비고) reads as a group, so
 * it should not begin wherever the previous row happened to run out. */
.doc-meta--dense .form-field--rowstart { grid-column: 1; }
.doc-meta--dense .form-label {
  font-size: var(--fs-label);
  margin-bottom: 0;
  min-height: 0;
  justify-content: flex-end; /* .form-label is a flex row (label text + ℹ) */
  text-align: right;
  white-space: nowrap;
}
/* An inline error belongs under its CONTROL, not in the label column. */
.doc-meta--dense .form-error { grid-column: 2; }
/* 비고 is the one field whose value is free text, so it gets TWICE the width of the others — but as
 * two columns, NOT its own row. A full-width row costs ~46px of the line grid below it every time,
 * and 비고 spanning 2 cells drops into the slack the last row already has, for nothing. Falls back to
 * the full row only when the band is down to one column (mobile), where "2 columns" has no meaning —
 * and worse than meaningless: `span 2` against a single-column track list makes the grid INVENT a
 * second column, so the band came out 36px wider than a 375px phone and the page scrolled sideways.
 * Scoped to the same 769px breakpoint the shell uses. */
@media (min-width: 769px) {
  .doc-meta--dense .form-field--wide {
    grid-column: span 2;
  }
}
.doc-meta--dense .form-input,
.doc-meta--dense select.form-input {
  height: var(--control-height-sm);
  padding-block: 0;
  font-size: var(--fs-table);
}
.doc-meta--dense textarea.form-input {
  min-height: var(--control-height-sm);
  height: var(--control-height-sm);
}
/* Height stays at one control — the gain is WIDTH. A long note drags taller (textareas resize
 * vertical in this kit) rather than taxing every 발주 that has no note at all. */
/* compact card — tighter padding for a short single-row form (e.g. 사이즈구분 inline edit). */
.form-section--compact { padding: var(--space-4); }
.record-form__actions {
  display: flex;
  gap: var(--space-2);
}
/* soft-delete (사용중지/사용재개) sits at the RIGHT of the action bar, apart from 저장/취소 */
.record-form__deactivate { margin-left: auto; }
/* Right-aligned variant — for an action bar that lives INSIDE a card (rather than below the whole
 * form), where the commit button reads better trailing its own section than floating at the left. */
.record-form__actions--end { justify-content: flex-end; }

/* The 품번/오더 picker reuses the EXISTING .typeahead / __menu / __item / __empty kit above
 * (ADR 0052 §2). Only this one modifier is new: the muted 브랜드·시즌 suffix on a result row. */
.typeahead__meta {
  color: var(--color-text-muted);
}

/* A master-detail is NEVER both-stacked. The instant the two panes stop sitting side by
 * side they become single-pane DRILL-DOWN (master list → tap a row → detail pane + a back
 * link) — there is no "list on top, detail below" state at any width. The side-by-side →
 * drill-down breakpoint differs by variant so drill-down covers the ENTIRE non-side-by-side
 * range and the breakpoints stay in sync (ADR 0020/0025):
 *   - .masterdetail--wide-detail (wide value table, e.g. 기초코드): side-by-side >1280, drill ≤1280
 *   - plain .masterdetail: side-by-side >768, drill ≤768 (below)
 * A ~5-column value table beside a ~300px list only fits without an internal horizontal
 * scrollbar once the window is >1280px; below that the table gets the full width via drill-down
 * (verified by the table-fit gate in responsive_check.py — the earlier ≤1024 threshold let the
 * table scroll inside its pane in the 1040–1220 zone). */
@media (max-width: 1280px) {
  .masterdetail--wide-detail { grid-template-columns: 1fr; }
  .masterdetail--wide-detail .masterdetail__aside { max-width: none; margin-bottom: 0; }
  .masterdetail--wide-detail .masterdetail__main { display: none; } /* list-only by default */
  .masterdetail--wide-detail.masterdetail--detail-open .masterdetail__aside { display: none; }
  .masterdetail--wide-detail.masterdetail--detail-open .masterdetail__main { display: block; }
  .masterdetail--wide-detail.masterdetail--detail-open .masterdetail__back { display: inline-flex; }
  /* Drill-down is single-pane → no gutter, one column (overrides the --resizable track). */
  .masterdetail--wide-detail.masterdetail--resizable { grid-template-columns: 1fr; }
  .masterdetail--resizable .split-gutter--v { display: none; }
}

/* ── Responsive ───────────────────────────────────────────────────────────
 * base.css is desktop-first; these breakpoints degrade the kit down to ~375px. */
@media (max-width: 768px) {
  .record-form { grid-template-columns: 1fr; }
  .form-row { grid-template-columns: 1fr; }  /* dense 2-col form → single column on narrow */
  /* Plain master-detail drills down here (same rule shape as --wide-detail above; the
   * list is the full-width first view, tapping a row opens the detail + back link). */
  .masterdetail { grid-template-columns: 1fr; }
  .masterdetail__aside { max-width: none; margin-bottom: 0; }
  .masterdetail__main { display: none; }
  .masterdetail--detail-open .masterdetail__aside { display: none; }
  .masterdetail--detail-open .masterdetail__main { display: block; }
  .masterdetail--detail-open .masterdetail__back { display: inline-flex; }
  /* Plain resizable master-detail also drills down single-pane here. */
  .masterdetail--resizable { grid-template-columns: 1fr; }
  /* Single pane here → header-row alignment is moot; let the folded toolbar wrap
   * instead of forcing one row (which overflowed the narrow viewport). */
  .masterdetail__detail-head { flex-wrap: wrap; min-height: 0; }
  /* The left caption head only labels the desktop side-by-side list — drop it on mobile. */
  .masterdetail__aside > .masterdetail__detail-head { display: none; }
  /* Stack the 관리 콘솔 shell: nav on top, main full-width below. */
  .console { flex-direction: column; }
  .console__nav {
    flex: 0 0 auto;
    width: 100%;
  }
  /* Larger touch targets on small screens (~44px). */
  .btn { min-height: 44px; }
}

/* ══════════════════════════════════════════════════════════════════════════
 * CANONICAL COMPONENTS — grid density · context strip · loading state.
 * Kit-level (ADR 0016). Grounded in Carbon data-table density, Ant descriptions,
 * and Material progress indicators.
 * ══════════════════════════════════════════════════════════════════════════ */

/* ══════════════════════════════════════════════════════════════════════════
 * ONE TABLE SYSTEM (ADR 0040) — NO flexbox tables. Every table (read-only list, editable
 * grid, totals row) lays out on a shared CSS-grid track list (`--grid-cols`, set ONCE on the
 * `.admin-table`). The header row, every body row, AND the totals row consume the SAME tracks,
 * so the header is ALWAYS over its column and totals sit under their numbers — by construction
 * (flexbox let header/body size independently → drift). Alignment is by column TYPE (`.col-num`
 * right, `.col-center` centre, default left), identical on header/body/total. Cells clip (no
 * bleed into the next column on a narrow viewport). EVERY `.admin-table` must set `--grid-cols`. */
.admin-table__head,
.admin-table__row {
  display: grid;
  grid-template-columns: var(--grid-cols);
  gap: 0;
  align-items: center;
  padding: 0 var(--space-3);
}
.admin-table__row {
  height: var(--row-height);
}
.admin-table__h,
.admin-table__cell {
  display: flex;
  align-items: center;
  min-width: 0;
  padding: 0 var(--space-2);
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
/* Text-child spans clip with an ellipsis (full value on the cell's `title`). */
.admin-table__cell > .dg-ellipsis,
.admin-table__cell > .admin-table__name,
.admin-table__cell > .admin-table__mono {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 100%;
}
a.admin-table__mono--link {
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* Work-order source PDF and its user-selected product image.  The component stays deliberately
 * independent of AI extraction: page one is simply a rendered source image until it is cropped. */
.work-order-panel {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-4);
}
.work-order-panel__upload,
.work-order-panel__image {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  min-width: 0;
}
.work-order-panel__upload-controls,
.work-order-panel__image-actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}
.work-order-panel__image > img,
.work-order-panel__empty {
  width: 100%;
  max-height: 24rem;
  object-fit: contain;
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  background: var(--color-bg-base);
}
.work-order-panel__empty {
  display: grid;
  place-items: center;
  min-height: 12rem;
  padding: var(--space-4);
  color: var(--color-text-muted);
  text-align: center;
}
.work-order-crop-modal {
  position: fixed;
  inset: 0;
  z-index: 30;
  display: grid;
  place-items: center;
  padding: var(--space-4);
}
.work-order-crop-modal[hidden] {
  display: none;
}
.work-order-upload-modal {
  position: fixed;
  inset: 0;
  z-index: 30;
  display: grid;
  place-items: center;
  padding: var(--space-4);
}
.work-order-upload-modal[hidden] {
  display: none;
}
.work-order-crop-modal__backdrop {
  position: absolute;
  inset: 0;
  background: color-mix(in srgb, var(--color-text-primary) 45%, transparent);
}
.work-order-upload-modal__backdrop {
  position: absolute;
  inset: 0;
  background: color-mix(in srgb, var(--color-text-primary) 45%, transparent);
}
.work-order-crop-modal__dialog {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  width: min(100%, 56rem);
  max-height: calc(100dvh - var(--space-8));
  padding: var(--space-4);
  gap: var(--space-4);
  overflow: auto;
  background: var(--color-bg-surface);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
}
.work-order-upload-modal__dialog {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  width: min(100%, 36rem);
  padding: var(--space-4);
  gap: var(--space-4);
  background: var(--color-bg-surface);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
}
.work-order-upload-modal__dialog form {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
.work-order-crop-modal__header,
.work-order-crop-modal__footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}
.work-order-crop-modal__body {
  min-height: 0;
  overflow: hidden;
}
.work-order-crop-modal__body img {
  display: block;
  max-width: 100%;
}
.work-order-upload-dropzone {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 14rem;
  padding: var(--space-4);
  border: 1px dashed var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-bg-base);
  cursor: pointer;
  text-align: center;
}
.work-order-upload-dropzone--dragging {
  border-color: var(--color-accent);
  background: var(--color-bg-hover);
}
.work-order-upload-dropzone__input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}
.work-order-upload-dropzone__title {
  color: var(--color-text-primary);
  font-weight: var(--fw-medium);
}
.work-order-upload-dropzone__browse {
  pointer-events: none;
}
.work-order-preview-toggle {
  position: fixed;
  top: 50%;
  right: 0;
  z-index: 20;
  display: inline-flex;
  align-items: center;
  flex-direction: column;
  gap: var(--space-1);
  width: 2.75rem;
  padding: var(--space-2) var(--space-1);
  color: var(--color-text-inverse);
  background: var(--color-accent);
  border: 0;
  border-radius: var(--radius-md) 0 0 var(--radius-md);
  box-shadow: var(--shadow-card);
  cursor: pointer;
  transform: translateY(-50%);
  transition: right 0.15s ease;
}
.work-order-preview-toggle--open {
  right: min(36rem, 92vw);
}
.work-order-preview-toggle__label {
  writing-mode: vertical-rl;
  font-size: var(--fs-meta);
  font-weight: var(--fw-medium);
}
.work-order-preview-toggle__icon {
  font-size: var(--fs-h3);
  line-height: 1;
}
.work-order-preview-drawer {
  position: fixed;
  inset: 0 0 0 auto;
  z-index: 20;
  display: flex;
  flex-direction: column;
  width: min(36rem, 92vw);
  background: var(--color-bg-surface);
  border-left: 1px solid var(--color-border-subtle);
  box-shadow: var(--shadow-card);
}
.work-order-preview-drawer[hidden] {
  display: none;
}
.work-order-preview-drawer__header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-4);
  border-bottom: 1px solid var(--color-border-subtle);
}
.work-order-preview-drawer__header h2 {
  margin: 0;
  font-size: var(--fs-h3);
}
.work-order-preview-drawer__header p {
  margin: var(--space-1) 0 0;
  overflow-wrap: anywhere;
}
.work-order-preview-drawer__document {
  flex: 1 1 auto;
  width: 100%;
  min-height: 0;
  border: 0;
}
@media (max-width: 768px) {
  .work-order-panel { grid-template-columns: minmax(0, 1fr); }
  .work-order-preview-toggle--open { right: 100vw; }
  .work-order-preview-drawer { width: 100vw; }
}
/* Centre column (checkboxes: M-CHART, 삭제) — pairs with `.col-num` (right). */
.col-center {
  justify-content: center;
  text-align: center;
}

/* A cell whose value failed a value-integrity check (e.g. an unresolved FK in the 오더 대량 업로드
 * preview) — a light-red fill + danger ink so the user spots what to fix. */
.admin-table__cell.cell-bad,
.cell-bad {
  background: var(--color-bg-danger-tint);
  color: var(--color-danger);
}

/* Read-only LISTS fill the pane - a `minmax(Npx,1fr)` track (in --grid-cols) absorbs slack. Fixed
 * columns are rigid px; the fr track truncates down to its min. Below the columns' minimums the
 * table STOPS shrinking (`min-width: min-content` = the track mins) and the `.table-scroll` wrapper
 * scrolls horizontally instead - a track that crushed to 0 let its cell's padding box paint over
 * the next column (unreadable), so lists now scroll rather than crush. */
.admin-table:not(.admin-table--grid) {
  width: 100%;
  min-width: min-content;
}

/* ── Bounded self-scrolling LIST panes (`.dg-split` / `.dg-split--viewport`) ─────────────────
 * The interactive/detail GRIDS are AG Grid now (ADR 0044) — the hand-rolled `.admin-table--grid`
 * two-panel frozen-column kit (`.dg-pinned` freeze, the editable quiet-cell input skin, the
 * JS-synced pinned panel) is RETIRED. What remains is the bounded, self-scrolling pane used by the
 * 마감 관리 master-detail (거래처 master + 마감 내역 — plain navigation `.admin-table` lists): a
 * `.dg-split` row whose `.table-scroll` caps its height (inline max-height) and scrolls INSIDE the
 * box, with a sticky header + slim scrollbars. All read-only LIST tables use the shared grid tracks
 * above; editable/detail data grids are AG Grid (.ag-theme-sheens). */
.dg-split {
  display: flex;
  align-items: flex-start;
}
.dg-split .table-scroll {
  flex: 1 1 auto;
  min-width: 0;
}
.dg-split .admin-table__head {
  height: var(--table-header-height); /* one header height for every table */
  min-height: var(--table-header-height);
}
.dg-split .admin-table__row {
  height: var(--row-height-grid);
  min-height: var(--row-height-grid);
}
/* Bounded viewport: the pane caps its height (inline max-height on the .table-scroll) and scrolls
 * INSIDE the box; the header row pins so rows scroll under it. */
.dg-split--viewport .table-scroll {
  max-height: 60vh;
  overflow-y: auto;
  scrollbar-gutter: stable;
}
.dg-split--viewport .admin-table__head {
  position: sticky;
  top: 0;
  z-index: 3;
  background: var(--table-header-bg); /* one header background for every table; opaque so rows scroll under */
}
/* Slim scrollbars (Linear/Notion): the horizontal bar is always visible, the vertical thumb fades
 * in on hover. `::-webkit-scrollbar` (width set) reserves the track so revealing it never shifts
 * cells. */
.dg-split--viewport .table-scroll::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}
.dg-split--viewport .table-scroll::-webkit-scrollbar-track {
  background: transparent;
}
.dg-split--viewport .table-scroll::-webkit-scrollbar-thumb {
  border-radius: 5px;
  border: 2px solid transparent; /* inset the thumb → a slim rounded bar */
  background-clip: content-box;
}
.dg-split--viewport .table-scroll::-webkit-scrollbar-thumb:horizontal {
  background-color: var(--color-border); /* always visible */
}
.dg-split--viewport .table-scroll::-webkit-scrollbar-thumb:vertical {
  background-color: transparent; /* invisible when idle */
  transition: background-color 0.2s ease;
}
.dg-split--viewport .table-scroll:hover::-webkit-scrollbar-thumb:vertical {
  background-color: var(--color-border); /* fades in while hovering the grid */
}

/* ── Context strip (Ant "descriptions") ────────────────────────────────────
 * A read-only band of label/value pairs above an editor, so a screen (BOM) isn't
 * edited blind to its parent record. One row of pairs; wraps on narrow widths. */
.context-strip {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-6);
  padding: var(--space-3) var(--space-4);
  /* Sized to its content, left-packed (NOT edge-to-edge) — no empty right; the normal block
   * gap to what follows. (It does not mirror the full-width BOM header.) */
  width: max-content;
  max-width: 100%;
  margin-bottom: var(--space-block-gap);
  background: var(--color-bg-surface);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-card);
}
/* When a summary strip precedes the record form, it already owns the block gap — don't
 * double it with the form's own top margin. */
.context-strip + .record-form {
  margin-top: 0;
}

/* ── Bulk-upload row-list preview ────────────────────────────────────────────
 * The counts are demoted to a one-line summary; the body is a row list, grouped
 * ADD / 수정 / 건너뜀 so the user reviews the actual values before committing. */
.upload-summary {
  margin-bottom: var(--space-6);
  font-size: var(--fs-body);
  color: var(--color-text-secondary);
}
.upload-summary strong {
  color: var(--color-text-primary);
  font-variant-numeric: tabular-nums;
}
.preview-group {
  margin-bottom: var(--space-6);
}
.preview-group__title {
  margin: 0 0 var(--space-2);
  font-size: var(--fs-body);
  font-weight: 600;
  color: var(--color-text-primary);
}
/* a "label value" pair inside the ADD 값 cell — quiet key, emphasised value */
.kv {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-1);
  margin: 0 var(--space-3) 2px 0;
}
.kv__k {
  font-size: var(--fs-caption);
  color: var(--color-text-muted);
}
.kv__v {
  color: var(--color-text-primary);
}
/* order colours render as compact chips */
.kv--tag {
  margin: 0 var(--space-1) 2px 0;
  padding: 1px var(--space-2);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-xs);
  background: var(--color-bg-base);
  font-size: var(--fs-caption);
  color: var(--color-text-secondary);
}
/* one old→new change line inside the 수정 변경 cell */
.diff {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px;
  padding: 1px 0;
}
.diff__k {
  font-size: var(--fs-caption);
  color: var(--color-text-muted);
  min-width: 5rem;
}
.diff__old {
  color: var(--color-text-secondary);
  text-decoration: line-through;
  text-decoration-color: var(--color-border);
}
.diff__arrow {
  color: var(--color-text-muted);
}
.diff__new {
  color: var(--color-text-primary);
  font-weight: 500;
}
.context-strip__item {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.context-strip__item--grow {
  flex: 1;
}
.context-strip__label {
  font-size: var(--fs-caption);
  color: var(--color-text-secondary);
}
.context-strip__value {
  font-size: var(--fs-body);
  color: var(--color-text-primary);
  font-variant-numeric: tabular-nums;
}

/* ══════════════════════════════════════════════════════════════════════════
 * STRIPE-CALM POLISH (kit-wide; restrained) — cards read via whitespace + a soft hairline
 * + a very subtle lift, not a boxy border; one accent (neutral grays elsewhere).
 * ══════════════════════════════════════════════════════════════════════════ */
/* Softer card surfaces. Table GRIDLINES keep the stronger --color-border (defined edges);
 * only the outer card border softens. */
.form-section,
.admin-table,
.pagination {
  border-color: var(--color-border-subtle);
  box-shadow: var(--shadow-card);
}
/* One accent, no decoration: the active nav item is a white pill, and its icon takes the
 * ACCENT (on a faint accent tint) — not the old per-item rainbow (`--ico`). Inactive icons stay
 * neutral white on the navy rail (base). Accent appears on active-nav; hue is never decoration. */
.console__item.is-active .console__ico {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  color: var(--accent);
}
/* The accent (blue), decoupled from the navy chrome: primary button + active-nav text. */
.btn-primary {
  background: var(--accent);
}
.btn-primary:hover {
  background: var(--accent-hover);
}
.console__item.is-active {
  color: var(--accent);
}

/* Offset pager (ADR 0016): range/current text are quiet meta; the right cluster groups the
 * page-size selector + First/Prev/Next/Last; disabled ends are dimmed and inert. */
.pagination__controls {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  flex-wrap: wrap;
}
.pagination__range,
.pagination__cur,
.pagination__size-label {
  font-size: var(--fs-meta);
  color: var(--color-text-secondary);
  white-space: nowrap;
}
.pagination__cur {
  padding: 0 var(--space-2);
}
.pagination__btn.is-disabled {
  color: var(--color-text-muted);
  opacity: var(--disabled-opacity);
  pointer-events: none;
}

/* Quiet inputs — subtle hairline at rest, full border on hover; accent ring on focus (above).
 * Bg stays opaque, so a frozen grid cell (which carries its own opaque bg) is never see-through. */
.form-input {
  border-color: var(--color-border-subtle);
}
.form-input:hover {
  border-color: var(--color-border);
}
/* Table header: lighter than the row data — muted colour does the distinguishing, not weight
 * (no uppercase; Korean). Stays ≥12px. */
.admin-table__h {
  font-weight: var(--fw-regular);
}
/* Sortable header cell (flat-list tables) — an <a> that reads like the header cell + ↑/↓ indicator. */
.admin-table__h--sort {
  gap: var(--space-1);
  text-decoration: none;
  color: inherit;
  cursor: pointer;
}
.admin-table__h--sort:hover { color: var(--color-primary); }
.admin-table__h--sort.is-sorted { color: var(--color-accent); font-weight: var(--fw-medium); }
/* the ↕ affordance is faint by default (so a column reads as sortable) and solid on the active sort */
.sort-ind { font-size: var(--fs-caption); opacity: 0.4; }
.admin-table__h--sort:hover .sort-ind,
.admin-table__h--sort.is-sorted .sort-ind { opacity: 1; }
/* tabular numerals on all numbers (aligned digits): numeric cells, numeric inputs, code/mono. */
.col-num,
.admin-table__mono,
input.form-input[type="number"] {
  font-variant-numeric: tabular-nums;
}

/* Solid-fill status pills (not tinted/outlined) — 진행구분 etc. read as filled chips. */
.status-badge--info,
.status-badge--in_progress {
  background: var(--color-info);
  color: var(--color-text-inverse);
}
.status-badge--success,
.status-badge--done,
.status-badge--pass {
  background: var(--color-success);
  color: var(--color-text-inverse);
}
.status-badge--danger {
  background: var(--color-danger);
  color: var(--color-text-inverse);
}
.status-badge--warning {
  background: var(--color-warning);
  color: var(--ink);
}
.status-badge--pending,
.status-badge--muted {
  background: var(--color-bg-base);
  color: var(--color-text-secondary);
}

/* ── Column TYPE-classes (Fiori by-type alignment) — one per column, header == cell ──
 * .col-text LEFT (names/desc/IDs) · .col-num RIGHT (numbers) · .col-code CENTRE (short
 * codes/units/status/actions/checkboxes). Never centre text, never right-align text. */
.col-text {
  justify-content: flex-start;
  text-align: left;
}
.col-code,
.col-center {
  justify-content: center;
  text-align: center;
}

/* ── Loading state (Material circular progress) ────────────────────────────
 * One spinner + message for any async wait. Reduced-motion users get a static ring. */
.loading-state {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--space-8);
  color: var(--color-text-muted);
}
.loading-state__spinner {
  width: 18px;
  height: 18px;
  border: 2px solid var(--color-border);
  border-top-color: var(--color-accent);
  border-radius: 50%;
  animation: kit-spin 0.7s linear infinite;
}
@keyframes kit-spin {
  to {
    transform: rotate(360deg);
  }
}
@media (prefers-reduced-motion: reduce) {
  .loading-state__spinner {
    animation: none;
  }
}

/* ══════════════════════════════════════════════════════════════════════════
 * PAGE RHYTHM + LIST CONTROLS — the same title→content gap and the same
 * search/filter row on every screen (kit-level; ADR 0016). Screens stop
 * setting ad-hoc `margin-top`/inline flex on the header's neighbour.
 * ══════════════════════════════════════════════════════════════════════════ */

/* ONE gap from the page title to the first content block (Atlassian block gap).
 * List/console/full-width headers own the gap; the record header defers to the
 * record form's own top margin (so a record form isn't double-gapped). */
.page-header {
  margin-bottom: var(--space-block-gap);
}
.record-header {
  margin-bottom: 0;
}
/* 오더-edit top bar: the summary strip + the 소요량 관리 action share one row ABOVE the cards,
 * capped to the card-grid width so the action right-aligns over the right card (생산·사이즈) and
 * sits level with the strip — not on a card, not in the title row. The strip keeps its own box;
 * the action is pushed to the grid's right edge. */
.order-topbar {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  max-width: var(--record-width);
  margin-bottom: var(--space-block-gap);
}
.order-topbar .context-strip {
  margin-bottom: 0; /* the topbar owns the gap to the form now */
}
.order-topbar__action {
  margin-left: auto; /* right-align to the card-grid edge, over the 생산·사이즈 card */
  flex: 0 0 auto;
}
.order-topbar__actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-left: auto;
}
.order-unsaved-modal {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: grid;
  place-items: center;
  padding: var(--space-4);
}
.order-unsaved-modal[hidden] {
  display: none;
}
.order-unsaved-modal__backdrop {
  position: absolute;
  inset: 0;
  background: color-mix(in srgb, var(--color-text-primary) 45%, transparent);
}
.order-unsaved-modal__dialog {
  position: relative;
  width: min(100%, 32rem);
  padding: var(--space-6);
  background: var(--color-bg-surface);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
}
.order-unsaved-modal__dialog h2 {
  margin: 0;
  font-size: var(--fs-h3);
}
.order-unsaved-modal__dialog p {
  margin: var(--space-3) 0 var(--space-6);
  color: var(--color-text-secondary);
}
.order-unsaved-modal__actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
  flex-wrap: wrap;
}
.order-topbar + .record-form {
  margin-top: 0;
}

/* The list search/filter row — one flex row, tokenised gap, consistent bottom
 * gap to the table. Fields flow and wrap; the 총 N건 count pins right. */
.list-controls {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: center;
  margin-bottom: var(--space-3);
}
.list-controls__search {
  flex: 1 1 220px;
  max-width: 320px;
}
.list-controls__select {
  width: 160px;
}
.list-controls__date-range {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}
.list-controls__date {
  width: 150px;
}
.list-controls__count {
  margin-left: auto;
}

/* A read-only predecessor document plus its chooser are one control, not two
 * stacked form actions.  Used by 새 입고/출고's 발주 전표 selection. */
.po-picker-control {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}
.po-picker-control .form-input {
  min-width: 0;
  flex: 1 1 auto;
}
.po-picker-control .form-readonly {
  min-width: 0;
  flex: 1 1 auto;
}
.po-picker-control .btn {
  flex: 0 0 auto;
  white-space: nowrap;
}
.doc-meta--dense .form-readonly {
  height: var(--control-height-sm);
  padding-block: 0;
  font-size: var(--fs-table);
}
.form-readonly--derived {
  background: var(--color-bg-base);
  color: var(--color-text-secondary);
  cursor: default;
}
.form-readonly__value {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* 대량 업로드 page — header + card share one capped column so the ← back button (in the page-header's
 * right cluster) lines up with the card's right edge instead of floating at the full-width region edge. */
.upload-page {
  max-width: 600px;
}
/* The dry-run PREVIEW shows the full uploaded table (every column) — let it fill the content pane so
 * the wide table is visible (it still scrolls inside its .table-scroll). The upload FORM stays capped. */
.upload-page--wide {
  max-width: none;
}

/* ── File dropzone (대량 업로드) — a styled drop/click target over a clipped real <input type=file>.
 * Calm + quiet (kit tokens): dashed hairline at rest, accent border + subtle wash on hover/focus/drag.
 * The <input> is sr-only-clipped (focusable + label-clickable), so click/keyboard/no-JS all work. */
.dropzone {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 150px; /* a reasonable fixed target — never gigantic; sits inside the capped form width */
  padding: var(--space-6);
  border: 1.5px dashed var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-bg-base);
  color: var(--color-text-secondary);
  text-align: center;
  cursor: pointer;
  transition: border-color 0.15s, background 0.15s;
}
.dropzone:hover,
.dropzone:focus-within,
.dropzone.is-drag {
  border-color: var(--accent);
  background: var(--color-bg-active);
}
.dropzone__icon {
  color: var(--color-text-muted);
}
.dropzone__text {
  font-size: var(--fs-body);
}
.dropzone__hint {
  font-size: var(--fs-caption);
  color: var(--color-text-muted);
}
.dropzone__file {
  font-size: var(--fs-body);
  font-weight: var(--fw-medium);
  color: var(--color-text-primary);
}
/* Real input: clipped (sr-only) but focusable + label-forwards clicks — never display:none. */
.dropzone__input {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* ── Split primary action (menu_button.js) ────────────────────────────────────
 * [주 동작 ▾] — one primary button that opens a short list of destinations. Used where a list has
 * more than one way in (발주: 발주만 등록 / 일괄 등록) but the kit still wants ONE primary per screen
 * (ADR 0016). Each item gets a hint line, so the choice is explained rather than guessed. */
.menu-btn {
  position: relative;
  display: inline-flex;
}
.menu-btn__caret {
  margin-left: var(--space-2);
  font-size: 10px;
  line-height: 1;
}
.menu-btn__list {
  position: absolute;
  top: calc(100% + var(--space-1));
  right: 0;
  z-index: 40;
  min-width: 220px;
  padding: var(--space-1);
  background: var(--color-bg-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
}
.menu-btn__list[hidden] {
  display: none;
}
.menu-btn__item {
  display: block;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  color: var(--color-text-primary);
  text-decoration: none;
}
/* A hairline between options, so two titles read as two choices rather than one paragraph. Not on the
 * first item — a rule above the top entry would look like a stray header divider. */
.menu-btn__item + .menu-btn__item {
  border-top: 1px solid var(--color-border-subtle);
}
.menu-btn__item:hover {
  background: var(--color-bg-hover);
  color: var(--color-text-primary);
  text-decoration: none;
}
.menu-btn__item:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}
.menu-btn__item-label {
  display: block;
  font-size: var(--fs-body);
  font-weight: var(--fw-medium);
}
.menu-btn__item-hint {
  display: block;
  margin-top: 2px;
  font-size: var(--fs-meta);
  color: var(--color-text-secondary);
}

/* Two header cards SIDE BY SIDE on a fill-height document page (일괄 등록: 발주 정보 + 입고·출고 실적).
 * Stacked, the pair is ~350px and pushes a 1280x800 fixed shell into a document scroll — which the
 * shell invariant (ADR 0013/0020) forbids, and which `responsive_check` catches. Side by side the pair
 * is as tall as the taller card, and the grid keeps the rest. Below 1024 they stack, where a narrower
 * window has the vertical room anyway. */
.form-section-row {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-4);
  flex: 0 0 auto;
}
.form-section-row > .form-section {
  min-width: 0; /* the #1 responsive bug — a grid child holding a form must be allowed to shrink */
  margin: 0;
}
.form-section-row > .split-gutter {
  display: none; /* one column below 1024 — there is no boundary to drag */
}
@media (min-width: 1024px) {
  /* [card | gutter | card]. The LEFT card's width is what the drag writes (--hdr-split); the right
   * one takes whatever is left via 1fr, so the pair always fills the row exactly. Reuses the shared
   * [data-splitter] gutter (splitter.js), which clamps, persists to localStorage and resets on
   * double-click — the same one the 마감 screen drags. */
  .form-section-row {
    grid-template-columns: minmax(280px, var(--hdr-split, 1fr)) auto minmax(280px, 1fr);
    align-items: start;
  }
  .form-section-row > .split-gutter {
    display: block;
  }
}

/* Sort arrow on a HINTED grid header (hint.js draws its own — a custom header component replaces
 * AG Grid's, sort indicator included). */
.hint-head__sort {
  display: none;
  font-size: 9px;
  line-height: 1;
  color: var(--color-text-muted);
}
.hint-head__sort--active {
  display: inline-block;
  margin-left: var(--space-1);
}
