/* =============================================================
   Samoo C.M. — BIM Virtual Assistant
   styles.css — light professional engineering-dashboard theme
   -------------------------------------------------------------
   Drop-in replacement for the original dark-navy stylesheet.
   Every class name, ID, and DOM hook the existing app.js relies
   on is preserved; only visuals (color, type, spacing, radius,
   elevation, density) are reworked.

   Design language summary:
     - Warm light canvas + white surfaces, hairline dividers
     - Black ink for primary type + primary CTA
     - Samoo red (#E41E26) used scarcely: brand mark, active rail,
       danger, the brand underline on the login card
     - Mode accents in three muted hues (chat=blue, bim=emerald,
       design=violet) so the orchestrator's current mode is legible
       at a glance without becoming the dominant chrome
     - Single elevation tier; depth comes from hairlines + surface
       separation, not stacked shadows
     - 4px base spacing, 6/10/14 radius scale (tighter than the
       previous 14–24 to read more engineering / less consumer)
   ============================================================= */

@import url("./colors_and_type.css");

/* =============================================================
   Reset + base
   ============================================================= */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html, body {
  margin: 0;
  padding: 0;
}

body {
  min-height: 100vh;
  font-family: var(--font-sans);
  font-size: var(--t-md);
  font-weight: var(--w-regular);
  line-height: var(--lh-normal);
  color: var(--ink-2);
  background: var(--canvas);
  overflow: hidden;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  /* Korean-typography default: don't break between Hangul syllables
     in the middle of a word. Inline runs of Hangul still wrap at
     spaces / explicit boundaries; only character-by-character
     fragmentation (시스템 상 / 태) is suppressed. */
  word-break: keep-all;
  overflow-wrap: break-word;
}

button, input, textarea, select {
  font-family: inherit;
  color: inherit;
}

a {
  color: var(--ink);
  text-decoration: underline;
  text-decoration-color: var(--samoo-red);
  text-decoration-thickness: 2px;
  text-underline-offset: 3px;
}

/* =============================================================
   App shell — 3-column grid
   ============================================================= */

.app-shell {
  display: grid;
  grid-template-columns: var(--sidebar-w) minmax(0, 1fr) var(--exec-w);
  height: 100vh;
  overflow: hidden;
  background: var(--canvas);
}

.app-shell.sidebar-collapsed {
  grid-template-columns: 0 minmax(0, 1fr) var(--exec-w);
}
.app-shell.sidebar-collapsed > .sidebar {
  visibility: hidden;
  pointer-events: none;
  border-right: 0;
}
.app-shell.exec-sidebar-collapsed {
  grid-template-columns: var(--sidebar-w) minmax(0, 1fr) 0;
}
.app-shell.exec-sidebar-collapsed > .execution-sidebar {
  visibility: hidden;
  pointer-events: none;
  padding: 0;
  border-left: 0;
}
.app-shell.exec-sidebar-collapsed.sidebar-collapsed {
  grid-template-columns: 0 minmax(0, 1fr) 0;
}

/* Edge rails — slim "tab" affordances for re-opening collapsed
   sidebars. Re-themed to match the warm canvas. */
.sidebar-rail {
  position: fixed;
  top: 0;
  width: 14px;
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  cursor: pointer;
  background: var(--surface);
  color: var(--muted);
  z-index: 30;
  transition: background 0.15s ease, color 0.15s ease, width 0.15s ease;
}
.sidebar-rail:hover,
.sidebar-rail:focus-visible {
  background: var(--surface-strong);
  color: var(--ink);
  width: 18px;
  outline: none;
}
.sidebar-rail-left  { left: 0;  border-right: 1px solid var(--line); }
.sidebar-rail-right { right: 0; border-left:  1px solid var(--line); }
.sidebar-rail[hidden] { display: none; }
.sidebar-rail-icon { font-size: var(--t-sm); line-height: 1; }

/* =============================================================
   Left sidebar — sessions + uploads
   ============================================================= */

.sidebar {
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
  padding: var(--s-5) var(--s-4);
  background: var(--surface);
  border-right: 1px solid var(--line);
  min-height: 0;
  overflow: hidden;
}

.sidebar > .brand,
.sidebar > .primary-btn { flex: 0 0 auto; }

/* Brand block: red dot + wordmark + collapse button.
   The brand-mark <img> renders the Samoo C.M. logo on white. The
   text fallback (the existing .brand-mark div from app.js) keeps
   the same footprint so layout doesn't shift when the image is
   swapped on load error. */
.brand {
  display: flex;
  gap: var(--s-3);
  align-items: center;
  padding-bottom: var(--s-3);
  border-bottom: 1px solid var(--line);
}

.brand-mark {
  width: 36px;
  height: 36px;
  border-radius: var(--r-sm);
  display: grid;
  place-items: center;
  background: var(--samoo-red);
  color: var(--on-brand);
  font-weight: var(--w-bold);
  font-size: var(--t-xs);
  letter-spacing: 0.05em;
  flex: 0 0 auto;
}

.brand-mark-img {
  width: 36px;
  height: 36px;
  background: var(--surface);
  object-fit: contain;
  padding: 2px;
  border-radius: var(--r-sm);
  border: 1px solid var(--line);
}

.brand-text { min-width: 0; flex: 1 1 auto; }

.brand h1 {
  margin: 0;
  font-size: var(--t-md);
  font-weight: var(--w-bold);
  letter-spacing: -0.005em;
  color: var(--ink);
  line-height: 1.25;
}

.brand p {
  margin: 2px 0 0;
  font-size: var(--t-sm);
  color: var(--muted);
  line-height: 1.35;
}

.sidebar-collapse-btn {
  margin-left: auto;
  padding: 4px 8px;
  font-size: var(--t-sm);
  line-height: 1;
  flex: 0 0 auto;
}

/* ---- Buttons ---------------------------------------------------- */

.primary-btn,
.session-item {
  border: 1px solid transparent;
  font: inherit;
}

/* Primary CTA — bold black with white text. The Samoo identity
   gives equal weight to red + black; using black for the CTA
   reserves red for active/brand moments only, which reads more
   technical and lets the brand red feel deliberate when it does
   appear. */
.primary-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  height: 36px;
  padding: 0 var(--s-4);
  border-radius: var(--r-sm);
  background: var(--samoo-black);
  color: var(--on-dark);
  font-size: var(--t-md);
  font-weight: var(--w-semibold);
  letter-spacing: -0.005em;
  cursor: pointer;
  transition: background 0.12s ease, transform 0.06s ease;
}
.primary-btn:hover:not(:disabled) { background: #1F1F1F; }
.primary-btn:active:not(:disabled) { background: var(--samoo-black-strong); transform: translateY(1px); }
.primary-btn:focus-visible { outline: none; box-shadow: var(--focus-ring-ink); }
.primary-btn:disabled { opacity: 0.45; cursor: not-allowed; }

.secondary-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  height: 32px;
  padding: 0 var(--s-3);
  border-radius: var(--r-sm);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  color: var(--ink);
  font-size: var(--t-md);
  font-weight: var(--w-medium);
  cursor: pointer;
  transition: background 0.12s ease, border-color 0.12s ease;
}
.secondary-btn:hover:not(:disabled) { background: var(--surface-soft); border-color: var(--ink); }
.secondary-btn:focus-visible { outline: none; box-shadow: var(--focus-ring-ink); }
.secondary-btn:disabled { opacity: 0.55; cursor: wait; }

/* ---- Sidebar sections -------------------------------------------- */

.sidebar-section {
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
}
.sidebar-section.scrollable { flex: 1 1 0; min-height: 0; }
.sidebar-section.is-collapsed { flex: 0 0 auto; }
.sidebar-section.is-collapsed .attachment-list { display: none; }

.section-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-2);
  padding: 0 2px;
}
.section-actions { display: flex; align-items: center; gap: var(--s-2); }

.section-header h2 {
  margin: 0;
  font-size: var(--t-xs);
  font-weight: var(--w-bold);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
  word-break: keep-all;
}

.section-count {
  min-width: 22px;
  height: 20px;
  padding: 0 6px;
  border-radius: var(--r-pill);
  display: grid;
  place-items: center;
  background: var(--surface-strong);
  color: var(--ink-2);
  font-size: var(--t-xs);
  font-weight: var(--w-bold);
  letter-spacing: 0;
}

.sidebar-section-toggle {
  padding: 3px 8px;
  font-size: 10px;
  line-height: 1.2;
  border-radius: var(--r-pill);
  letter-spacing: 0;
}

.session-list,
.attachment-list {
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
  flex: 1 1 0;
  min-height: 0;
  overflow-y: auto;
  padding-right: 2px;
  padding-bottom: var(--s-2);
}

/* Custom scrollbars — slim, neutral, only visible on hover.
   Applies to every scroll container inside the app shell. */
.app-shell *::-webkit-scrollbar { width: 8px; height: 8px; }
.app-shell *::-webkit-scrollbar-track { background: transparent; }
.app-shell *::-webkit-scrollbar-thumb {
  background: transparent;
  border-radius: var(--r-pill);
}
.app-shell *:hover::-webkit-scrollbar-thumb { background: var(--line-strong); }
.app-shell *::-webkit-scrollbar-thumb:hover { background: var(--muted-soft); }

/* ---- Session list items ----------------------------------------- */

.session-item {
  position: relative;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-sm);
  background: transparent;
  border: 1px solid transparent;
  color: var(--ink-2);
  cursor: pointer;
  transition: background 0.1s ease, border-color 0.1s ease;
}
.session-item:hover { background: var(--surface-soft); }

/* Active session — soft warm fill + a red left bar. The bar is
   the *only* place red appears in the sidebar at rest, which
   makes "this is your current conversation" unambiguous without
   shouting. */
.session-item.active {
  background: var(--surface-strong);
  border-color: var(--line);
}
.session-item.active::before {
  content: "";
  position: absolute;
  left: -1px;
  top: 8px;
  bottom: 8px;
  width: 3px;
  border-radius: var(--r-pill);
  background: var(--samoo-red);
}

.session-row-left {
  flex: 1 1 auto;
  min-width: 0;
  cursor: pointer;
}
.session-row-left:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring-ink);
  border-radius: var(--r-xs);
}

.session-row-right {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  flex: 0 0 auto;
  gap: var(--s-1);
}

.session-edit-btn,
.session-delete-btn {
  white-space: nowrap;
  flex: 0 0 auto;
  position: relative;
  z-index: 1;
}

.session-title-input {
  width: 100%;
  min-width: 0;
  border-radius: var(--r-xs);
  border: 1px solid var(--ink);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-weight: var(--w-semibold);
  font-size: var(--t-base);
  padding: 6px 8px;
}
.session-title-input:focus { outline: none; box-shadow: var(--focus-ring-ink); }

/* ---- Icon button (.icon-btn) ---- */

.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--muted);
  border-radius: var(--r-xs);
  padding: 5px 9px;
  font: inherit;
  font-size: var(--t-sm);
  cursor: pointer;
  white-space: nowrap;
  word-break: keep-all;
  line-height: 1.2;
  transition: background 0.1s ease, color 0.1s ease, border-color 0.1s ease;
}
.icon-btn:hover { background: var(--surface-soft); border-color: var(--line-strong); color: var(--ink); }
.icon-btn:focus-visible { outline: none; box-shadow: var(--focus-ring-ink); }
.icon-btn.danger {
  color: var(--danger-ink);
  border-color: var(--line);
}
.icon-btn.danger:hover {
  background: var(--danger-soft);
  border-color: var(--danger);
  color: var(--danger-ink);
}

/* === Left-sidebar typography consistency =================== */

.sidebar .section-header h2 {
  font-size: var(--t-xs);
  font-weight: var(--w-bold);
  letter-spacing: 0.08em;
}
.sidebar .section-header .section-count {
  font-size: var(--t-xs);
  font-weight: var(--w-bold);
}
.sidebar .session-title,
.sidebar .attachment-name,
.sidebar .attachment-filename {
  font-size: var(--t-base);
  font-weight: var(--w-semibold);
  line-height: 1.35;
  color: var(--ink);
}
.sidebar .session-title {
  /* Allow natural Hangul wrapping; ``word-break: keep-all`` (inherited
     from body) keeps syllables together so titles wrap at spaces
     rather than mid-word. */
  word-break: keep-all;
  overflow-wrap: break-word;
}
.sidebar .session-meta,
.sidebar .attachment-meta {
  font-size: 11px;
  font-weight: var(--w-regular);
  line-height: 1.45;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-top: 2px;
}
.sidebar .session-edit-btn,
.sidebar .session-delete-btn,
.sidebar .attachment-card .icon-btn,
.sidebar .attachment-card .icon-btn.danger,
.sidebar .attachment-card .icon-btn.use-design {
  height: 24px;
  min-width: 0;
  padding: 0 var(--s-2);
  font-size: 11px;
  font-weight: var(--w-medium);
  border-radius: var(--r-pill);
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  white-space: nowrap;
  word-break: keep-all;
  flex: 0 0 auto;
}
/* "디자인 생성" — image-like attachments only. Neutral ink treatment (was
   violet): violet reads as a Design-MODE state colour, so this design-source
   affordance uses the warm-neutral chrome vocabulary. Hover lifts the border
   to ink for the same focus weight as other sidebar pills. */
.sidebar .attachment-card .icon-btn.use-design {
  color: var(--ink);
  background: var(--surface-soft);
  border-color: var(--line-strong);
}
.sidebar .attachment-card .icon-btn.use-design:hover {
  background: var(--surface-strong);
  border-color: var(--ink);
}
/* Artifact-card variant lives inside message bubbles, so it inherits
   .secondary-btn sizing. Neutral ink treatment (was violet): violet
   reads as a Design-MODE state colour, so this design-source action uses
   the warm-neutral chrome vocabulary instead, matching the viewer CTA. */
.artifact-use-design-btn {
  color: var(--ink);
  background: var(--surface-soft);
  border-color: var(--line-strong);
}
.artifact-use-design-btn:hover:not(:disabled) {
  background: var(--surface-strong);
  border-color: var(--ink);
}

/* ── Design 설정 panel ───────────────────────────────────────────── */
.design-options-panel {
  /* v4 (2026-05-15): neutral palette. The previous violet tint
     borrowed from the Design *mode* indicator and read consumer /
     creative-app inside a BIM dashboard. The panel now uses the same
     warm-white + hairline vocabulary as the rest of the chrome;
     selection states resolve to ink, summary chips to warm-gray, and
     the single red moment is the 2px accent on the active main tab. */
  margin: 0 0 var(--s-2);
  padding: var(--s-2) var(--s-3);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--surface);
}
.design-options-panel[hidden] { display: none; }
.design-options-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-2);
}
.design-options-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  padding: 4px 6px;
  background: transparent;
  border: none;
  font: inherit;
  color: var(--ink);
  cursor: pointer;
  font-weight: var(--w-bold);
}
.design-options-toggle:focus-visible { outline: none; box-shadow: var(--focus-ring-ink); border-radius: var(--r-sm); }
.design-options-title { font-size: var(--t-sm); }
.design-options-caret { font-size: 12px; transition: transform 0.15s ease; }
.design-options-panel[data-collapsed="1"] .design-options-caret { transform: rotate(-90deg); }
.design-options-panel[data-collapsed="1"] .design-options-body { display: none; }
.design-options-reset-btn { padding: 2px 8px; font-size: 11px; }

.design-options-body {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  margin-top: var(--s-2);
}

.design-options-row {
  display: grid;
  grid-template-columns: 140px 1fr;
  gap: var(--s-2);
  align-items: start;
}
@media (max-width: 1100px) {
  .design-options-row { grid-template-columns: 1fr; }
}
.design-options-label {
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-size: var(--t-sm);
  font-weight: var(--w-semibold);
  color: var(--ink);
  padding-top: 4px;
}
.design-options-help {
  font-size: 11px;
  font-weight: var(--w-medium);
  color: var(--muted);
}
.design-options-control {
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
  min-width: 0;
}

/* Pill grids (detail rows + edit-intensity + building-type items). */
.design-options-control[data-design-pill-group],
.design-building-group-list {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.design-pill {
  /* v5 (2026-05-15): Airbnb-style refined filter chip. White surface,
     thin neutral border, dark readable text. Hover lifts the border
     and adds the lightest shadow tier. Selected state is a white
     surface with a 1.5px ink ring and a small Samoo-red leading dot;
     hover-on-selected stays high-contrast. */
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 28px;
  padding: 0 12px;
  font-size: 11px;
  font-weight: var(--w-semibold);
  border-radius: var(--r-pill);
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--ink);
  cursor: pointer;
  white-space: nowrap;
  word-break: keep-all;
  transition: background 0.12s ease, border-color 0.12s ease,
              box-shadow 0.12s ease, color 0.12s ease;
}
.design-pill:hover:not(:disabled) {
  border-color: var(--ink);
  background: var(--surface);
  color: var(--ink);
  box-shadow: var(--shadow-1);
}
.design-pill.selected {
  /* Ink ring via inset box-shadow so the chip's outer footprint
     matches its unselected sibling (no row-shift on select). */
  background: var(--surface);
  border-color: var(--ink);
  color: var(--ink);
  font-weight: var(--w-bold);
  box-shadow:
    inset 0 0 0 1px var(--ink),
    var(--shadow-1);
}
.design-pill.selected::before {
  /* Small leading dot in Samoo red — one brand moment per selected
     chip. */
  content: "";
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: var(--r-pill);
  background: var(--samoo-red);
  flex: 0 0 auto;
  margin-right: 2px;
}
.design-pill.selected:hover:not(:disabled) {
  background: var(--surface-soft);
  border-color: var(--ink);
  color: var(--ink);
}
.design-pill:disabled {
  opacity: 1;
  cursor: not-allowed;
  background: var(--surface-soft);
  border-color: var(--line);
  color: var(--muted-soft);
}
.design-pill:focus-visible { outline: none; box-shadow: var(--focus-ring-ink); }

/* Category tabs row sits above the building-type grid. */
.design-category-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.design-category-tab {
  /* v5: matches .design-pill base + selected so the category row and
     the building-type grid read as one family. */
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 28px;
  padding: 0 12px;
  font-size: 11px;
  font-weight: var(--w-semibold);
  border-radius: var(--r-pill);
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--ink);
  cursor: pointer;
  transition: background 0.12s ease, border-color 0.12s ease,
              box-shadow 0.12s ease, color 0.12s ease;
}
.design-category-tab:hover:not(:disabled) {
  border-color: var(--ink);
  box-shadow: var(--shadow-1);
}
.design-category-tab.selected {
  background: var(--surface);
  border-color: var(--ink);
  color: var(--ink);
  font-weight: var(--w-bold);
  box-shadow:
    inset 0 0 0 1px var(--ink),
    var(--shadow-1);
}
.design-category-tab.selected::before {
  content: "";
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: var(--r-pill);
  background: var(--samoo-red);
  flex: 0 0 auto;
  margin-right: 2px;
}
.design-category-tab.selected:hover:not(:disabled) {
  background: var(--surface-soft);
  border-color: var(--ink);
  color: var(--ink);
}
.design-category-tab:disabled {
  opacity: 1;
  cursor: not-allowed;
  background: var(--surface-soft);
  border-color: var(--line);
  color: var(--muted-soft);
}

.design-building-type-groups {
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
  margin-top: 4px;
}
.design-building-group {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.design-building-group-title {
  font-size: 11px;
  font-weight: var(--w-bold);
  color: var(--muted);
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

/* Reference style image select + preview. */
.design-reference-select {
  height: 28px;
  padding: 0 8px;
  font-size: 12px;
  border-radius: var(--r-sm);
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--ink);
  max-width: 100%;
}
.design-reference-select:focus-visible { outline: none; box-shadow: var(--focus-ring-ink); }
.design-reference-preview {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  margin-top: 4px;
  padding: 4px;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--surface);
}
.design-reference-preview img {
  width: 48px;
  height: 48px;
  object-fit: cover;
  border-radius: var(--r-xs);
  display: block;
}
.design-reference-preview-name {
  font-size: 11px;
  color: var(--ink-2);
  word-break: break-all;
}
.sidebar .attachment-card .attachment-card-top {
  flex-wrap: nowrap;
  align-items: center;
  gap: var(--s-2);
}

/* ---- Attachment cards ------------------------------------------ */

.attachment-card {
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-sm);
  background: transparent;
  border: 1px solid transparent;
  transition: background 0.1s ease, border-color 0.1s ease;
}
.attachment-card:hover { background: var(--surface-soft); border-color: var(--line); }
.attachment-card.is-selected {
  background: var(--samoo-red-soft);
  border-color: var(--samoo-red);
}

.attachment-select { display: inline-flex; align-items: center; flex: 0 0 auto; cursor: pointer; }
.attachment-select-input {
  width: 14px; height: 14px; margin: 0;
  cursor: pointer;
  accent-color: var(--samoo-red);
}

.attachment-bulk-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-2);
  padding: var(--s-2) var(--s-3);
  margin-bottom: var(--s-2);
  border-radius: var(--r-sm);
  background: var(--samoo-red-soft);
  border: 1px solid var(--samoo-red);
}
.attachment-bulk-count {
  font-size: var(--t-sm);
  font-weight: var(--w-bold);
  color: var(--ink);
}
.attachment-bulk-actions { display: flex; align-items: center; gap: var(--s-1); }
.attachment-bulk-actions .icon-btn { padding: 4px 8px; font-size: 11px; }

.attachment-card-top {
  display: flex;
  gap: var(--s-2);
  align-items: flex-start;
}

.attachment-name {
  /* Fill the row so the badge is always flush against the same gap
     after the checkbox. Without this, the default ``flex: 0 1 auto``
     plus ``justify-content: space-between`` made the badge start at
     a different x-position depending on filename length (short IFC
     / "var_01.png" filenames left a wide gap on the checkbox side
     and visually shifted the badge to the right). The delete button
     is ``flex: 0 0 auto`` so it stays right-aligned regardless. */
  flex: 1 1 auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-2);
  font-weight: var(--w-semibold);
  word-break: break-word;
  min-width: 0;
}

.attachment-badge {
  /* Pin every kind-* badge to the same x-position inside
     ``.attachment-name`` so IFC / 업로드 이미지 / 스냅샷 / 디자인
     chips align across cards regardless of badge text width. */
  flex: 0 0 auto;
  align-self: flex-start;
}

.attachment-filename {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: var(--w-semibold);
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  letter-spacing: -0.01em;
}

.attachment-meta {
  margin-top: 4px;
  font-size: 11px;
  line-height: 1.45;
  color: var(--muted);
}

/* Attachment filter chips (전체 / 업로드 / 스냅샷 …) — segmented
   feel, restrained color. The active chip uses ink fill + white
   text so the user reads it as a hard "selected" rather than a
   colored tint that competes with the brand red. */
.attachment-filter-row {
  display: flex;
  flex-wrap: nowrap;
  gap: 4px;
  margin-bottom: var(--s-2);
  overflow-x: auto;
  padding-bottom: 2px;
}
.attachment-filter-row .attachment-filter-chip { flex: 0 0 auto; white-space: nowrap; }
.attachment-filter-chip {
  padding: 3px 9px;
  border-radius: var(--r-pill);
  background: var(--surface-soft);
  border: 1px solid var(--line);
  color: var(--muted);
  font-size: var(--t-sm);
  font-weight: var(--w-medium);
  line-height: 1.2;
  cursor: pointer;
}
.attachment-filter-chip:hover { color: var(--ink); border-color: var(--line-strong); }
.attachment-filter-chip.active {
  color: var(--on-dark);
  background: var(--ink);
  border-color: var(--ink);
}
.attachment-filter-chip:focus-visible { outline: none; box-shadow: var(--focus-ring-ink); }

/* ---- Kind badges (uploaded_image, ifc, render, snapshot) -------- */

