/* BTI360 Web UI — Single CSS file */

/* ===== Custom Properties ===== */
:root {
  --color-bg: #f8fafc;
  --color-surface: #ffffff;
  --color-surface-alt: #f1f5f9;
  --color-border: #e2e8f0;
  --color-text: #1e293b;
  --color-text-muted: #64748b;
  --color-primary: #2563eb;
  --color-primary-hover: #1d4ed8;
  --color-primary-light: #dbeafe;
  --color-danger: #dc2626;
  --color-danger-hover: #b91c1c;
  --color-success: #16a34a;
  --color-success-light: #dcfce7;
  --color-warning: #d97706;
  --color-warning-light: #fef3c7;
  --color-modified: #eab308;
  --radius: 8px;
  --radius-sm: 4px;
  --shadow: 0 1px 3px rgba(0,0,0,0.1);
  --shadow-md: 0 4px 6px rgba(0,0,0,0.07);
  --font-sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-mono: ui-monospace, "Cascadia Code", "Fira Code", monospace;
  --max-width: 1200px;
  --nav-height: 56px;
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-bg: #0f172a;
    --color-surface: #1e293b;
    --color-surface-alt: #334155;
    --color-border: #475569;
    --color-text: #f1f5f9;
    --color-text-muted: #94a3b8;
    --color-primary: #3b82f6;
    --color-primary-hover: #60a5fa;
    --color-primary-light: #1e3a5f;
    --color-success-light: #14532d;
    --color-warning-light: #451a03;
    --color-modified: #facc15;
    --shadow: 0 1px 3px rgba(0,0,0,0.3);
    --shadow-md: 0 4px 6px rgba(0,0,0,0.2);
  }
}

/* ===== Reset ===== */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { font-family: var(--font-sans); line-height: 1.6; color: var(--color-text); background: var(--color-bg); -webkit-text-size-adjust: 100%; }
/* Anchor jumps (e.g. the asset-card ↗ link → /web/assets#asset-card-<id>) must clear
   the fixed .nav, or the target lands half-hidden under it. Match .page's top offset. */
html { scroll-padding-top: calc(var(--nav-height) + 1.5rem); }
body { min-height: 100vh; }
a { color: var(--color-primary); text-decoration: none; }
a:hover { text-decoration: underline; }
img, video { max-width: 100%; height: auto; display: block; }
/* Audio is excluded from `height: auto`: a native <audio controls> has no CSS
   intrinsic height (its ~54px is a UA default), so `height: auto` resolves to 0
   and collapses the player to its padding. Keep block + max-width only. */
audio { max-width: 100%; display: block; }
code { font-family: var(--font-mono); font-size: 0.875em; background: var(--color-surface-alt); padding: 0.125em 0.375em; border-radius: var(--radius-sm); }
table { border-collapse: collapse; width: 100%; }
th, td { padding: 0.5rem 0.75rem; text-align: left; border-bottom: 1px solid var(--color-border); }
th { font-weight: 600; color: var(--color-text-muted); font-size: 0.8125rem; text-transform: uppercase; letter-spacing: 0.05em; }

/* ===== Utilities ===== */
/* Visually hidden but present for assistive tech (e.g. aria-live regions). */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* ===== Headings ===== */
/* Shared type scale for the admin app, matching the de-facto sizes previously
   set inline (page-header h1 1.5rem, dashboard h2 1.125rem, assets h3 1rem).
   Margins stay zeroed by the reset; callers add spacing where a heading needs
   it. .page-header h1 / .login-card h1 override the size where required. */
h1 { font-size: 1.5rem; font-weight: 700; line-height: 1.3; }
h2 { font-size: 1.125rem; font-weight: 700; line-height: 1.3; }
h3 { font-size: 1rem; font-weight: 600; line-height: 1.3; }

/* ===== Layout ===== */
.container { max-width: var(--max-width); margin: 0 auto; padding: 0 1rem; }
.page { padding-top: calc(var(--nav-height) + 1.5rem); padding-bottom: 2rem; }

/* ===== Nav ===== */
.nav { position: fixed; top: 0; left: 0; right: 0; height: var(--nav-height); background: var(--color-surface); border-bottom: 1px solid var(--color-border); z-index: 100; box-shadow: var(--shadow); }
.nav__inner { max-width: var(--max-width); margin: 0 auto; padding: 0 1rem; display: flex; align-items: center; height: 100%; gap: 1rem; }
.nav__brand-group { display: flex; align-items: baseline; gap: 0.5rem; }
.nav__brand { font-weight: 700; font-size: 1.125rem; color: var(--color-text); }
.nav__brand:hover { text-decoration: none; }
.nav__links { display: flex; gap: 0.25rem; flex: 1; }
.nav__link { padding: 0.375rem 0.75rem; border-radius: var(--radius-sm); color: var(--color-text-muted); font-size: 0.875rem; font-weight: 500; white-space: nowrap; transition: background 0.15s, color 0.15s; }
.nav__link:hover { background: var(--color-surface-alt); color: var(--color-text); text-decoration: none; }
.nav__link--active { background: var(--color-primary-light); color: var(--color-primary); }
.nav__right { display: flex; align-items: center; gap: 0.75rem; }
.nav__user { font-size: 0.8125rem; color: var(--color-text-muted); white-space: nowrap; }
.nav__version { font-size: 0.5625rem; color: var(--color-text-muted); font-weight: 400; font-family: var(--font-mono); white-space: nowrap; }
.nav__pr { color: var(--color-primary); font-family: var(--font-mono); white-space: nowrap; }
.nav__pr:hover { text-decoration: underline; }
.nav__menu-wrap { position: relative; }
.nav__menu-trigger { display: inline-flex; align-items: center; gap: 0.375rem; cursor: pointer; list-style: none; user-select: none; }
.nav__menu-trigger::-webkit-details-marker { display: none; }
.nav__menu-trigger::after { content: ""; width: 0; height: 0; border-left: 0.25rem solid transparent; border-right: 0.25rem solid transparent; border-top: 0.3rem solid currentColor; transition: transform 0.15s; }
.nav__menu-wrap[open] .nav__menu-trigger::after { transform: rotate(180deg); }
.nav__menu { position: absolute; top: calc(100% + 0.25rem); left: 0; min-width: 10rem; display: flex; flex-direction: column; gap: 0.125rem; padding: 0.25rem; background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-sm); box-shadow: var(--shadow-md); z-index: 110; }
.nav__menu-item { padding: 0.375rem 0.75rem; border-radius: var(--radius-sm); color: var(--color-text-muted); font-size: 0.875rem; font-weight: 500; white-space: nowrap; transition: background 0.15s, color 0.15s; }
.nav__menu-item:hover { background: var(--color-surface-alt); color: var(--color-text); text-decoration: none; }
.nav__menu-item--active { background: var(--color-primary-light); color: var(--color-primary); }