.attachment-badge,
.artifact-badge {
  display: inline-flex;
  align-items: center;
  padding: 2px 7px;
  border-radius: var(--r-pill);
  background: var(--surface-strong);
  color: var(--ink-2);
  font-size: 10px;
  font-weight: var(--w-bold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
  border: 1px solid var(--line);
}
.artifact-badge.kind-uploaded_image,
.attachment-badge.kind-uploaded_image {
  background: var(--mode-chat-soft);
  color: var(--info-ink);
  border-color: rgba(31, 111, 235, 0.22);
}
.artifact-badge.kind-ifc,
.attachment-badge.kind-ifc {
  background: var(--warn-soft);
  color: var(--warn-ink);
  border-color: rgba(180, 83, 9, 0.22);
}
.artifact-badge.kind-render,
.attachment-badge.kind-render {
  background: var(--mode-design-soft);
  color: var(--mode-design-ink);
  border-color: rgba(122, 61, 186, 0.22);
}
.artifact-badge.kind-snapshot,
.artifact-badge.kind-bim_render,
.artifact-badge.kind-inspect,
.artifact-badge.kind-capture,
.attachment-badge.kind-snapshot,
.attachment-badge.kind-bim_render,
.attachment-badge.kind-inspect,
.attachment-badge.kind-capture {
  background: var(--mode-bim-soft);
  color: var(--success-ink);
  border-color: rgba(19, 138, 78, 0.22);
}
.artifact-badge.kind-region_mask {
  background: var(--warn-soft);
  color: var(--warn-ink);
  border-color: rgba(180, 83, 9, 0.22);
}
/* Region Edit transparency preview: secondary, slightly inset, dashed
   edge so it reads as an annotation of the result above it rather than a
   reusable artifact. */
.mask-preview-card {
  border-style: dashed;
  opacity: 0.95;
}

/* =============================================================
   Workspace (center column)
   ============================================================= */

.workspace {
  display: grid;
  grid-template-rows: auto auto minmax(0, 1fr) auto;
  min-height: 0;
  height: 100vh;
  padding: 0;
  gap: 0;
  overflow: hidden;
  background: var(--canvas);
}

/* Workspace header — flat horizontal strip with a hairline bottom,
   replacing the previous floating card. Higher density, more
   dashboard-like. */
.workspace-header {
  background: var(--surface);
  border: 0;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
  box-shadow: none;
  padding: 0 var(--s-6);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  flex-wrap: wrap;
  height: var(--header-h);
  min-height: var(--header-h);
}

.workspace-header-left {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  flex: 1 1 auto;
  min-width: 0;
}

.workspace-title-block { min-width: 0; flex: 1 1 auto; }

.workspace-title-row {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  min-width: 0;
}

.workspace-title {
  font-size: var(--t-lg);
  font-weight: var(--w-bold);
  color: var(--ink);
  letter-spacing: -0.005em;
}
.workspace-title-row .workspace-title {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

#session-meta.workspace-subtitle { display: none; }
.workspace-subtitle { margin: 0; color: var(--muted); font-size: var(--t-sm); }

.workspace-title-edit-btn {
  flex: 0 0 auto;
  padding: 3px 9px;
  font-size: var(--t-sm);
  line-height: 1.2;
  border-radius: var(--r-pill);
  white-space: nowrap;
  word-break: keep-all;
}

.workspace-title-input {
  flex: 1 1 auto;
  min-width: 0;
  font-size: var(--t-lg);
  font-weight: var(--w-bold);
  padding: 2px 8px;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-xs);
  background: var(--surface);
  color: var(--ink);
}
.workspace-title-input:focus { outline: none; box-shadow: var(--focus-ring-ink); border-color: var(--ink); }

.header-actions {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  flex-wrap: wrap;
}

/* ---- Status strip (status pill + error banner) ----------------- */

.status-strip {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  padding: var(--s-3) var(--s-6) 0;
}
.status-strip:empty { display: none; }

.upload-state {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  width: fit-content;
  border-radius: var(--r-pill);
  padding: 4px 10px;
  background: var(--surface-strong);
  border: 1px solid var(--line);
  color: var(--ink-2);
  font-size: var(--t-sm);
  font-weight: var(--w-semibold);
  letter-spacing: 0;
  white-space: nowrap;
  word-break: keep-all;
}
.upload-state:empty { display: none; }
.upload-state { margin-bottom: var(--s-2); }

.error-banner {
  border-radius: var(--r-sm);
  padding: var(--s-2) var(--s-3);
  background: var(--danger-soft);
  border: 1px solid var(--danger);
  color: var(--danger-ink);
  font-size: var(--t-md);
  white-space: pre-wrap;
}

/* =============================================================
   Messages column
   ============================================================= */

.messages {
  min-height: 0;
  overflow: auto;
  display: flex;
  flex-direction: column;
  gap: var(--s-5);
  padding: var(--s-5) var(--s-6) 160px;
  overscroll-behavior: contain;
  background: var(--canvas);
}

.msg-wrapper {
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
  max-width: min(820px, 92%);
}

.msg-wrapper.pending .msg { border-color: var(--line-strong); opacity: 0.92; }
.msg-wrapper.failed .msg  { border-color: var(--danger); }

.msg-wrapper.user { align-self: flex-end; }
.msg-wrapper.bot { align-self: flex-start; }

.msg-meta {
  font-size: 11px;
  color: var(--muted);   /* was --muted-soft; #9A9A95 is ~2.5:1 on white — below AA */
  padding: 0 4px;
  letter-spacing: 0.02em;
}

.msg {
  padding: var(--s-3) var(--s-4);
  border-radius: var(--r-lg);
  line-height: var(--lh-loose);
  font-size: var(--t-md);
  white-space: pre-wrap;
  word-break: break-word;
  background: var(--surface);
  border: 1px solid var(--line);
  color: var(--ink-2);
}

/* User bubble — solid black, white text. Mirrors the C.M. side of
   the wordmark and gives the conversation a clear "outgoing
   side" without inventing a new colored bubble. */
.user .msg {
  background: var(--ink);
  color: var(--on-dark);
  border-color: var(--ink);
  border-bottom-right-radius: var(--r-xs);
}

.bot .msg {
  background: var(--surface);
  border: 1px solid var(--line);
  border-bottom-left-radius: var(--r-xs);
  box-shadow: var(--shadow-1);
}

/* Upload-confirmation bubbles sit on the ROLE's side (user right /
   assistant left) but keep the amber "notice" tone so they read as a
   workflow event, not a typed turn. Overrides the dark user tint.
   (The old `.system` message class is gone — renderMessages assigns
   only user/bot + this upload modifier.) */
.msg-wrapper.upload .msg {
  background: var(--warn-soft);
  border: 1px solid rgba(180, 83, 9, 0.22);
  color: var(--warn-ink);
}

/* Per-message delete — hover-revealed in the meta row; keyboard focus
   also reveals it (focus-visible) so it stays reachable without a mouse. */
.msg-meta {
  display: flex;
  align-items: center;
  gap: 8px;
}
.msg-wrapper.user .msg-meta { justify-content: flex-end; }
.msg-delete-btn {
  border: none;
  background: none;
  padding: 0 2px;
  font-size: 11px;
  color: var(--muted);
  cursor: pointer;
  /* Visible by default so touch devices (no hover) can reach it. */
  opacity: 0.55;
  transition: opacity 0.12s ease, color 0.12s ease;
}
/* Hover-capable devices keep the tidy reveal-on-hover behaviour. */
@media (hover: hover) {
  .msg-delete-btn { opacity: 0; }
}
.msg-wrapper:hover .msg-delete-btn,
.msg-delete-btn:focus-visible {
  opacity: 1;
}
.msg-delete-btn:hover { color: var(--danger, #b3261e); text-decoration: underline; }

.render-img {
  width: 100%;
  max-width: 720px;
  border-radius: var(--r-md);
  border: 1px solid var(--line);
  cursor: zoom-in;
  background: var(--surface-inset);
}

/* ---- Artifact card (inline image + filename + review button) -- */

.artifact-card {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  padding: var(--s-3);
  border-radius: var(--r-md);
  background: var(--surface);
  border: 1px solid var(--line);
  max-width: 760px;
  box-shadow: var(--shadow-1);
}

.artifact-card-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-2);
  min-width: 0;
}

.artifact-card-head .artifact-filename {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--t-sm);
  color: var(--muted);
  font-family: var(--font-mono);
  letter-spacing: -0.01em;
}

.artifact-image { margin: 0; }

/* Image slot wraps the <img> so we can paint a loading skeleton +
   error-state placeholder around it without disturbing the click-to-
   zoom behaviour or the card head row above. */
.artifact-image-slot {
  position: relative;
  width: 100%;
  max-width: 720px;
  min-height: 120px;
  border-radius: var(--r-md);
  overflow: hidden;
  background: var(--surface-inset);
}
.artifact-image-slot.is-loading {
  background: linear-gradient(90deg,
    var(--surface-strong) 0%,
    var(--surface-soft) 50%,
    var(--surface-strong) 100%);
  background-size: 200% 100%;
  animation: artifact-image-shimmer 1.4s ease-in-out infinite;
}
.artifact-image-slot.is-loaded { background: transparent; }
.artifact-image-slot.is-error {
  background: var(--surface-soft);
  border: 1px dashed var(--line-strong);
}
.artifact-image-slot .render-img { display: block; }
@keyframes artifact-image-shimmer {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

.artifact-image-fallback {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  padding: var(--s-5) var(--s-3);
  color: var(--muted);
  text-align: center;
}
.artifact-image-fallback-glyph {
  font-size: 28px;
  line-height: 1;
  opacity: 0.55;
}
.artifact-image-fallback-text {
  font-size: var(--t-sm);
  color: var(--ink-2);
  font-weight: var(--w-semibold);
}
.artifact-image-fallback-name {
  font-size: 11px;
  color: var(--muted);
  font-family: var(--font-mono);
  letter-spacing: -0.01em;
  word-break: break-all;
}

.msg-note {
  font-size: var(--t-sm);
  color: var(--muted);
  padding: 0 4px;
}
.msg-note.error { color: var(--danger-ink); }

/* =============================================================
   Composer
   ============================================================= */

.composer-shell {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  position: sticky;
  bottom: 0;
  z-index: 5;
  padding: var(--s-3) var(--s-6) var(--s-4);
  background: linear-gradient(180deg,
    rgba(246, 245, 242, 0) 0%,
    rgba(246, 245, 242, 0.92) 30%,
    var(--canvas) 100%);
}

.composer-panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-2);
  padding: var(--s-3) var(--s-4) var(--s-4);
}

.composer-attachments {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  margin-bottom: var(--s-3);
}
.composer-attachments:empty { display: none; }

.composer-pending-image {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: 6px 32px 6px 6px;
  border-radius: var(--r-sm);
  background: var(--surface-soft);
  border: 1px solid var(--line);
  max-width: 100%;
}
.composer-pending-image-thumb {
  width: 48px;
  height: 48px;
  object-fit: cover;
  border-radius: var(--r-xs);
  flex: 0 0 auto;
  background: var(--surface-inset);
}
.composer-pending-image-meta { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.composer-pending-image-meta strong {
  font-size: var(--t-sm);
  color: var(--ink);
  font-weight: var(--w-semibold);
}
.composer-pending-image-meta small {
  font-size: 11px;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 220px;
  font-family: var(--font-mono);
}
.composer-pending-image-dismiss {
  position: absolute;
  top: 4px;
  right: 4px;
  width: 20px;
  height: 20px;
  border-radius: var(--r-pill);
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--muted);
  font-size: var(--t-md);
  line-height: 1;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.composer-pending-image-dismiss:hover {
  background: var(--danger-soft);
  border-color: var(--danger);
  color: var(--danger-ink);
}

.msg-pending-image-preview {
  display: block;
  margin-top: var(--s-2);
  max-width: 220px;
  max-height: 220px;
  border-radius: var(--r-sm);
  border: 1px solid var(--line);
  background: var(--surface-inset);
}

.upload-state {
  background: var(--surface-soft);
  border: 1px solid var(--line);
  color: var(--muted);
}

.composer-input-row {
  /* Shared single-line height for plus button / textarea / send button.
     Keeping the value here (instead of repeating 44px three times)
     lets the autosize JS read it via getComputedStyle if we ever want
     to drive the min-height from CSS — for now app.js mirrors this
     constant. */
  --composer-control-h: 44px;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: var(--s-2);
  align-items: end;
}
.composer-input-row .composer-add { align-self: end; }

/* ---- "+" add menu --------------------------------------------- */

.composer-add { position: relative; display: inline-block; }

.composer-add-btn {
  width: var(--composer-control-h, 44px);
  height: var(--composer-control-h, 44px);
  padding: 0;
  border-radius: var(--r-sm);
  font-size: var(--t-xl);
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  color: var(--ink);
  cursor: pointer;
}
.composer-add-btn:hover { background: var(--surface-soft); border-color: var(--ink); }
.composer-add-btn.is-open { background: var(--surface-strong); border-color: var(--ink); }

.composer-add-menu {
  position: absolute;
  bottom: calc(100% + 6px);
  left: 0;
  z-index: 20;
  display: flex;
  flex-direction: column;
  min-width: 200px;
  padding: 4px;
  border-radius: var(--r-md);
  background: var(--surface);
  border: 1px solid var(--line);
  box-shadow: var(--shadow-pop);
}
.composer-add-menu[hidden] { display: none; }

.composer-add-item {
  appearance: none;
  text-align: left;
  border: 0;
  background: transparent;
  color: var(--ink-2);
  font: inherit;
  font-size: var(--t-md);
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-xs);
  cursor: pointer;
}
.composer-add-item:hover { background: var(--surface-soft); outline: none; }
.composer-add-item:focus-visible { background: var(--surface-soft); outline: none; box-shadow: var(--focus-ring-ink); }

.composer-input-row .composer-add-btn {
  width: var(--composer-control-h, 44px);
  height: var(--composer-control-h, 44px);
  border-radius: var(--r-sm);
}

textarea {
  width: 100%;
  min-height: 44px;
  max-height: 200px;
  resize: none;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  /* Vertical padding picked so a single 14px line at line-height 1.45
     centers inside the 44px border-box: (44 - 2 border - 20.3 line) / 2
     ≈ 10.85px → 11px top/bottom. */
  padding: 11px var(--s-3);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: var(--t-md);
  line-height: 1.45;
  outline: none;
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
.composer-input-row textarea { min-height: var(--composer-control-h, 44px); }
textarea::placeholder { color: var(--muted-soft); }
textarea:focus { border-color: var(--ink); box-shadow: var(--focus-ring-ink); }

.send-btn {
  min-width: 80px;
  height: var(--composer-control-h, 44px);
  border-radius: var(--r-sm);
}

.empty-state {
  padding: var(--s-4);
  border-radius: var(--r-md);
  background: var(--surface);
  border: 1px dashed var(--line-strong);
  color: var(--muted);
  font-size: var(--t-md);
  text-align: center;
}

.typing-indicator { display: flex; gap: 6px; }
.typing-indicator .dot {
  width: 6px; height: 6px;
  border-radius: var(--r-pill);
  background: var(--muted-soft);
  animation: bounce 1.2s infinite ease-in-out;
}
.typing-indicator .dot:nth-child(2) { animation-delay: 0.18s; }
.typing-indicator .dot:nth-child(3) { animation-delay: 0.36s; }
@keyframes bounce {
  0%, 80%, 100% { transform: translateY(0); opacity: 0.55; }
  40% { transform: translateY(-5px); opacity: 1; }
}

/* =============================================================
   Right sidebar — execution + system status
   ============================================================= */

.execution-sidebar {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  height: 100vh;
  padding: var(--s-4);
  overflow: hidden;
  gap: var(--s-3);
  background: var(--surface);
  border-left: 1px solid var(--line);
}

/* System status panel — capped at ~58% of the right column so the
   run-progress panel always has visible breathing room; same
   policy as the original CSS, just re-skinned. */
.execution-sidebar .system-status-panel {
  flex: 0 1 auto;
  min-height: 0;
  max-height: 58%;
  overflow: hidden;
}
.execution-sidebar .system-status-panel.collapsed { flex: 0 0 auto; max-height: none; }
.execution-sidebar .system-status-panel.collapsed .system-status-body { display: none; }
.system-status-body {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  overflow-y: auto;
  min-height: 0;
  scrollbar-width: thin;
}

.execution-sidebar .run-progress-panel {
  flex: 1 1 auto;
  min-height: 180px;
}

.system-status-panel {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  padding: var(--s-3);
  border-radius: var(--r-md);
  background: var(--surface);
  border: 1px solid var(--line);
  box-shadow: none;
}

.system-status-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-bottom: var(--s-2);
  border-bottom: 1px solid var(--line);
  margin-bottom: var(--s-1);
}
.system-status-header strong {
  font-size: var(--t-xs);
  font-weight: var(--w-bold);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
  word-break: keep-all;
}
.system-status-header-actions { display: flex; align-items: center; gap: var(--s-1); }
.system-status-collapse-btn {
  font-size: 11px;
  padding: 2px 8px;
  height: 22px;
  white-space: nowrap;
  word-break: keep-all;
  border-radius: var(--r-pill);
}

/* === Right-sidebar consistency block (preserved selectors) ==== */

.execution-sidebar .system-status-header strong,
.execution-sidebar #run-progress-title,
.execution-sidebar .run-progress-header strong {
  font-size: var(--t-xs);
  font-weight: var(--w-bold);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
  word-break: keep-all;
}

/* Status pills + indicators — unified pill family (compact). */
.execution-sidebar .run-progress-status {
  height: 22px;
  padding: 0 10px;
  font-size: 10px;
  font-weight: var(--w-bold);
  letter-spacing: 0.06em;
  border-radius: var(--r-pill);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  white-space: nowrap;
  text-transform: uppercase;
}

.execution-sidebar .run-progress-cancel-btn {
  height: 24px;
  padding: 0 var(--s-3);
  font-size: 11px;
  font-weight: var(--w-semibold);
  border-radius: var(--r-pill);
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  white-space: nowrap;
}
.resource-mode-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-2);
}
.resource-mode-row-end {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  flex: 0 0 auto;
}

.resource-mode-refresh-btn {
  width: 24px;
  height: 24px;
  padding: 0 !important;
  border-radius: var(--r-pill) !important;
  font-size: var(--t-md);
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  color: var(--muted);
  background: var(--surface);
  border-color: var(--line);
  letter-spacing: 0;
  cursor: pointer;
  transition: background-color 120ms ease, color 120ms ease, opacity 120ms ease, border-color 120ms ease;
}
.resource-mode-refresh-btn:hover:not(:disabled) {
  background: var(--surface-soft);
  color: var(--ink);
  border-color: var(--line-strong);
}
.resource-mode-refresh-btn:disabled { opacity: 0.55; cursor: progress; }
.resource-mode-refresh-btn.is-refreshing { animation: resource-mode-refresh-spin 0.9s linear infinite; }
@keyframes resource-mode-refresh-spin {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}

.resource-mode-label {
  font-size: 11px;
  color: var(--muted);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  font-weight: var(--w-bold);
  white-space: nowrap;
  word-break: keep-all;
}

.resource-mode-chip {
  font-size: 11px;
  font-weight: var(--w-bold);
  padding: 2px 10px;
  border-radius: var(--r-pill);
  white-space: nowrap;
  word-break: keep-all;
  border: 1px solid transparent;
  letter-spacing: 0.02em;
}
.resource-mode-chip.chat   { color: var(--info-ink);     background: var(--mode-chat-soft);   border-color: rgba(31, 111, 235, 0.30); }
.resource-mode-chip.bim    { color: var(--success-ink);  background: var(--mode-bim-soft);    border-color: rgba(19, 138, 78, 0.30); }
.resource-mode-chip.design { color: var(--mode-design-ink); background: var(--mode-design-soft); border-color: rgba(122, 61, 186, 0.32); }
.resource-mode-chip.mixed  { color: var(--warn-ink);     background: var(--warn-soft);        border-color: rgba(180, 83, 9, 0.30); }
.resource-mode-chip.error  { color: var(--danger-ink);   background: var(--danger-soft);      border-color: rgba(195, 21, 21, 0.32); }
.resource-mode-chip.unknown,
.resource-mode-chip.switching {
  color: var(--ink-2); background: var(--surface-strong); border-color: var(--line);
}

.resource-mode-meta {
  font-size: var(--t-sm);
  color: var(--muted);
  word-break: keep-all;
  line-height: 1.4;
}

.resource-mode-services {
  display: flex;
  flex-wrap: nowrap;
  gap: 4px;
  overflow-x: auto;
  scrollbar-width: none;
}
.resource-mode-services::-webkit-scrollbar { display: none; }

.service-chip {
  font-size: 10px;
  font-weight: var(--w-semibold);
  padding: 2px 7px;
  border-radius: var(--r-pill);
  background: var(--surface-strong);
  color: var(--ink-2);
  border: 1px solid var(--line);
  display: inline-flex;
  align-items: center;
  gap: 4px;
  white-space: nowrap;
  word-break: keep-all;
  flex: 0 0 auto;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.service-chip .dot {
  width: 5px;
  height: 5px;
  border-radius: var(--r-pill);
  background: currentColor;
  opacity: 0.85;
  flex: 0 0 auto;
}
.service-chip .state {
  font-size: 9px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  opacity: 0.85;
}
.service-chip.on        { color: var(--success-ink);  background: var(--success-soft); border-color: rgba(19, 138, 78, 0.30); }
.service-chip.ready     { color: var(--info-ink);     background: var(--info-soft);    border-color: rgba(31, 111, 235, 0.30); }
.service-chip.off       { color: var(--muted);        background: var(--surface-soft); border-color: var(--line); }
.service-chip.err       { color: var(--danger-ink);   background: var(--danger-soft);  border-color: rgba(195, 21, 21, 0.32); }
.service-chip.switching { color: var(--warn-ink);     background: var(--warn-soft);    border-color: rgba(180, 83, 9, 0.30); }
.service-chip.generating { color: var(--mode-design-ink); background: var(--mode-design-soft); border-color: rgba(122, 61, 186, 0.32); }
.service-chip.generating .dot { animation: chip-pulse 1s ease-in-out infinite; }
.service-chip.busy      { color: var(--info-ink);     background: var(--info-soft);    border-color: rgba(31, 111, 235, 0.32); }
.service-chip.busy .dot { animation: chip-pulse 1s ease-in-out infinite; }
.service-chip.unknown   { color: var(--muted);         background: var(--surface-soft); border-color: var(--line); }
@keyframes chip-pulse {
  0%, 100% { opacity: 0.85; }
  50%      { opacity: 0.35; }
}

/* ---- Variant-count picker inside design-mode confirm modal --- */

.mode-switch-design-options {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-sm);
  background: var(--surface-soft);
  border: 1px solid var(--line);
}
.mode-switch-design-options[hidden] { display: none; }
.mode-switch-design-options-label {
  font-size: var(--t-sm);
  font-weight: var(--w-semibold);
  color: var(--ink);
}
.variant-count-row { display: flex; gap: 6px; }
.variant-count-btn {
  flex: 1 1 auto;
  height: 32px;
  border-radius: var(--r-xs);
  border: 1px solid var(--line-strong);
  background: var(--surface);
  color: var(--ink-2);
  font-size: var(--t-md);
  font-weight: var(--w-semibold);
  cursor: pointer;
  transition: background 0.12s ease, border-color 0.12s ease;
}
.variant-count-btn:hover { background: var(--surface-soft); }
.variant-count-btn.selected {
  background: var(--mode-design-soft);
  border-color: var(--mode-design);
  color: var(--mode-design-ink);
}
/* ---- Mode-switch buttons ------------------------------------- */

.resource-mode-actions {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-top: 2px;
}
.resource-mode-btn {
  width: 100%;
  font-size: var(--t-sm);
  font-weight: var(--w-semibold);
  padding: 8px var(--s-3);
  border-radius: var(--r-sm);
  border: 1px solid var(--line-strong);
  background: var(--surface);
  color: var(--ink-2);
  text-align: left;
  cursor: pointer;
  transition: background 0.12s ease, border-color 0.12s ease, opacity 0.12s ease;
  white-space: nowrap;
  word-break: keep-all;
  position: relative;
}
.resource-mode-btn:hover:not(:disabled) {
  background: var(--surface-soft);
  border-color: var(--ink);
}
.resource-mode-btn:disabled { cursor: not-allowed; opacity: 0.45; }

/* Active mode — filled bim-green pill with a leading check dot.
   We keep the green so users still feel "the server is running BIM"
   while the chrome is now light. */
.resource-mode-btn.is-current {
  border-color: var(--success);
  background: var(--success);
  color: var(--on-dark);
  font-weight: var(--w-bold);
}
.resource-mode-btn.is-current:hover:not(:disabled) {
  background: var(--success-ink);
  border-color: var(--success-ink);
}
.resource-mode-btn.is-current::before {
  content: "●";
  margin-right: var(--s-2);
  color: var(--on-dark);
  opacity: 0.92;
}
.resource-mode-btn.is-current:disabled,
.resource-mode-btn.is-current[disabled] { opacity: 1; cursor: not-allowed; color: var(--on-dark); }
.resource-mode-btn.is-current:disabled::before,
.resource-mode-btn.is-current[disabled]::before { color: var(--on-dark); }
.resource-mode-btn.is-current,
.resource-mode-btn.is-current * { color: var(--on-dark); }

.resource-mode-btn:not(.is-current) { color: var(--ink-2); }
.resource-mode-btn:not(.is-current):not(:disabled):hover { color: var(--ink); }

.resource-mode-btn.is-target.switching {
  border-color: var(--info);
  background: var(--info-soft);
  color: var(--info-ink);
  position: relative;
}
.resource-mode-btn.is-target.switching::after { content: " ⌛"; }

.resource-mode-permission {
  font-size: 11px;
  color: var(--warn-ink);
  word-break: keep-all;
  line-height: 1.4;
  padding: var(--s-2);
  border-radius: var(--r-xs);
  background: var(--warn-soft);
  border: 1px solid rgba(180, 83, 9, 0.30);
}
/* Cost tier (저비용/중간/고비용) — three buttons side by side rather than the
   mode buttons' full-width stack; the labels are short and there's no confirm
   modal to reach for, so a row reads better than a column here. Reuses
   .resource-mode-btn for identical chrome/is-current styling. */
.cost-tier-actions {
  display: flex;
  flex-direction: row;
  gap: 4px;
  margin-top: 2px;
}
.cost-tier-btn { text-align: center; }
.cost-tier-meta {
  font-size: 11px;
  color: var(--ink-2);
  margin-top: 2px;
  word-break: keep-all;
}

.resource-mode-permission.error {
  color: var(--danger-ink);
  background: var(--danger-soft);
  border-color: var(--danger);
}

/* =============================================================
   Mode-switch overlay + confirm modal
   ============================================================= */

.mode-switch-overlay {
  position: fixed;
  inset: 0;
  background: rgba(17, 17, 17, 0.32);
  backdrop-filter: blur(3px);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 998;
}
.mode-switch-overlay[hidden] { display: none; }
.mode-switch-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: var(--s-6) var(--s-8);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-3);
  max-width: 380px;
  text-align: center;
  box-shadow: var(--shadow-pop);
}
.mode-switch-card strong {
  font-size: var(--t-md);
  color: var(--ink);
  font-weight: var(--w-bold);
}
.mode-switch-card p {
  margin: 0;
  font-size: var(--t-sm);
  color: var(--muted);
  word-break: keep-all;
  line-height: 1.5;
}
.mode-switch-spinner {
  width: 32px;
  height: 32px;
  border: 3px solid var(--line);
  border-top-color: var(--samoo-red);
  border-radius: var(--r-pill);
  animation: mode-switch-spin 0.85s linear infinite;
}
@keyframes mode-switch-spin { to { transform: rotate(360deg); } }