/* ===== Buttons ===== */
.btn { display: inline-flex; align-items: center; gap: 0.375rem; padding: 0.5rem 1rem; border-radius: var(--radius); font-size: 0.875rem; font-weight: 500; border: 1px solid transparent; cursor: pointer; transition: background 0.15s, border-color 0.15s; text-decoration: none; line-height: 1.4; font-family: inherit; }
.btn:hover { text-decoration: none; }
.btn--primary { background: var(--color-primary); color: white; }
.btn--primary:hover { background: var(--color-primary-hover); }
.btn:disabled, [class*="btn--"]:disabled { opacity: 0.45; cursor: not-allowed; }
.btn--outline { background: transparent; color: var(--color-text); border-color: var(--color-border); }
.btn--outline:hover { background: var(--color-surface-alt); }
.btn--danger { background: var(--color-danger); color: white; }
.btn--danger:hover { background: var(--color-danger-hover); }
.btn--sm { padding: 0.25rem 0.625rem; font-size: 0.8125rem; }
.btn--ghost { background: transparent; color: var(--color-text-muted); }
.btn--ghost:hover { background: var(--color-surface-alt); color: var(--color-text); }

/* ===== Cards ===== */
.card { background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius); padding: 1.25rem; box-shadow: var(--shadow); }
.card--clickable { transition: box-shadow 0.15s, border-color 0.15s; }
.card--clickable:hover { box-shadow: var(--shadow-md); border-color: var(--color-primary); }

/* ===== Forms ===== */
.form-group { margin-bottom: 1rem; }
.form-group label { display: block; font-size: 0.8125rem; font-weight: 600; margin-bottom: 0.25rem; color: var(--color-text); }
.form-input { width: 100%; padding: 0.5rem 0.75rem; border: 1px solid var(--color-border); border-radius: var(--radius); font-size: 0.875rem; font-family: inherit; background: var(--color-surface); color: var(--color-text); transition: border-color 0.15s; }
.form-input:focus { outline: none; border-color: var(--color-primary); box-shadow: 0 0 0 3px var(--color-primary-light); }
.form-input--inline { border-color: transparent; background: transparent; padding: 0.125rem 0.375rem; font-size: 0.875rem; }
.form-input--inline:hover { border-color: var(--color-border); background: var(--color-surface); }
.form-input--inline:focus { border-color: var(--color-primary); background: var(--color-surface); }
.form-input--inline[name="display_name"] { font-weight: 600; }
.form-input--description { resize: vertical; min-height: 1.5rem; font-size: 0.8125rem; color: var(--color-text-muted); }
.form-input--compact { font-size: 0.75rem; padding: 0.125rem 0.375rem; margin-bottom: 0.25rem; }
.form-input--properties { font-family: var(--font-mono, ui-monospace, monospace); resize: vertical; min-height: 1.25rem; }
.form-select { appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M6 8L1 3h10z' fill='%2364748b'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 0.75rem center; padding-right: 2rem; }

/* ===== Flash messages ===== */
.flash { padding: 0.75rem 1rem; border-radius: var(--radius); margin-bottom: 1rem; font-size: 0.875rem; border: 1px solid transparent; }
.flash--success { background: var(--color-success-light); color: var(--color-success); border-color: var(--color-success); }
.flash--error { background: #fef2f2; color: var(--color-danger); border-color: var(--color-danger); }
.flash--warning { background: var(--color-warning-light); color: var(--color-warning); border-color: var(--color-warning); }
@media (prefers-color-scheme: dark) {
  .flash--error { background: #450a0a; }
}

/* ===== Login page ===== */
.login-page { display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 1rem; }
.login-card { width: 100%; max-width: 400px; }
.login-card h1 { font-size: 1.5rem; margin-bottom: 0.25rem; }
.login-card .subtitle { color: var(--color-text-muted); font-size: 0.875rem; margin-bottom: 1.5rem; }

/* ===== Page header ===== */
.page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.5rem; flex-wrap: wrap; gap: 0.75rem; }
.page-header h1 { font-size: 1.5rem; font-weight: 700; }
.page-header p { color: var(--color-text-muted); font-size: 0.875rem; }
.breadcrumb { display: flex; gap: 0.375rem; font-size: 0.8125rem; color: var(--color-text-muted); margin-bottom: 1rem; }
.page-header .breadcrumb { margin-bottom: 0; }
.breadcrumb a { color: var(--color-text-muted); }
.breadcrumb a:hover { color: var(--color-primary); }

/* ===== Tabs ===== */
.tabs { display: flex; border-bottom: 2px solid var(--color-border); margin-bottom: 1.5rem; gap: 0; }
.tabs__tab { padding: 0.625rem 1.25rem; font-size: 0.875rem; font-weight: 500; color: var(--color-text-muted); border-bottom: 2px solid transparent; margin-bottom: -2px; transition: color 0.15s, border-color 0.15s; }
.tabs__tab:hover { color: var(--color-text); text-decoration: none; }
.tabs__tab--active { color: var(--color-primary); border-bottom-color: var(--color-primary); }

/* ===== Stats grid ===== */
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 1.5rem; }
.stat-card { text-align: center; }
.stat-card .stat-value { font-size: 2rem; font-weight: 700; color: var(--color-primary); }
.stat-card .stat-label { font-size: 0.8125rem; color: var(--color-text-muted); }

/* ===== Experience grid ===== */
.exp-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1rem; }
.exp-card { display: flex; flex-direction: column; }
.exp-card__thumb { width: 100%; aspect-ratio: 16/9; object-fit: contain; border-radius: var(--radius-sm); background: var(--color-surface-alt); margin-bottom: 0.75rem; }

/* ===== Inline asset preview (next to image/video selects) ===== */
.asset-preview { display: inline-block; position: relative; width: 48px; height: 48px; flex-shrink: 0; border-radius: var(--radius-sm); background: var(--color-surface-alt); overflow: hidden; vertical-align: middle; }
.asset-preview--empty { background-image: linear-gradient(45deg, var(--color-surface-alt) 25%, transparent 25%, transparent 75%, var(--color-surface-alt) 75%), linear-gradient(45deg, var(--color-surface-alt) 25%, transparent 25%, transparent 75%, var(--color-surface-alt) 75%); background-size: 12px 12px; background-position: 0 0, 6px 6px; background-color: var(--color-surface); }
.asset-preview img, .asset-preview video { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Type-icon fallback for assigned non-visual assets (audio, model) — fills the tile so it reads as "filled", not empty (BTI-601). */
.asset-preview__icon { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; font-size: 1.5rem; line-height: 1; }
/* Hologram framing (BTI-1188): hologram videos are 1:2 with the subject in the top
   half, so a centered `object-fit: cover` crops to empty space. Anchor to the top
   instead. Driven per-tile by the DISPLAYED asset's video_type — not by which
   fieldset or picker the tile happens to sit in, which mis-cropped a plain image
   inside a hologram node and left a hologram video uncropped everywhere else. */
.asset-preview--hologram-crop img,
.asset-preview--hologram-crop video { object-position: top center; }
.asset-preview--wide { width: 96px; height: 54px; }
.asset-preview--lg { width: 64px; height: 64px; }
/* Clickable preview tiles open the visual asset picker (BTI-701). */
.asset-preview--clickable { cursor: pointer; position: relative; transition: box-shadow 0.15s; }
.asset-preview--clickable:hover { box-shadow: 0 0 0 2px var(--color-primary); }
.asset-preview--clickable:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--color-primary-light); }
/* A faint "+" on empty clickable tiles signals they are pickable. */
.asset-preview--clickable.asset-preview--empty::after { content: "+"; position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: 1.25rem; line-height: 1; color: var(--color-text-muted); pointer-events: none; }

/* Video-type overlay badge (BTI-944): 360 / hologram corner marker on immersive &
   hologram thumbnails so co-mingled gallery items are distinguishable at a glance.
   Colors mirror the node-type badges (immersive orange, hologram green). Anchored
   top-left; pointer-events:none so it never blocks the click-to-pick tile. */
.video-type-badge { position: absolute; top: 2px; left: 2px; z-index: 1; font-size: 0.5rem; font-weight: 700; line-height: 1.4; padding: 0 3px; border-radius: 999px; letter-spacing: 0.03em; color: #fff; pointer-events: none; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35); }
.video-type-badge--immersive { background: #f97316; }
.video-type-badge--hologram { background: #22c55e; }

/* ===== Asset picker popover (BTI-701) ===== */
/* Flex column with a pinned header: only the grid scrolls, so the header's
   height doesn't get counted into the scroll budget (BTI-701). */
.asset-picker { position: fixed; z-index: 200; width: 380px; max-height: 400px; display: flex; flex-direction: column; background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius); box-shadow: var(--shadow-md); padding: 0.5rem; }
.asset-picker__header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.5rem; flex-shrink: 0; }
.asset-picker__title { font-size: 0.8125rem; font-weight: 600; color: var(--color-text); }
.asset-picker__close { background: none; border: none; font-size: 1.25rem; line-height: 1; color: var(--color-text-muted); cursor: pointer; padding: 0 0.25rem; }
.asset-picker__close:hover { color: var(--color-text); }
.asset-picker__grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); gap: 0.5rem; overflow-y: auto; min-height: 0; }
.asset-picker__cell { display: flex; flex-direction: column; align-items: center; gap: 0.25rem; padding: 0.25rem; background: none; border: 1px solid transparent; border-radius: var(--radius-sm); cursor: pointer; font-family: inherit; }
/* Larger picker thumbnails: many assets crop to near-identical tiles, so the
   tile must be big enough to tell apart and the caption wide enough to show a
   typical filename in full (BTI-701). */
.asset-picker__cell .asset-preview { width: 96px; height: 96px; }
.asset-picker__cell:hover { background: var(--color-surface-alt); }
.asset-picker__cell:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--color-primary-light); }
.asset-picker__cell--selected { border-color: var(--color-primary); background: var(--color-primary-light); }
/* Wrap to two lines so a typical filename shows in full; long outliers (e.g.
   UUID names) break and clamp with an ellipsis rather than widening the cell. */
.asset-picker__caption { font-size: 0.75rem; line-height: 1.25; color: var(--color-text-muted); text-align: center; max-width: 100%; overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow-wrap: anywhere; }
.exp-card__title { font-weight: 600; margin-bottom: 0.25rem; }
.exp-card__meta { font-size: 0.8125rem; color: var(--color-text-muted); }