.mode-switch-confirm {
  position: fixed;
  inset: 0;
  /* Above the BIM viewer overlay (z 9100) — the confirm must be reachable
     from viewer-initiated Design handoffs (VWorld 캡처, 이 뷰를 디자인에 사용);
     at the old 999 it painted invisibly BEHIND the open viewer. Same tier
     policy as the OpenAI settings modal (9500). */
  z-index: 9600;
  display: flex;
  align-items: center;
  justify-content: center;
}
.mode-switch-confirm[hidden] { display: none; }
.mode-switch-confirm-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(17, 17, 17, 0.32);
  backdrop-filter: blur(3px);
  cursor: pointer;
}
.mode-switch-confirm-card {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: var(--s-5) var(--s-6);
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  max-width: 460px;
  width: 92%;
  box-shadow: var(--shadow-pop);
  z-index: 1;
}
.mode-switch-confirm-card h2 {
  margin: 0;
  font-size: var(--t-lg);
  color: var(--ink);
  font-weight: var(--w-bold);
}
.mode-switch-confirm-card p {
  margin: 0;
  font-size: var(--t-md);
  color: var(--ink-2);
  line-height: 1.55;
  word-break: keep-all;
}
.mode-switch-confirm-error {
  font-size: var(--t-sm);
  color: var(--danger-ink);
  background: var(--danger-soft);
  border: 1px solid var(--danger);
  border-radius: var(--r-xs);
  padding: 6px 10px;
  word-break: break-word;
}
.mode-switch-confirm-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--s-2);
}

/* =============================================================
   Admin (user management) modal
   ============================================================= */
.admin-modal {
  position: fixed;
  inset: 0;
  z-index: 999;
  display: flex;
  align-items: center;
  justify-content: center;
}
.admin-modal[hidden] { display: none; }
.admin-modal-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(17, 17, 17, 0.32);
  backdrop-filter: blur(3px);
  cursor: pointer;
}
.admin-modal-card {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: var(--s-5) var(--s-6);
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
  max-width: 720px;
  width: 92%;
  max-height: 86vh;
  overflow-y: auto;
  box-shadow: var(--shadow-pop);
  z-index: 1;
}
.admin-modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-2);
}
.admin-modal-card h2 {
  margin: 0;
  font-size: var(--t-lg);
  color: var(--ink);
  font-weight: var(--w-bold);
}
.admin-modal-card h3 {
  margin: 0 0 var(--s-2);
  font-size: var(--t-md);
  color: var(--ink);
  font-weight: var(--w-semibold);
}
.admin-modal-close {
  font-size: 22px;
  line-height: 1;
}
.admin-modal-close:focus-visible { outline: none; box-shadow: var(--focus-ring-ink); border-radius: var(--r-xs); }
.admin-modal-error {
  font-size: var(--t-sm);
  color: var(--danger-ink);
  background: var(--danger-soft);
  border: 1px solid var(--danger);
  border-radius: var(--r-xs);
  padding: 6px 10px;
  word-break: break-word;
}
.admin-modal-section {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  border-top: 1px solid var(--line);
  padding-top: var(--s-3);
}
.admin-modal-section:first-of-type { border-top: 0; padding-top: 0; }
.admin-add-form {
  display: grid;
  grid-template-columns: 1fr 1fr auto auto;
  gap: var(--s-2);
  align-items: end;
}
.admin-field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: var(--t-sm);
  color: var(--ink-2);
}
.admin-field input[type="text"],
.admin-field input[type="password"] {
  padding: 6px 10px;
  border: 1px solid var(--line);
  border-radius: var(--r-xs);
  font: inherit;
  background: var(--surface);
  color: var(--ink);
}
.admin-field-checkbox {
  flex-direction: row;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
}
.admin-user-list-wrap {
  overflow-x: auto;
}
.admin-user-list {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--t-sm);
}
.admin-user-list th,
.admin-user-list td {
  text-align: left;
  padding: 8px 10px;
  border-bottom: 1px solid var(--line);
  color: var(--ink);
}
.admin-user-list th {
  font-weight: var(--w-semibold);
  color: var(--ink-2);
  background: var(--surface-soft);
}
.admin-user-list-actions-col {
  width: 1%;
  white-space: nowrap;
}
.admin-user-actions {
  display: flex;
  gap: 6px;
  justify-content: flex-end;
  white-space: nowrap;
}
.admin-user-actions .secondary-btn {
  padding: 4px 10px;
  font-size: var(--t-xs);
}
.admin-user-self-tag {
  color: var(--muted);
  font-size: var(--t-xs);
}
.admin-user-created {
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.admin-user-list-empty td {
  text-align: center;
  color: var(--muted);
  padding: 14px 10px;
}
@media (max-width: 640px) {
  .admin-add-form {
    grid-template-columns: 1fr;
  }
}

/* =============================================================
   Run progress panel
   ============================================================= */

.run-progress-panel {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  padding: var(--s-3);
  border-radius: var(--r-md);
  background: var(--surface);
  border: 1px solid var(--line);
  box-shadow: none;
  min-height: 0;
  max-height: 100%;
}

.run-progress-header {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: var(--s-2);
  flex: 0 0 auto;
  padding-bottom: var(--s-2);
  border-bottom: 1px solid var(--line);
}

.run-progress-status-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-2);
  flex-wrap: wrap;
  flex: 0 0 auto;
}

.run-progress-meta {
  color: var(--muted);
  font-size: var(--t-sm);
  line-height: 1.45;
  flex: 1 1 auto;
  min-width: 0;
}

.run-progress-summary {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 6px;
  align-items: stretch;
  flex: 0 0 auto;
}

.run-progress-actions {
  display: flex;
  align-items: center;
  gap: var(--s-2);
}

.run-progress-status {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 60px;
  height: 22px;
  padding: 0 10px;
  border-radius: var(--r-pill);
  font-size: 10px;
  font-weight: var(--w-bold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
  word-break: keep-all;
  flex: 0 0 auto;
}

.run-progress-cancel-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 24px;
  min-width: 0;
  padding: 0 var(--s-3);
  border-radius: var(--r-pill);
  font-size: 11px;
  font-weight: var(--w-semibold);
  line-height: 1;
  white-space: nowrap;
  word-break: keep-all;
  flex: 0 0 auto;
}

.run-progress-status.running    { background: var(--info-soft);    color: var(--info-ink); }
.run-progress-status.completed  { background: var(--success-soft); color: var(--success-ink); }
.run-progress-status.error,
.run-progress-status.failed     { background: var(--danger-soft);  color: var(--danger-ink); }
.run-progress-status.waiting    { background: var(--surface-strong); color: var(--muted); }
.run-progress-status.timeout,
.run-progress-status.interrupted,
.run-progress-status.cancelled  { background: var(--warn-soft);    color: var(--warn-ink); }

.run-progress-cancel-btn { padding: 4px 12px; font-size: 11px; line-height: 1.2; border-radius: var(--r-pill); }
.run-progress-cancel-btn[hidden] { display: none; }

.run-progress-chip {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 4px;
  padding: 5px var(--s-2);
  border-radius: var(--r-xs);
  background: var(--surface-soft);
  border: 1px solid var(--line);
  color: var(--ink);
  font-size: 11px;
  white-space: nowrap;
  min-width: 0;
}
.run-progress-chip span { color: var(--muted); }
.run-progress-chip strong {
  font-weight: var(--w-bold);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.run-progress-body {
  display: grid;
  grid-template-columns: minmax(0, 1.3fr) minmax(280px, 0.9fr);
  gap: var(--s-3);
  min-height: 0;
  overflow: hidden;
  flex: 1 1 auto;
}

.execution-sidebar .run-progress-body {
  grid-template-columns: 1fr;
  grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--s-2);
}
.execution-sidebar .run-progress-column {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-height: 0;
}
.execution-sidebar .run-progress-column .run-progress-list {
  flex: 1 1 0;
  min-height: 0;
  overflow-y: auto;
}

.run-progress-column { display: flex; flex-direction: column; gap: var(--s-2); min-width: 0; }
.run-progress-column h3 {
  margin: 0;
  font-size: var(--t-xs);
  font-weight: var(--w-bold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
  word-break: keep-all;
}

.run-progress-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
  max-height: 260px;
  overflow: auto;
  padding-right: 2px;
}

.run-progress-item {
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-sm);
  background: var(--surface-soft);
  border: 1px solid var(--line);
}
.run-progress-item-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-2);
  margin-bottom: 4px;
}
.run-progress-item-title {
  font-weight: var(--w-bold);
  font-size: var(--t-sm);
  color: var(--ink);
  white-space: nowrap;
  word-break: keep-all;
}
.run-progress-item-meta {
  color: var(--muted);
  font-size: 11px;
  font-family: var(--font-mono);
  letter-spacing: -0.01em;
}
.run-progress-item-summary {
  color: var(--ink-2);
  font-size: var(--t-sm);
  line-height: 1.5;
  white-space: pre-wrap;
  word-break: break-word;
}
.run-progress-item-flags {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-top: 6px;
}
.run-progress-flag {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 8px;
  border-radius: var(--r-pill);
  background: var(--surface-strong);
  border: 1px solid var(--line);
  color: var(--muted);
  font-size: 10px;
  font-weight: var(--w-semibold);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* =============================================================
   Auth overlay (login)
   ============================================================= */

.auth-loading {
  position: fixed;
  inset: 0;
  background: var(--canvas);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 50;
}
.auth-loading[hidden] { display: none; }
.auth-loading-spinner {
  width: 36px;
  height: 36px;
  border-radius: var(--r-pill);
  border: 3px solid var(--line);
  border-top-color: var(--samoo-red);
  animation: auth-spin 0.9s linear infinite;
}
@keyframes auth-spin { to { transform: rotate(360deg); } }

.auth-overlay {
  position: fixed;
  inset: 0;
  background: var(--canvas);
  /* Subtle architectural grid backdrop — a print-blueprint reference,
     restrained at 6% so it reads as texture, not pattern. */
  background-image:
    linear-gradient(rgba(17, 17, 17, 0.045) 1px, transparent 1px),
    linear-gradient(90deg, rgba(17, 17, 17, 0.045) 1px, transparent 1px);
  background-size: 32px 32px, 32px 32px;
  background-position: -1px -1px;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 60;
  padding: var(--s-6);
}
.auth-overlay[hidden] { display: none; }

.auth-card {
  width: min(420px, 100%);
  background: var(--surface);
  border: 1px solid var(--line);
  border-top: 3px solid var(--samoo-red);
  border-radius: var(--r-lg);
  padding: var(--s-8) var(--s-8) var(--s-6);
  box-shadow: var(--shadow-pop);
  color: var(--ink-2);
}

.auth-brand {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  margin-bottom: var(--s-5);
  padding-bottom: var(--s-4);
  border-bottom: 1px solid var(--line);
}
.auth-brand h1 {
  font-size: var(--t-lg);
  margin: 0;
  font-weight: var(--w-bold);
  color: var(--ink);
  letter-spacing: -0.005em;
}
.auth-brand p {
  font-size: var(--t-sm);
  margin: 2px 0 0;
  color: var(--muted);
}

.auth-form { display: flex; flex-direction: column; gap: var(--s-3); }
.auth-field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: var(--t-sm);
}
.auth-field span {
  color: var(--ink-2);
  font-weight: var(--w-semibold);
  font-size: var(--t-sm);
  letter-spacing: 0;
}
.auth-field input {
  width: 100%;
  padding: 10px var(--s-3);
  font-size: var(--t-md);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  color: var(--ink);
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
.auth-field input:focus { outline: none; border-color: var(--ink); box-shadow: var(--focus-ring-ink); }
.auth-error {
  color: var(--danger-ink);
  background: var(--danger-soft);
  border: 1px solid var(--danger);
  border-radius: var(--r-sm);
  padding: var(--s-2) var(--s-3);
  font-size: var(--t-sm);
}
.auth-error[hidden] { display: none; }
.auth-submit { margin-top: var(--s-1); height: 40px; }

/* =============================================================
   Header user pill / logout
   ============================================================= */

.user-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: 4px 8px 4px 12px;
  border-radius: var(--r-pill);
  background: var(--surface);
  border: 1px solid var(--line);
  color: var(--ink);
  font-size: var(--t-sm);
  white-space: nowrap;
  word-break: keep-all;
  flex: 0 0 auto;
}
.user-pill-label { color: var(--muted); font-size: 11px; white-space: nowrap; word-break: keep-all; }
.user-pill strong { letter-spacing: 0; font-weight: var(--w-semibold); white-space: nowrap; word-break: keep-all; }
.user-pill-btn {
  padding: 0 10px;
  height: 24px;
  font-size: 11px;
  border-radius: var(--r-pill);
  white-space: nowrap;
  word-break: keep-all;
  flex: 0 0 auto;
}

/* =============================================================
   Responsive — app-shell breakpoints
   ============================================================= */

/* ── ≤1100px: RIGHT RAIL AUTO-COLLAPSES ───────────────────────
   Drop from 3-column to 2-column: left sidebar + workspace.
   The exec-sidebar (run-progress) is hidden so the workspace
   gains the freed ~360 px.  The existing JS collapse classes
   still work as overrides inside this breakpoint. */
@media (max-width: 1100px) {
  .app-shell {
    grid-template-columns: var(--sidebar-w) minmax(0, 1fr);
  }
  /* The right rail becomes an OFF-CANVAS DRAWER instead of vanishing — without
     this it was display:none with no opener, so OpenAI settings, mode-switch,
     GPU status and run-progress were all unreachable below 1100px. It slides in
     over the workspace when the header's "실행 패널" toggle sets .exec-drawer-open. */
  .execution-sidebar {
    position: fixed;
    top: 0;
    right: 0;
    height: 100vh;
    /* Below the modals (998/999) so OpenAI/mode-switch dialogs opened FROM the
       drawer render on top of it, not behind. Above workspace content. */
    z-index: 700;
    width: min(360px, 92vw);
    transform: translateX(100%);
    transition: transform 0.2s ease;
    box-shadow: -8px 0 24px rgba(0, 0, 0, 0.18);
    overflow-y: auto;
    max-height: 100vh;
  }
  .app-shell.exec-drawer-open > .execution-sidebar {
    transform: translateX(0);
  }
  .exec-drawer-toggle { display: inline-flex !important; }
  .exec-drawer-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.34);
    z-index: 690;
  }
  .app-shell.sidebar-collapsed {
    grid-template-columns: 0 minmax(0, 1fr);
  }
  .app-shell.sidebar-collapsed > .sidebar { display: none; }
  .workspace { min-height: 0; height: auto; }
}

/* Wide screens: the drawer toggle + backdrop never show (rail is inline). */
.exec-drawer-toggle { display: none; }
.exec-drawer-backdrop[hidden] { display: none; }

/* ── ≤900px: SINGLE COLUMN — workspace dominant ───────────────
   Both side rails collapse; left sidebar shrinks to a slim
   header row so the chat/workspace is the primary surface. */
@media (max-width: 900px) {
  .app-shell {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto minmax(0, 1fr);
    height: 100vh;
  }
  .sidebar {
    border-right: 0;
    border-bottom: 1px solid var(--line);
    max-height: 28vh;
    min-height: 0;
  }
  .app-shell.sidebar-collapsed > .sidebar { display: none; }
  .app-shell.sidebar-collapsed { grid-template-rows: 0 minmax(0, 1fr); }
  .workspace { min-height: 0; height: auto; }
}

@media (max-width: 720px) {
  body { overflow: auto; }
  .workspace, .sidebar { padding: var(--s-4); }
  .app-shell { height: auto; min-height: 100vh; overflow: visible; }
  .workspace { height: auto; min-height: 0; overflow: visible; padding: 0; }
  /* Keep the off-canvas drawer scrollable (it is position:fixed from the
     ≤1100px rule); do NOT revert it to a visible-overflow stacked block. */
  .execution-sidebar { max-height: 100vh; overflow-y: auto; }
  .workspace-header,
  .composer-input-row { grid-template-columns: 1fr; display: grid; }
  .run-progress-body { grid-template-columns: 1fr; }
  .execution-sidebar .run-progress-body { grid-template-columns: 1fr; }
  .workspace-header { gap: var(--s-4); height: auto; }
  .msg-wrapper { max-width: 100%; }
  .messages { padding-bottom: 180px; }
  .composer-shell { position: sticky; bottom: 0; }
  .send-btn { width: 100%; }
}

/* =================================================================
   Design 설정 panel — v3 compact layout + v4 neutral palette
                       + v5 Airbnb-style chip refinement (2026-05-15)

   Layered on top of the v2 base rules above; nothing earlier is
   removed so legacy demos / preview rows still render. The overrides
   take effect once the v3 markup is in place
   (#design-options-summary, .design-options-tabs,
   .design-options-tab-panel).
   ================================================================= */

/* Panel itself — hard height cap so the composer is never pushed
   below the fold. */
.design-options-panel {
  max-height: clamp(280px, 36vh, 360px);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.design-options-panel[data-collapsed="1"] {
  max-height: 44px;
}

/* Header — toggle + intensity segmented control + reset, pinned right. */
.design-options-header {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding-bottom: 0;
  border-bottom: 0;
  justify-content: flex-start;
  flex-wrap: wrap;
  row-gap: var(--s-2);
}
.design-options-header .design-options-toggle {
  flex: 0 0 auto;
  white-space: nowrap;
  word-break: keep-all;
}
.design-options-header .design-options-reset-btn {
  margin-left: auto;
  flex: 0 0 auto;
}

/* Edit-intensity inline 3-segment control. */
.design-options-intensity {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: 0 0 auto;
  flex-shrink: 0;
  white-space: nowrap;
}
.design-options-intensity-label {
  font-size: 10px;
  font-weight: var(--w-bold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
}
/* 구도 고정 toggle (v7) — a pill-shaped checkbox switch in the Design options
   header, a peer of the 생성 개수 / 개입 수준 segmented controls. Height matches
   the segmented control so the row aligns; muted when off, soft Design-violet
   when on (mirrors a selected pill's state read). Pins the input image
   composition (see design_alt_prompts v7). */
.design-complock-switch {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 28px;
  padding: 0 11px;
  font-size: 11px;
  font-weight: var(--w-semibold);
  color: var(--muted);
  cursor: pointer;
  white-space: nowrap;
  background: var(--surface-soft);
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}
.design-complock-switch:hover { background: var(--surface-strong); color: var(--ink); }
.design-complock-switch:has(input:checked) {
  background: var(--mode-design-soft);
  border-color: var(--mode-design);
  color: var(--mode-design-ink);
}
.design-complock-switch:focus-within {
  outline: none;
  box-shadow: 0 0 0 3px var(--mode-design-soft);
}
.design-complock-switch input {
  width: 13px;
  height: 13px;
  accent-color: var(--mode-design);
  cursor: pointer;
  margin: 0;
}
.design-options-intensity-control {
  display: flex !important;
  flex-direction: row !important;
  flex-wrap: nowrap !important;
  align-items: center;
  gap: 2px !important;
  background: var(--surface-soft);
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  padding: 2px;
  min-width: 240px;
  flex-shrink: 0;
}
.design-options-intensity-control .design-pill {
  height: 22px;
  padding: 0 12px;
  border: 0;
  background: transparent;
  border-radius: var(--r-pill);
  font-size: 11px;
  font-weight: var(--w-semibold);
  color: var(--muted);
  white-space: nowrap;
  word-break: keep-all;
}
.design-options-intensity-control .design-pill:hover:not(:disabled) {
  background: var(--surface-strong);
  border: 0;
  box-shadow: none;
}
.design-options-intensity-control .design-pill.selected {
  background: var(--surface);
  color: var(--ink);
  font-weight: var(--w-bold);
  box-shadow: inset 0 0 0 1px var(--ink), var(--shadow-1);
}
.design-options-intensity-control .design-pill::before {
  display: none !important;
  content: none !important;
}

/* Body — vertical stack of summary + tabs + tab panels. */
.design-options-body {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  padding-top: var(--s-2);
  overflow: hidden;
}

/* ---- Summary chip strip ---------------------------------------- */
.design-options-summary {
  flex: 0 0 auto;
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  padding: 0 0 var(--s-2);
  border-bottom: 1px solid var(--line-soft);
}
.design-options-summary[hidden] { display: none; }

.design-options-summary-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 24px;
  padding: 0 10px;
  border-radius: var(--r-pill);
  background: var(--surface-soft);
  border: 1px solid var(--line);
  color: var(--ink-2);
  font-size: 11px;
  font-weight: var(--w-semibold);
  cursor: pointer;
  white-space: nowrap;
  word-break: keep-all;
  transition: background 0.12s ease, border-color 0.12s ease;
}
.design-options-summary-chip:hover {
  background: var(--surface-strong);
  border-color: var(--line-strong);
}
.design-options-summary-chip:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring-ink);
}
.design-options-summary-chip .key {
  color: var(--muted);
  font-weight: var(--w-bold);
  letter-spacing: 0.02em;
}
.design-options-summary-chip .value {
  color: var(--ink);
  font-weight: var(--w-bold);
}
.design-options-summary-chip.is-set {
  background: var(--surface);
  border-color: var(--ink);
  padding-left: 12px;
  box-shadow:
    inset 0 0 0 1px var(--ink),
    inset 3px 0 0 var(--samoo-red),
    var(--shadow-1);
}
.design-options-summary-chip.is-set .key { color: var(--muted); }
.design-options-summary-chip.is-set .value { color: var(--ink); }

/* ---- Tab strip ------------------------------------------------- */
.design-options-tabs {
  flex: 0 0 auto;
  display: flex;
  border-radius: var(--r-sm);
  background: var(--canvas);
  border: 1px solid var(--line);
  overflow: hidden;
  position: relative;
}
.design-options-tab {
  flex: 1 1 0;
  height: 32px;
  padding: 0 12px;
  border: 0;
  background: transparent;
  border-radius: 0;
  font-size: var(--t-sm);
  font-weight: var(--w-semibold);
  color: var(--ink-2);
  cursor: pointer;
  white-space: nowrap;
  word-break: keep-all;
  position: relative;
  transition: background 0.12s ease, color 0.12s ease;
}
.design-options-tab + .design-options-tab {
  border-left: 1px solid var(--line);
}
.design-options-tab:hover:not([aria-selected="true"]) {
  background: var(--surface-soft);
  color: var(--ink);
}
.design-options-tab[aria-selected="true"] {
  background: var(--surface);
  color: var(--ink);
  font-weight: var(--w-bold);
}
.design-options-tab[aria-selected="true"]::after {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 2px;
  background: var(--samoo-red);
}
.design-options-tab:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 2px rgba(17, 17, 17, 0.18);
}

/* ---- Tab panels ------------------------------------------------ */
.design-options-tab-panels {
  flex: 1 1 auto;
  min-height: 0;
  position: relative;
}
.design-options-tab-panel {
  position: absolute;
  inset: 0;
  overflow-y: auto;
  padding: var(--s-1) 2px var(--s-1) 0;
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
}
.design-options-tab-panel[hidden] { display: none; }

#design-options-tab-building .design-options-control {
  gap: var(--s-2);
}
#design-options-tab-building .design-building-type-groups {
  gap: var(--s-2);
}
#design-options-tab-building .design-building-group {
  gap: 2px;
}

.design-options-tab-panel .design-options-row {
  grid-template-columns: 110px 1fr;
  gap: var(--s-2);
  padding: 4px 0;
}
@media (max-width: 1100px) {
  .design-options-tab-panel .design-options-row {
    grid-template-columns: 1fr;
  }
}
.design-options-tab-panel .design-options-label {
  font-size: var(--t-sm);
  padding-top: 2px;
}
.design-options-tab-panel .design-options-help {
  font-size: 10px;
}

.design-options-tab-panel .design-pill {
  height: 24px;
  padding: 0 9px;
  font-size: 11px;
}
.design-options-tab-panel .design-category-tab {
  height: 24px;
  padding: 0 9px;
  font-size: 11px;
}

#design-options-tab-reference .design-reference-preview img {
  width: 40px;
  height: 40px;
}

.design-options-panel[data-collapsed="1"] .design-options-body { display: none; }


/* =================================================================
   v6 holistic + v7 chip refinements (2026-05-18, design hash
   Xn1b6BWXWFt5XN3QNTl5VQ). Surgical CSS-only polish pass on top of
   the v5 layout — no JS, no HTML, no token changes. Every override
   is placed AFTER the original rule so the cascade picks it up
   without touching the base definition.
   ================================================================= */

/* Bot bubble — softer line-height for denser Korean paragraphs and
   symmetric corners (was: r-xs on the bottom-left tail). */
.bot .msg { border-radius: var(--r-lg); line-height: 1.62; }

/* User bubble — ink → ink-2 so the white-on-dark Hangul stops
   shouting; symmetric r-lg corners; tiny tracking bump for legibility
   on the dark fill. */
.user .msg {
  background: var(--ink-2);
  border-color: var(--ink-2);
  border-radius: var(--r-lg);
  letter-spacing: 0.005em;
}

/* Selected attachment — one "selected" vocabulary across the sidebar:
   warm-strong fill + 3px red left bar, same as active session-item.
   Was: red-wash with red border. */
.attachment-card.is-selected {
  position: relative;
  background: var(--surface-strong);
  border-color: var(--line);
}
.attachment-card.is-selected::before {
  content: "";
  position: absolute;
  left: -1px;
  top: 8px;
  bottom: 8px;
  width: 3px;
  border-radius: var(--r-pill);
  background: var(--samoo-red);
}
/* Active mode button — restored filled style. The earlier neutral
   ink-on-white treatment was too subtle to scan at a glance, so we
   bring back a clear filled fill but key it off ``data-target-mode``
   so each mode shows its accent (no more green button under a violet
   chip). Inactive buttons stay neutral. */
.resource-mode-btn.is-current {
  border-color: var(--success);
  background: var(--success);
  color: var(--on-dark);
  font-weight: var(--w-bold);
  box-shadow: var(--shadow-1);
}
.resource-mode-btn.is-current[data-target-mode="chat"] {
  border-color: var(--mode-chat);
  background: var(--mode-chat);
}
.resource-mode-btn.is-current[data-target-mode="bim"] {
  border-color: var(--mode-bim);
  background: var(--mode-bim);
}
.resource-mode-btn.is-current[data-target-mode="design"] {
  border-color: var(--mode-design);
  background: var(--mode-design);
}
.resource-mode-btn.is-current,
.resource-mode-btn.is-current * { color: var(--on-dark); }
.resource-mode-btn.is-current:hover:not(:disabled) {
  filter: brightness(0.92);
}
.resource-mode-btn.is-current::before {
  content: "●";
  display: inline-block;
  margin-right: var(--s-2);
  color: var(--on-dark);
  opacity: 0.92;
  background: transparent;
  width: auto;
  height: auto;
  border-radius: 0;
  vertical-align: baseline;
  flex: 0 0 auto;
}
.resource-mode-btn.is-current:disabled,
.resource-mode-btn.is-current[disabled] {
  opacity: 1;
  cursor: not-allowed;
  color: var(--on-dark);
}
.resource-mode-btn.is-current:disabled::before,
.resource-mode-btn.is-current[disabled]::before { color: var(--on-dark); }
/* Design-mode chip saturation bump (0.32 → 0.45) so it actually
   reads as a mode indicator. */
.resource-mode-chip.design {
  color: var(--mode-design-ink);
  background: rgba(122, 61, 186, 0.10);
  border-color: rgba(122, 61, 186, 0.45);
  font-weight: var(--w-bold);
}
.resource-mode-chip.bim,
.resource-mode-chip.chat,
.resource-mode-chip.mixed,
.resource-mode-chip.error { font-weight: var(--w-bold); }

/* Section count — outlined transparent chip instead of filled grey. */
.section-count {
  background: transparent;
  border: 1px solid var(--line);
  color: var(--muted);
}

/* Design Settings header toggle — drops to muted uppercase 11px so
   the inline 개입 수준 control reads as the primary header element. */
.design-options-toggle {
  padding: 4px 6px 4px 0;
  font-weight: var(--w-semibold);
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.design-options-title { font-size: 11px; }
.design-options-caret { font-size: 9px; opacity: 0.7; }

/* v7 — refined selected design-pill: warm-strong fill, single 1px
   ink border, 4px red leading dot, no shadow. (Was: ink-ring +
   shadow-1 layered, which read as a stamp.) */
.design-pill.selected {
  background: var(--surface-strong);
  border-color: var(--ink);
  color: var(--ink);
  font-weight: var(--w-bold);
  box-shadow: none;
}
.design-pill.selected::before {
  width: 4px;
  height: 4px;
  margin-right: 4px;
}
.design-pill.selected:hover:not(:disabled) {
  background: var(--surface-soft);
  border-color: var(--ink);
}

/* v7 — same refined treatment for the category tab row so they
   read as one family with the building-type grid. */
.design-category-tab.selected {
  background: var(--surface-strong);
  border-color: var(--ink);
  color: var(--ink);
  font-weight: var(--w-bold);
  box-shadow: none;
}
.design-category-tab.selected::before {
  width: 4px;
  height: 4px;
  margin-right: 4px;
}
.design-category-tab.selected:hover:not(:disabled) {
  background: var(--surface-soft);
  border-color: var(--ink);
}

/* v7 — elegant summary chip "is-set" state. Dropped the dual
   ring-and-bar in favour of a soft-strong fill + a 4px red dot
   wedged between the key and value labels. */
.design-options-summary-chip {
  height: 26px;
  padding: 0 12px;
  gap: 8px;
  background: var(--surface);
}
.design-options-summary-chip.is-set {
  background: var(--surface-strong);
  border-color: var(--line-strong);
  padding-left: 10px;
  box-shadow: none;
}
.design-options-summary-chip.is-set .key { color: var(--muted); }
.design-options-summary-chip.is-set .value {
  color: var(--ink);
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.design-options-summary-chip.is-set .value::before {
  content: "";
  display: inline-block;
  width: 4px;
  height: 4px;
  border-radius: var(--r-pill);
  background: var(--samoo-red);
  flex: 0 0 auto;
}

/* Error banner — leading ⚠ glyph + tighter flex layout so it reads
   as a status banner rather than a paragraph. The :not([hidden])
   guard is load-bearing: without it, ``display: flex`` on the bare
   selector overrides the user-agent ``display: none`` that the
   [hidden] attribute relies on, leaving an empty red bar floating
   above the chat at all times. */
.error-banner:not([hidden]) {
  display: flex;
  align-items: flex-start;
  gap: var(--s-2);
  padding: var(--s-2) var(--s-3);
}
.error-banner:not([hidden])::before {
  content: "⚠";
  flex: 0 0 auto;
  font-size: var(--t-md);
  line-height: 1.5;
  color: var(--danger);
}
.error-banner[hidden] { display: none; }

/* Composer breathing room — bottom padding bump so the last message
   is never hidden beneath the Design-mode composer. */
.messages { padding-bottom: 200px; }

/* Empty-state — soft warm card treatment instead of bare text. */
.empty-state {
  padding: var(--s-6) var(--s-4);
  border-radius: var(--r-md);
  background: var(--surface-soft);
  border: 1px solid var(--line-soft);
  color: var(--muted);
  font-size: var(--t-md);
  line-height: 1.55;
  text-align: center;
  word-break: keep-all;
}



/* =================================================================
   v6.1 regression fixes (2026-05-18, follow-up).
   - Design Settings panel was clipping first-row tab content on
     shorter viewports because the max-height cap left too little
     room after the (now 2-row) header. Bump the cap.
   - 개입 수준 was crowding the toggle title. Promote it to its own
     row inside the header via order/full-width.
   ================================================================= */

/* Bigger panel cap so the active tab's first rows are visible
   without any internal scrolling on a typical desktop viewport.
   Long tab content still scrolls internally — only the floor is
   raised. */
.design-options-panel {
  max-height: clamp(360px, 46vh, 480px);
}
.design-options-panel[data-collapsed="1"] {
  max-height: 44px;
}

/* Header reflow: toggle title + reset on row 1, intensity full-width
   on row 2. The header still uses flex-wrap from the base rule; we
   just force the intensity onto its own row by ordering it last and
   giving it a 100% basis. The reset button keeps margin-left: auto
   on row 1 so it stays right-pinned. */
.design-options-header .design-options-toggle { order: 1; }
.design-options-header .design-options-reset-btn { order: 2; }
/* 생성 개수 + 개입 수준 share a single row. They wrap to two rows on
   narrow widths via the header's existing flex-wrap. */
.design-options-header .design-options-intensity {
  order: 3;
  flex: 1 1 auto;
  min-width: 0;
  justify-content: flex-start;
  margin-top: 2px;
}
.design-options-header #design-options-variant-group {
  order: 3;
  flex: 0 1 auto;
}
.design-options-header [aria-label="개입 수준"] {
  order: 4;
  flex: 0 1 auto;
}
/* 구도 고정 sits last in the controls row, as a peer of the segmented controls. */
.design-options-header [aria-label="구도 고정"] {
  order: 5;
  flex: 0 1 auto;
  margin-top: 2px;
}
/* Wrap-target: the title row gets the natural row, the two controls
   wrap together onto row 2 — but if the rail is wide enough they all
   sit side-by-side. */
.design-options-header { column-gap: var(--s-3); }

/* Slightly tighter intensity control so the second row doesn't feel
   bulky. min-width drops from 240px so the segmented control hugs
   its content on narrower side-rails. */
.design-options-intensity-control {
  min-width: 0 !important;
}
.design-options-intensity-control .design-pill {
  padding: 0 14px;
}

/* Tab-panel inner padding tightened so the first card is closer to
   the tab strip and more of the grid is visible without scrolling. */
.design-options-tab-panel {
  padding-top: 0;
}

/* Belt-and-braces: ensure the body's internal min-height collapses
   cleanly so the absolute-positioned tab panels can grow into the
   freed space without overflowing the panel. */
.design-options-body { min-height: 0; }
.design-options-tab-panels { min-height: 140px; }


/* =================================================================
   v6.2 refinements (2026-05-18)
   - Compact image thumbnails with expand/shrink toggle.
   - Refined active mode button (gradient + ring instead of flat fill).
   ================================================================= */

/* Artifact image — compact by default, expand on user request. The
   slot caps the rendered image height; the toggle button in the card
   head flips ``.is-expanded`` on the slot. Click-to-zoom on the image
   still opens the full-resolution file in a new tab. */
.artifact-image-slot {
  max-height: 220px;
  transition: max-height 0.2s ease;
}
.artifact-image-slot.is-expanded { max-height: 640px; }
.artifact-image-slot .render-img {
  width: 100%;
  height: 100%;
  max-height: inherit;
  object-fit: contain;
  background: var(--surface-inset);
}
.artifact-image-slot.is-error { max-height: none; }


/* ---- Active mode button — refined fill (v6.2) ----------------- */
/* Replaces the flat saturated fill with a soft top-down gradient,
   a subtle inset highlight, and a 1px tonal ring. Each mode picks
   up its own accent via [data-target-mode]. Text stays white for
   contrast; inactive buttons remain untouched. */
.resource-mode-btn.is-current {
  background: linear-gradient(180deg,
    rgba(255, 255, 255, 0.12) 0%,
    rgba(0, 0, 0, 0.06) 100%),
    var(--success);
  border-color: var(--success);
  color: var(--on-dark);
  font-weight: var(--w-bold);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.18),
    inset 0 0 0 1px rgba(0, 0, 0, 0.08),
    0 1px 2px rgba(0, 0, 0, 0.10),
    0 2px 6px rgba(0, 0, 0, 0.06);
}
.resource-mode-btn.is-current[data-target-mode="chat"] {
  background: linear-gradient(180deg,
    rgba(255, 255, 255, 0.14) 0%,
    rgba(0, 0, 0, 0.08) 100%),
    var(--mode-chat);
  border-color: var(--mode-chat);
}
.resource-mode-btn.is-current[data-target-mode="bim"] {
  background: linear-gradient(180deg,
    rgba(255, 255, 255, 0.14) 0%,
    rgba(0, 0, 0, 0.08) 100%),
    var(--mode-bim);
  border-color: var(--mode-bim);
}
.resource-mode-btn.is-current[data-target-mode="design"] {
  background: linear-gradient(180deg,
    rgba(255, 255, 255, 0.14) 0%,
    rgba(0, 0, 0, 0.08) 100%),
    var(--mode-design);
  border-color: var(--mode-design);
}
.resource-mode-btn.is-current:hover:not(:disabled) {
  filter: brightness(1.06) saturate(1.04);
}
.resource-mode-btn.is-current:active:not(:disabled) {
  filter: brightness(0.94);
  box-shadow:
    inset 0 1px 2px rgba(0, 0, 0, 0.18),
    0 1px 0 rgba(255, 255, 255, 0.04);
}


/* =================================================================
   v6.3 refinements (2026-05-18)
   - Design Settings header: row 1 (title + 초기화), row 2 (생성 개수 + 개입 수준).
   - Image expand control becomes an overlay on the image itself.
   - External reference style image upload affordance.
   ================================================================= */

/* Header is now a stack: row 1 + row 2. Drop the row-level wrap rules
   that lived on the old single-row header; each row handles its own
   horizontal layout via the new .design-options-header-row class. */
.design-options-header {
  flex-direction: column;
  align-items: stretch;
  gap: var(--s-2);
  flex-wrap: nowrap;
  column-gap: 0;
}
.design-options-header-row {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  flex-wrap: wrap;
  row-gap: var(--s-2);
}
.design-options-header-row-1 {
  justify-content: space-between;
}
.design-options-header-row-1 .design-options-toggle { flex: 0 1 auto; }
.design-options-header-row-1 .design-options-reset-btn { flex: 0 0 auto; margin-left: auto; }
.design-options-header-row-2 {
  justify-content: flex-start;
}
.design-options-header-row-2 .design-options-intensity {
  flex: 0 1 auto;
  margin-top: 0;
  min-width: 0;
}

/* Overlay expand control on artifact images. Top-right corner, semi-
   transparent ink-on-white, hover bumps to fully opaque. */
.artifact-image-slot { position: relative; }
.artifact-expand-overlay {
  position: absolute;
  top: 6px;
  right: 6px;
  z-index: 2;
  width: 28px;
  height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: var(--r-pill);
  background: rgba(17, 17, 17, 0.42);
  color: var(--on-dark);
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  backdrop-filter: blur(4px);
  transition: background 0.12s ease, border-color 0.12s ease, transform 0.12s ease;
}
.artifact-expand-overlay:hover {
  background: rgba(17, 17, 17, 0.72);
  border-color: rgba(255, 255, 255, 0.32);
}
.artifact-expand-overlay:active { transform: scale(0.94); }
.artifact-expand-overlay:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--samoo-red);
}
/* Hide the overlay over the broken-image fallback (the slot's
   max-height is uncapped there; the icon would float over the
   placeholder copy and read as junk). */
.artifact-image-slot.is-error .artifact-expand-overlay { display: none; }

/* External reference upload — affordance row inside the 참조 이미지
   tab. Keeps the existing dropdown above it; adds an "외부 이미지 추가"
   button + clear button + thumbnail row. */
.design-reference-upload {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  align-items: center;
  margin-top: var(--s-2);
}
.design-reference-external {
  margin-top: var(--s-2);
  border: 1px dashed var(--line-strong);
  background: var(--surface-soft);
  padding: 6px 8px;
  border-radius: var(--r-sm);
}
/* When the external upload is active, dim the dropdown a touch so the
   user reads "this dropdown is overridden". */
.design-reference-select[data-overridden="1"] {
  opacity: 0.55;
}


/* =================================================================
   v6.4 (2026-05-18) — Soften the active mode button fill.
   The v6.2 gradient + saturated mode color read slightly too solid in
   the right rail. v6.4 layers a translucent white wash on top of a
   diluted mode color (85% mode + 15% white via color-mix) so the chip
   still reads mode-coloured but feels softer. Token names + DOM hooks
   unchanged; pure CSS override placed after the v6.2 block so the
   cascade picks it up.
   ================================================================= */
.resource-mode-btn.is-current {
  background:
    linear-gradient(180deg,
      rgba(255, 255, 255, 0.18) 0%,
      rgba(255, 255, 255, 0.04) 100%),
    color-mix(in srgb, var(--success) 86%, #ffffff 14%);
  border-color: color-mix(in srgb, var(--success) 70%, #000000 30%);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.22),
    inset 0 0 0 1px rgba(0, 0, 0, 0.05),
    0 1px 2px rgba(0, 0, 0, 0.06),
    0 2px 5px rgba(0, 0, 0, 0.04);
}
.resource-mode-btn.is-current[data-target-mode="chat"] {
  background:
    linear-gradient(180deg,
      rgba(255, 255, 255, 0.18) 0%,
      rgba(255, 255, 255, 0.04) 100%),
    color-mix(in srgb, var(--mode-chat) 86%, #ffffff 14%);
  border-color: color-mix(in srgb, var(--mode-chat) 70%, #000000 30%);
}
.resource-mode-btn.is-current[data-target-mode="bim"] {
  background:
    linear-gradient(180deg,
      rgba(255, 255, 255, 0.18) 0%,
      rgba(255, 255, 255, 0.04) 100%),
    color-mix(in srgb, var(--mode-bim) 86%, #ffffff 14%);
  border-color: color-mix(in srgb, var(--mode-bim) 70%, #000000 30%);
}
.resource-mode-btn.is-current[data-target-mode="design"] {
  background:
    linear-gradient(180deg,
      rgba(255, 255, 255, 0.18) 0%,
      rgba(255, 255, 255, 0.04) 100%),
    color-mix(in srgb, var(--mode-design) 86%, #ffffff 14%);
  border-color: color-mix(in srgb, var(--mode-design) 70%, #000000 30%);
}
.resource-mode-btn.is-current:hover:not(:disabled) {
  filter: brightness(1.04) saturate(1.02);
}
/* Defence-in-depth: ensure text never reads dimmed even when the
   browser doesn't support color-mix() (the layered gradient still
   ships, just with the v6.2 saturated base showing through). */
.resource-mode-btn.is-current,
.resource-mode-btn.is-current * { color: var(--on-dark); }


/* =================================================================
   v6.5 (2026-05-18)
   - 환경 / 재료·분위기 rows stack the label above the chips so the
     visual hierarchy matches the 건물 tab (small section title +
     chip row underneath).
   - 참조 이미지 tab: dropdown hidden, compact thumbnail with inline ×.
   ================================================================= */
#design-options-tab-environment .design-options-row,
#design-options-tab-material .design-options-row {
  grid-template-columns: 1fr;
  gap: 4px;
  padding: 6px 0;
}
#design-options-tab-environment .design-options-label,
#design-options-tab-material .design-options-label {
  font-size: 10px;
  font-weight: var(--w-bold);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
  padding-top: 0;
}
#design-options-tab-environment .design-options-control,
#design-options-tab-material .design-options-control {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

/* 참조 이미지 tab — v3 simplified layout. */
.design-reference-select[hidden] { display: none; }
.design-reference-upload {
  display: flex;
  gap: var(--s-2);
  align-items: center;
  margin-top: 0;
}
.design-reference-thumb-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  margin-top: var(--s-2);
}
.design-reference-thumb {
  position: relative;
  display: inline-flex;
  width: 56px;
  height: 56px;
  border-radius: var(--r-sm);
  background: var(--surface-soft);
  border: 1px solid var(--line);
  overflow: hidden;
}
.design-reference-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.design-reference-thumb-remove {
  position: absolute;
  top: 2px;
  right: 2px;
  width: 18px;
  height: 18px;
  padding: 0;
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: var(--r-pill);
  background: rgba(17, 17, 17, 0.66);
  color: var(--on-dark);
  font-size: 11px;
  line-height: 1;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  backdrop-filter: blur(2px);
  transition: background 0.12s ease, transform 0.12s ease;
}
.design-reference-thumb-remove:hover { background: rgba(17, 17, 17, 0.85); }
.design-reference-thumb-remove:active { transform: scale(0.92); }
.design-reference-thumb-remove:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--samoo-red);
}


/* =================================================================
   v7 final (2026-05-18, design hash jmL1_TIsqEQoPXBTeE7WBA)
   - Active mode buttons: Airbnb-style soft fill + 3px coloured left
     bar (no more saturated gradient). Design mode hue refined from
     violet to slate-indigo.
   - 환경 / 재료·분위기 tabs: chip-card layout matching the 건물 tab.
   - 참조 이미지 tab: simplified vertical pane with compact CTA.
   This block sits AFTER every prior is-current/data-target-mode
   rule so the cascade resolves it last. Tokens, JS, and DOM hooks
   unchanged.
   ================================================================= */

/* ---- Active mode button — Airbnb-style soft fill --------------- */
.system-status-panel .resource-mode-btn.is-current,
.resource-mode-btn.is-current {
  position: relative;
  background: var(--surface-strong);
  border: 1px solid var(--line-strong);
  color: var(--ink);
  font-weight: var(--w-bold);
  padding-left: calc(var(--s-3) + 6px);
  box-shadow: none;
  filter: none;
}
.system-status-panel .resource-mode-btn.is-current,
.system-status-panel .resource-mode-btn.is-current *,
.resource-mode-btn.is-current,
.resource-mode-btn.is-current * {
  color: var(--ink);
}
.system-status-panel .resource-mode-btn.is-current::before,
.resource-mode-btn.is-current::before {
  content: "";
  position: absolute;
  left: 0;
  top: 8px;
  bottom: 8px;
  width: 3px;
  margin: 0;
  border-radius: var(--r-pill);
  background: var(--ink-2);
  opacity: 1;
}
.system-status-panel .resource-mode-btn.is-current[data-target-mode="chat"]::before,
.resource-mode-btn.is-current[data-target-mode="chat"]::before { background: var(--mode-chat); }
.system-status-panel .resource-mode-btn.is-current[data-target-mode="bim"]::before,
.resource-mode-btn.is-current[data-target-mode="bim"]::before { background: var(--mode-bim); }
.system-status-panel .resource-mode-btn.is-current[data-target-mode="design"]::before,
.resource-mode-btn.is-current[data-target-mode="design"]::before {
  /* Refined slate-indigo replaces the playful violet. Still visually
     distinct from chat (blue) and bim (emerald), no longer competes
     with the brand red. */
  background: #4B5A86;
}
.system-status-panel .resource-mode-btn.is-current[data-target-mode="chat"],
.resource-mode-btn.is-current[data-target-mode="chat"],
.system-status-panel .resource-mode-btn.is-current[data-target-mode="bim"],
.resource-mode-btn.is-current[data-target-mode="bim"],
.system-status-panel .resource-mode-btn.is-current[data-target-mode="design"],
.resource-mode-btn.is-current[data-target-mode="design"] {
  background: var(--surface-strong);
  border-color: var(--line-strong);
}
.system-status-panel .resource-mode-btn.is-current:hover:not(:disabled),
.resource-mode-btn.is-current:hover:not(:disabled) {
  background: var(--surface-soft);
  border-color: var(--ink);
  filter: none;
}
.system-status-panel .resource-mode-btn.is-current:disabled,
.system-status-panel .resource-mode-btn.is-current[disabled],
.resource-mode-btn.is-current:disabled,
.resource-mode-btn.is-current[disabled] {
  opacity: 1;
  cursor: not-allowed;
  color: var(--ink);
}

/* ---- Resource-mode chip — Design hue refinement --------------- */
.resource-mode-chip.design {
  color: #2F3A57;
  background: #EEF1F8;
  border-color: rgba(75, 90, 134, 0.32);
  font-weight: var(--w-bold);
}

/* ---- 환경 / 재료·분위기 chip-card layout ---------------------- */
/* The chip container carries both ``design-building-group-list`` (so
   it picks up the building-tab visual treatment) AND
   ``design-options-control`` + ``data-design-pill-group="…"`` (so
   ``_renderDesignPillGroups`` writes pills into it). The base
   ``.design-options-control`` rule (line ~610) sets
   ``flex-direction: column`` and the ``[data-design-pill-group]``
   follow-up doesn't reset it — so without this override the chips
   stack vertically. Force ``flex-direction: row`` here on every
   pill-grid container so the cascade picks it up regardless of
   class order. ``!important`` only because the legacy
   ``.design-options-control`` rule and the
   ``[data-design-pill-group]`` rule are equal-specificity to this
   override; using ``!important`` keeps the fix immune to source
   order shuffling in future polish blocks. */
.design-options-control[data-design-pill-group],
.design-building-group-list {
  flex-direction: row !important;
}
#design-options-tab-environment .design-building-group-list,
#design-options-tab-material .design-building-group-list {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  gap: 6px;
}
#design-options-tab-environment .design-building-type-groups,
#design-options-tab-material .design-building-type-groups {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
}
/* Nullify the older v6.5 per-row label rules — the row layout no
   longer exists in these tabs. Harmless on the new markup; explicit
   so future refactors don't leave a half-styled label floating. */
#design-options-tab-environment .design-options-label,
#design-options-tab-material .design-options-label {
  display: none;
}

/* ---- 참조 이미지 tab — cleaned up vertical pane --------------- */
.design-reference-pane {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  padding: 4px 0;
}
.design-reference-help {
  margin: 0;
  font-size: 11px;
  line-height: 1.5;
  color: var(--muted);
  word-break: keep-all;
}
.design-reference-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  align-items: center;
}
.design-reference-upload-btn {
  height: 30px;
  padding: 0 var(--s-3);
  font-size: 12px;
  font-weight: var(--w-semibold);
  border-radius: var(--r-sm);
}
.design-reference-upload-btn span[aria-hidden="true"] {
  font-size: 13px;
  margin-right: 4px;
  color: var(--muted);
  font-weight: var(--w-bold);
}
.design-reference-upload-btn:hover span[aria-hidden="true"] { color: var(--ink); }
/* Hide the legacy preview div fully so it can never reserve space. */
#design-options-tab-reference .design-reference-preview { display: none !important; }
.design-reference-thumb-row[hidden] { display: none; }
.design-reference-thumb-row:not([hidden]) {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  margin-top: 0;
}
/* Smaller, cleaner thumbnail than the earlier 56px version. */
.design-reference-thumb {
  width: 48px;
  height: 48px;
}


/* =================================================================
   v7.1 (2026-05-18) — header alignment + service-chip mode color
   - 'Design 설정' toggle and '초기화' reset chip share a 28 px
     equal-height baseline so their text is exactly co-centered.
   - .service-chip.generating ('IMAGE 생성 중') moves off the old
     violet palette and onto the refined slate-indigo used by the
     Design mode chip + active-button bar. Animation rule unchanged
     (only .busy + .generating pulse).
   ================================================================= */

/* ---- Header row 1 — equal-height toggle + reset ---------------- */
.design-options-header-row-1 .design-options-toggle {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  height: 28px;
  padding: 0 2px 0 0;
  line-height: 1;
}
.design-options-header-row-1 .design-options-reset-btn {
  display: inline-flex;
  align-items: center;
  height: 28px;
  padding: 0 12px;
  line-height: 1;
}
/* Title + caret inherit the 28 px line — keep their existing sizes
   but force vertical centring so the down-caret never floats high. */
.design-options-header-row-1 .design-options-title,
.design-options-header-row-1 .design-options-caret {
  display: inline-flex;
  align-items: center;
  line-height: 1;
}

/* ---- Service-chip "generating" — slate-indigo Design palette --- */
/* Aligns 'IMAGE 생성 중' with the mode chip ('Design 생성 가능')
   and the active mode button bar. The chip-pulse animation is set on
   the .dot child by the older rule (line ~1893); we leave that intact
   so generating + busy continue to pulse and off/ready/on/err/
   switching/unknown stay static. */
.service-chip.generating {
  color: #2F3A57;
  background: #EEF1F8;
  border-color: rgba(75, 90, 134, 0.32);
  font-weight: var(--w-bold);
}


/* =================================================================
   v7.2 (2026-05-18) — Mode-switch confirm modal polish
   - Variant count selected state: purple → refined Samoo red.
   - Modal action buttons (취소 / 전환 / 전환 중...) normalized to
     equal height + min-width so they read as one button group.
   The in-panel 생성 개수 selector (which lives under
   .design-options-intensity-control) is unaffected — it uses
   .design-pill, not .variant-count-btn.
   ================================================================= */