/* ===== Badge ===== */
.badge { display: inline-flex; align-items: center; padding: 0.125rem 0.5rem; border-radius: 999px; font-size: 0.75rem; font-weight: 600; }
.badge--published { background: var(--color-success-light); color: var(--color-success); }
.badge--draft { background: var(--color-surface-alt); color: var(--color-text-muted); }
.badge--warning { background: var(--color-warning-light); color: var(--color-warning); }
.badge--deleted { background: var(--color-danger-light, #fde8e8); color: var(--color-danger, #b91c1c); }
.badge--system { background: var(--color-surface-alt); color: var(--color-text-muted); }

/* ===== Details/Summary accordion ===== */
/* Scoped with :not(.nav__menu-wrap) so these accordion styles don't bleed into
   the nav Admin dropdown, which is also a <details> but styled separately. */
details:not(.nav__menu-wrap) { border: 1px solid var(--color-border); border-radius: var(--radius); margin-bottom: 0.5rem; }
details:not(.nav__menu-wrap) summary { padding: 0.75rem 1rem; cursor: pointer; font-weight: 600; font-size: 0.875rem; background: var(--color-surface-alt); border-radius: var(--radius); list-style: none; display: flex; justify-content: space-between; align-items: center; }
details:not(.nav__menu-wrap) summary::-webkit-details-marker { display: none; }
details:not(.nav__menu-wrap) summary::after { content: "+"; font-size: 1.125rem; color: var(--color-text-muted); transition: transform 0.2s; }
details:not(.nav__menu-wrap)[open] summary::after { content: "\2212"; }
details:not(.nav__menu-wrap)[open] summary { border-radius: var(--radius) var(--radius) 0 0; }
details:not(.nav__menu-wrap) .details-content { padding: 1rem; }

/* ===== Asset list ===== */
.asset-list { display: flex; flex-direction: column; gap: 0.75rem; align-items: flex-start; }
/* User-controlled sort control on the Assets tab header (BTI-677) */
.asset-sort { display: flex; align-items: center; gap: 0.5rem; }
.asset-sort__label { font-size: 0.8125rem; color: var(--color-text-muted); }
.asset-sort__field, .asset-sort__dir { width: auto; padding-top: 0.3rem; padding-bottom: 0.3rem; font-size: 0.8125rem; }
/* Generic tooltip hover zone — shared by asset-card--modified and node-card--modified/--added */
.modified-border-zone {
  position: absolute; top: 0; left: 0; bottom: 0; width: 12px; cursor: help;
}
.modified-border-zone::after {
  content: attr(data-modified-tooltip);
  position: absolute; left: 16px; top: 50%; transform: translateY(-50%);
  background: var(--color-text); color: var(--color-bg); font-size: 0.75rem;
  padding: 0.25rem 0.625rem; border-radius: var(--radius-sm);
  white-space: nowrap; pointer-events: none; opacity: 0; transition: opacity 0.15s;
  z-index: 10;
}
.modified-border-zone:hover::after { opacity: 1; }

/* ===== Media preview ===== */
.media-preview { position: relative; border-radius: var(--radius); overflow: hidden; background: #000; width: 120px; height: 90px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; }
.media-preview:has(audio) { background: var(--color-surface-alt); }
.media-preview video, .media-preview img { width: 100%; height: 100%; object-fit: cover; display: block; }
.media-preview audio { width: 100%; padding: 0.5rem; }

/* ===== Upload ===== */
.upload-zone { border: 2px dashed var(--color-border); border-radius: var(--radius); padding: 2rem; text-align: center; transition: border-color 0.15s, background 0.15s; cursor: pointer; }
.upload-zone:hover, .upload-zone.dragover { border-color: var(--color-primary); background: var(--color-primary-light); }
.upload-zone p { color: var(--color-text-muted); margin-top: 0.5rem; font-size: 0.875rem; }
.progress-bar { width: 100%; height: 8px; background: var(--color-surface-alt); border-radius: 999px; overflow: hidden; margin-top: 0.75rem; }
.progress-bar__fill { height: 100%; background: var(--color-primary); border-radius: 999px; transition: width 0.3s; width: 0%; }
.upload-status { font-size: 0.8125rem; color: var(--color-text-muted); margin-top: 0.5rem; }

/* Multi-file upload list */
.upload-file-list { margin: 0.5rem 0; max-height: 200px; overflow-y: auto; }
.upload-file-list__item { display: flex; align-items: center; gap: 0.5rem; padding: 0.35rem 0.25rem; border-bottom: 1px solid var(--color-border); font-size: 0.85rem; }
.upload-file-list__name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.upload-file-list__type { color: var(--color-text-muted); font-size: 0.75rem; padding: 0.1rem 0.4rem; background: var(--color-surface-alt); border-radius: var(--radius-sm); }
.upload-file-list__status { font-size: 0.75rem; min-width: 5rem; text-align: right; color: var(--color-text-muted); }
.upload-file-list__status--progress { color: var(--color-primary); }
.upload-file-list__status--done { color: var(--color-success); }
.upload-file-list__status--error { color: var(--color-danger); }

/* Upload form layout */
.upload-form__progress { margin-top: 0.5rem; }
.upload-form__actions { display: flex; align-items: center; gap: 0.75rem; margin-top: 0.5rem; flex-wrap: wrap; }

/* ===== Tenant switcher ===== */
.tenant-switcher { display: inline-flex; align-items: center; gap: 0.5rem; }
.tenant-switcher select { padding: 0.25rem 0.5rem; font-size: 0.8125rem; border: 1px solid var(--color-border); border-radius: var(--radius-sm); background-color: var(--color-surface); color: var(--color-text); }

/* ===== Empty state ===== */
.empty-state { text-align: center; padding: 3rem 1rem; color: var(--color-text-muted); }
.empty-state__icon { font-size: 3rem; margin-bottom: 0.75rem; }
.empty-state__title { font-size: 1.125rem; font-weight: 600; color: var(--color-text); margin-bottom: 0.25rem; }
.empty-state__text { font-size: 0.875rem; }

/* ===== HTMX indicator ===== */
.htmx-indicator { display: none; }
.htmx-request .htmx-indicator { display: inline-block; }
.htmx-request.htmx-indicator { display: inline-block; }
.spinner { width: 16px; height: 16px; border: 2px solid var(--color-border); border-top-color: var(--color-primary); border-radius: 50%; animation: spin 0.6s linear infinite; display: inline-block; }
@keyframes spin { to { transform: rotate(360deg); } }

/* ===== Node editor ===== */
.container-section { background: var(--color-surface-alt); border-radius: var(--radius-sm); padding: 0.75rem; margin-bottom: 0.5rem; }
/* Media-type visual distinction (BTI-599): accent + pill colors come from
   per-type CSS custom props set by the node-editor JS (display_type_config).
   Blank/unknown display types get no accent (escape hatch). */
.ne-container--typed { border-top: 4px solid var(--ct-stroke, var(--color-border)); }
/* Merged display-type pill (BTI-768): the combo-box input IS the colored pill —
   the old standalone .ne-container-type-chip is gone. Per-type colors come from the
   inherited --ct-* props (set on the section); a blank/free-typed type falls back to a
   neutral pill (transparent fill, muted text). We draw our own always-visible caret
   (a background SVG) and hide Chrome's native datalist arrow below, so the affordance
   reads at rest instead of only on hover. Vertical padding matches the base .form-input
   so the pill is the same height as the neighboring Title/Placement fields and lines up
   in the flex-end header row. Both classes are used in the selector so it outweighs the
   base .form-input rule. */
.form-input.ne-container-display-type {
  border-radius: 999px;
  padding: 0.5rem 1.4rem 0.5rem 0.75rem;
  font-weight: 600;
  color: var(--ct-color, var(--color-text-muted));
  border-color: var(--ct-stroke, var(--color-border));
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'%3E%3Cpath d='M2 3.5 5 6.5 8 3.5' fill='none' stroke='%2364748b' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") no-repeat right 0.5rem center / 0.55rem, var(--ct-fill, transparent);
  cursor: pointer;
  transition: border-color 0.15s, filter 0.15s;
}
/* Suppress Chrome's native <input list> dropdown arrow (appears on hover/focus) so it
   doesn't double up with our always-visible caret. !important is required — a UA rule
   otherwise wins and the native arrow still shows on hover. Focusing the field still opens
   the datalist (the onfocus handler clears the value to reveal all options). */
.form-input.ne-container-display-type::-webkit-calendar-picker-indicator { display: none !important; }
.form-input.ne-container-display-type:hover { filter: brightness(0.96); border-color: var(--ct-stroke, var(--color-text-muted)); }
.form-input.ne-container-display-type:focus { cursor: text; filter: none; }
/* Single-Section collapse (BTI-789): when a node has exactly one Section, drop
   the per-section chrome (type/title/description fields, remove-×,
   accent border) — leaving only the Placement field and media items. The section
   keeps its surface-alt background box so the area still reads as a section;
   padding is normalized to symmetric (!important to beat the section's inline
   padding-right, which only reserved room for the now-hidden remove-×). The flag
   class is set server-side (container_count == 1) and kept in sync by the
   node-editor JS on add/remove. Inputs stay in the DOM (hidden, not removed) so
   values still serialize and round-trip. The :has() selectors target the
   unclassed inline-styled field wrappers via their input classes. */
.ne-container-list--single .container-section { padding: 0.75rem !important; }
.ne-container-list--single .ne-container--typed { border-top: none; }
.ne-container-list--single .ne-remove-section,
.ne-container-list--single div:has(> .ne-container-display-type),
.ne-container-list--single div:has(> .ne-container-title),
.ne-container-list--single div:has(> .ne-container-description) { display: none; }
/* BTI-941: gray the placement select while it shows the "(Auto Placement)" placeholder
   (empty value selected), so an unset auto-placement reads as a hint. A real placement
   restores the default text color. Covers the node-editor select and the asset-card one. */
.ne-container-suggested-placement:has(option[value=""]:checked),
select[name="suggested_placement"]:has(option[value=""]:checked) { color: var(--color-text-muted); }
/* BTI-827: with the per-section chrome hidden above, a single Section holds only the
   Placement dropdown (.ne-container-header), the media items (.ne-media-items), and the
   +Add Item button — three visible children; the hidden chrome is display:none and drops
   out of the layout. Lay the section out as a 2-column grid so the Placement dropdown sits
   beside the media row (top-right, per BTI-827) instead of stacked above it. Column 2 hugs
   the 160px dropdown; column 1 (minmax 0 1fr) takes the rest and lets long media names
   truncate. align-items:start keeps the dropdown level with the first media row even when
   items stack. Purely CSS-keyed on .ne-container-list--single, so it un-applies the moment
   a second Section is added and re-applies on removal — no DOM or JS change. */
.ne-container-list--single .container-section {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  column-gap: 0.75rem;
  align-items: start;
}
.ne-container-list--single .ne-container-header { grid-column: 2; grid-row: 1; }
.ne-container-list--single .ne-media-items { grid-column: 1; grid-row: 1; }
.ne-container-list--single .ne-add-item-btn { grid-column: 1; grid-row: 2; }

/* ===== Node editor collapse (BTI-948) =====
   The generic node editor opens collapsed: only the type row, the first asset
   slot, and the read-only Details summary show; the gear toggles
   .ne-node-form--collapsed off to reveal today's full editor. Everything hidden
   here stays in the DOM (display:none) so values still serialize on Save —
   collapsing never wipes or drops sections/items (same idiom as --single above).
   The form class is the single source of truth for gear state.
   Exception (BTI-1111): Gallery nodes always render expanded — the gear still
   toggles manually from there, but Gallery never starts collapsed. */
.ne-node-form .ne-node-summary { font-size: 0.8125rem; color: var(--color-text-muted); margin-top: 0.5rem; }
/* The Details line only makes sense collapsed; drop it when expanded. */
.ne-node-form:not(.ne-node-form--collapsed) .ne-node-summary { display: none; }
/* Hide node Title/Description while collapsed (the gear reveals them). */
.ne-node-form--collapsed .ne-node-details { display: none; }
/* Hide the "Sections" legend, every Section past the first, and +Add Section.
   NOTE: updateAddSectionVisibility() has a gallery branch that sets
   `addBtn.style.display = ''` (clears the inline prop) so THIS rule still wins
   when collapsed. Keep it clearing (not setting a non-empty value) — an explicit
   inline display would beat this stylesheet and silently break the collapse. */
.ne-node-form--collapsed fieldset > legend,
.ne-node-form--collapsed .ne-container-list > .ne-container:not(:first-child),
.ne-node-form--collapsed .ne-add-section-btn { display: none; }
/* First (visible) Section: strip its chrome down to the single asset slot —
   hide the drag handle, remove-×, +Add Item, the whole section header
   (display-type / title / placement), description, and every media item after
   the first. */
.ne-node-form--collapsed .ne-container-list > .ne-container:first-child .ne-section-drag-handle,
.ne-node-form--collapsed .ne-container-list > .ne-container:first-child .ne-remove-section,
.ne-node-form--collapsed .ne-container-list > .ne-container:first-child .ne-add-item-btn,
.ne-node-form--collapsed .ne-container-list > .ne-container:first-child .ne-container-header,
.ne-node-form--collapsed .ne-container-list > .ne-container:first-child div:has(> .ne-container-description),
.ne-node-form--collapsed .ne-container-list > .ne-container:first-child .ne-media-item:not(:first-child) { display: none; }
/* Normalize the lone section's padding to a symmetric box. The section root has
   an INLINE padding-left/right:1.75rem reserving space for the now-hidden drag
   handle + remove-×; only !important beats an inline style. Match the 0.75rem the
   single-Section collapse (--single) already uses, so single- and multi-Section
   collapsed states look identical. Higher specificity than the --single rule, so
   it wins where both are !important. */
.ne-node-form--collapsed .ne-container-list > .ne-container:first-child { padding: 0.75rem !important; }

/* ===== Node cards (Content tab) ===== */
.node-card { transition: border-color 0.15s; position: relative; }
.node-card:hover { border-color: var(--color-primary); }
.node-card--library { opacity: 0.7; border-style: dashed; }
.node-card--library:hover { opacity: 1; }
.node-card--editing { border-left: 3px solid var(--color-primary); }

/* Content tab: unpublished change indicators — border-left-style:solid
   overrides .node-card--library's border-style:dashed on library cards */
.node-card--modified { border-left: 3px solid var(--color-modified); border-left-style: solid; }
.node-card--added { border-left: 3px solid var(--color-success); border-left-style: solid; }

/* BTI-784: in-place editable node card fields — seamless (flat until focus). */
.node-card__field {
  width: 100%;
  font-family: inherit;
  font-size: 0.875rem;
  color: var(--color-text);
  background: transparent;
  border: 1px solid transparent;
  border-radius: 5px;
  padding: 0.2rem 0.4rem;
  margin: 0 0 0.15rem 0;
  transition: background 0.12s, border-color 0.12s, box-shadow 0.5s;
}
.node-card__field--desc {
  font-size: 0.8125rem;
  color: var(--color-text-muted);
  /* autosize() (node_card_inline_edit.js) sets min-height to fit the content, so
     the full description always shows and a scrollbar is never needed — hide it
     so the browser can't paint its own unthemed (white) scrollbar on sub-pixel
     overflow (or momentarily before the JS runs on the rows=1 base). resize is
     left on (vertical): because autosize drives min-height rather than height, a
     manual drag sticks instead of being undone on the next keystroke, and the
     min-height floor stops the box being dragged shorter than its content (which
     is what would bring a scrollbar back). */
  overflow: hidden;
  resize: vertical;
}
.node-card__field:hover {
  background: var(--color-surface);
  border-color: var(--color-border);
}
.node-card__field:focus {
  outline: none;
  background: var(--color-surface);
  border-color: var(--color-primary);
}
.node-card__field-error {
  color: var(--color-danger);
  font-size: 0.75rem;
}
/* Collapse the slot entirely in the happy path — no reserved space. */
.node-card__field-error:empty { display: none; }

/* Save-confirmation flashes: brief coloured border/glow that fades out. */
@keyframes node-card-field-saved {
  0%   { border-color: var(--color-success); box-shadow: 0 0 6px var(--color-success); }
  100% { border-color: transparent; box-shadow: none; }
}
@keyframes node-card-field-error {
  0%   { border-color: var(--color-danger); box-shadow: 0 0 6px var(--color-danger); }
  100% { border-color: transparent; box-shadow: none; }
}
.node-card__field--flash-saved { animation: node-card-field-saved 1s ease-out; }
.node-card__field--flash-error { animation: node-card-field-error 1.2s ease-out; }

.flow-connector { user-select: none; display: none; }
#flow-node-list > .node-card-wrapper:not(:first-child) .flow-connector { display: block; }
/* Drag handle for reordering a node card within a flow (BTI-933). Inline grab
   affordance at the left of the card; the ☰ glyph is both the mouse drag target
   and the keyboard reorder focus target (BTI-1098). Mirrors the ne-drag-handle /
   ne-section-drag-handle affordance one level down. */
.node-card-drag-handle {
  flex-shrink: 0; display: flex; align-items: center; justify-content: center;
  width: 1.25rem; padding-top: 0.25rem; align-self: stretch;
  user-select: none; font-size: 0.875rem; color: var(--color-text-muted); cursor: grab;
}
.node-card-drag-handle:active { cursor: grabbing; }
/* Solid ring rather than the box-shadow glow used elsewhere (.asset-picker__cell):
   a crisp outline reads more clearly against the small ☰ glyph (BTI-1098). */
.node-card-drag-handle:focus-visible {
  outline: 2px solid var(--color-primary); outline-offset: 2px; border-radius: var(--radius-sm);
}
/* The dragged card dims while its drag-image ghost follows the cursor. */
.node-card-wrapper--dragging { opacity: 0.5; }
.node-card__thumb { flex-shrink: 0; }
.node-card__thumb--hologram-crop img,
.node-card__thumb--hologram-crop video { object-position: top center; }
.node-type-badge { display: inline-block; font-size: 0.6875rem; font-weight: 600; padding: 0.125rem 0.5rem; border-radius: 999px; text-transform: uppercase; letter-spacing: 0.03em; color: #fff; }
.node-type-badge--ambient { background: #3b82f6; }
.node-type-badge--gallery { background: #8b5cf6; }
.node-type-badge--hologram { background: #22c55e; }
.node-type-badge--immersive { background: #f97316; }
.node-type-badge--content { background: #64748b; }
.node-type-badge--fixed_frame { background: #14b8a6; }
.node-type-badge--audio { background: #ec4899; }
.node-type-badge--selection { background: #eab308; color: #1e293b; }
/* Reorder grab strip (BTI-851 items, BTI-887 sections): a full-height affordance on
   the left edge of a draggable item row or section; the ☰ glyph is vertically
   centered and the whole strip is the drag target. The host is position:relative and
   pads its left edge to make room (item card below; section root in the macro). */
.ne-drag-handle,
.ne-section-drag-handle {
  position: absolute; top: 0; left: 0; bottom: 0; width: 1.75rem;
  display: flex; align-items: center; justify-content: center;
  user-select: none; font-size: 0.875rem; color: var(--color-text-muted); cursor: grab;
}
.ne-drag-handle:active,
.ne-section-drag-handle:active { cursor: grabbing; }
/* The element being dragged dims while its drag-image ghost follows the cursor. */
.ne-media-item--dragging,
.ne-container--dragging { opacity: 0.5; }
/* Single-section nodes have nothing to reorder; hide the section handle. The existing
   updateSingleSectionLayout() toggles .ne-container-list--single (BTI-789), so
   visibility follows with no JS change. */
.ne-container-list--single .ne-section-drag-handle { display: none; }
/* While a section is being dragged, collapse every section to its handle + header
   row so all sections fit on screen and any drop target is reachable. The header holds
   the merged display-type pill (BTI-768), so the type indicator survives the reorder.
   Native HTML5 DnD can't auto-scroll, so a tall section would otherwise push the drop
   zone off-screen and make downward/non-adjacent reorders impossible (BTI-887). The
   class is toggled on the list by makeReorderable() for the duration of the drag. */
.ne-container-list--reordering .ne-container > :not(.ne-section-drag-handle):not(.ne-container-header) {
  display: none;
}

/* ===== Unified asset-card BEM component (BTI-601) =====
   The single asset-display component, used by the Assets-list manage card,
   the node-editor assign card, and the picker cells. Supersedes the former
   .asset-item* / .asset-info* / .asset-icon rules (removed in BTI-601). */
.asset-card { padding: 0.75rem; border: 1px solid var(--color-border); border-radius: var(--radius); background: var(--color-surface); position: relative; }
.asset-card--manage { display: grid; grid-template-columns: auto minmax(240px, auto) auto; gap: 1rem; align-items: center; }
/* Quiet "less-critical" meta line — shared by asset cards and node cards (BTI-601).
   The BTI-706 filename subtext reuses this muted styling; it adds ellipsis truncation. */
.asset-card__meta, .node-card__meta, .asset-card__filename { font-size: 0.75rem; color: var(--color-text-muted); }
.asset-card__filename { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Align meta text with the Description above it: the field insets its text by its
   left padding (0.4rem) plus the 1px transparent border, so match that here. */
.node-card__meta { padding-left: calc(0.4rem + 1px); }
.asset-card--modified { border-left: 3px solid var(--color-modified); }
.asset-card--confirm-delete { display: flex; flex-direction: column; gap: 0.5rem; border-left: 3px solid var(--color-warning); background: var(--color-warning-light); }
.asset-card__confirm-heading { margin: 0; font-weight: 600; font-size: 0.875rem; color: var(--color-text); }
.asset-card__confirm-body { margin: 0; font-size: 0.8125rem; color: var(--color-text); }
.asset-card__confirm-list { margin: 0; padding-left: 1.25rem; font-size: 0.8125rem; color: var(--color-text); }
.asset-card__confirm-actions { display: flex; gap: 0.5rem; }
.asset-card__actions-col { display: flex; flex-direction: column; align-items: flex-end; justify-content: space-between; align-self: stretch; gap: 0.25rem; }
.asset-card__actions { display: flex; gap: 0.375rem; align-items: center; }
.asset-card__progress { margin-top: 0.5rem; }
.asset-card__icon { width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; border-radius: var(--radius-sm); background: var(--color-surface-alt); font-size: 1.125rem; }
.asset-card__info { min-width: 0; display: flex; gap: 0.75rem; align-items: flex-start; }
.asset-card__info__fields { flex: 1; min-width: 0; }

/* ===== asset-card--assign (node-editor media rows, BTI-601) ===== */
/* Inherits border/radius/background from the base .asset-card; only adds the
   tighter padding, the unit's shadow, and inter-row spacing. */
.asset-card--assign { padding: 0.5rem 0.5rem 0.5rem 1.75rem; box-shadow: var(--shadow); margin-bottom: 0.5rem; }
.asset-card--assign .asset-card__row { min-width: 0; }
/* BTI-706: the placement Item Title leads the assign card as an inline-editable field —
   flat until hover/focus (mirrors the BTI-784 node-card field), with the asset name's
   prominence. Carries .ne-mi-title so it saves via the node-form serializer; hidden until
   an asset is picked so the row shows the "Choose an asset…" placeholder first. */
.asset-card__title-input {
  width: 100%; font-family: inherit; font-size: 0.875rem; font-weight: 500;
  color: var(--color-text); background: transparent; border: 1px solid transparent;
  border-radius: 5px; padding: 0.15rem 0.35rem; margin: 0 0 0.1rem -0.35rem;
  transition: background 0.12s, border-color 0.12s, box-shadow 0.12s;
}
.asset-card__title-input:hover { background: var(--color-surface); border-color: var(--color-border); }
.asset-card__title-input:focus { outline: none; background: var(--color-surface); border-color: var(--color-primary); box-shadow: 0 0 0 3px var(--color-primary-light); }
.asset-card__title-input--hidden { display: none; }
.asset-card__placeholder { color: var(--color-text-muted); font-size: 0.8125rem; }
.asset-card__zone-label { font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; color: var(--color-text-muted); margin: 0.5rem 0 0.25rem; }
.asset-card__desc { white-space: pre-wrap; color: var(--color-text); }
.asset-card__link { flex-shrink: 0; text-decoration: none; }
.asset-card--assign .ne-toggle-details,
.asset-card--assign .ne-remove-item { flex-shrink: 0; }

/* ===== asset-card--pick (asset picker cells, BTI-601) =====
   Marker modifier applied to .asset-picker__cell buttons so picker cells
   read as part of the unified asset-card family.
   Layout/spacing intentionally left to .asset-picker__cell — this rule
   adds no overrides so existing picker behaviour is unchanged. */
.asset-card--pick { /* intentional: layout provided by .asset-picker__cell */ }

.ne-mi-thumbnail-block label { margin-bottom: 0.125rem; }

/* ===== Scene editor — spatial slot grid (BTI-785) ===== */
/* Container: stacks rows with consistent gap. Capped width so the positional
   slots stay compact rather than stretching across the full page. */
.scene-editor { display: flex; flex-direction: column; gap: 0.75rem; max-width: 640px; }
/* Header: title + Done button, row-aligned. Inline styles in the template serve as a
   progressive-enhancement fallback; this rule is the canonical source. */
.scene-editor__header { display: flex; align-items: center; justify-content: space-between; }
/* Three-column row — maps Front / Middle / Back spatial positions, plus the
   bottom scene-wide row (Environment / Logo / Ambient Audio) added in BTI-1069. */
.scene-editor__row--three { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.5rem; align-items: stretch; }
/* Slot card: compact, muted-surface background, small radius. */
.scene-editor__slot { background: var(--color-surface-alt); border: 1px solid var(--color-border); border-radius: var(--radius-sm); padding: 0.5rem; display: flex; flex-direction: column; gap: 0.25rem; }
/* Wide slots (Environment, Logo, Ambient Audio) sit on the default card surface to
   visually distinguish them as scene-wide elements from the positional slot cards. */
.scene-editor__slot--wide { background: var(--color-surface); }
/* Empty spacer — holds the back-center grid cell; no visible border or background. */
.scene-editor__slot--empty { background: transparent; border: none; }
/* Slot label: small muted uppercase, matching .asset-card__zone-label pattern. */
.scene-editor__slot-label { font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; color: var(--color-text-muted); text-align: center; }
/* Slot pick wrapper: centers the preview tile within the slot (the sibling
   <select> is display:none). */
.scene-editor__slot-pick { display: flex; justify-content: center; }
/* Viewer marker — centered 👁 glyph + caption; muted and non-interactive. */
.scene-editor__viewer { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 0.5rem; font-size: 0.75rem; line-height: 1.4; color: var(--color-text-muted); text-align: center; }

/* ===== Toast snackbar ===== */
#toast-container { position: fixed; bottom: 1.5rem; left: 50%; transform: translateX(-50%); z-index: 200; display: flex; flex-direction: column; gap: 0.5rem; }
.toast { display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem 1rem; background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius); box-shadow: var(--shadow-md); font-size: 0.875rem; animation: toast-in 0.3s ease; min-width: 280px; }
.toast--dismiss { animation: toast-out 0.3s ease forwards; }
.toast--error { border-color: var(--color-danger); border-left: 3px solid var(--color-danger); }
.toast__message { flex: 1; }
.toast__undo { color: var(--color-primary); font-weight: 600; background: none; border: 1px solid var(--color-primary); white-space: nowrap; }
.toast__undo:hover { background: var(--color-primary-light); }
.toast__close { background: none; border: none; font-size: 1.25rem; color: var(--color-text-muted); cursor: pointer; padding: 0 0.25rem; line-height: 1; }
.toast__close:hover { color: var(--color-text); }
@keyframes toast-in { from { opacity: 0; transform: translateY(1rem); } to { opacity: 1; transform: translateY(0); } }
@keyframes toast-out { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(1rem); } }

/* ===== Flow graph tab (BTI-439) ===== */
/* Debug gate: the experience detail tab bar (#detail-tabs) is hidden until debug is
   latched. The <head> script in detail.html adds html.fg-debug when
   localStorage.flowGraphDebug is set (via ?debug=1). Hidden by default so normal users
   see the page unchanged; enable with ?debug=1 while the feature is refined. */
#detail-tabs { display: none; }
html.fg-debug #detail-tabs { display: flex; }

/* Outer wrapper rendered by _tab_graph_partial.html. All rules are scoped under
   .flow-graph so nothing leaks — card/panel class names are intentionally generic. */

/* Layout: SVG canvas + side panel stacked (panel below on narrow, row on wider screens). */
.flow-graph { position: relative; display: flex; flex-direction: column; gap: 1rem; }

/* The D3 SVG canvas. Must have a real height so getBoundingClientRect() returns a
   non-zero value (JS falls back to 800×500 only when height is zero). */
.flow-graph .flow-graph__svg {
  display: block;
  width: 100%;
  height: min(70vh, 640px);
  min-height: 400px;
  background: var(--color-surface-alt);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  cursor: default;
}

/* Node-detail side panel. Sits below the SVG by default; empty state is invisible. */
.flow-graph .flow-graph__panel { min-height: 0; }

/* ---- Node cards (rendered as HTML inside SVG <foreignObject>) ---- */

/* The card root. box-sizing must be border-box so the JS-set width/height are exact. */
.flow-graph .fg-card {
  box-sizing: border-box;
  background: var(--color-surface);
  border-radius: var(--radius);
  overflow: hidden;
  font-family: var(--font-sans);
  box-shadow: var(--shadow);
  cursor: grab;
  user-select: none;
}
.flow-graph .fg-card:active { cursor: grabbing; }

/* Header: tinted by the node color via inline `background: color + "18"` from JS. */
.flow-graph .fg-card-header {
  padding: 6px 10px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  border-bottom: 1px solid var(--color-border);
  gap: 6px;
}
.flow-graph .fg-card-title {
  font-size: 12px;
  font-weight: 700;
  color: var(--color-text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  flex: 1;
  min-width: 0;
}
/* Badge color + background are applied inline by JS from node.color. */
.flow-graph .fg-card-badge {
  font-size: 9px;
  font-weight: 600;
  padding: 1px 5px;
  border-radius: var(--radius-sm);
  text-transform: uppercase;
  white-space: nowrap;
  flex-shrink: 0;
}

/* Body: file rows, section titles, or summary text. */
.flow-graph .fg-card-body {
  padding: 6px 10px;
  font-size: 10px;
  color: var(--color-text-muted);
}
.flow-graph .fg-file-row {
  display: flex;
  align-items: center;
  margin: 2px 0;
  gap: 4px;
}
/* Colored dot — background applied inline by JS from mediaType. */
.flow-graph .fg-file-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  flex-shrink: 0;
}
.flow-graph .fg-file-name {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  flex: 1;
  min-width: 0;
  font-size: 10px;
}
.flow-graph .fg-file-slot {
  font-size: 9px;
  color: var(--color-text-muted);
  white-space: nowrap;
  flex-shrink: 0;
  margin-left: 4px;
}
/* Section heading in gallery-type nodes; color applied inline from node.color. */
.flow-graph .fg-section-title {
  font-weight: 600;
  font-size: 10px;
  margin-top: 4px;
  margin-bottom: 2px;
}
.flow-graph .fg-summary {
  font-size: 10px;
  color: var(--color-text-muted);
  padding: 2px 0;
}

/* ---- Read-only node-detail panel ---- */

.flow-graph .fg-editor {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: 1rem 1.25rem;
  font-family: var(--font-sans);
  font-size: 0.8125rem;
}
/* Empty state (no node selected): text only, no visible box chrome. */
.flow-graph .fg-editor--empty {
  color: var(--color-text-muted);
  text-align: center;
  padding: 1.5rem;
  background: transparent;
  border-color: transparent;
}
.flow-graph .fg-editor-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 0.75rem;
}
.flow-graph .fg-editor-title {
  font-size: 0.875rem;
  font-weight: 700;
  color: var(--color-text);
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 1;
  min-width: 0;
}
/* Badge color + background applied inline by JS. */
.flow-graph .fg-editor-badge {
  font-size: 10px;
  font-weight: 600;
  padding: 2px 8px;
  border-radius: var(--radius-sm);
  text-transform: uppercase;
  flex-shrink: 0;
}
.flow-graph .fg-editor-close {
  background: none;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  padding: 2px 8px;
  cursor: pointer;
  font-size: 0.875rem;
  color: var(--color-text-muted);
  flex-shrink: 0;
}
.flow-graph .fg-editor-close:hover { background: var(--color-surface-alt); }
/* Two-column label/value grid. */
.flow-graph .fg-editor-grid {
  display: grid;
  grid-template-columns: 120px 1fr;
  gap: 6px 12px;
  align-items: start;
}
.flow-graph .fg-editor-label {
  font-size: 11px;
  font-weight: 600;
  color: var(--color-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding-top: 6px;
}
.flow-graph .fg-editor-value {
  font-size: 0.8125rem;
  color: var(--color-text);
  padding: 4px 0;
  word-break: break-word;
}
.flow-graph .fg-editor-value code {
  font-family: var(--font-mono);
  font-size: 11px;
  background: var(--color-surface-alt);
  padding: 1px 4px;
  border-radius: var(--radius-sm);
}
.flow-graph .fg-editor-section {
  margin-top: 0.75rem;
  padding-top: 0.75rem;
  border-top: 1px solid var(--color-border);
}
.flow-graph .fg-editor-section-title {
  font-size: 0.75rem;
  font-weight: 700;
  color: var(--color-text);
  margin-bottom: 0.5rem;
}
.flow-graph .fg-editor-file {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 0;
  font-size: 0.75rem;
  color: var(--color-text);
}
/* Dot color applied inline by JS. */
.flow-graph .fg-editor-file-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex-shrink: 0;
}
.flow-graph .fg-editor-file-slot {
  font-size: 11px;
  color: var(--color-text-muted);
  margin-left: auto;
}

/* ---- Physics tune panel (debug-gated; appended as sibling of the SVG) ---- */

.flow-graph .flow-graph-tune {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: 5px 7px;
  font-size: 9.5px;
  color: var(--color-text-muted);
  box-shadow: var(--shadow);
  /* Floats over the bottom-left corner of the graph canvas. */
  position: absolute;
  bottom: calc(1rem + 12px); /* 1rem gap + 12px clearance above the SVG bottom edge */
  left: 12px;
  width: 200px;
  z-index: 10;
}
.flow-graph .flow-graph-tune > summary {
  cursor: pointer;
  font-weight: 700;
  font-size: 10.5px;
  color: var(--color-text);
  user-select: none;
  list-style: none;
}
.flow-graph .flow-graph-tune > summary::-webkit-details-marker { display: none; }
/* Hide non-summary content when collapsed (details[open] toggling is native, but
   being explicit avoids flash on repaint in some browsers). */
.flow-graph .flow-graph-tune:not([open]) > :not(summary) { display: none; }
.flow-graph .flow-graph-tune-row { margin-top: 3px; }
.flow-graph .flow-graph-tune-row .fg-tune-head {
  display: flex;
  justify-content: space-between;
  font-size: 9.5px;
}
.flow-graph .flow-graph-tune-row output { font-variant-numeric: tabular-nums; }
.flow-graph .flow-graph-tune-row input[type="range"] { width: 100%; margin: 0; height: 12px; }

/* ===== Responsive ===== */
@media (max-width: 640px) {
  .stats-grid { grid-template-columns: repeat(2, 1fr); }
  .exp-grid { grid-template-columns: 1fr; }
  .nav__links { display: none; }
  .page-header { flex-direction: column; align-items: flex-start; }
}