/* ---- Variant count picker — red accent ------------------------- */
.variant-count-btn {
  width: 36px;
  flex: 0 0 36px;
}
.variant-count-btn.selected {
  background: var(--samoo-red-soft);
  border-color: var(--samoo-red);
  color: var(--samoo-red-strong);
  font-weight: var(--w-bold);
}
.variant-count-btn.selected:hover { background: var(--samoo-red-soft); }
.variant-count-btn:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

/* ---- Modal action buttons — equal-height group ----------------- */
/* Inside the mode-switch confirm modal, normalize both the secondary
   (취소) and primary (전환 / 전환 중...) buttons to a 36 px row so
   their heights, paddings, and border-radii line up. min-width keeps
   the loading-state text "전환 중..." from inflating the button
   footprint dramatically. */
.mode-switch-confirm-actions .secondary-btn,
.mode-switch-confirm-actions .primary-btn {
  height: 36px;
  min-width: 88px;
  padding: 0 var(--s-4);
  font-size: var(--t-md);
  font-weight: var(--w-semibold);
  border-radius: var(--r-sm);
  line-height: 1;
}
.mode-switch-confirm-actions { align-items: center; }

/* ─── Region Edit (부분 수정) modal ──────────────────────────────────
   v2 (2026-05-19) redesign — calm "preview card + control panel"
   pattern.

   Goals addressed (from the QA pass):
     1. Modal width: 1080 → 960 max so the left/right ratio reads
        like a paired preview+inspector, not a wide hero canvas.
     2. Right panel: tightened to 300px, structured with explicit
        section headers (도구 · 크기 · 수정 요청) and dividers.
     3. Stronger left/right hierarchy: stage gets a recessed warm
        canvas, panel keeps pure white so the eye reads
        "preview · controls" left-to-right.
     4. The hint pill that floated over the image is gone; the
        help line lives inside the panel under the tool group.
     5. Tool buttons follow the Design Settings v7 chip language —
        white surface + 1.5px ink ring + 4px red leading dot when
        selected. No more red-soft fills.
     6. Action row: "영역 다시 선택" sits left as a tertiary reset,
        "취소" + "부분 수정 실행" pair on the right. Submit is the
        only primary CTA, no margin-left:auto isolation.
     7. Tone: calm white/warm-gray surfaces, hairline borders,
        single shadow tier — matches the rest of the app.

   The DOM and JS hooks are unchanged. Every class name used by
   app.js (.region-edit-overlay, .region-edit-modal, .region-edit-
   stage, .region-edit-image-frame, .region-edit-mask-canvas, etc.)
   stays in place; only their visual treatment is rewritten.
   ──────────────────────────────────────────────────────────────── */

.artifact-region-edit-btn {
  font-size: var(--t-md);
  font-weight: var(--w-semibold);
}

/* Overlay scrim. Slightly lighter ink than v1 (0.55 → 0.46) so the
   warm canvas underneath still reads through; matches the
   mode-switch confirm modal's scrim. */
.region-edit-overlay {
  position: fixed;
  inset: 0;
  z-index: 9000;
  background: rgba(17, 17, 17, 0.46);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}
.region-edit-overlay[hidden] { display: none; }
body.region-edit-open { overflow: hidden; }

/* Modal shell — narrower than v1 and slightly less heavy shadow.
   Stays centred at any viewport via the overlay's flex centering. */
.region-edit-modal {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  box-shadow:
    0 1px 2px rgba(17, 17, 17, 0.06),
    0 12px 32px rgba(17, 17, 17, 0.18);
  width: min(960px, 94vw);
  max-height: 92vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* Header — flatter than v1 (no surface-strong fill), title at 14px
   ink + a quiet close affordance. */
.region-edit-modal-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 20px;
  border-bottom: 1px solid var(--line);
  background: var(--surface);
}
.region-edit-modal-title {
  font-weight: var(--w-bold);
  font-size: 14px;
  color: var(--ink);
  letter-spacing: -0.005em;
}
.region-edit-close {
  border: none;
  background: transparent;
  font-size: 20px;
  line-height: 1;
  color: var(--muted);
  cursor: pointer;
  padding: 4px 8px;
  border-radius: var(--r-sm);
  transition: background 0.12s ease, color 0.12s ease;
}
.region-edit-close:hover { background: var(--surface-soft); color: var(--ink); }

/* Body — 2 columns: left preview stage (fluid) + right control
   panel (fixed 300px). At ≤900px the panel slides under the stage. */
.region-edit-modal-body {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 300px;
  gap: 0;
  flex: 1;
  min-height: 0;
}
@media (max-width: 900px) {
  .region-edit-modal-body {
    grid-template-columns: 1fr;
    grid-template-rows: minmax(0, 1fr) auto;
  }
}

/* ───── Left: preview stage ───── */
/* Recessed warm-canvas surface so it reads "viewport / editor"
   distinctly from the white control panel on the right. */
.region-edit-stage {
  position: relative;
  background: var(--canvas);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  overflow: auto;
  min-height: 320px;
}
.region-edit-image-frame {
  position: relative;
  display: inline-block;
  line-height: 0;
  max-width: 100%;
  max-height: calc(92vh - 160px);
  background: var(--surface);
  border-radius: var(--r-sm);
  box-shadow:
    0 0 0 1px var(--line),
    0 4px 12px rgba(17, 17, 17, 0.06);
  overflow: hidden;
}
.region-edit-image {
  display: block;
  max-width: 100%;
  max-height: calc(92vh - 160px);
  width: auto;
  height: auto;
  user-select: none;
  -webkit-user-drag: none;
  background: var(--surface);
}
.region-edit-mask-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  cursor: crosshair;
  touch-action: none;
  /* Internal paint is fully opaque; CSS opacity gives a stable ~42%
     translucency regardless of how many brush strokes layered. */
  opacity: 0.42;
}
.region-edit-mask-canvas[data-tool="brush"],
.region-edit-mask-canvas[data-tool="eraser"] {
  cursor: none;
}
.region-edit-brush-cursor {
  position: absolute;
  pointer-events: none;
  border: 1.5px solid var(--samoo-red);
  background: rgba(228, 30, 38, 0.10);
  border-radius: 50%;
  box-sizing: border-box;
}

/* The hint pill that v1 floated over the image is hidden — the
   help line now lives inside the control panel as static text.
   Kept in the DOM (rules don't remove the node) so any JS that
   targets it still resolves; we just don't show it. */
/* Model-ceiling note, at the moment of authoring (directly under 수정 요청) so it
   can still change what gets typed. Never repeated after a failure — post-hoc it
   reads as blame. */
.region-edit-ceiling-note {
  margin-top: var(--s-1);
  font-family: var(--font-sans);
  font-size: var(--t-xs);
  color: var(--muted);
  line-height: var(--lh-snug);
}

/* Restored 2026-07-30: the claim above was wrong — the control panel never
   carried this text, so hiding it left NOTHING on screen stating that the red
   overlay is the area that gets repainted. The mask convention was conveyed by
   colour alone. */
.region-edit-hint {
  margin-top: var(--s-2);
  font-family: var(--font-sans);
  font-size: var(--t-xs);
  color: var(--muted);
  line-height: var(--lh-snug);
  text-align: center;
}

/* ───── Right: control panel ───── */
.region-edit-controls {
  display: flex;
  flex-direction: column;
  gap: 0;
  padding: 0;
  border-left: 1px solid var(--line);
  background: var(--surface);
  min-width: 0;
  overflow-y: auto;
}
@media (max-width: 900px) {
  .region-edit-controls { border-left: none; border-top: 1px solid var(--line); }
}

/* Section labels — uppercase 10px ink-muted, with a top hairline
   for everything except the first section. Achieved with sibling
   selectors so the markup doesn't need to change. */
.region-edit-controls > * {
  padding: 14px 20px;
  position: relative;
}
.region-edit-controls > * + * {
  border-top: 1px solid var(--line-soft);
}
.region-edit-tools {
  display: flex;
  gap: 6px;
  align-items: center;
}
.region-edit-tools::before {
  content: "도구";
  position: absolute;
  top: 10px;
  left: 20px;
  font-size: 10px;
  font-weight: var(--w-bold);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
.region-edit-tools {
  padding-top: 30px;
}

/* Tool buttons — Airbnb-style refined filter chip language, matching
   the Design Settings building-type chips. White surface, 1px line,
   selected = ink ring + 4px red leading dot. */
.region-edit-tool {
  flex: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  height: 30px;
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--ink-2);
  border-radius: var(--r-sm);
  font-size: 12px;
  font-weight: var(--w-semibold);
  cursor: pointer;
  padding: 0 10px;
  line-height: 1;
  white-space: nowrap;
  word-break: keep-all;
  transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}
.region-edit-tool:hover { border-color: var(--ink); background: var(--surface); color: var(--ink); }
.region-edit-tool.selected {
  background: var(--surface-strong);
  border-color: var(--ink);
  color: var(--ink);
  font-weight: var(--w-bold);
  padding-left: 8px;
}
.region-edit-tool.selected::before {
  content: "";
  display: inline-block;
  width: 4px;
  height: 4px;
  border-radius: var(--r-pill);
  background: var(--samoo-red);
  flex: 0 0 auto;
  margin-right: 2px;
}
.region-edit-tool:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring-ink, 0 0 0 2px rgba(17, 17, 17, 0.22));
}

/* Brush size row — same language as the tool buttons but smaller.
   Section label "크기" sits above; the label span inside is hidden
   (the section header takes its job). */
.region-edit-brush-row {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  padding-top: 30px;
}
.region-edit-brush-row[hidden] { display: none; }
.region-edit-brush-row::before {
  content: "크기";
  position: absolute;
  top: 10px;
  left: 20px;
  font-size: 10px;
  font-weight: var(--w-bold);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
.region-edit-brush-label {
  /* The "크기" label inside the row is now redundant with the
     section header — hide it but keep it in the DOM. */
  display: none;
}
.region-edit-brush-size {
  height: 26px;
  padding: 0 10px;
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--ink-2);
  border-radius: var(--r-pill);
  font-size: 11px;
  font-weight: var(--w-semibold);
  cursor: pointer;
  line-height: 1;
  transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}
.region-edit-brush-size:hover { border-color: var(--ink); color: var(--ink); }
.region-edit-brush-size.selected {
  background: var(--surface-strong);
  border-color: var(--ink);
  color: var(--ink);
  font-weight: var(--w-bold);
}

/* Prompt label — replaces the floating overlay hint with a clear
   "수정 요청" section header. */
.region-edit-label {
  /* Existing inline label sits *inside* the controls column. The
     section-header pseudo-element on the textarea would conflict,
     so render the inline label as the section header instead. */
  display: block;
  font-size: 10px;
  font-weight: var(--w-bold);
  color: var(--muted);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  margin: 0;
  padding: 0;
  position: absolute;
  top: 10px;
  left: 20px;
}

/* The label + textarea are siblings inside .region-edit-controls.
   The textarea is the actual content row that needs padding + the
   help line. */
.region-edit-prompt {
  width: 100%;
  resize: vertical;
  min-height: 84px;
  max-height: 220px;
  padding: 10px 12px;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  font-family: inherit;
  font-size: 13px;
  line-height: 1.5;
  color: var(--ink);
  background: var(--surface);
  box-sizing: border-box;
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
.region-edit-prompt::placeholder { color: var(--muted-soft); }
.region-edit-prompt:focus {
  outline: none;
  border-color: var(--ink);
  box-shadow: var(--focus-ring-ink, 0 0 0 3px rgba(17, 17, 17, 0.12));
}
/* The 네이티브 인페인트 toggle + 인페인트 강도 sub-option were removed 2026-07-30
   along with their styles: region edit now has ONE backend, so the controls named a
   distinction that no longer exists. */

/* Adjust the section-header pseudo-element strategy: the label
   element above is already the section header, so the .region-
   edit-prompt block sits right under it without its own header.
   To avoid the previous "padding-top: 30px" stack, override the
   default sibling padding here. */
.region-edit-controls .region-edit-prompt {
  margin-top: 0;
}
/* The .region-edit-label is a *direct child* of the controls column,
   acting as the section header for the prompt. Section-divider hairline
   sits on the label so the textarea below does not also paint one;
   the textarea keeps its own 1px field border on all four sides. */
.region-edit-controls > .region-edit-label + .region-edit-prompt {
  /* The "* + *" sibling rule above would paint a section divider on
     the textarea — suppress it via a transparent override so the
     textarea's OWN border-top (from .region-edit-prompt) stays
     visible. Specificity 0,3,0 beats the section-divider rule. */
  border-top: 1px solid var(--line-strong);
}
.region-edit-controls > .region-edit-label {
  padding: 0;
  border-top: 1px solid var(--line-soft);
  /* Absolute label is positioned relative to its sibling textarea's
     padded box; pull it back into the section flow. */
  position: relative;
  top: auto;
  left: auto;
  padding: 14px 20px 6px;
}
.region-edit-controls > .region-edit-label + .region-edit-prompt {
  margin: 0 20px 14px;
  width: calc(100% - 40px);
  /* Inner padding matches the visual rhythm of the field — same value
     on all four sides so the placeholder sits comfortably from the
     top edge without a 30px top-overhang artefact from the old
     overlaid-label layout. */
  padding: 10px 12px;
}

/* Action row — three buttons, "영역 다시 선택" as a tertiary reset on
   the left, "취소 / 부분 수정 실행" pair on the right. Submit is the
   only filled CTA. */
.region-edit-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0;
  padding: 14px 20px 18px;
  border-top: 1px solid var(--line);
  background: var(--surface);
  align-items: center;
}
.region-edit-actions::before {
  /* Hide the implicit section header from the parent rule — this
     section is action-only, no label needed. */
  content: none;
}
.region-edit-actions .secondary-btn,
.region-edit-actions .primary-btn {
  height: 34px;
  padding: 0 14px;
  font-size: 13px;
  font-weight: var(--w-semibold);
  border-radius: var(--r-sm);
  line-height: 1;
  white-space: nowrap;
  word-break: keep-all;
}
/* Reset button slides left as a tertiary action; the cancel +
   submit pair stay grouped on the right via margin-left: auto on
   the cancel button. */
.region-edit-actions .region-edit-reset {
  margin-right: auto;
  background: transparent;
  border: 1px solid transparent;
  color: var(--muted);
  padding: 0 8px;
}
.region-edit-actions .region-edit-reset:hover {
  color: var(--ink);
  background: var(--surface-soft);
  border-color: var(--line);
}
/* Override v1's "margin-left: auto" on submit — it pushed the
   button into a corner instead of pairing it with cancel. */
.region-edit-actions .region-edit-submit { margin-left: 0; }

/* Status pill — sits beneath the action row, full-width, hidden by
   default. */
.region-edit-status {
  font-size: 12px;
  color: var(--ink-2);
  padding: 8px 20px;
  border-radius: 0;
  background: var(--surface-soft);
  border-top: 1px solid var(--line-soft);
}
.region-edit-status[data-kind="error"] {
  color: var(--danger-ink);
  background: var(--danger-soft);
}
.region-edit-status[hidden] { display: none; }


/* =================================================================
   Region Edit (부분 수정) modal — v3 tweaks (2026-05-20)
   -----------------------------------------------------------------
   • Tool buttons now host an inline SVG icon + label; the layout
     gives them slightly more vertical room and tightens the label
     gap so the icon and text read as one unit.
   • Action row drops the redundant 취소 button (X in the header
     does the same job) and renames 부분 수정 실행 → 실행. The two
     remaining buttons are right-aligned: 영역 다시 선택 sits as a
     secondary, 실행 as the lone primary CTA on the bottom-right.
   ================================================================= */

/* Tool buttons — icon + label layout. */
.region-edit-tool {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  height: 34px;
  padding: 0 12px;
}
.region-edit-tool-icon {
  flex: 0 0 auto;
  color: var(--muted);
  transition: color 0.12s ease;
}
.region-edit-tool:hover .region-edit-tool-icon,
.region-edit-tool.selected .region-edit-tool-icon {
  color: var(--ink);
}
.region-edit-tool-label {
  font-size: 12px;
  line-height: 1;
}

/* Selected state — keep the v2 ink-ring + red dot, but ensure the
   icon flips to ink (handled above) and the leading dot sits to the
   left of the icon, not crammed between icon and label. */
.region-edit-tool.selected {
  padding-left: 10px;
}
.region-edit-tool.selected::before {
  margin-right: 4px;
}

/* Disabled state — keep readable, slightly muted. */
.region-edit-tool:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}
.region-edit-tool:disabled .region-edit-tool-icon { color: var(--muted-soft); }

/* Action row — right-aligned 2-button structure.
   .region-edit-reset stays as a quiet bordered secondary; .region-
   edit-submit is the primary CTA. Push both to the right with a
   leading auto-margin on the reset button. */
.region-edit-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  padding: 14px 20px 18px;
  border-top: 1px solid var(--line);
  background: var(--surface);
  align-items: center;
}
/* Reset visible as a normal outlined secondary — no longer the
   "tertiary text" treatment from v2 since there's no separate
   cancel button to anchor against. */
.region-edit-actions .region-edit-reset {
  margin-right: 0;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  color: var(--ink);
  padding: 0 14px;
  height: 36px;
  font-size: 13px;
  font-weight: var(--w-semibold);
  border-radius: var(--r-sm);
}
.region-edit-actions .region-edit-reset:hover {
  background: var(--surface-soft);
  border-color: var(--ink);
  color: var(--ink);
}
.region-edit-actions .region-edit-submit {
  height: 36px;
  padding: 0 18px;
  font-size: 13px;
  font-weight: var(--w-bold);
  border-radius: var(--r-sm);
}

/* ===== BIM 3D Viewer (Phase 1) ======================================
   In-browser IFC viewer modal. Visual sibling of .region-edit-modal:
   same surface/line/canvas token usage, same header and close-button
   pattern, same shadow recipe. z-index 9100 sits one layer above the
   region-edit overlay (9000) so both can theoretically co-exist.

   Layout: fixed overlay scrim → centred modal shell (column flex) →
   bv-head (title + close) → bv-body (2-col grid: stage | side).
   At ≤900 px the grid collapses to a single column, stage on top.
   ==================================================================== */

/* ── Scroll lock ─────────────────────────────────────────────────── */
body.bv-open { overflow: hidden; }

/* ── Overlay scrim ───────────────────────────────────────────────── */
.bv-overlay {
  position: fixed;
  inset: 0;
  z-index: 9100;
  background: rgba(17, 17, 17, 0.46);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--s-6);
}
.bv-overlay[hidden] { display: none; }

/* ── Modal shell ─────────────────────────────────────────────────── */
.bv-modal {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  box-shadow:
    0 1px 2px rgba(17, 17, 17, 0.06),
    0 12px 32px rgba(17, 17, 17, 0.18);
  width: min(1180px, 96vw);
  height: 92vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* ── Header ──────────────────────────────────────────────────────── */
/* Mirrors .region-edit-modal-head exactly: flat white surface,
   14px bold title, hairline bottom border, quiet close affordance. */
.bv-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px var(--s-5);
  border-bottom: 1px solid var(--line);
  background: var(--surface);
  flex: 0 0 auto;
  gap: var(--s-4);
}

.bv-head-titles {
  display: flex;
  align-items: baseline;
  gap: var(--s-3);
  min-width: 0;
}

.bv-title {
  font-family: var(--font-sans);
  font-size: var(--t-md);
  font-weight: var(--w-bold);
  color: var(--ink);
  letter-spacing: -0.005em;
  white-space: nowrap;
  flex: 0 0 auto;
}

.bv-filename {
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  font-weight: var(--w-regular);
  letter-spacing: -0.01em;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}

/* Close button — identical to .region-edit-close */
.bv-close {
  border: none;
  background: transparent;
  font-size: 20px;
  line-height: 1;
  color: var(--muted);
  cursor: pointer;
  padding: 4px 8px;
  border-radius: var(--r-sm);
  transition: background 0.12s ease, color 0.12s ease;
  flex: 0 0 auto;
}
.bv-close:hover  { background: var(--surface-soft); color: var(--ink); }
.bv-close:focus-visible { outline: none; box-shadow: var(--focus-ring-ink); }

/* ── Body grid ───────────────────────────────────────────────────── */
/* Left: fluid 3D stage. Right: fixed 390 px properties sidebar.
   Widened from 320px so property keys/values fit without cramped
   wrapping; the modal at min(1180px,96vw) hosts this comfortably. */
.bv-body {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 390px;
  flex: 1;
  min-height: 0;
  overflow: hidden;
}

@media (max-width: 900px) {
  .bv-body {
    grid-template-columns: 1fr;
    grid-template-rows: minmax(52vh, 1fr) auto;
    overflow-y: auto;
  }
}

/* ── 3D Stage ────────────────────────────────────────────────────── */
/* Recessed warm-canvas surface — reads "viewport / editor" vs the
   white control panel. Relative so the canvas-host, toolbar, and
   veil all stack correctly via absolute positioning. */
.bv-stage {
  position: relative;
  background: var(--canvas);
  overflow: hidden;
  /* Ensure a real pixel size so the WebGL canvas is not 0×0 */
  min-height: 0;
}

/* WebGL canvas host — must fill the stage exactly; Three.js appends
   a <canvas> into this div. position:absolute + inset:0 guarantees
   a real bounding box for the renderer's resize observer. */
.bv-canvas-host {
  position: absolute;
  inset: 0;
  /* clips the capture-guide's outside-dim (huge box-shadow) to the stage */
  overflow: hidden;
}
/* 캡처 비율 가이드 — 카메라 파인더식 프레임. 캡처는 이 사각형만 크롭되므로
   프레임 밖은 어둡게 눌러 "잘려나갈 영역"임을 보여준다. DOM 오버레이라
   캡처(캔버스 픽셀)에는 찍히지 않는다. */
.bv-capture-guide {
  position: absolute;
  z-index: 4; /* canvas 위, viewcube/toolbar(스테이지 형제)에는 영향 없음 */
  pointer-events: none;
  border: 1px solid rgba(255, 255, 255, 0.85);
  box-shadow: 0 0 0 200vmax rgba(17, 17, 17, 0.38);
}
/* Three.js canvas fills its host */
.bv-canvas-host canvas {
  display: block;
  width: 100% !important;
  height: 100% !important;
}

/* ── Toolbar (two-cluster segmented strip, top-left) ──────────────── */
/* 10 buttons in two clusters:
   Navigation (전체 맞춤 보행 그리드) | Inspection (강조 격리 단면 박스 측정 층).
   flex-wrap:wrap is intentional — on a narrow stage the second cluster
   wraps to a second row.
   ViewCube clearance: 92px + 12px + 16px = 120px from right edge;
   max-width keeps the toolbar from ever reaching that zone.
   At 4 × ~34px per cluster, two rows of 5 sit at ~170px each — well
   within the 120px guard. padding:2px gives even intra-row height. */
.bv-toolbar {
  position: absolute;
  top: var(--s-3);
  left: var(--s-3);
  z-index: 10;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  /* Guard: never reach the ViewCube (92+28px from right) */
  max-width: calc(100% - 120px);
  gap: 0;
  row-gap: 2px;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  /* Slightly deeper shadow than a flat card — it floats over the scene */
  box-shadow: 0 2px 6px rgba(17, 17, 17, 0.10),
              0 1px 2px rgba(17, 17, 17, 0.06);
  padding: 2px;
}

.bv-tool {
  border: none;
  background: transparent;
  font-family: var(--font-sans);
  /* 11px / semibold: Korean glyphs are still crisp, fits 5–6 per row */
  font-size: var(--t-xs);
  font-weight: var(--w-semibold);
  color: var(--muted);
  cursor: pointer;
  /* 4px vertical / 8px horizontal — compact without feeling cramped */
  padding: 4px 8px;
  border-radius: calc(var(--r-sm) - 2px);
  line-height: 1.25;
  transition: background 0.10s ease, color 0.10s ease;
  white-space: nowrap;
  letter-spacing: -0.01em;
  min-height: 24px;
  display: inline-flex;
  align-items: center;
}
.bv-tool + .bv-tool {
  /* hairline separator between segments when neither is active */
  border-left: 1px solid var(--line);
}
/* When a sibling is active, dissolve the adjacent hairlines so the
   red pill has clean edges. */
.bv-tool.is-active + .bv-tool,
.bv-tool + .bv-tool.is-active {
  border-left-color: transparent;
}
.bv-tool:hover:not(.is-active) {
  background: var(--surface-soft);
  color: var(--ink);
}
.bv-tool:focus-visible { outline: none; box-shadow: var(--focus-ring-ink); }
/* Aria-pressed fallback (momentary buttons: 전체 맞춤) */
.bv-tool.active,
.bv-tool[aria-pressed="true"] {
  background: var(--surface-strong);
  color: var(--ink);
  font-weight: var(--w-semibold);
}

/* Cluster divider: 1px × full-height rule between the two clusters.
   Inserted as <span class="bv-toolbar-sep" aria-hidden="true">.
   margin:2px 4px keeps it visually centred within the strip height.
   The tool immediately after must not add its own left border. */
.bv-toolbar-sep {
  flex: 0 0 1px;
  align-self: stretch;
  min-height: 18px;
  background: var(--line-strong);
  margin: 2px 4px;
  border-radius: 1px;
  pointer-events: none;
}
.bv-toolbar-sep + .bv-tool {
  border-left: none;
}

/* ── Loading / error veil ────────────────────────────────────────── */
/* Absolute overlay covering the stage while the IFC loads.
   Hidden via the [hidden] attribute. */
.bv-veil {
  position: absolute;
  inset: 0;
  z-index: 20;
  background: rgba(246, 245, 242, 0.82);  /* --canvas at 82% opacity */
  display: flex;
  align-items: center;
  justify-content: center;
}
.bv-veil[hidden] { display: none; }

.bv-veil-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-3);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-2);
  padding: var(--s-6) var(--s-8);
  font-family: var(--font-sans);
  font-size: var(--t-sm);
  color: var(--muted);
  font-weight: var(--w-medium);
}

/* CSS spinner — samoo-red accent, matches auth/mode-switch spinners */
.bv-spinner {
  width: 24px;
  height: 24px;
  border: 2px solid var(--line-strong);
  border-top-color: var(--samoo-red);
  border-radius: 50%;
  animation: bv-spin 0.7s linear infinite;
  flex: 0 0 auto;
}
@keyframes bv-spin {
  to { transform: rotate(360deg); }
}

/* Error state modifier — danger-tinted card; spinner hidden */
.bv-veil.bv-veil-error .bv-veil-card {
  color: var(--danger);
  border-color: rgba(195, 21, 21, 0.28);
  background: var(--danger-soft);   /* light-pink tint so the card reads as error */
}
.bv-veil.bv-veil-error .bv-spinner {
  display: none;
}

/* ── Veil action-button row (닫기 / 다시 시도) ─────────────────
   Layout helper used by bim-viewer.js when injecting action
   buttons into the error card.  Any .bv-* button inside the
   error card is given a white surface so it stands out against
   the danger-soft tint.  Both .bv-tool (toolbar) and
   .secondary-btn (app-level) variants are covered. */
.bv-veil-actions {
  display: flex;
  gap: var(--s-2);
  justify-content: center;
  flex-wrap: wrap;
  margin-top: var(--s-1);
}
.bv-veil.bv-veil-error .bv-veil-card .bv-tool,
.bv-veil.bv-veil-error .bv-veil-card .secondary-btn {
  background: var(--surface);
  color: var(--ink-2);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  padding: 0 var(--s-4);
  height: 32px;
  min-width: 72px;
  font-family: var(--font-sans);
  font-size: var(--t-sm);
  font-weight: var(--w-medium);
  cursor: pointer;
  transition: background 0.12s ease;
}
.bv-veil.bv-veil-error .bv-veil-card .bv-tool:hover,
.bv-veil.bv-veil-error .bv-veil-card .secondary-btn:hover {
  background: var(--surface-strong);
}
.bv-veil.bv-veil-error .bv-veil-card .bv-tool:focus-visible,
.bv-veil.bv-veil-error .bv-veil-card .secondary-btn:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring-ink);
}

/* ── Side panel ──────────────────────────────────────────────────── */
/* Right column: white surface, hairline left border, column flex.
   Three zones: properties (scroll, flex:1) → edit groups (fixed
   height sections) → footer CTA (pinned). The side never overflows
   its column; the props region carries all the scroll debt. */
.bv-side {
  display: flex;
  flex-direction: column;
  border-left: 1px solid var(--line);
  background: var(--surface);
  min-height: 0;
  overflow: hidden;
}

@media (max-width: 900px) {
  .bv-side {
    border-left: none;
    border-top: 1px solid var(--line);
    min-height: 0;
    height: auto;
  }
}

/* ── Side section (선택한 부재) ──────────────────────────────────── */
/* flex: 1 1 0 + min-height: 0 lets this region compress so the
   edit groups and footer are never pushed off screen on a laptop. */
.bv-side-section {
  display: flex;
  flex-direction: column;
  /* PROPORTIONAL SPLIT with .bv-edit below: the property panel keeps the
     larger share (3 vs 2) and both regions scroll independently. The old
     `flex: 1 1 0` paired with `.bv-edit { flex: 0 1 auto }` let the edit
     accordions' tall content dominate and collapse the props to its floor
     when every accordion was expanded. */
  flex: 3 1 0;
  /* keep the props header + several rows always visible even when the edit
     accordions below are fully expanded (the edit zone scrolls) */
  min-height: 180px;
  padding: var(--s-3) var(--s-4) 0;
  gap: var(--s-1);
}

/* Section label — exact .ds-section-title recipe:
   10px / bold / 0.08em tracking / uppercase / var(--muted).
   Used both inside .bv-side-section and above each .bv-edit-group. */
.bv-side-title {
  font-family: var(--font-sans);
  font-size: var(--t-xs);
  font-weight: var(--w-bold);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  flex: 0 0 auto;
  line-height: var(--lh-snug);
}

/* Scrollable property region */
.bv-props {
  flex: 1 1 0;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  display: flex;
  flex-direction: column;
  gap: 0;
  padding-bottom: var(--s-3);
  /* Tight custom scrollbar — matches the region-edit modal feel */
  scrollbar-width: thin;
  scrollbar-color: var(--line-strong) transparent;
}
.bv-props::-webkit-scrollbar        { width: 4px; }
.bv-props::-webkit-scrollbar-track  { background: transparent; }
.bv-props::-webkit-scrollbar-thumb  { background: var(--line-strong); border-radius: 2px; }

/* Empty-state hint */
.bv-empty {
  font-family: var(--font-sans);
  font-size: var(--t-sm);
  color: var(--muted);   /* was --muted-soft; insufficient contrast for empty-state text */
  line-height: var(--lh-normal);
  padding: var(--s-3) 0;
}

.bv-empty-sm {
  font-family: var(--font-sans);
  font-size: var(--t-xs);
  color: var(--muted);   /* was --muted-soft */
  line-height: var(--lh-snug);
  padding-top: var(--s-1);
}

/* ── Property display ────────────────────────────────────────────── */

/* Prop header: type badge + element name.
   Sits at the very top of .bv-props — give it a touch more bottom
   padding and a slightly stronger divider to anchor the panel. */
.bv-prop-head {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  flex-wrap: wrap;
  padding: var(--s-2) 0 var(--s-3);
  border-bottom: 1px solid var(--line);
  flex: 0 0 auto;
}

/* Type badge — same recipe as .attachment-badge / .artifact-badge */
.bv-prop-type {
  display: inline-flex;
  align-items: center;
  padding: 2px 7px;
  border-radius: var(--r-pill);
  background: var(--mode-bim-soft);
  color: var(--success-ink);
  border: 1px solid rgba(19, 138, 78, 0.22);
  font-family: var(--font-sans);
  font-size: 10px;
  font-weight: var(--w-bold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
  flex: 0 0 auto;
}

/* Element name — the most prominent text in the panel */
.bv-prop-name {
  font-family: var(--font-sans);
  font-size: var(--t-md);
  font-weight: var(--w-semibold);
  color: var(--ink);
  letter-spacing: -0.005em;
  word-break: break-word;
  line-height: var(--lh-snug);
}

/* Property set block.
   Each pset is a labelled group of rows. The name is a sticky
   section header — it stays pinned at the top of the scroll
   container while its rows scroll past, so the user always knows
   which pset they're reading. A hairline bottom border on the pset
   body gives clear group separation without heavy rules. */
.bv-pset {
  display: flex;
  flex-direction: column;
  gap: 0;
  padding: 0;
  border-bottom: 1px solid var(--line-soft);
  flex: 0 0 auto;
}
.bv-pset:last-child { border-bottom: none; }

/* Sticky pset header — scrolls with .bv-props but pins at 0 when
   the group scrolls past the top. Background must match the scroll
   container's own background (--surface) so rows slide under it
   cleanly. A hairline bottom separates the label from its rows. */
.bv-pset-name {
  position: sticky;
  top: 0;
  z-index: 1;
  font-family: var(--font-sans);
  font-size: var(--t-xs);
  font-weight: var(--w-bold);
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
  background: var(--surface);
  padding: var(--s-2) 0 var(--s-1);
  border-bottom: 1px solid var(--line-soft);
  /* Subtle top shadow to cue "pinned" status without decoration */
  box-shadow: 0 1px 0 var(--surface);
}

/* Individual key–value row within a pset.
   Key takes 45% of width (enough for most IFC property names without
   ellipsis); value takes the rest. Zebra colouring on even rows adds
   a subtle grid reference that makes long psets (19 rows) scannable
   without column lines. Padding is tight — 3px top/bottom — so 10+
   rows stay on screen without feeling cramped. */
.bv-prop-row {
  display: grid;
  grid-template-columns: 45% 1fr;
  gap: var(--s-2);
  align-items: baseline;
  font-size: var(--t-sm);
  line-height: var(--lh-snug);
  padding: 3px var(--s-1);
  border-radius: 2px;
}
/* Zebra: every even row gets the faintest canvas tint */
.bv-prop-row:nth-child(even) {
  background: var(--surface-soft);
}

.bv-prop-k {
  font-family: var(--font-sans);
  font-size: var(--t-sm);
  color: var(--muted);
  font-weight: var(--w-regular);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.bv-prop-v {
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  letter-spacing: -0.01em;
  color: var(--ink-2);
  word-break: break-word;
  overflow-wrap: anywhere;
}

/* ── Side footer ─────────────────────────────────────────────────── */
/* Pinned at bottom; slightly more breathing room than the edit zone
   because the violet CTA is the primary page action. */
.bv-side-foot {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  padding: var(--s-3) var(--s-4) var(--s-4);
  /* No border-top: .bv-edit (surface-soft) sits directly above the foot. A
     hairline here floated as a stray line against the near-same surface — it
     read as a doubled/orphan divider. Matching the foot to --surface-soft
     makes the edit zone + footer one continuous bottom panel; the only
     divider in the lower region is the draggable .bv-side-resize bar. */
  background: var(--surface-soft);
}

/* Interaction hint — ds-meta style */
.bv-hint {
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--t-xs);
  color: var(--muted);   /* was --muted-soft */
  line-height: var(--lh-snug);
  letter-spacing: 0.01em;
}

/* 캡처 배율 — foot 내 한 줄 컨트롤. 라벨은 hint와 같은 muted 톤,
   select는 편집 패널의 .bv-edit-row select 룩을 미러 (컬럼 flex 안이므로
   flex-grow 없이 content 크기 — .bv-site-segmented 붕괴 교훈과 동일 계열). */
.bv-capture-scale {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  font-family: var(--font-sans);
  font-size: var(--t-xs);
  color: var(--muted);
  white-space: nowrap;
}
.bv-capture-scale select {
  flex: 1 1 auto;
  min-width: 0;
  height: 26px;
  padding: 0 var(--s-2);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-xs);
  background: var(--surface);
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: var(--t-sm);
}
.bv-capture-scale select:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring-ink);
}

/* Primary CTA — near-black (--samoo-black) so it reads as the panel's
   dominant action and sits in clear hierarchy above the green apply
   buttons (--mode-bim-soft) in the edit groups above.  Deliberately
   NOT violet: violet = Design mode indicator elsewhere in the app;
   using it here would conflate a "launch design" action with a mode
   state pill.  Near-black matches .primary-btn across the rest of the
   app.  Full 40px height; --r-sm radius consistent with the rest of
   the modal. */
.bv-use-design {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 40px;
  padding: 0 var(--s-4);
  border: none;
  border-radius: var(--r-sm);
  background: var(--samoo-black);
  color: var(--on-dark);
  font-family: var(--font-sans);
  font-size: var(--t-md);
  font-weight: var(--w-semibold);
  letter-spacing: -0.005em;
  cursor: pointer;
  transition: background 0.12s ease, transform 0.06s ease;
}
.bv-use-design:hover:not(:disabled)  { background: var(--samoo-black-strong); }
.bv-use-design:active:not(:disabled) { background: var(--samoo-black-strong); transform: translateY(1px); }
.bv-use-design:focus-visible         { outline: none; box-shadow: var(--focus-ring-ink); }
.bv-use-design:disabled              { opacity: 0.45; cursor: not-allowed; }

/* ── Edit panel (재질 편집 + 위치·회전) ─────────────────────────── */
/* The outer .bv-edit wrapper is a flex column with no background of
   its own — it provides the top border that visually separates the
   write zone from the read zone above, and carries two .bv-edit-group
   cards inside it. flex: 0 0 auto prevents it from growing and
   pushing the footer off screen. */
.bv-edit {
  /* USER-DRIVEN SPLIT with .bv-side-section above: the .bv-side-resize bar
     between them lets the operator drag the boundary. The section gets a
     pinned pixel height (flex:0 0 <px>) from the drag; this edit zone takes
     whatever remains (flex:1 1 0) and scrolls INTERNALLY when the accordions
     expand past it. No `max-height` cap — the resizer (not a fixed cap) owns
     the range now. The visual divider is the resizer bar, so there is no
     border-top here (a hairline + the bar read as a doubled line). */
  flex: 1 1 0;
  min-height: 160px;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--line-strong) transparent;
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  padding: var(--s-3) var(--s-4);
  background: var(--surface-soft);
}

/* ── Side-panel resizer (선택한 부재 ↔ edit accordions) ──────────────── */
/* Draggable horizontal divider: doubles as the single visual separator
   between the property panel and the edit zone (so .bv-edit drops its
   border-top). Thin hit band, short centered grip, row-resize cursor. */
.bv-side-resize {
  flex: 0 0 auto;
  position: relative;
  height: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface-soft);
  border-top: 1px solid var(--line);
  cursor: row-resize;
  touch-action: none;
  transition: background 0.12s ease, border-color 0.12s ease;
}
.bv-side-resize-grip {
  display: block;
  width: 30px;
  height: 2px;
  border-radius: 1px;
  background: var(--line-strong);
  transition: background 0.12s ease, width 0.12s ease;
}
.bv-side-resize:hover {
  background: var(--surface-inset);
}
.bv-side-resize:hover .bv-side-resize-grip {
  background: var(--muted);
  width: 40px;
}
.bv-side-resize.is-dragging {
  background: var(--surface-inset);
  border-color: var(--line-strong);
}
.bv-side-resize.is-dragging .bv-side-resize-grip {
  background: var(--muted);
  width: 40px;
}
.bv-side-resize:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring-ink);
}
@media (max-width: 900px) {
  /* stacked layout: the vertical split is meaningless — hide the bar */
  .bv-side-resize { display: none; }
}
.bv-edit::-webkit-scrollbar       { width: 3px; }
.bv-edit::-webkit-scrollbar-track { background: transparent; }
.bv-edit::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 2px; }

/* Group card — each edit sub-group (material / transform) lives
   inside an inset card: --surface-inset fill, hairline border,
   small radius, tighter internal padding. The .bv-side-title above
   each card acts as the group label. */
.bv-edit-group {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  background: var(--surface-inset);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  padding: var(--s-3) var(--s-3) var(--s-2);
}

/* Section title appearing directly above (outside) each card */
.bv-edit > .bv-side-title {
  margin-bottom: 0;
  padding-top: var(--s-1);
}
/* First group's label needs no extra top padding */
.bv-edit > .bv-side-title:first-child {
  padding-top: 0;
}

/* ── Edit rows ── */
/* Labels fixed at 88px, controls fill the remainder. This aligns
   the right-side input column across all three row types
   (color / range / number). */
.bv-edit-row {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  min-height: 26px;
}
.bv-edit-row label {
  flex: 0 0 88px;
  font-family: var(--font-sans);
  font-size: var(--t-sm);
  color: var(--muted);
  white-space: nowrap;
  line-height: var(--lh-snug);
  /* Numeric suffix spans (메탈릭 %, 러프니스 %) stay inline */
}
.bv-edit-row label span {
  font-family: var(--font-mono);
  font-size: var(--t-xs);
  color: var(--muted);   /* was --muted-soft; unit labels are real text */
  margin-left: 2px;
}

/* Color swatch */
.bv-edit-row input[type="color"] {
  flex: 0 0 auto;
  width: 40px;
  height: 24px;
  padding: 0;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-xs);
  background: var(--surface);
  cursor: pointer;
}

/* Range slider — stretches to fill the label-remainder column */
.bv-edit-row input[type="range"] {
  flex: 1 1 0;
  min-width: 0;
  accent-color: var(--mode-bim);
  cursor: pointer;
  height: 4px;
}

/* Number inputs (X/Y/Z/rotZ) — fixed mono width, right-aligned */
.bv-edit-row input[type="number"] {
  flex: 0 0 auto;
  width: 80px;
  height: 26px;
  padding: 0 var(--s-2);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-xs);
  background: var(--surface);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  letter-spacing: -0.01em;
  text-align: right;
}

.bv-edit-row input:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
.bv-edit-row input[type="number"]:disabled { opacity: 0.5; cursor: not-allowed; }
.bv-edit-row input[type="number"]:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring-ink);
}

/* Create-panel controls (부재 추가): type/storey selects + name text input */
.bv-edit-row select,
.bv-edit-row input[type="text"] {
  flex: 1 1 0;
  min-width: 0;
  height: 26px;
  padding: 0 var(--s-2);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-xs);
  background: var(--surface);
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: var(--t-sm);
}
.bv-edit-row select:disabled,
.bv-edit-row input[type="text"]:disabled { opacity: 0.5; cursor: not-allowed; }
.bv-edit-row select:focus-visible,
.bv-edit-row input[type="text"]:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring-ink);
}
/* wall/slab parameter sub-groups keep the group's vertical rhythm */
.bv-create-fields {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
}
.bv-create-fields[hidden] { display: none; }

/* Natural-language create block — sits above the manual form */
.bv-edit-nl {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
}
.bv-edit-nl-label {
  font-family: var(--font-sans);
  font-size: var(--t-sm);
  color: var(--muted);
  line-height: var(--lh-snug);
}
.bv-edit-nl textarea {
  width: 100%;
  min-height: 44px;
  resize: vertical;
  padding: var(--s-2);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-xs);
  background: var(--surface);
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: var(--t-sm);
  line-height: var(--lh-snug);
}
.bv-edit-nl textarea:disabled { opacity: 0.5; cursor: not-allowed; }
.bv-edit-nl textarea:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring-ink);
}
/* "또는 직접 입력" divider between NL and the manual form */
.bv-edit-nl-divider {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  margin: var(--s-2) 0;
  color: var(--muted);   /* was --muted-soft; this is real UI label text */
  font-family: var(--font-sans);
  font-size: var(--t-xs);
}
.bv-edit-nl-divider::before,
.bv-edit-nl-divider::after {
  content: "";
  flex: 1 1 0;
  height: 1px;
  background: var(--line);
}

/* Apply button — secondary weight inside the group card.
   Smaller height (30px) and medium weight so it reads as a
   "confirm this group" action, not a page-level CTA. The violet
   footer button retains visual dominance. */
.bv-edit-apply {
  width: 100%;
  height: 30px;
  padding: 0 var(--s-3);
  border: 1px solid rgba(19, 138, 78, 0.35);
  border-radius: var(--r-xs);
  background: var(--mode-bim-soft);
  color: var(--success-ink);
  font-family: var(--font-sans);
  font-size: var(--t-sm);
  font-weight: var(--w-medium);
  letter-spacing: -0.005em;
  cursor: pointer;
  transition: background 0.12s ease, border-color 0.12s ease;
  margin-top: var(--s-1);
}
.bv-edit-apply:hover:not(:disabled) {
  background: var(--mode-bim);
  border-color: var(--mode-bim);
  color: var(--on-dark);
}
.bv-edit-apply:active:not(:disabled) {
  background: var(--success-ink);
  border-color: var(--success-ink);
  color: var(--on-dark);
}
.bv-edit-apply:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(19, 138, 78, 0.25); }
.bv-edit-apply:disabled { opacity: 0.45; cursor: not-allowed; }

/* Multi-select summary panel — compact header + 전체 해제 button. The clear
   button reuses .bv-edit-apply sizing but reads as a neutral dismiss (grey),
   not the green commit palette. */
.bv-multi-head .bv-prop-type { display: block; }
.bv-multi-clear {
  margin-top: var(--s-2);
  border-color: var(--line);
  background: var(--surface-2, #f2f1ee);
  color: var(--ink-2, #555);
}
.bv-multi-clear:hover:not(:disabled) {
  background: var(--ink-2, #555);
  border-color: var(--ink-2, #555);
  color: var(--on-dark);
}

/* ── 비활성화 / 숨김 해제 toolbar buttons ─────────────────────────── */
/* 비활성화: no special idle style — it reads as a normal inspection tool.
   숨김 해제: disabled = near-invisible (opacity 0.38) so the pair reads as
   "one-way trigger / pending restore". When elements are hidden and the
   button becomes enabled, a faint warn-soft tint signals a recoverable
   state that needs attention — not alarming, just present. */
.bv-tool--showhidden:disabled {
  opacity: 0.38;
  cursor: not-allowed;
}
.bv-tool--showhidden:not(:disabled) {
  color: var(--warn-ink);
  background: var(--warn-soft);
}
.bv-tool--showhidden:not(:disabled):hover {
  background: var(--warn);
  color: var(--on-dark);
}
/* Count badge: appears inside 숨김 해제 when JS populates data-bv="hiddenCount".
   Hidden via :empty so the button label width stays stable with 0 items. */
.bv-hidden-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 14px;
  height: 14px;
  padding: 0 3px;
  margin-left: 3px;
  border-radius: var(--r-pill);
  font-size: 9px;
  font-weight: var(--w-bold);
  line-height: 1;
  background: var(--warn);
  color: var(--on-dark);
  letter-spacing: 0;
  vertical-align: middle;
}
.bv-hidden-badge:empty { display: none; }
/* Keep the tool's label text baseline-aligned with the badge */
.bv-tool--showhidden .bv-tool-label { vertical-align: middle; }

/* ── 부재 삭제 (destructive) — warning copy + danger-coloured apply button ── */
/* .bv-edit-collapse--delete: a 3px danger-tinted left border on the
   collapse wrapper distinguishes this accordion from the constructive
   ones above at a glance. The border is muted (danger at 55% opacity)
   rather than full-red so it signals "use with care" rather than "error". */
.bv-edit-collapse--delete {
  border-left: 3px solid rgba(195, 21, 21, 0.42);
  margin-left: calc(-1 * var(--s-4));         /* bleed to .bv-edit padding edge */
  padding-left: calc(var(--s-4) - 3px);       /* compensate so content aligns */
  border-radius: 0 var(--r-xs) var(--r-xs) 0;
  background: rgba(195, 21, 21, 0.025);        /* barely-there danger blush */
}

/* Delete head: inherit the uppercase label recipe but tint the text and
   icon to danger-ink so the label reads as "destructive zone" without
   being hostile. The arrow retains its muted-soft color. */
.bv-edit-head--delete {
  color: var(--danger-ink);
}
.bv-edit-head--delete:hover {
  background: rgba(195, 21, 21, 0.07);
  color: var(--danger-ink);
}
.bv-edit-head--delete .bv-edit-arrow {
  color: rgba(195, 21, 21, 0.45);
}
.bv-edit-head--delete:hover .bv-edit-arrow {
  color: var(--danger-ink);
}
/* Inner span keeps icon + text on one baseline row */
.bv-edit-head-inner {
  display: inline-flex;
  align-items: center;
  gap: 5px;
}
/* Warning triangle icon inherits the head's danger-ink color */
.bv-delete-icon {
  flex: 0 0 auto;
  color: currentColor;
  opacity: 0.75;
  position: relative;
  top: -0.5px; /* optical center against the caps text */
}

/* Delete group card: very faint danger-soft background so the interior
   reads as a "red-zone" container, distinct from the neutral surface-inset
   used by the constructive accordions. */
.bv-edit-group--delete {
  background: rgba(251, 224, 225, 0.45);  /* --danger-soft at ~45% */
  border-color: rgba(195, 21, 21, 0.18);
}

/* Warning copy block */
.bv-edit-warn {
  font-size: var(--t-xs);
  line-height: var(--lh-snug);
  color: var(--danger-ink);
  background: var(--danger-soft);
  border: 1px solid rgba(195, 21, 21, 0.22);
  border-left: 3px solid var(--danger);   /* left accent stripe for emphasis */
  border-radius: var(--r-xs);
  padding: 7px 9px;
  margin-bottom: var(--s-1);
}

/* Delete apply button — inherits .bv-edit-apply sizing; palette overrides.
   Idle: danger-soft fill + danger-ink text (danger without hover-aggression).
   Hover: full danger red fill + white text.
   .is-armed (two-click confirm): danger fill PLUS a repeating-ring pulse
   so the "confirm" state is unambiguous even on a glance. */
.bv-delete-apply {
  border-color: rgba(195, 21, 21, 0.4);
  background: var(--danger-soft);
  color: var(--danger-ink);
  transition: background 0.12s ease, border-color 0.12s ease,
              color 0.12s ease, box-shadow 0.15s ease;
}
.bv-delete-apply:hover:not(:disabled) {
  background: var(--danger);
  border-color: var(--danger);
  color: var(--on-dark);
}
.bv-delete-apply.is-armed {
  background: var(--danger);
  border-color: var(--danger);
  color: var(--on-dark);
  box-shadow: 0 0 0 3px rgba(195, 21, 21, 0.22),
              0 0 0 5px rgba(195, 21, 21, 0.10);
  animation: bv-armed-pulse 1.4s ease-in-out infinite;
}
@keyframes bv-armed-pulse {
  0%, 100% { box-shadow: 0 0 0 2px rgba(195, 21, 21, 0.28), 0 0 0 5px rgba(195, 21, 21, 0.10); }
  50%       { box-shadow: 0 0 0 4px rgba(195, 21, 21, 0.18), 0 0 0 7px rgba(195, 21, 21, 0.06); }
}
.bv-delete-apply:active:not(:disabled) {
  background: var(--danger-ink);
  border-color: var(--danger-ink);
  color: var(--on-dark);
  animation: none;
}
.bv-delete-apply:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px rgba(195, 21, 21, 0.30);
}
.bv-delete-apply:disabled { opacity: 0.45; cursor: not-allowed; animation: none; }

/* Status / feedback note below each apply button */
.bv-edit-note {
  min-height: 13px;
  font-family: var(--font-sans);
  font-size: var(--t-xs);
  color: var(--muted);
  line-height: 1.4;
}

/* ViewCube (Revit-style orientation widget) */
/* Sits top-right of the stage, above the canvas but below the veil.
   Drop shadow trimmed to near-invisible — the transparent PNG cube
   already has its own 3D shading; a heavy shadow reads as a border
   box fighting the geometry. Opacity fades at rest so it stays
   ambient, snaps to full on hover to confirm interactivity. */
.bv-viewcube {
  position: absolute;
  top: var(--s-3);
  right: var(--s-3);
  width: 92px;
  height: 92px;
  z-index: 4;
  cursor: pointer;
  opacity: 0.82;
  filter: drop-shadow(0 1px 3px rgba(17, 17, 17, 0.10));
  transition: opacity 0.15s ease, filter 0.15s ease;
}
.bv-viewcube:hover {
  opacity: 1;
  filter: drop-shadow(0 2px 6px rgba(17, 17, 17, 0.18));
}
.bv-viewcube canvas { display: block; }

/* Walk-mode hint pill + cursor + active toolbar button */
/* Translucent dark pill, bottom-centre, pointer-events:none.
   backdrop-filter adds just enough frosted depth so it reads
   clearly against both light and dark parts of the scene.
   Key sequence uses font-mono for the letter tokens and regular
   weight for the separators to give a keyboard legend feel. */
.bv-walkhint {
  position: absolute;
  left: 50%;
  bottom: var(--s-5);
  transform: translateX(-50%);
  z-index: 5;
  padding: 6px 16px;
  border-radius: var(--r-pill);
  background: rgba(17, 17, 17, 0.78);
  border: 1px solid rgba(255, 255, 255, 0.08);
  color: rgba(255, 255, 255, 0.92);
  font-family: var(--font-sans);
  font-size: var(--t-xs);
  font-weight: var(--w-medium);
  letter-spacing: 0.01em;
  white-space: nowrap;
  pointer-events: none;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
.bv-walkhint[hidden] { display: none; }
.bv-canvas-host.bv-walking { cursor: crosshair; }

/* Toggle-ON active state — samoo-red fill.
   Both toggle buttons (보행/그리드/강조/격리/단면/측정) and the
   momentary buttons share this selector; the JS is responsible for
   never setting .is-active on momentary buttons (전체/맞춤).
   A subtle inset shadow reinforces "pressed/latched" vs a plain
   momentary hover lift, making the toggle-on state unambiguous even
   for the default-on 그리드 button. */
.bv-tool.is-active {
  background: var(--samoo-red);
  color: var(--on-dark);
  font-weight: var(--w-semibold);
  /* remove the inter-segment hairline on the active segment itself */
  border-left-color: transparent;
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.18);
}
.bv-tool.is-active:hover {
  background: var(--samoo-red-strong);
  color: var(--on-dark);
}

/* ── Collapsible edit sections (재질 편집 / 위치·회전) ────────────── */
/* Each .bv-edit-collapse block is a standalone accordion row.
   The first collapse has no top border (the .bv-side-resize bar above the
   edit zone is the divider); subsequent collapses add a hairline above
   themselves so the two rows are always separated by a clean divider
   regardless of expanded/collapsed state. */
.bv-edit-collapse {
  /* hairline above every collapse — first child drops it (the resizer bar
     above the edit zone is the divider) so the line is never doubled */
  border-top: 1px solid var(--line-soft);
}
.bv-edit-collapse:first-child {
  border-top: none;
}

/* Accordion header button — full-width, 36px min hit target.
   Reads as a labelled row with a trailing chevron; hover lifts the
   background one token step so it reads as interactive without
   competing with the bv-edit-group card below. */
.bv-edit-head {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-2);
  background: none;
  border: none;
  /* side padding matches .bv-edit's own padding so labels align */
  padding: 9px var(--s-1);
  min-height: 36px;
  cursor: pointer;
  font-family: var(--font-sans);
  font-size: var(--t-xs);
  font-weight: var(--w-bold);
  letter-spacing: 0.06em;
  color: var(--muted);
  text-transform: uppercase;
  border-radius: var(--r-xs);
  transition: background 0.10s ease, color 0.10s ease;
}
.bv-edit-head:hover {
  background: var(--surface-strong);
  color: var(--ink);
}
.bv-edit-head:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring-ink);
}

/* Chevron — rotates 180° when the section is expanded.
   Transition already declared here; the state rule below triggers it. */
.bv-edit-arrow {
  font-size: 10px;
  color: var(--muted-soft);
  flex: 0 0 auto;
  transition: transform 0.18s ease, color 0.10s ease;
}
.bv-edit-head:hover .bv-edit-arrow {
  color: var(--muted);
}

/* Expanded: chevron points up */
.bv-edit-collapse:not(.is-collapsed) .bv-edit-arrow {
  transform: rotate(180deg);
}

/* Collapsed: body hidden; a small bottom padding on the head gives
   visual breathing room between the chevron and the next divider. */
.bv-edit-collapse.is-collapsed .bv-edit-group {
  display: none;
}

/* Expanded: give the group card a small top gap so it sits clearly
   under the header row rather than touching it. */
.bv-edit-collapse:not(.is-collapsed) .bv-edit-group {
  margin-top: var(--s-1);
  margin-bottom: var(--s-2);
}

/* ── Element search (top of the side panel) ──────────────────────── */
/* .bv-search is flex:0 0 auto so it never compresses and never
   pushes the property panel; it gets a bottom hairline that acts as
   the divider before the "선택한 부재" heading, replacing an extra
   margin hack. The wrapper is position:relative so the absolute
   dropdown is anchored to it, not to .bv-side. */
.bv-search {
  position: relative;
  flex: 0 0 auto;
  padding: var(--s-3) var(--s-4) var(--s-3);
  border-bottom: 1px solid var(--line-soft);
}

/* Input field — left-padded to 32px to make room for the magnifier
   pseudo-element on .bv-search itself (rendered via background-image
   on the wrapper div, not the input, so we avoid ::-webkit-search
   chrome conflicts). Focus ring uses the red accent. */
.bv-search-input {
  width: 100%;
  height: 32px;
  /* 30px left-pad = 12px icon zone + 6px gap + 12px text start */
  padding: 0 var(--s-3) 0 30px;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  background: var(--surface-soft);
  font-family: var(--font-sans);
  font-size: var(--t-sm);
  color: var(--ink);
  transition: border-color 0.12s ease, background 0.12s ease;
  box-sizing: border-box;
}
.bv-search-input::placeholder {
  color: var(--muted-soft);
}
.bv-search-input:focus {
  outline: none;
  border-color: var(--samoo-red);
  background: var(--surface);
  box-shadow: 0 0 0 2px var(--samoo-red-ring);
}

/* Magnifier icon — SVG data URI, 14×14, muted token colour,
   positioned inside the .bv-search wrapper left-of-input.
   Uses a non-interactive pseudo element on the wrapper so focus
   indicators on the input are unaffected. */
.bv-search::before {
  content: "";
  position: absolute;
  left: calc(var(--s-4) + 9px);
  top: 50%;
  transform: translateY(-50%);
  width: 14px;
  height: 14px;
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 14 14' fill='none'%3E%3Ccircle cx='5.8' cy='5.8' r='4' stroke='%236B6B6B' stroke-width='1.4'/%3E%3Cline x1='9' y1='9' x2='12.5' y2='12.5' stroke='%236B6B6B' stroke-width='1.4' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-size: 14px 14px;
  /* Nudge 3px down because of border padding accounting */
  margin-top: 1px;
}

/* Results dropdown — floats over the property panel.
   top is calculated from the input bottom edge inside .bv-search:
   padding-top(12px) + input-height(32px) + 2px overlap = 46px from
   wrapper top. left/right mirror the wrapper's own side padding. */
.bv-search-results {
  position: absolute;
  left: var(--s-4);
  right: var(--s-4);
  top: calc(var(--s-3) + 32px + 2px); /* padding-top + height + gap */
  z-index: 30; /* above .bv-pset-name sticky (z:1) and side panel */
  max-height: 280px;
  overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  box-shadow: var(--shadow-pop);
  /* Thin scrollbar matching .bv-props */
  scrollbar-width: thin;
  scrollbar-color: var(--line-strong) transparent;
}
.bv-search-results::-webkit-scrollbar       { width: 4px; }
.bv-search-results::-webkit-scrollbar-track { background: transparent; }
.bv-search-results::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 2px; }
.bv-search-results[hidden] { display: none; }

/* Result row — type tag (muted, fixed width) + name (ink, truncated).
   8px vertical padding gives comfortable 48px-equivalent tap target.
   Hairline dividers between rows; last child omits the bottom one. */
.bv-search-item {
  display: flex;
  align-items: baseline;
  gap: var(--s-2);
  width: 100%;
  padding: 7px var(--s-3) 6px;
  border: none;
  border-bottom: 1px solid var(--line-soft);
  background: none;
  text-align: left;
  cursor: pointer;
  font-family: var(--font-sans);
  transition: background 0.08s ease;
}
.bv-search-item:last-child { border-bottom: none; }
.bv-search-item:hover { background: var(--surface-soft); }
.bv-search-item:focus-visible { outline: none; background: var(--surface-strong); }

/* Type tag — BIM type abbreviation, muted, monospace for the
   all-caps IFC class strings (e.g. WALL, SLAB, BEAM). Narrow fixed
   width so name column stays stable regardless of type string length. */
.bv-search-type {
  flex: 0 0 auto;
  font-family: var(--font-mono);
  font-size: var(--t-xs);
  font-weight: var(--w-regular);
  color: var(--muted);   /* was --muted-soft; type labels are real identification text */
  letter-spacing: 0.01em;
  min-width: 48px;
}

/* Name — primary reading text, truncated with ellipsis */
.bv-search-name {
  flex: 1 1 0;
  min-width: 0;
  font-size: var(--t-sm);
  font-weight: var(--w-medium);
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  letter-spacing: -0.005em;
}

/* Empty state — centered, ds-meta styling */
.bv-search-empty {
  padding: var(--s-4) var(--s-3);
  font-size: var(--t-sm);
  color: var(--muted);
  text-align: center;
  line-height: var(--lh-snug);
}

/* ── Floating tool panels (단면 박스 / 층별 표시) ─────────────────── */
/* Both panels are HUD overlays that float over the 3D stage.
   They share a surface/hairline/shadow language matching the toolbar
   above them.  Position: absolute inside .bv-stage, top cleared from
   the toolbar (toolbar ≈ 2 rows × 28px + 12px top = ~68px;
   88px gives an 8px gap which reads as a deliberate air gap).
   sboxPanel anchors left; storeyPanel offsets right of it with an 8px
   gap.  z-index:8 sits above the viewcube (z:4) and canvas (z:0) but
   below the veil (z:20). */
.bv-toolpanel {
  position: absolute;
  /* Docked below the floating toolbar. The toolbar wraps (flex-wrap) on
     narrower viewer widths, dropping the "편집 이력" group onto a 2nd row; at
     the old 88px the panels overlapped that row. 128px clears a two-row
     toolbar so a freshly-opened panel never starts on top of it. */
  top: 128px;
  left: var(--s-3);
  z-index: 8;
  /* sboxPanel width: axis(16px) + gap(8px) + range(~86px) × 2 + gaps = 224px */
  width: 224px;
  padding: 0;
  /* Translucent + blurred so geometry stays legible behind the panel —
     live-use report: an opaque panel floating over the model "covers the
     view". Fallback solid surface for browsers without backdrop-filter. */
  background: var(--surface);
  background: rgba(255, 255, 255, 0.86);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-md);
  /* Elevated shadow — these float over scene geometry */
  box-shadow: 0 4px 16px rgba(17, 17, 17, 0.12),
              0 1px 3px  rgba(17, 17, 17, 0.07);
  overflow: hidden; /* clips title bg + body into rounded rect */
}
/* All tool panels dock at the canvas' top-left by default; when the
   section-box panel is open, later sibling panels shift right of it so
   simultaneous panels never stack on top of each other. */
.bv-toolpanel[data-bv="storeyPanel"],
.bv-toolpanel[data-bv="measurePanel"] {
  /* checklist only needs ~180px; stays compact */
  width: 196px;
}
.bv-toolpanel[data-bv="sectionPanel"] {
  /* just the 3 axis buttons — keep it tight */
  width: 168px;
}
.bv-toolpanel[data-bv="sboxPanel"]:not([hidden]) ~ .bv-toolpanel[data-bv="storeyPanel"],
.bv-toolpanel[data-bv="sboxPanel"]:not([hidden]) ~ .bv-toolpanel[data-bv="measurePanel"] {
  left: calc(var(--s-3) + 224px + 8px);
}
/* Section is mutually exclusive with the box + measure, but NOT with the
   storey filter — when both are open, dock storey to the right of the section
   panel so they don't stack. */
.bv-toolpanel[data-bv="sectionPanel"]:not([hidden]) ~ .bv-toolpanel[data-bv="storeyPanel"] {
  left: calc(var(--s-3) + 168px + 8px);
}
.bv-toolpanel[hidden] { display: none; }

/* Panel title bar — hairline-bottom strip, uppercase section label.
   Slightly recessed (surface-soft) so the body content surface reads
   as forward. */
.bv-toolpanel-title {
  display: block;
  padding: 7px var(--s-3) 6px;
  background: var(--surface-soft);
  border-bottom: 1px solid var(--line);
  font-family: var(--font-sans);
  font-size: var(--t-xs);
  font-weight: var(--w-bold);
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--muted);
  line-height: var(--lh-snug);
}

/* Panel body — the scrollable/content zone */
.bv-toolpanel-body {
  padding: var(--s-2) var(--s-3) var(--s-3);
}

.bv-toolpanel-empty {
  font-family: var(--font-sans);
  font-size: var(--t-sm);
  color: var(--muted);
  padding: var(--s-2) 0 var(--s-1);
}

/* ── Section-box slider rows (.bv-sbox-row) ─────────────────────── */
/* Each row: axis letter | range-min | range-max.
   Axis letter is monospace, emerald-accented — it's a data label.
   Two range sliders share the remaining width evenly with a 6px gap
   between them.  A thin divider below each row (except last) keeps
   the three axes visually separated. */
.bv-sbox-row {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding: 5px 0;
  border-bottom: 1px solid var(--line-soft);
}
.bv-sbox-row:last-child {
  border-bottom: none;
  padding-bottom: 0;
}

/* Axis letter: mono, semibold, emerald — unmistakable data label */
.bv-sbox-axis {
  flex: 0 0 14px;
  font-family: var(--font-mono);
  font-size: var(--t-xs);
  font-weight: var(--w-semibold);
  color: var(--mode-bim);
  letter-spacing: 0;
  text-align: center;
  line-height: 1;
}

/* Min/max sliders — equal share of the remaining width.
   height:3px via accent-color track; no extra wrapper needed since
   the row's align-items:center keeps them vertically centred. */
.bv-sbox-row input[type="range"] {
  flex: 1 1 0;
  min-width: 0;
  height: 3px;
  accent-color: var(--mode-bim);
  cursor: pointer;
  /* Slightly tighter appearance — matches edit-row range in the side panel */
  margin: 0;
}

/* ── Storey checklist (.bv-storey-item) ─────────────────────────── */
/* Items are injected directly into .bv-toolpanel-body[data-bv="storeyList"]
   — there is no .bv-storey-list wrapper.  Scroll constraint lives on
   the storeyPanel's .bv-toolpanel-body via the targeted rule below. */
.bv-toolpanel[data-bv="storeyPanel"] .bv-toolpanel-body {
  max-height: 220px;
  overflow-y: auto;
  padding-top: var(--s-1);
  /* Thin scrollbar — matches .bv-props */
  scrollbar-width: thin;
  scrollbar-color: var(--line-strong) transparent;
}
.bv-toolpanel[data-bv="storeyPanel"] .bv-toolpanel-body::-webkit-scrollbar       { width: 3px; }
.bv-toolpanel[data-bv="storeyPanel"] .bv-toolpanel-body::-webkit-scrollbar-track { background: transparent; }
.bv-toolpanel[data-bv="storeyPanel"] .bv-toolpanel-body::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 2px; }

.bv-storey-item {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding: 5px 0;
  border-bottom: 1px solid var(--line-soft);
  font-family: var(--font-sans);
  font-size: var(--t-sm);
  font-weight: var(--w-regular);
  color: var(--ink-2);
  cursor: pointer;
  border-radius: var(--r-xs);
  transition: background 0.08s ease, color 0.08s ease;
  /* Small horizontal indent so text doesn't kiss the panel edge */
  padding-left: 2px;
  padding-right: 2px;
}
.bv-storey-item:last-child {
  border-bottom: none;
}
.bv-storey-item:hover {
  background: var(--surface-soft);
  color: var(--ink);
}
.bv-storey-item input[type="checkbox"] {
  flex: 0 0 auto;
  accent-color: var(--mode-bim);
  cursor: pointer;
  width: 13px;
  height: 13px;
  margin: 0;
}

/* ===== end BIM 3D Viewer =========================================== */

/* ===== BIM 3D Viewer — 측정 패널 · 측정 라벨 · 도구 힌트 ========== */

/* ── Toggle-mode toolbar buttons: distinct active color ──────────────
   Momentary buttons (전체/맞춤) never receive .is-active from JS.
   Mode/toggle buttons (보행/그리드/강조/격리/단면/박스/측정/층) do.
   We target them by data-bv so they get a charcoal-on-white active
   state instead of the red fill — red is reserved for brand moments.
   The 2px bottom indicator line anchors the "latched" reading. */
.bv-tool[data-bv="walk"].is-active,
.bv-tool[data-bv="grid"].is-active,
.bv-tool[data-bv="ghost"].is-active,
.bv-tool[data-bv="isolate"].is-active,
.bv-tool[data-bv="section"].is-active,
.bv-tool[data-bv="sectionbox"].is-active,
.bv-tool[data-bv="measure"].is-active,
.bv-tool[data-bv="storey"].is-active {
  background: var(--ink);
  color: var(--on-dark);
  font-weight: var(--w-semibold);
  border-left-color: transparent;
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.22);
}
.bv-tool[data-bv="walk"].is-active:hover,
.bv-tool[data-bv="grid"].is-active:hover,
.bv-tool[data-bv="ghost"].is-active:hover,
.bv-tool[data-bv="isolate"].is-active:hover,
.bv-tool[data-bv="section"].is-active:hover,
.bv-tool[data-bv="sectionbox"].is-active:hover,
.bv-tool[data-bv="measure"].is-active:hover,
.bv-tool[data-bv="storey"].is-active:hover {
  background: var(--samoo-black-strong);
  color: var(--on-dark);
}

/* Axis-lock segmented control (자유 / X / Y / Z) */
.bv-measure-axis {
  display: flex;
  gap: 0;
  margin-bottom: var(--s-2);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  overflow: hidden;
}
.bv-axis-btn {
  flex: 1 1 0;
  padding: 5px 0;
  border: none;
  border-right: 1px solid var(--line);
  background: transparent;
  font-family: var(--font-mono);
  font-size: var(--t-xs);
  font-weight: var(--w-semibold);
  color: var(--muted);
  cursor: pointer;
  line-height: var(--lh-snug);
  letter-spacing: 0.02em;
  transition: background 0.09s ease, color 0.09s ease;
}
.bv-axis-btn:last-child { border-right: none; }
.bv-axis-btn:hover:not(.is-active) {
  background: var(--surface-strong);
  color: var(--ink);
}
.bv-axis-btn.is-active {
  background: var(--mode-bim);
  color: #fff;
  font-weight: var(--w-bold);
  letter-spacing: 0;
}
.bv-axis-btn:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 2px rgba(19, 138, 78, 0.5);
}

/* Measurement readout list */
.bv-measure-list {
  display: flex;
  flex-direction: column;
  /* Thin scrollable if many measurements pile up */
  max-height: 160px;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--line-strong) transparent;
}
.bv-measure-list::-webkit-scrollbar       { width: 3px; }
.bv-measure-list::-webkit-scrollbar-track { background: transparent; }
.bv-measure-list::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 2px; }

.bv-measure-item {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas: "len del" "deltas del";
  align-items: center;
  column-gap: var(--s-2);
  row-gap: 1px;
  padding: 6px 0;
  border-bottom: 1px solid var(--line-soft);
}
.bv-measure-item:last-child { border-bottom: none; }
.bv-measure-len {
  grid-area: len;
  font-family: var(--font-mono);
  /* Step up to t-md: the primary measurement readout deserves prominence */
  font-size: var(--t-md);
  font-weight: var(--w-semibold);
  color: var(--ink);
  letter-spacing: -0.02em;
  line-height: var(--lh-tight);
}
.bv-measure-deltas {
  grid-area: deltas;
  font-family: var(--font-mono);
  font-size: var(--t-xs);
  color: var(--muted);
  letter-spacing: -0.01em;
  line-height: var(--lh-snug);
}
.bv-measure-del {
  grid-area: del;
  border: none;
  background: transparent;
  color: var(--muted);   /* was --muted-soft; active delete button needs ≥4.5:1 */
  font-size: var(--t-sm);
  cursor: pointer;
  /* 28×28 hit target with a generous border area */
  padding: 4px 6px;
  min-width: 28px;
  min-height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-xs);
  line-height: 1;
  transition: background 0.08s ease, color 0.08s ease;
}
.bv-measure-del:hover {
  background: var(--danger-soft);
  color: var(--danger-ink);
}
.bv-measure-del:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--danger-soft);
}

/* 전체 삭제 — bordered ghost by default; on hover becomes a mild
   danger button to signal irreversibility. */
.bv-measure-clear {
  width: 100%;
  margin-top: var(--s-2);
  padding: 6px 0;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  background: var(--surface);
  font-family: var(--font-sans);
  font-size: var(--t-xs);
  font-weight: var(--w-semibold);
  color: var(--muted);
  cursor: pointer;
  letter-spacing: 0.01em;
  transition: background 0.10s ease, color 0.10s ease, border-color 0.10s ease;
}
.bv-measure-clear:hover:not(:disabled) {
  background: var(--danger-soft);
  border-color: rgba(195, 21, 21, 0.30);
  color: var(--danger-ink);
}
.bv-measure-clear:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--danger-soft);
}
.bv-measure-clear:disabled { opacity: 0.45; cursor: default; }

/* Measure panel: widen slightly so the 4-button axis row breathes */
.bv-toolpanel[data-bv="measurePanel"] {
  width: 212px;
}
/* When sboxPanel is open, the sibling offset must account for the new width */
.bv-toolpanel[data-bv="sboxPanel"]:not([hidden]) ~ .bv-toolpanel[data-bv="measurePanel"] {
  left: calc(var(--s-3) + 224px + 8px);
}

/* Measure panel body — constrain and scroll if list grows */
.bv-toolpanel[data-bv="measurePanel"] .bv-toolpanel-body {
  max-height: 280px;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--line-strong) transparent;
}
.bv-toolpanel[data-bv="measurePanel"] .bv-toolpanel-body::-webkit-scrollbar       { width: 3px; }
.bv-toolpanel[data-bv="measurePanel"] .bv-toolpanel-body::-webkit-scrollbar-track { background: transparent; }
.bv-toolpanel[data-bv="measurePanel"] .bv-toolpanel-body::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 2px; }

/* Floating panel title bar — slightly more opaque so the blurred
   geometry behind doesn't wash the title text. */
.bv-toolpanel-title {
  background: rgba(246, 245, 242, 0.96);
}

/* Screen-projected measurement labels — overlay layer covering the stage.
   Each .bv-mlabel is positioned via transform from a rAF projection loop. */
.bv-measure-labels {
  position: absolute;
  inset: 0;
  z-index: 6; /* above canvas/viewcube(4), below toolpanels(8) */
  pointer-events: none;
  overflow: hidden;
}
.bv-mlabel {
  position: absolute;
  top: 0;
  left: 0;
  padding: 3px 8px;
  background: rgba(17, 17, 17, 0.86);
  color: #fff;
  /* Thin white ring for legibility against pale/white geometry */
  border: 1px solid rgba(255, 255, 255, 0.20);
  border-radius: var(--r-sm);
  font-family: var(--font-mono);
  font-size: var(--t-xs);
  font-weight: var(--w-semibold);
  /* Offset shadow keeps the chip readable on both light and dark areas */
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.40);
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.50);
  white-space: nowrap;
  will-change: transform;
  /* Smooth entry when a new measurement completes */
  animation: bv-label-in 0.12s ease;
}
/* Opacity ONLY — the chip's position is an inline transform set every
   frame by the rAF projection loop; animating transform here would
   override it for the animation's duration and flash the chip at the
   stage origin. */
@keyframes bv-label-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Tool hint pill (단면/측정) — steps up slightly from the walk legend:
   larger font + brighter outline so an operator glancing at the canvas
   reads the hint without hunting for it. */
.bv-walkhint.bv-toolhint {
  /* Slightly larger so it reads without squinting */
  font-size: var(--t-sm);
  padding: 7px 18px;
  /* A touch more luminance in the background for the shorter,
     action-oriented copy (vs the dense walk key legend) */
  background: rgba(17, 17, 17, 0.84);
  border-color: rgba(255, 255, 255, 0.14);
  letter-spacing: 0.005em;
}

/* ── Toolbar cluster labels (탐색 | 검사) ───────────────────────── */
/* Each cluster: tiny uppercase label above its button row. Labels are
   aria-hidden — purely a visual grouping aid; button titles carry the
   accessible descriptions. */
.bv-tool-group {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 1px;
  padding: 2px 2px 1px;
}
.bv-tool-group-label {
  font-family: var(--font-sans);
  font-size: 9px;
  font-weight: var(--w-bold);
  letter-spacing: 0.1em;
  color: var(--muted);   /* was --muted-soft */
  line-height: 1;
  padding: 1px 0 0 4px;
  user-select: none;
  pointer-events: none;
}
.bv-tool-group-btns {
  display: flex;
  align-items: center;
  gap: 0;
}

/* ── Edit-history (되돌리기 · 다시 실행 · 초기화) ─────────────────── */
/* The buttons are plain .bv-tool — size/shape/hover/active/focus-visible and
   the .bv-tool + .bv-tool hairline separator are inherited, so the group reads
   identically to 탐색 / 검사. Only three deltas below: glyph alignment, the
   restrained 초기화 treatment, and the version meta-chip. */
.bv-version-group[hidden] { display: none; }

/* undo/redo arrow glyph: a fixed-width inline box so ↶/↷ size + baseline match
   the Korean label and sit a hair larger (text arrows read thin otherwise). */
.bv-version-glyph {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 12px;
  margin-right: 3px;
  font-size: 13px;
  line-height: 1;
}

/* Disabled undo/redo: match the established 숨김 해제 disabled treatment
   (opacity 0.38 + not-allowed) so all disabled toolbar buttons read alike. */
.bv-version-btn:disabled {
  opacity: 0.38;
  cursor: not-allowed;
}
.bv-version-btn:disabled:hover {
  background: transparent;
  color: var(--muted);
}

/* 초기화: a larger jump (back to 원본) but recoverable via 다시 실행 — so it
   borrows the same restrained warn tint as 숨김 해제, not the loud danger red.
   Slightly heavier than undo/redo, never shouting. */
.bv-version-btn--reset:not(:disabled) {
  color: var(--warn-ink);
  font-weight: var(--w-bold);
}
.bv-version-btn--reset:not(:disabled):hover {
  background: var(--warn-soft);
  color: var(--warn-ink);
}

/* Version meta-chip (vN/총M): quiet, mono, token-based — reads as state, not a
   button. Separated from 초기화 by the same hairline the tools use between
   each other, and inset slightly so it doesn't look pressable. */
.bv-version-indicator {
  display: inline-flex;
  align-items: center;
  margin-left: 4px;
  padding: 0 7px;
  min-height: 24px;
  border-radius: calc(var(--r-sm) - 2px);
  border: 1px solid var(--line);
  background: var(--surface-inset);
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: var(--w-semibold);
  color: var(--muted);
  letter-spacing: 0;
  white-space: nowrap;
  user-select: none;
  pointer-events: none;
}

/* ── Walk-hint key tokens ───────────────────────────────────────── */
.bv-walkhint kbd {
  display: inline-block;
  min-width: 16px;
  margin: 0 1px;
  padding: 1px 4px 2px;
  border: 1px solid rgba(255, 255, 255, 0.22);
  border-bottom-width: 2px;
  border-radius: 4px;
  background: rgba(255, 255, 255, 0.10);
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: var(--w-semibold);
  line-height: 1.2;
  text-align: center;
  color: rgba(255, 255, 255, 0.95);
}

/* Widen edit scrollbar from 3 → 4px for better discoverability */
.bv-edit::-webkit-scrollbar { width: 4px; }

/* ===== BIM 3D Viewer — 대지 컨텍스트 (Site Context) ================= */
/* Address-type segmented toggle (지번 / 도로명).
   Token-for-token identical to .bv-measure-axis / .bv-axis-btn so the two
   segmented controls in the panel are visually indistinguishable — only the
   font differs (sans here for Korean glyphs, mono there for X/Y/Z). */
.bv-site-segmented {
  display: flex;
  /* SAFE DEFAULT: content height + full width. Inside a COLUMN flex parent,
     flex:1 1 0 sizes on the VERTICAL axis and collapses the control to ~2px
     (this silently hid the 고도 표현 segmented once, then the GLB/OBJ/STL
     format picker — the overflow:hidden clips the buttons entirely). Only a
     row context should stretch it. */
  flex: 0 0 auto;
  width: 100%;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  overflow: hidden;
}
/* Inside a row (.bv-edit-row, next to a label) the segmented fills the
   remaining row WIDTH — the horizontal axis, where flex:1 1 0 is correct. */
.bv-edit-row .bv-site-segmented { flex: 1 1 0; width: auto; }
.bv-site-seg {
  flex: 1 1 0;
  padding: 5px 0;
  border: none;
  border-right: 1px solid var(--line);
  background: transparent;
  font-family: var(--font-sans);
  font-size: var(--t-xs);
  font-weight: var(--w-semibold);
  color: var(--muted);
  cursor: pointer;
  line-height: var(--lh-snug);
  transition: background 0.09s ease, color 0.09s ease;
}
.bv-site-seg:last-child { border-right: none; }
.bv-site-seg:hover:not(.is-active) { background: var(--surface-strong); color: var(--ink); }
.bv-site-seg.is-active {
  background: var(--mode-bim);
  color: var(--on-dark);
  font-weight: var(--w-bold);
}
.bv-site-seg:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 2px rgba(19, 138, 78, 0.5);
}

/* Status line under 대지 불러오기 — inherits .bv-edit-note look (carries its
   own min-height so the result block doesn't jump when a message appears).
   .is-error tints to danger for failed loads / validation. */
.bv-site-status { margin-top: var(--s-1); }
.bv-site-status.is-error { color: var(--danger-ink); }

/* Result block revealed after a successful load. */
.bv-site-result {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  margin-top: var(--s-2);
  padding-top: var(--s-3);
  border-top: 1px solid var(--line);
}

/* Layer visibility chips (정사영상 / 지적 경계 / 주변 건물).
   Pill toggles in the BIM emerald accent — same active fill as the
   segmented control so the panel reads as one accent family. */
.bv-site-toggles {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
}
.bv-site-chip {
  padding: 4px 10px;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-pill);
  background: transparent;
  font-family: var(--font-sans);
  font-size: var(--t-xs);
  font-weight: var(--w-semibold);
  color: var(--muted);
  cursor: pointer;
  line-height: var(--lh-snug);
  transition: background 0.09s ease, color 0.09s ease, border-color 0.09s ease;
}
.bv-site-chip:hover:not(.is-active) { background: var(--surface-strong); color: var(--ink); }
.bv-site-chip.is-active {
  background: var(--mode-bim);
  border-color: var(--mode-bim);
  color: var(--on-dark);
}
.bv-site-chip:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px rgba(19, 138, 78, 0.25);
}

/* Manual-alignment sub-title — same micro-label recipe as .bv-side-title
   (uppercase / bold / tracked / muted) so it groups the alignment rows the
   way every other section label in the panel does. */
.bv-site-align-title {
  font-family: var(--font-sans);
  font-size: var(--t-xs);
  font-weight: var(--w-bold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
  line-height: var(--lh-snug);
  margin-top: var(--s-1);
}

/* 보정 대상 checkbox row — picks which VWorld layer the ΔX/ΔZ/회전 inputs drive */
.bv-site-offtargets {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--s-2);
  margin-top: var(--s-1);
}
.bv-site-offtargets-label {
  font-size: var(--t-xs);
  color: var(--muted);
}
.bv-site-check {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  font-size: var(--t-sm);
  color: var(--ink);
  cursor: pointer;
}
.bv-site-check input { accent-color: var(--mode-bim); cursor: pointer; }
.bv-site-range { flex: 1; min-width: 0; accent-color: var(--mode-bim); cursor: pointer; }
/* 일조 시뮬레이션 (sun study) — month/hour sliders + solar read-out. Indented
   under the 그림자 toggle it belongs to, shown only while 그림자 is on. */
.bv-site-sun {
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
  margin: var(--s-1) 0 var(--s-1) var(--s-3);
  padding-left: var(--s-2);
  border-left: 2px solid var(--line-2, #e5e3de);
}
.bv-site-sun .bv-edit-row { align-items: center; }
.bv-site-sun-num {
  font-variant-numeric: tabular-nums;
  color: var(--mode-bim);
  font-weight: 600;
}
.bv-site-sun-readout {
  font-size: var(--t-xs, 11px);
  font-variant-numeric: tabular-nums;
  color: var(--ink-3, #8a8f98);
}
/* 방위표 + 축척바 — map-style viewport overlay, bottom-left of the stage.
   Decorative (pointer-events none), muted, never competes with the model. */
.bv-maptools {
  position: absolute;
  left: var(--s-4);
  bottom: var(--s-4);
  z-index: 5;
  display: flex;
  align-items: flex-end;
  gap: var(--s-3);
  pointer-events: none;
  user-select: none;
}
.bv-north {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: rgba(17, 17, 17, 0.62);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
}
.bv-scalebar {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding-bottom: 2px;
}
.bv-scalebar-line {
  height: 0;
  width: 100px;
  border-bottom: 2px solid rgba(255, 255, 255, 0.92);
  border-left: 2px solid rgba(255, 255, 255, 0.92);
  border-right: 2px solid rgba(255, 255, 255, 0.92);
  min-height: 6px;
  box-sizing: border-box;
  filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.5));
  transition: width 0.15s ease-out;
}
.bv-scalebar-label {
  font-size: var(--t-xs, 11px);
  font-variant-numeric: tabular-nums;
  color: #fff;
  /* dark pill like .bv-north — white text alone washes out over bright
     orthophoto ground */
  align-self: flex-start;
  padding: 1px 6px;
  border-radius: var(--r-pill, 999px);
  background: rgba(17, 17, 17, 0.62);
}

/* 일조 분석 (동지 sun-hours) — run button + banded legend */
.bv-site-sunhours { display: flex; flex-direction: column; gap: var(--s-1); }
.bv-site-sunhours .bv-edit-apply { width: 100%; }
.bv-sunhours-legend {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--s-2);
  font-size: var(--t-xs, 11px);
  color: var(--ink);
}
.bv-sh-item { display: inline-flex; align-items: center; gap: 4px; }
.bv-sh-swatch {
  width: 10px; height: 10px;
  border-radius: 2px;
  display: inline-block;
}
.bv-sh-red { background: #d63c2f; }
.bv-sh-amber { background: #e8b31a; }
.bv-sh-green { background: #2f9e44; }
.bv-sh-clear {
  margin-left: auto;
  border: 1px solid var(--line-2, #e5e3de);
  background: transparent;
  color: var(--ink-2, #55595f);
  font-size: var(--t-xs, 11px);
  padding: 2px 8px;
  border-radius: var(--r-sm, 4px);
  cursor: pointer;
}
.bv-sh-clear:hover { background: var(--surface-2, #f2f1ee); }
.bv-sunhours-note {
  font-size: var(--t-xs, 11px);
  line-height: 1.35;
  color: var(--ink-3, #8a8f98);
}

/* 3D 내보내기 (site context → GLB/OBJ/STL download) */
.bv-site-export { display: flex; flex-direction: column; gap: var(--s-1); }
.bv-site-export-btn { width: 100%; }
.bv-site-export-note {
  font-size: var(--t-xs, 11px);
  line-height: 1.35;
  color: var(--ink-3, #8a8f98);
}
/* DEM source attribution credit (licence requirement) — tiny, muted, unobtrusive. */
.bv-site-credit {
  font-size: var(--t-xs, 11px);
  line-height: 1.35;
  color: var(--ink-3, #8a8f98);
  opacity: 0.85;
}

/* ── VWorld 3D 뷰 overlay ─────────────────────────────────────────────────────
   Same-origin iframe hosting the VWorld WebGL 3D map, layered over the
   viewer (above the side panel, below nothing — it owns the screen while
   open). The iframe page carries its own dark UI; this shell only frames
   and closes it. */
.bv-vworld-overlay {
  position: absolute;
  inset: 20px;
  z-index: 40;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  overflow: hidden;
  box-shadow:
    0 1px 2px rgba(17, 17, 17, 0.06),
    0 12px 32px rgba(17, 17, 17, 0.22);
}
.bv-vworld-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 14px;
  color: var(--ink);
  font-weight: 600;
  border-bottom: 1px solid var(--line);
  flex: 0 0 auto;
}
.bv-vworld-close {
  background: none;
  border: 0;
  color: var(--ink-3, #8a8f98);
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
  padding: 0 2px;
}
.bv-vworld-close:hover { color: var(--ink); }
.bv-vworld-frame {
  flex: 1 1 auto;
  width: 100%;
  border: 0;
  min-height: 0;
  background: #141412; /* engine canvas is dark — avoid a white flash; warm ink, not blue-gray */
}

/* ── 지표 방식 method-select popup ───────────────────────────────────
   A small dialog layered OVER the viewer modal (z above .bv-modal's
   content). Reuses the viewer's surface/line tokens + the .bv-edit-apply
   button rhythm so it reads as part of the viewer, not the chat app. */
.bv-dialog-backdrop {
  position: absolute;
  inset: 0;
  z-index: 30;
  background: rgba(17, 17, 17, 0.46);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--s-5);
  backdrop-filter: blur(2px);
}
.bv-dialog-backdrop[hidden] { display: none; }
.bv-dialog {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  box-shadow:
    0 1px 2px rgba(17, 17, 17, 0.06),
    0 12px 32px rgba(17, 17, 17, 0.22);
  width: min(420px, 94vw);
  max-height: 88vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.bv-dialog-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px var(--s-4);
  border-bottom: 1px solid var(--line);
}
.bv-dialog-title {
  font-family: var(--font-sans);
  font-size: var(--t-md);
  font-weight: var(--w-bold);
  color: var(--ink);
}
.bv-dialog-close {
  border: none;
  background: transparent;
  font-size: 18px;
  line-height: 1;
  color: var(--muted);
  cursor: pointer;
  padding: 2px 6px;
}
.bv-dialog-close:hover { color: var(--ink); }
.bv-dialog-body {
  padding: var(--s-4);
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  overflow-y: auto;
}
.bv-dialog-foot {
  display: flex;
  gap: var(--s-2);
  padding: 12px var(--s-4);
  border-top: 1px solid var(--line);
}
.bv-dialog-foot .bv-edit-apply { flex: 1 1 0; margin-top: 0; }

/* method option cards (radio-group) */
.bv-method-list {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
}
.bv-method-opt {
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-rows: auto auto;
  column-gap: var(--s-2);
  align-items: center;
  padding: 10px 12px;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  cursor: pointer;
  transition: border-color 0.09s ease, background 0.09s ease;
}
.bv-method-opt:hover:not(.is-active) { background: var(--surface-strong); }
.bv-method-opt.is-active {
  border-color: var(--mode-bim);
  background: rgba(19, 138, 78, 0.06);
}
.bv-method-opt input[type=radio] {
  grid-row: 1 / 3;
  accent-color: var(--mode-bim);
  cursor: pointer;
}
.bv-method-main {
  font-family: var(--font-sans);
  font-size: var(--t-sm);
  font-weight: var(--w-semibold);
  color: var(--ink);
}
.bv-method-sub {
  font-size: var(--t-xs);
  color: var(--muted);
  line-height: var(--lh-snug);
}

/* NGII DEM upload box (revealed under option ③) */
.bv-method-ngii {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  padding-top: var(--s-3);
  border-top: 1px solid var(--line);
}
.bv-method-ngii[hidden] { display: none; }
.bv-method-uprow {
  display: flex;
  align-items: center;
  gap: var(--s-2);
}
.bv-method-uprow input[type=file] {
  flex: 1 1 0;
  min-width: 0;
  font-size: var(--t-xs);
}
.bv-method-upbtn { flex: 0 0 auto; margin-top: 0; width: auto; padding: 6px 14px; }
.bv-method-upstatus { margin-top: 0; }
.bv-method-upstatus.is-error { color: var(--danger-ink); }
.bv-method-hint {
  font-size: var(--t-xs);
  color: var(--muted);
  line-height: 1.4;
  margin: 0;
}
.bv-method-confirm:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* EPSG control group inside the NGII upload option — reads as a labeled,
   actionable field (not a bare dropdown). The manual select is the active
   path for .asc/.xyz; the auto-detect note is a calm inset confirmation
   shown instead for .zip (수치지형도 SHP carries its own .prj CRS). */
.bv-epsg-group {
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
  padding: var(--s-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm, 6px);
  background: var(--surface-soft);
}
.bv-epsg-group[hidden] { display: none; }
.bv-epsg-label {
  font-size: var(--t-xs);
  font-weight: 600;
  color: var(--ink-2);
  line-height: 1.3;
}
.bv-epsg-select {
  width: 100%;
  font-size: var(--t-xs);
  padding: 6px 8px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm, 6px);
  background: var(--surface);
  color: var(--ink-1);
}
.bv-epsg-select:disabled { opacity: 0.5; cursor: not-allowed; }
.bv-epsg-select:focus-visible {
  outline: none;
  border-color: var(--accent, var(--ink-2));
}
.bv-epsg-auto {
  font-size: var(--t-xs);
  color: var(--ink-2);
  line-height: 1.4;
  padding: var(--s-2);
  border-radius: var(--radius-sm, 6px);
  background: var(--surface-inset, var(--surface-strong));
  border: 1px solid var(--line);
}
.bv-epsg-auto[hidden] { display: none; }

/* 정합 초기화 — quiet neutral secondary against the emerald 정합 적용 above
   it. Inherits .bv-edit-apply sizing/rhythm; overrides the green palette to a
   plain outlined button (incl. :active so the inherited green press never
   leaks through). */
.bv-site-reset {
  border-color: var(--line-strong);
  background: var(--surface);
  color: var(--ink-2);
}
.bv-site-reset:hover:not(:disabled) {
  background: var(--surface-strong);
  border-color: var(--line-strong);
  color: var(--ink);
}
.bv-site-reset:active:not(:disabled) {
  background: var(--surface-inset);
  border-color: var(--line-strong);
  color: var(--ink);
}
.bv-site-reset:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring-ink);
}

/* 기즈모 (TransformControls) mode row — 이동/회전/크기/끄기 segmented chips,
   reusing the .bv-site-chip palette so it reads as one with the layer toggles. */
.bv-site-gizmo {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--s-2);
  margin-top: var(--s-1);
}
.bv-site-gizmo-label {
  font-size: var(--t-xs);
  color: var(--muted);
}
.bv-site-gizmo-btns {
  display: inline-flex;
  flex-wrap: wrap;
  gap: var(--s-1);
}
.bv-site-gizmo-btn {
  padding: 4px 10px;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-pill);
  background: transparent;
  font-family: var(--font-sans);
  font-size: var(--t-xs);
  font-weight: var(--w-semibold);
  color: var(--muted);
  cursor: pointer;
  line-height: var(--lh-snug);
  transition: background 0.09s ease, color 0.09s ease, border-color 0.09s ease;
}
.bv-site-gizmo-btn:hover:not(.is-active) { background: var(--surface-strong); color: var(--ink); }
.bv-site-gizmo-btn.is-active {
  background: var(--mode-bim);
  border-color: var(--mode-bim);
  color: var(--on-dark);
}
.bv-site-gizmo-btn:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px rgba(19, 138, 78, 0.25);
}

/* 스냅 정합 — inherits .bv-edit-apply sizing; quiet neutral until armed, then
   the active (picking) state reads in the BIM mode colour. */
.bv-site-snap {
  border-color: var(--line-strong);
  background: var(--surface);
  color: var(--ink-2);
}
.bv-site-snap:hover:not(:disabled) {
  background: var(--surface-strong);
  border-color: var(--line-strong);
  color: var(--ink);
}
.bv-site-snap.is-active {
  background: var(--mode-bim);
  border-color: var(--mode-bim);
  color: var(--on-dark);
}
.bv-site-snap:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px rgba(19, 138, 78, 0.25);
}
.bv-site-snaphint { color: var(--mode-bim); }

/* =================================================================
   Reduced-motion accessibility.

   Users who set the OS "reduce motion" preference (vestibular
   sensitivity) get all transitions and the looping indicators
   (auth spinner, mode-switch spinner, busy/generating chip pulse)
   neutralised. Functionality is unchanged — only the animation is
   removed; state still updates instantly. Kept last so it overrides
   the per-component animation declarations above.
   ================================================================= */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}

/* ===== External-model (OpenAI) settings — popup modal + provider chips =====
   The provider config now lives in a popup (mirrors .admin-modal) opened from
   the 시스템 상태 "모델 제공자" row — NOT inside the Design panel. The reused
   field/toggle/action/warn/status rules below are shared by the modal body. */
.openai-modal {
  position: fixed;
  inset: 0;
  z-index: 999;
  display: flex;
  align-items: center;
  justify-content: center;
}
.openai-modal[hidden] { display: none; }
.openai-modal-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(17, 17, 17, 0.32);
  backdrop-filter: blur(3px);
  cursor: pointer;
}
.openai-modal-card {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: var(--s-5) var(--s-6);
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  max-width: 460px;
  width: 92%;
  max-height: 86vh;
  overflow-y: auto;
  box-shadow: var(--shadow-pop);
  z-index: 1;
}
.openai-modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-2);
}
.openai-modal-card h2 {
  margin: 0;
  font-size: var(--t-lg);
  color: var(--ink);
  font-weight: var(--w-bold);
}
.openai-modal-close { font-size: 22px; line-height: 1; }
.openai-modal-close:focus-visible { outline: none; box-shadow: var(--focus-ring-ink); border-radius: var(--r-xs); }
.openai-modal-body {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
}
.openai-modal-statusrow { display: flex; }

/* Single summary chip + ⚙ entry in the 시스템 상태 card.
   States: local (default) · gpt (both paths OpenAI) · mixed (one path each). */
.provider-row { align-items: center; }
.provider-chips { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
.provider-chip {
  display: inline-flex;
  align-items: center;
  height: 22px;
  padding: 0 9px;
  border-radius: var(--r-pill);
  font-size: 11px;
  font-weight: var(--w-bold);
  border: 1px solid var(--line);
  background: var(--surface-soft);
  color: var(--ink-2);
  white-space: nowrap;
  transition: opacity 0.15s;
}
.provider-chip:hover { opacity: 0.75; }
.provider-chip:focus-visible { outline: none; box-shadow: var(--focus-ring-ink); }
.provider-chip.gpt {
  color: var(--success-ink);
  border-color: var(--success-ink);
  background: var(--surface-soft);
}
.provider-chip.mixed {
  color: var(--warn-ink);
  border-color: var(--warn-ink);
  background: var(--surface-soft);
}
.provider-settings-btn { font-size: 14px; line-height: 1; }
.openai-provider-status { font-size: var(--t-xs); color: var(--muted); white-space: nowrap; }
.openai-provider-status[data-state="on"] { color: var(--success-ink); font-weight: var(--w-medium); }
.openai-provider-warn {
  margin: 0;
  font-size: var(--t-xs);
  line-height: var(--lh-snug);
  color: var(--warn-ink);
  background: var(--warn-soft);
  border-radius: var(--r-xs);
  padding: 6px 8px;
}
.openai-field { display: flex; flex-direction: column; gap: 3px; font-size: var(--t-xs); color: var(--muted); }
.openai-field input,
.openai-field select {
  font-size: var(--t-sm);
  padding: 6px 8px;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-xs);
  background: var(--surface);
  color: var(--ink);
}
.openai-field input:focus-visible,
.openai-field select:focus-visible {
  outline: none;
  border-color: var(--ink);
  box-shadow: var(--focus-ring-ink);
}
.openai-field-row { display: flex; gap: var(--s-2); }
.openai-field-row .openai-field { flex: 1 1 0; min-width: 0; }
.openai-toggles {
  display: flex;
  flex-direction: column;
  gap: 4px;
  border: 1px solid var(--line);
  border-radius: var(--r-xs);
  padding: 8px 10px 8px;
  margin: 0;
}
.openai-toggles-legend {
  font-size: var(--t-xs);
  font-weight: var(--w-medium);
  color: var(--ink-2);
  padding: 0 4px;
}
.openai-check { display: flex; align-items: center; gap: 6px; font-size: var(--t-xs); color: var(--ink-2); cursor: pointer; }
.openai-actions { display: flex; gap: var(--s-2); }
.openai-actions .primary-btn,
.openai-actions .secondary-btn { flex: 0 0 auto; height: 30px; padding: 0 var(--s-3); font-size: var(--t-sm); }
.openai-note { font-size: var(--t-xs); color: var(--muted); min-height: 1em; }

/* ── Toast notices (setStatus) — transient, top-right, non-blocking. ──────
   Success/progress feedback for actions whose only signal used to route into
   the retired status pill (session delete, cancel accepted, background run
   started, …). Errors stay in the persistent #error-banner. */
.toast-host {
  position: fixed;
  top: 16px;
  right: 16px;
  /* Above the Region Edit (9000) and BIM viewer (9100) overlays so a toast
     raised while either is open stays visible instead of rendering behind it. */
  z-index: 9500;
  display: flex;
  flex-direction: column;
  gap: 8px;
  max-width: min(360px, calc(100vw - 32px));
  pointer-events: none;
}
.toast {
  pointer-events: auto;
  padding: 10px 14px;
  border-radius: var(--r-md, 8px);
  font-size: var(--t-sm, 13px);
  line-height: 1.45;
  color: var(--ink, #111);
  background: var(--surface, #fff);
  border: 1px solid var(--line, #e5e4df);
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.14);
  cursor: pointer;
  opacity: 0;
  transform: translateY(-8px);
  transition: opacity 0.2s ease, transform 0.2s ease;
}
.toast.is-in { opacity: 1; transform: translateY(0); }
.toast.is-out { opacity: 0; transform: translateY(-8px); }
.toast-warn { background: var(--warn-soft, #fbebd4); border-color: rgba(180, 83, 9, 0.28); color: var(--warn-ink, #8a4106); }
.toast-error { background: var(--danger-soft, #fbe0e1); border-color: rgba(195, 21, 21, 0.30); color: var(--danger-ink, #9b1010); }
@media (prefers-reduced-motion: reduce) {
  .toast { transition: none; }
}

/* Admin password-reset sub-panel (in-modal, masked input — replaces the
   old window.prompt cleartext flow). */
.admin-pw-panel {
  margin-top: var(--s-3, 12px);
  padding: 12px 14px;
  border: 1px solid var(--line, #e5e4df);
  border-left: 3px solid var(--danger, #c31515);
  border-radius: var(--r-md, 8px);
  background: var(--surface-inset, #faf9f7);
}
.admin-pw-title { font-size: var(--t-sm, 13px); font-weight: var(--w-bold, 600); color: var(--ink, #111); margin-bottom: 8px; }
.admin-pw-form { display: flex; flex-direction: column; gap: 6px; }
.admin-pw-form label { font-size: var(--t-xs, 11px); color: var(--muted, #6b6b6b); }
.admin-pw-form input { padding: 6px 8px; border: 1px solid var(--line, #e5e4df); border-radius: 6px; font-size: var(--t-sm, 13px); }
.admin-pw-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 4px; }
