/*
 * Design tokens. Two blocks: :root (dark, default) and
 * :root[data-theme="light"] (light override). Theme is switched by setting the
 * data-theme attribute on <html>. See STYLE_GUIDE section 1.
 */

:root {
  /* Dynamic accent - a single HSL accent drives all "life" in the UI. */
  --accent-hue: 28;
  --accent-saturation: 90%;
  --accent-lightness: 58%;
  --accent: hsl(var(--accent-hue) var(--accent-saturation) var(--accent-lightness));
  --on-accent: #fff;
  --danger: #d9534f;

  /* Per-entity-type accent hues - the shared "each object type has its own colour" data, mirrored
     by src/theme/entityAccent.ts (the 3D selection gizmo reads the same hues). Emitter keeps the
     app accent hue. */
  --hue-emitter: var(--accent-hue);
  --hue-group: 150;
  --hue-mesh: 212;

  /* Per-entity-type row accents (timeline outline): same saturation + lightness as the accent, only
     the hue differs, so emitter / VFX group / mesh read apart by colour rather than brightness. */
  --accent-emitter: hsl(var(--hue-emitter) var(--accent-saturation) var(--accent-lightness));
  --accent-group: hsl(var(--hue-group) var(--accent-saturation) var(--accent-lightness));
  --accent-mesh: hsl(var(--hue-mesh) var(--accent-saturation) var(--accent-lightness));

  /* The small-text size used across labels, fields, node params and timeline rows. */
  --font-size-small: 11px;

  /* Surfaces */
  --panel-background: #1b1d22;
  --panel-foreground: #e6e6e6;
  --stage-background: #0e0f12;
  --stage-dot: rgba(255, 255, 255, 0.05);
  --surface-raised: #2a2d33;
  --surface-deep: #15171b;
  /* Alpha checkerboard behind translucent swatches/gradients (color picker + ramp). */
  --checker: repeating-conic-gradient(var(--border-soft) 0% 25%, transparent 0% 50%) 0 0 / 10px 10px;

  /* Text tiers: --muted (brighter) / --dim (quieter) feed the group/subgroup/field caption
     ladder used for structural, never-user-edited headings (viewport tabs, stats panels, the
     timeline inspector, the Content sheet) - group: 10px/uppercase/0.06em/weight 600/--muted; subgroup: same but
     weight 400/--dim, a rarer divider one level down; field: --font-size-small, plain case,
     --muted, repeats every row so legibility beats tracking. Clickable/user-named labels (node
     titles, timeline outline rows, asset names) sit outside this ladder - they stay bright. */
  --muted: #9aa0a6;
  --dim: #6a7077;

  /* Borders */
  --border-hard: #000;
  --border-mid: #444;
  --border-soft: #333;
  --border-group: #535c6b;

  /* Overlays */
  --overlay-subtle: rgba(255, 255, 255, 0.04);
  --well: rgba(0, 0, 0, 0.22);

  --card-accent: var(--accent);

  /* Timeline spawn events read amber against the blue value keyframes. Deliberately the
     same amber in both themes so bursts/play-bars stay recognisable on either background. */
  --timeline-event: #e0a33e;
  /* The caret + the infinite play bar wear this violet while the timeline is infinite (an infinite
     play, tasks 2/3), same S/L as the accent so it reads as a state, not a different brightness. */
  --timeline-infinite: hsl(272 var(--accent-saturation) var(--accent-lightness));

  /* Node category tints - a header/frame accent that marks a node's role. The output sink (the
     graph's terminus) wears the app's own accent, so it reads as an accented/highlighted node at
     a glance. Timeline Value (a named, runtime-tunable input feeding the graph) gets its own
     violet hue, same lightness as the accent but less saturated, so it reads apart from both the
     app accent and the socket-type palette rather than doubling as "just another accented node". */
  --node-output: var(--accent);
  --node-timeline-value: hsl(272 55% var(--accent-lightness));

  /* Screen-space overlay stacking, centralised (exact legacy values; higher sits on top). Small
     local z-indexes - an active tab, the marquee over the nodes - stay as inline literals. */
  --z-add-node-menu: 1100;
  --z-popover: 1200; /* dropdown menu + node help card */
  --z-color-picker: 1300;
  --z-modal-scrim: 2000;
  --z-modal-sheet: 2001;
  --z-timeline-marquee: 2050;
  --z-context-menu: 2100;
  --z-tooltip: 2200; /* shared hover tooltip - non-interactive, sits above every surface */
}

:root[data-theme="light"] {
  --accent-lightness: 42%;
  --on-accent: #fff;
  --danger: #c0342f;

  --panel-background: #ededf0;
  --panel-foreground: #18191d;
  --stage-background: #d4d5db;
  --stage-dot: rgba(0, 0, 0, 0.06);
  --surface-raised: #dfe0e5;
  --surface-deep: #f8f8fb;

  --muted: #606870;
  --dim: #888f96;

  --border-hard: #b0b2b8;
  --border-mid: #9a9da3;
  --border-soft: #c0c2c8;
  --border-group: #757c8a;

  --overlay-subtle: rgba(0, 0, 0, 0.05);
  --well: rgba(0, 0, 0, 0.06);
}
/*
 * Base chrome. Flat, monochrome, sharp corners, zero transitions.
 * See STYLE_GUIDE sections 0, 3, 7.
 *
 * Frame is a top region over a resizable bottom band, split by draggable dividers:
 *   preview | graph      (top region - fills the height above the bottom band)
 *   outline | timeline   (bottom band - a draggable height, moved together)
 * The preview|graph and outline|timeline splits are independent. Middlebar carries the
 * graph switch. Assets is a bottom modal sheet, not part of frame.
 */

:root {
  /* Shared vertical rhythm so an outline row lines up with a timeline row. */
  --row-height: 30px;
}

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

html,
body {
  margin: 0;
  height: 100%;
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: 13px;
  background: var(--stage-background);
  color: var(--panel-foreground);
}

body {
  overflow: hidden;
  /* The whole editor is a direct-manipulation surface - no prose to select. Blanket
     text selection off so a drag never leaves a stray highlight; re-enabled on the
     text fields that genuinely need it below. */
  user-select: none;
  -webkit-user-select: none;
}

input,
textarea,
[contenteditable] {
  user-select: text;
  -webkit-user-select: text;
}

button {
  font-family: inherit;
}
/* Application frame */

#application {
  height: 100vh;
  display: grid;
  grid-template-rows: 1fr;
}

.workspace {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.region {
  display: flex;
  min-width: 0;
  min-height: 0;
}

/* Top region fills the height left above the bottom band; bottom band is a fixed
   (draggable) height, kept small by default. panelLayout.ts overrides these via
   inline styles as the dividers are dragged. */
.region--top {
  flex: 1;
}

.region--bottom {
  flex: none;
  height: 338px;
}

.preview {
  flex: none;
  width: 600px;
}

.graph {
  flex: 1;
  min-width: 0;
}

/* Focus-follows-mouse: the panel the cursor last settled over wears a thin accent ring so it
   is clear which panel's hotkeys are live. Drawn as an inset overlay (::after) rather than a
   border or box-shadow so it sits above the panel's own content - the viewport's opaque WebGL
   canvas would otherwise hide an inset shadow - and takes no layout space. The panels already
   establish a positioning context for their absolute overlays, so `position: relative` here is
   a safe no-op that guarantees it. */
.is-active-panel {
  position: relative;
}

.is-active-panel::after {
  content: "";
  position: absolute;
  inset: 0;
  border: 1px solid var(--accent);
  pointer-events: none;
  z-index: 20;
}

/* Draggable dividers - a thin grey seam with a wider transparent grab area that overflows
   into the neighbours without taking layout space. */
.resizer {
  flex: none;
  position: relative;
  z-index: 10;
  background: var(--border-hard);
}

.resizer::before {
  content: "";
  position: absolute;
}

.resizer--v {
  width: 1px;
  cursor: ew-resize;
}

.resizer--v::before {
  top: 0;
  bottom: 0;
  left: -3px;
  right: -3px;
}

.resizer--h {
  height: 1px;
  cursor: ns-resize;
}

.resizer--h::before {
  left: 0;
  right: 0;
  top: -3px;
  bottom: -3px;
}

.resizer:hover {
  background: var(--accent);
}

/* Hold the resize cursor across the whole document while a divider is dragged. */
body.resizing-x,
body.resizing-x * {
  cursor: ew-resize !important;
}

body.resizing-y,
body.resizing-y * {
  cursor: ns-resize !important;
}
/* Countdown-confirm control (components/countdownConfirm.ts): a danger-tinted guarded button, shared
   by the timeline row remove and the middlebar reset - each caller adds only its own size class. The
   `counting` class (set by the control while a countdown is live) fills danger like :hover, and
   tabular-nums keeps the remaining-clicks digit from shifting width as it counts down. */
.confirm-danger {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  font-weight: 700;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--danger);
  background: transparent;
  border: 1px solid color-mix(in srgb, var(--danger) 45%, var(--border-mid));
  cursor: pointer;
}

.confirm-danger:hover,
.confirm-danger.counting {
  color: var(--on-accent);
  background: var(--danger);
  border-color: var(--danger);
}
/* Middlebar */

/* Sits between the viewport+graph region and the timeline (panelLayout.ts); the row resizers are
   its top and bottom seams, so it carries no border of its own. flex:none keeps it at content
   height in the column - its own height never changes, only the regions above/below it do. */
.middlebar {
  flex: none;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px;
  background: var(--panel-background);
}

.middlebar__spacer {
  flex: 1;
}

/* Icon-only square button - a fixed 28x28 cell with the glyph centred (STYLE_GUIDE section 4.1). */
.middlebar__button {
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  color: var(--panel-foreground);
  background: var(--surface-raised);
  border: 1px solid var(--border-mid);
  cursor: pointer;
}

.middlebar__button svg {
  width: 15px;
  height: 15px;
}

/* Active applies to the theme toggles (sun/moon); both active reads as "auto". */
.middlebar__button:hover:not(:disabled),
.middlebar__button--active {
  border-color: var(--accent);
  color: var(--accent);
}

.middlebar__button:disabled {
  color: var(--dim);
  cursor: default;
}

/* A thin vertical seam between middlebar control clusters (STYLE_GUIDE section 7). */
.middlebar__separator {
  flex: none;
  width: 1px;
  height: 20px;
  background: var(--border-mid);
}

/* Graph switch: two separate icon+label buttons (Behavior / Render), spaced apart, the
   active graph carrying the accent (STYLE_GUIDE section 4.7 look, but not a joined strip). */
.graph-switch {
  display: flex;
  gap: 6px;
}

/* `display: flex` above (author origin) beats the UA `[hidden] { display: none }`, so the switch
   would still show when JS sets `hidden` (a VFX mesh has no graph choice). Re-assert none, with the
   extra class specificity needed to win. */
.graph-switch[hidden] {
  display: none;
}

.graph-switch__button {
  height: 28px;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 0 10px;
  font-size: 13px;
  font-weight: 600;
  color: var(--muted);
  background: var(--surface-raised);
  border: 1px solid var(--border-mid);
  cursor: pointer;
}

.graph-switch__button svg {
  flex: none;
  width: 15px;
  height: 15px;
}

.graph-switch__button:hover {
  border-color: var(--accent);
  color: var(--accent);
}

.graph-switch__button--active {
  color: var(--accent);
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
}

/* Floating variant: the switch sits over the graph's top-left corner (moved off the middlebar).
   Its buttons need an opaque backdrop so the dotted graph grid doesn't bleed through them. */
.graph-switch--floating {
  position: absolute;
  top: 10px;
  left: 10px;
  z-index: 3;
}

.graph-switch--floating .graph-switch__button {
  background: var(--panel-background);
}

.graph-switch--floating .graph-switch__button--active {
  background: color-mix(in srgb, var(--accent) 16%, var(--panel-background));
}

/* Language switch: a compact middlebar-chrome trigger showing the active locale badge (e.g. "EN").
   Overrides the `.dropdown` node-field look with the middlebar button's border/background so it sits
   with the theme + navigation clusters. The floating option grid comes from `.dropdown__menu`. */
.middlebar__lang.dropdown {
  height: 28px;
  padding: 0 8px;
  gap: 6px;
  color: var(--panel-foreground);
  background: var(--surface-raised);
  border: 1px solid var(--border-mid);
  font-size: 13px;
  font-weight: 600;
}

.middlebar__lang.dropdown:hover,
.middlebar__lang.dropdown--open {
  color: var(--accent);
  border-color: var(--accent);
}

.middlebar__lang .dropdown__caret {
  color: inherit;
}

/* Input-mode toggle: the mouse/touchpad icons sit flush as one unit, borders merged like a
   segment pair. */
.middlebar__theme {
  display: flex;
}

.middlebar__theme .middlebar__button {
  position: relative;
}

.middlebar__theme .middlebar__button + .middlebar__button {
  margin-left: -1px;
}

/* The highlighted (or hovered) toggle floats its accent border above its neighbour. */
.middlebar__theme .middlebar__button:hover:not(:disabled),
.middlebar__theme .middlebar__button--active {
  z-index: 1;
}
/* Preview */

.preview {
  position: relative;
  background-color: var(--stage-background);
  background-image: radial-gradient(var(--stage-dot) 1px, transparent 1.5px);
  background-size: 24px 24px;
  overflow: hidden;
}

.preview__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

/* The modal transform tool's stand-in cursor (ui/tools/pointerWrap.ts): a drag holds a pointer lock
   to wrap at the viewport edge, and the lock hides the real cursor, so this crosshair is what
   visibly teleports. The negative margin centres it on the position the tool sets. */
.viewport-cursor {
  position: absolute;
  z-index: 5;
  width: 15px;
  height: 15px;
  margin: -8px 0 0 -8px;
  pointer-events: none;
  background-image:
    linear-gradient(var(--accent), var(--accent)), linear-gradient(var(--accent), var(--accent));
  background-size:
    100% 1px,
    1px 100%;
  background-position: center;
  background-repeat: no-repeat;
  /* A dark rim keeps the crosshair readable over a bright particle or HDRI background. */
  filter: drop-shadow(0 0 1px rgb(0 0 0 / 0.9));
}

/* Viewport N-panel: a Blender-style panel in the preview's top-right, toggled by
   N. A vertical strip of tabs on its outer (right) edge - Item (the selected object's name +
   position / rotation / scale) plus the moved-in view-settings tabs (Sun / Hemisphere / Scene /
   Gizmo) - each revealing its own content panel to the left. */
.viewport-transform {
  position: absolute;
  top: 8px;
  right: 8px;
  z-index: 3;
  display: flex;
  align-items: flex-start;
  /* A small gap sets the open panel off from the tab strip rather than butting against it. */
  gap: 4px;
}

/* Content area holding the tab panels; sits left of the strip. */
.viewport-transform__content {
  flex: none;
}

/* The tab strip on the outer (right) edge; its tabs stack vertically. */
.viewport-transform__tabs {
  flex: none;
  display: flex;
  flex-direction: column;
}

/* Vertical tab: icon over a top-to-bottom label. Stays visible when the panel is
   collapsed (it is the handle that reopens it) and wears the accent while its panel is open. */
.viewport-transform__tab {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 8px 4px;
  color: var(--muted);
  background: color-mix(in srgb, var(--surface-raised) 92%, transparent);
  border: 1px solid var(--border-mid);
  cursor: pointer;
}

/* Merge the seam between stacked tabs. */
.viewport-transform__tab + .viewport-transform__tab {
  margin-top: -1px;
}

/* The hovered/active tab floats above its neighbours so its full border is never covered. */
.viewport-transform__tab:hover,
.viewport-transform__tab--active {
  z-index: 1;
}

.viewport-transform__tab svg {
  width: 15px;
  height: 15px;
}

.viewport-transform__tab-label {
  writing-mode: vertical-rl;
  font-size: var(--font-size-small);
  font-weight: 600;
  letter-spacing: 0.04em;
}

.viewport-transform__tab:hover {
  color: var(--accent);
  border-color: var(--accent);
}

.viewport-transform__tab--active {
  color: var(--accent);
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 14%, var(--surface-raised));
}

/* A tab's content panel - hidden unless it is the active, non-collapsed tab. Its width tracks its
   contents up to a cap rather than a fixed 200px, so each tab is only as wide as it needs (a short
   tab like Scene shrinks past what a wider tab like Item needs) and long localized labels widen the
   panel instead of clipping. No floor: the fields inside (name, number-control, axis) already carry
   their own usable-width min-widths, so a shared panel-level floor only fought a narrow tab's own
   fit-to-content goal. */
.viewport-transform__panel {
  display: none;
  flex-direction: column;
  gap: 6px;
  width: max-content;
  max-width: 300px;
  padding: 8px;
  background: color-mix(in srgb, var(--surface-raised) 92%, transparent);
  border: 1px solid var(--border-mid);
  font-size: var(--font-size-small);
}

.viewport-transform__panel--open {
  display: flex;
}

/* Name row: a "Name" label before the editable name field. */
.viewport-transform__name-row {
  display: flex;
  align-items: center;
  gap: 6px;
}

/* The selected object's icon before the name field - emitter gem or VFX cube. */
.viewport-transform__name-icon {
  flex: none;
  width: 16px;
  height: 16px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--muted);
}

.viewport-transform__name-icon svg {
  width: 15px;
  height: 15px;
}

.viewport-transform__name {
  flex: 1;
  /* A floor so the max-content panel still gives the name field a usable width. */
  min-width: 110px;
  height: 22px;
  padding: 0 6px;
  font: inherit;
  font-weight: 600;
  color: var(--panel-foreground);
  background: var(--panel-background);
  border: 1px solid var(--border-mid);
}

/* Subgroup-tier caption: a named cluster of fields within a tab (POSITION/ROTATION/SCALE;
   Sun/Environment), or a named cluster of rows in the Content sheet (Images/HDRI/Meshes;
   Examples; Help's own subsections) - see tokens.css's caption-tier note for the full
   group/subgroup/field ladder. */
.viewport-transform__section-title,
.preview-settings__section-title,
.content-group__title {
  color: var(--dim);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.viewport-transform__section {
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.viewport-transform__comp {
  display: flex;
  align-items: center;
  gap: 6px;
}

/* A field-component micro-label (X/Y/Z beside an axis control): quieter than a field label
   proper since three repeat per row, not itself a rung on the caption ladder (tokens.css). */
.viewport-transform__axis {
  flex: none;
  width: 12px;
  color: var(--dim);
}

.viewport-transform__comp .number-control {
  flex: 1;
  /* A floor so the axis fields keep a scrub target under the max-content panel. */
  min-width: 64px;
}

/* Preview view-settings rows (Sun / Hemisphere / Scene / Gizmo). The tabs themselves live in the
   viewport N-panel (.viewport-transform); this grid lays out each panel's rows in two columns that
   are uniform across the whole tab - a label column sized to the widest label, and a control column
   sized to the widest control the tab actually needs (not stretched to whatever room the panel
   has - a fixed-width switch next to a color swatch would otherwise inflate the swatch far past
   its own control). Each row is `display: contents` so its label + single value cell drop straight
   into these shared columns. */
.preview-settings {
  display: grid;
  grid-template-columns: max-content max-content;
  align-items: center;
  column-gap: 8px;
  row-gap: 6px;
}

.preview-settings__row {
  display: contents;
}

/* Field-tier caption (tokens.css) - muted, not the control's own bright panel-foreground, so the
   field's own value (the color/number/switch beside it) stays the visually loudest thing on the
   row. Font-size inherited here (--font-size-small, from .viewport-transform__panel); the Content
   sheet's own field label (.content-export__label) has no such ambient context, so it sets its own
   font-size locally (modal.css) rather than joining that part of this rule. */
.preview-settings__label,
.content-export__label {
  min-width: 0;
  color: var(--muted);
}

/* Every control fills its (uniform, content-sized) column, so values still line up on one edge:
   neutralise the intrinsic widths of the number and asset-picker controls so the grid can stretch
   them; the color swatch and segment group already stretch. The switch is deliberately left out -
   it keeps its own fixed size, which is what sizes the column on a tab with no wider control. */
.preview-settings .number-control,
.preview-settings .asset-picker {
  width: auto;
}

/* Without a cap, a long environment name would make the picker's own max-content contribution
   (the intrinsic sizing basis of the control column) balloon past the panel's own max-width - the
   grid doesn't clip its tracks, so the panel would visibly overflow. Bounded, the label's existing
   ellipsis (params.css `.asset-picker__label`) takes over instead. */
.preview-settings .asset-picker {
  max-width: 160px;
}

.preview-settings .color-swatch {
  height: 18px;
}

/* Lighting tab subsection captions (Sun / Environment) - full-width above each group; typography
   is the shared subgroup-tier rule above. */
.preview-settings__section-title {
  grid-column: 1 / -1;
  /* Extra room over the row-gap's own 6px, so a section reads apart from the previous one's last
     row at a glance instead of looking like one more field in it. */
  margin-top: 12px;
}

/* An active environment preset fully replaces the manual Environment fallback fields (ADR-0004) -
   hidden outright, not greyed; Sun's own rows never get this class, since it keeps deriving from
   the preset without ever being blocked (previewSettings.ts). */
.preview-settings__row--hidden {
  display: none;
}

/* Snap rows pack a lock toggle and the step field into one value cell (the grid's control column). */
.preview-settings__value {
  display: flex;
  align-items: center;
  gap: 6px;
}

.preview-settings__value .number-control {
  flex: 1;
}

/* Icon segments (the Space Global/Local toggle) split their column evenly. */
.preview-settings .segment-group {
  width: 100%;
}

.preview-settings .segment {
  flex: 1;
  /* Matches the other controls' 20px row height (switch, number-control, dropdown) - the
     segment's own 26px (segmented.css) is sized for its other, taller-target use elsewhere. */
  height: 20px;
}

/* Add-object overlay - the "Spawn emitter" button in the bottom-right of the viewport
   (viewportObjects.ts). */
.viewport-objects {
  position: absolute;
  right: 8px;
  bottom: 8px;
  z-index: 4;
  display: flex;
  flex-direction: column;
  /* Stretch so Spawn mesh matches the wider Spawn emitter button. */
  align-items: stretch;
  gap: 6px;
}

/* A labelled icon+text button: "+ Spawn emitter". */
.viewport-objects__button {
  height: 28px;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 0 10px;
  font-size: 12px;
  font-weight: 600;
  color: var(--panel-foreground);
  background: var(--surface-raised);
  border: 1px solid var(--border-mid);
  cursor: pointer;
}

.viewport-objects__button svg {
  flex: none;
  width: 15px;
  height: 15px;
}

.viewport-objects__button:hover {
  border-color: var(--accent);
  color: var(--accent);
}

/* Preview performance stats - a small readout (FPS, particles, effect cost) in the bottom-left of
   the viewport (the top-left hosts the transform panel). A grid, not a column of independent
   rows: the two columns (value, label) are shared across every row, so the value column
   auto-sizes to its widest row instead of a hardcoded width (see .viewport-stats__row below). */
.viewport-stats {
  position: absolute;
  bottom: 8px;
  left: 8px;
  z-index: 4;
  display: grid;
  grid-template-columns: max-content max-content;
  column-gap: 8px;
  row-gap: 4px;
  padding: 6px 8px;
  font-size: var(--font-size-small);
  font-variant-numeric: tabular-nums;
  color: var(--panel-foreground);
  background: color-mix(in srgb, var(--surface-raised) 82%, transparent);
  border: 1px solid var(--border-mid);
  /* Never intercept orbit drags started over the readout. */
  pointer-events: none;
}

/* Group-tier caption (tokens.css's caption ladder) - the stats readout's own "STATS" heading and
   the Content sheet's per-block headings (CONTENT / PROJECT / HELP, .content-sheet__title) share
   this exact rung, one step brighter/bolder than the subgroup tier above. */
.viewport-stats__title,
.content-sheet__title {
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
}

.viewport-stats__title {
  grid-column: 1 / -1;
}

/* The row itself takes no box - its two children (value, label) become direct items of the
   `.viewport-stats` grid above, so they land in the shared value/label columns. */
.viewport-stats__row {
  display: contents;
}

/* Every value lands in the grid's value column, sized to the widest one across the panel (plus
   this padding as headroom) - not a hardcoded width. */
.viewport-stats__value {
  align-self: center;
  padding: 1px 4px;
  text-align: right;
  font-weight: 600;
  background: var(--surface-deep);
  border: 1px solid var(--border-mid);
}

.viewport-stats__label {
  align-self: center;
  color: var(--muted);
}
/* Graph */

.graph {
  position: relative;
  overflow: hidden;
  background-color: var(--stage-background);
  background-image: radial-gradient(var(--stage-dot) 1px, transparent 1.5px);
  background-size: 24px 24px;
}

.graph__nodes {
  position: absolute;
  top: 0;
  left: 0;
  transform-origin: 0 0;
}

.graph__hint {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--font-size-small);
  color: var(--dim);
  pointer-events: none;
}

/* `display: flex` above would override the plain `hidden` attribute, so hide explicitly. */
.graph__hint[hidden] {
  display: none;
}

/* No graph owner selected (the outliner's root VFX group - see graphPanel.ts): the canvas, the
   Behavior/Render switch, the stats readout and the error strip all give way to `.graph__hint`. */
.graph--no-owner .graph__comments,
.graph--no-owner .graph__wires,
.graph--no-owner .graph__nodes,
.graph--no-owner .graph__marquee,
.graph--no-owner .graph__knife,
.graph--no-owner .graph-switch,
.graph--no-owner .graph-stats,
.graph--no-owner .graph__errors {
  display: none;
}

/* Compile-failure state: the whole region lights up red and an error frame surfaces
   the engine's messages at the bottom (mirrors the middlebar's invalid status). The red
   frame is a pointer-transparent overlay so it paints *above* the nodes rather than
   being occluded by them (a container box-shadow would sit under the node layer). A
   crisp 2px inset border, no glow - the chrome stays sharp and angular. */
.graph--invalid::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 8;
  pointer-events: none;
  box-shadow: inset 0 0 0 2px var(--danger);
}

/* Flush to the graph's bottom edge and spanning its full width - a single strip merged with
   the region's borders, not a floating card. One flat list of messages, no nested wells. */
.graph__errors {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 9;
  display: flex;
  flex-direction: column;
  gap: 3px;
  max-height: 40%;
  overflow-y: auto;
  padding: 8px 12px;
  background: color-mix(in srgb, var(--danger) 20%, var(--surface-deep));
  border-top: 1px solid var(--danger);
}

/* `display: flex` above would override the plain `hidden` attribute, so hide explicitly. */
.graph__errors[hidden] {
  display: none;
}

.graph__errors-title {
  margin-bottom: 2px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--danger);
}

/* A row is a `<button>` when the compiler attributed it to a live node (graphPanel.ts's
   errorRow), a plain `<div>` otherwise - reset to the same look either way, so only the
   `--clickable` modifier below tells them apart. */
.graph__error {
  display: block;
  width: 100%;
  margin: 0;
  padding: 0;
  font: inherit;
  font-size: 12px;
  line-height: 1.4;
  color: var(--panel-foreground);
  text-align: left;
  white-space: pre-wrap;
  word-break: break-word;
  background: none;
  border: none;
}

/* Clicking jumps the canvas to the node this message names (GraphCanvas.focusNode). The hover/
   focus tint stays in the strip's own danger-red language rather than the app's accent color. */
.graph__error--clickable {
  cursor: pointer;
}

.graph__error--clickable:hover {
  text-decoration: underline;
  background: color-mix(in srgb, var(--danger) 30%, transparent);
}

.graph__error--clickable:focus-visible {
  outline: 1px solid var(--danger);
  outline-offset: 1px;
}

/* Complexity readout (reachable node count + total cost) - bottom-left corner, mirroring the
   viewport's FPS/particle-count panel (.viewport-stats in preview.css), the same corner of a
   different surface. A grid, not a column of independent rows: the two columns (value, label)
   are shared across every row, so the value column auto-sizes to its widest row instead of a
   hardcoded width (see .graph-stats__row below). */
.graph-stats {
  position: absolute;
  bottom: 10px;
  left: 10px;
  z-index: 3;
  display: grid;
  grid-template-columns: max-content max-content;
  column-gap: 8px;
  row-gap: 4px;
  padding: 6px 8px;
  font-size: var(--font-size-small);
  font-variant-numeric: tabular-nums;
  color: var(--panel-foreground);
  background: color-mix(in srgb, var(--surface-raised) 82%, transparent);
  border: 1px solid var(--border-mid);
  /* Never intercept graph pan/marquee drags started over the readout. */
  pointer-events: none;
}

.graph-stats__title {
  grid-column: 1 / -1;
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
}

/* The row itself takes no box - its two children (value, label) become direct items of the
   `.graph-stats` grid above, so they land in the shared value/label columns. */
.graph-stats__row {
  display: contents;
}

/* Every value lands in the grid's value column, sized to the widest one across the panel (plus
   this padding as headroom) - not a hardcoded width. */
.graph-stats__value {
  align-self: center;
  padding: 1px 4px;
  text-align: right;
  font-weight: 600;
  background: var(--surface-deep);
  border: 1px solid var(--border-mid);
}

.graph-stats__label {
  align-self: center;
  color: var(--muted);
}

/* Rubber-band selection rectangle (screen space, above the nodes, purely visual). */
.graph__marquee {
  position: absolute;
  z-index: 5;
  pointer-events: none;
  border: 1px dashed var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}
/* Nodes */

.node {
  /* Content-sized, then snapped to the grid: nodeView.finalizeSize() measures the natural
     width and rounds it up to a whole number of cells (min 6, max 16). Height falls out of
     the flex column - the title cell plus the body grid's rows, each one cell tall - so the
     box lands on the grid lines. The 1px frame is an inset box-shadow, NOT a border: a real
     border would add its 2px (top+bottom) to the auto height and push the box 2px off the
     grid. `--node-frame` carries the frame colour so the selection/category variants recolour
     it without re-declaring the shadow. `max-content` is the pre-measurement default. */
  position: absolute;
  top: 0;
  left: 0;
  width: max-content;
  min-width: 144px;
  max-width: 384px;
  display: flex;
  flex-direction: column;
  background: var(--panel-background);
  --node-frame: var(--border-mid);
  box-shadow: inset 0 0 0 1px var(--node-frame);
  user-select: none;
  cursor: grab;
}

.node--selected {
  --node-frame: var(--accent);
  background: color-mix(in srgb, var(--accent) 8%, var(--panel-background));
}

/* The permanent output sink: a subtle accent frame so it reads as the graph's terminus. */
.node--output {
  --node-frame: color-mix(in srgb, var(--node-output) 45%, var(--border-mid));
}

/* category tint fills are translucent (mixed against `transparent`, not `--panel-background`):
   `.node__title`/`.node__body` stretch flush to `.node`'s own edges, and an opaque fill would
   paint over (occlude) the parent's inset `box-shadow` frame, since children paint after a
   box's own background+box-shadow. Same reasoning as the ordinary `.node__title`'s translucent
   `--overlay-subtle` - category nodes just tint that overlay instead of replacing it. */
.node--output .node__title {
  background: color-mix(in srgb, var(--node-output) 18%, transparent);
}

.node--output .node__body,
.node--output .node__attributes {
  background: color-mix(in srgb, var(--node-output) 9%, transparent);
}

/* Timeline Value nodes (a named, runtime-tunable input): a violet frame so a graph's live
   inputs read apart from its ordinary operator nodes at a glance. */
.node--timeline-value {
  --node-frame: color-mix(in srgb, var(--node-timeline-value) 45%, var(--border-mid));
}

.node--timeline-value .node__title {
  background: color-mix(in srgb, var(--node-timeline-value) 18%, transparent);
}

.node--timeline-value .node__body {
  background: color-mix(in srgb, var(--node-timeline-value) 9%, transparent);
}

/* Selecting a category node keeps its own tint (accent for sinks, violet for Timeline Value)
   rather than switching to the generic accent - it just saturates: a full-strength
   frame and a deeper header/body fill so the selection reads in the node's color. */
.node--output.node--selected {
  --node-frame: var(--node-output);
  /* Overrides `.node--selected`'s app-accent background - else it would show through the now-
     translucent title/body category overlays and mix the app accent into the category tint. */
  background: color-mix(in srgb, var(--node-output) 8%, var(--panel-background));
}

.node--output.node--selected .node__title {
  background: color-mix(in srgb, var(--node-output) 32%, transparent);
}

.node--output.node--selected .node__body,
.node--output.node--selected .node__attributes {
  background: color-mix(in srgb, var(--node-output) 16%, transparent);
}

.node--timeline-value.node--selected {
  --node-frame: var(--node-timeline-value);
  /* Overrides `.node--selected`'s app-accent background - else it would show through the now-
     translucent title/body category overlays and mix the app accent into the category tint. */
  background: color-mix(in srgb, var(--node-timeline-value) 8%, var(--panel-background));
}

.node--timeline-value.node--selected .node__title {
  background: color-mix(in srgb, var(--node-timeline-value) 32%, transparent);
}

.node--timeline-value.node--selected .node__body {
  background: color-mix(in srgb, var(--node-timeline-value) 16%, transparent);
}

/* A node the compiler blamed for an error. Rather than recolor the node itself, we frame it
   from the outside - a red outline set off from the card by a small padding, with an "ERROR"
   tab above it - so it reads as a window spotlighting the problem while the node keeps its own
   look (category tint, selection accent). Red is reserved for error/invalid states
   (socketColors.ts). The node's own box keeps `overflow: visible`, so these pseudo-elements
   extend past its edges; a raised z-index floats the frame above neighbouring nodes. */
.node--error {
  z-index: 3;
}

.node--error::before {
  content: "";
  position: absolute;
  inset: -7px;
  border: 1px solid var(--danger);
  pointer-events: none;
}

.node--error::after {
  content: "ERROR";
  position: absolute;
  left: -7px;
  top: -7px;
  transform: translateY(-100%);
  padding: 1px 6px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  color: var(--on-accent);
  background: var(--danger);
  pointer-events: none;
}

.node__title {
  height: 24px;
  display: flex;
  align-items: center;
  padding: 0 8px;
  font-size: 12px;
  font-weight: 600;
  color: var(--panel-foreground);
  background: var(--overlay-subtle);
  border-bottom: 1px solid var(--border-soft);
}

/* The node's name fills the header, leaving room for the help icon at the far right. */
.node__title-text {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Live cost badge - the node's current complexity (domain/graphStats.ts), left of the help icon.
   Same framed-value-box look as the Stats panels' rows (.graph-stats__value / .viewport-stats__value),
   height-locked to the 16px help icon (.node__help) it sits beside so both read as one row. */
.node__cost {
  flex: none;
  display: inline-flex;
  align-items: center;
  height: 16px;
  margin-left: 6px;
  padding: 0 5px;
  font-size: 11px;
  font-weight: 600;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--panel-foreground);
  background: var(--surface-deep);
  border: 1px solid var(--border-mid);
}

/* Header help "?" - a dim squared glyph at the far right of the title (opposite the name) that
   brightens on hover and opens the node's help card (nodeView.buildHelpIcon). */
.node__help {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  margin-left: 6px;
  color: var(--dim);
  cursor: help;
}

.node__help:hover {
  color: var(--accent);
}

.node__help svg {
  width: 16px;
  height: 16px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Render-sink material selector: a full-width tab strip, the node's first body row,
   exactly one grid cell tall so the node stays on the grid. */
.node__tabs {
  height: 24px;
  display: flex;
  align-items: center;
  padding: 0 4px;
  border-bottom: 1px solid var(--border-soft);
}

.node__tabs .segment-group {
  display: flex;
  flex: 1;
}

.node__tabs .segment {
  flex: 1;
  height: 20px;
  padding: 0 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--font-size-small);
}

/* The node body is one CSS grid so dots, names and controls line up across every row.
   Five columns - [input dot][input label][centre control][output label][output dot] - with
   the centre lane taking the slack. Every row is exactly one grid cell tall (grid-auto-rows),
   so the node's height stays a whole number of cells. Side padding matches the title's. */
.node__body {
  display: grid;
  grid-template-columns: auto auto minmax(48px, 1fr) auto auto;
  grid-auto-rows: 24px;
  align-items: center;
  column-gap: 6px;
  padding: 0 8px;
}

/* The centre lane holds one control (a default-value editor or a param widget) and lets it
   fill the lane; a fixed-size checkbox opts out. Swallows pointerdown (see nodeView) so
   editing a field never starts a node drag. */
.node__center {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
}

.node__center > * {
  flex: 1 1 auto;
  min-width: 0;
}

.node__center > .param__checkbox {
  flex: none;
}

/* A socket's name label. Output labels right-align so they hug their dot on the right edge. */
.socket__label {
  min-width: 0;
  font-size: var(--font-size-small);
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.socket__label--out {
  justify-self: end;
  text-align: right;
  /* A spacer between the centre content (input default / param) and the output on the same
     row. Living on the output label, it only reserves width when an output exists - input-only
     rows (e.g. on sinks) stay tight to the right edge. */
  margin-left: 12px;
}

/* The output dot sits at the grid's right edge; the input dot at its left. */
.socket__dot--in {
  justify-self: start;
}

.socket__dot--out {
  justify-self: end;
}

.socket__dot {
  position: relative;
  width: 11px;
  height: 11px;
  background: var(--surface-deep);
  /* Outlined in the data type's color (set per dot in nodeView); filled with it when
     the port carries an edge or is hovered. Falls back to a neutral border if unset. */
  border: 1px solid var(--socket-color, var(--border-mid));
  cursor: crosshair;
}

/* Enlarge the click/hover target to a square the height of the socket row (24px)
   while the visible dot stays 11px. The transparent pseudo is part of the dot, so
   pointerdown, :hover and elementFromPoint all resolve to the dot itself. */
.socket__dot::before {
  content: "";
  position: absolute;
  inset: -6.5px;
}

/* A connected port reads filled; hovering any port previews its type color too. */
.socket__dot:hover,
.socket__dot--filled {
  background: var(--socket-color, var(--accent));
  border-color: var(--socket-color, var(--accent));
}
/* Node parameter widgets
   Body params live in the same body grid as the sockets: a label spanning the two socket
   columns and a control in the centre lane. Only the tall gradient block still uses `.param`
   as a full-width, multi-row cell. */

.param {
  display: flex;
  align-items: center;
  gap: 6px;
}

.param__label {
  min-width: 0;
  font-size: var(--font-size-small);
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.param__input {
  flex: 1;
  min-width: 0;
  height: 20px;
  padding: 0 4px;
  font-family: inherit;
  font-size: var(--font-size-small);
  color: var(--panel-foreground);
  background: var(--surface-deep);
  border: 1px solid var(--border-mid);
}

.param__input:focus {
  outline: none;
  border-color: var(--accent);
}

/* Custom dropdown (replaces the native <select>) - a flat field trigger + a floating,
   screen-space option menu (so pan/zoom never displaces it). STYLE_GUIDE sections 0/3. */
.dropdown {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 4px;
  /* The trigger also carries `.param__input`, so it inherits that field's chrome; match its
     height too (20px) so a closed dropdown sits flush with the number/text fields beside it. */
  height: 20px;
  text-align: left;
  cursor: pointer;
}

.dropdown__label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.dropdown__label--placeholder {
  color: var(--dim);
}

.dropdown__caret {
  flex: none;
  display: inline-flex;
  align-items: center;
  font-size: 12px;
  line-height: 1;
  color: var(--muted);
}

.dropdown:hover,
.dropdown--open {
  border-color: var(--accent);
}

.dropdown:hover .dropdown__caret,
.dropdown--open .dropdown__caret {
  color: var(--accent);
}

.dropdown__menu {
  position: fixed;
  z-index: var(--z-popover);
  max-height: 50vh;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  background: var(--panel-background);
  /* --popup-scale (Dropdown's `scale`, via openPopover) is the live graph zoom for a node's
     dropdown, so a row reads at the same size as the zoomed trigger it hangs from; it falls back
     to 1 (this popover's authored size) wherever no zoomable ancestor sets it. */
  border: calc(1px * var(--popup-scale, 1)) solid var(--border-mid);
}

.dropdown__item {
  height: calc(24px * var(--popup-scale, 1));
  flex: none;
  display: flex;
  align-items: center;
  /* Left padding matches `.param__input`'s (4px), not the right side's 10px: with the menu's own
     border also scaled by --popup-scale, this keeps an item's label flush with the closed
     trigger's own label at every zoom (both are (1px border + 4px padding) * scale from the left
     edge), instead of drifting apart as scale grows. */
  padding: 0 calc(10px * var(--popup-scale, 1)) 0 calc(4px * var(--popup-scale, 1));
  font-family: inherit;
  font-size: calc(var(--font-size-small) * var(--popup-scale, 1));
  text-align: left;
  white-space: nowrap;
  color: var(--panel-foreground);
  background: transparent;
  border: none;
  cursor: pointer;
}

.dropdown__item:hover {
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}

/* The stored value's own row, independent of the hover state above - a left accent bar plus a dim
   background of its own (stronger once hover's own tint also applies, on top), so the selection
   stays legible even once the pointer moves off it. Aligned with AssetPicker's own selected-row
   treatment (.asset-picker__item--selected below) so the two controls' popups read as one family. */
.dropdown__item--active {
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  box-shadow: inset calc(2px * var(--popup-scale, 1)) 0 var(--accent);
}

.dropdown__item-label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Trailing badge (e.g. the locale code) shown after a single-line option label. */
.dropdown__item-hint {
  flex: none;
  margin-left: calc(10px * var(--popup-scale, 1));
  color: var(--muted);
  font-size: calc(var(--font-size-small) * var(--popup-scale, 1));
}

/* Multi-column variant (the language switch), matched to the tesselot dropdown-pop: a padded
   floating card whose options are bordered cells stacking a bold label over a dimmed hint, the
   active/hovered cell carrying the accent border. */
.dropdown__menu--grid {
  display: grid;
  gap: 2px;
  padding: 4px;
}

.dropdown__menu--grid .dropdown__item {
  height: auto;
  flex-direction: column;
  align-items: flex-start;
  gap: 1px;
  padding: 6px 9px;
  border: 1px solid transparent;
}

.dropdown__menu--grid .dropdown__item:hover {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}

.dropdown__menu--grid .dropdown__item--active {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 16%, transparent);
}

.dropdown__menu--grid .dropdown__item-label {
  flex: none;
  font-weight: 600;
}

.dropdown__menu--grid .dropdown__item-hint {
  margin-left: 0;
  font-variant-numeric: tabular-nums;
}

/* Asset picker - a field trigger with the same param__input chrome/20px height and caret as a
   Dropdown, for choosing among a project asset library (textures, meshes, HDRI environments, ...)
   by thumbnail. Its floating search + preview list (not a stacked text menu) is what actually
   differs from Dropdown. Mirrors the graph's add-node search (addNodeMenu.ts / .add-node-menu*) - a
   pinned search box over a scrollable row list - minus that menu's fixed width and stepped
   wheel-scroll, which are specific to its right-click placement. */
.asset-picker {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 4px;
  height: 20px;
  text-align: left;
  cursor: pointer;
}

.asset-picker__swatch {
  flex: none;
  display: grid;
  place-items: center;
  width: 14px;
  height: 14px;
  color: var(--muted);
  background: var(--well);
  overflow: hidden;
}

.asset-picker__swatch svg {
  width: 10px;
  height: 10px;
}

.asset-picker__label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.asset-picker__label--placeholder {
  color: var(--dim);
}

.asset-picker__caret {
  flex: none;
  display: inline-flex;
  align-items: center;
  font-size: 12px;
  line-height: 1;
  color: var(--muted);
}

.asset-picker:hover,
.asset-picker--open {
  border-color: var(--accent);
}

.asset-picker:hover .asset-picker__caret,
.asset-picker--open .asset-picker__caret {
  color: var(--accent);
}

.asset-picker__menu {
  position: fixed;
  z-index: var(--z-popover);
  /* min-width is set inline (assetPicker.ts's `open()`), not here: it must combine MIN_MENU_WIDTH
     with the trigger's own (already-zoomed) width, which a single CSS value can't express, and an
     inline style would silently win over a competing rule here anyway. */
  /* Without a cap, a long asset name would grow the popup to fit it instead of ever triggering
     .asset-picker__item-label's own ellipsis (a shrink-to-fit box has no reason to clip a child it
     is free to grow around). */
  max-width: calc(280px * var(--popup-scale, 1));
  display: flex;
  flex-direction: column;
  background: var(--panel-background);
  /* --popup-scale (AssetPicker's `scale`, via openPopover) is the live graph zoom for a node's
     picker, so a row reads at the same size as the zoomed trigger it hangs from; it falls back
     to 1 (this popover's authored size) wherever no zoomable ancestor sets it. */
  border: calc(1px * var(--popup-scale, 1)) solid var(--border-mid);
}

.asset-picker__search {
  width: 100%;
  height: calc(24px * var(--popup-scale, 1));
  flex: none;
  padding: 0 calc(8px * var(--popup-scale, 1));
  font-family: inherit;
  font-size: calc(var(--font-size-small) * var(--popup-scale, 1));
  color: var(--panel-foreground);
  background: var(--surface-deep);
  border: none;
  border-bottom: calc(1px * var(--popup-scale, 1)) solid var(--border-mid);
}

.asset-picker__search:focus {
  outline: none;
}

.asset-picker__search::placeholder {
  color: var(--dim);
}

.asset-picker__list {
  overflow-y: auto;
}

.asset-picker__item {
  height: calc(24px * var(--popup-scale, 1));
  width: 100%;
  display: flex;
  align-items: center;
  gap: calc(6px * var(--popup-scale, 1));
  padding: 0 calc(8px * var(--popup-scale, 1));
  font-family: inherit;
  font-size: calc(var(--font-size-small) * var(--popup-scale, 1));
  text-align: left;
  color: var(--panel-foreground);
  background: transparent;
  border: none;
  cursor: pointer;
}

.asset-picker__item:hover,
.asset-picker__item--highlight {
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}

/* The stored value's own row, independent of the hover/keyboard cursor above - a left accent bar
   plus a dim background of its own (stronger once the highlight tint also applies, on top), so it
   stays legible even while some other row is highlighted. Aligned with Dropdown's own active-row
   treatment (.dropdown__item--active above) so the two controls' popups read as one family. */
.asset-picker__item--selected {
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  box-shadow: inset calc(2px * var(--popup-scale, 1)) 0 var(--accent);
}

.asset-picker__item-preview {
  flex: none;
  display: grid;
  place-items: center;
  width: calc(18px * var(--popup-scale, 1));
  height: calc(18px * var(--popup-scale, 1));
  color: var(--muted);
  background: var(--well);
  overflow: hidden;
}

.asset-picker__item-preview svg {
  width: calc(12px * var(--popup-scale, 1));
  height: calc(12px * var(--popup-scale, 1));
}

.asset-picker__item-label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.asset-picker__empty {
  padding: calc(8px * var(--popup-scale, 1)) calc(8px * var(--popup-scale, 1));
  font-size: calc(var(--font-size-small) * var(--popup-scale, 1));
  color: var(--dim);
}

.asset-picker__thumb {
  width: 100%;
  height: 100%;
  object-fit: contain;
  /* A checker so a transparent PNG's shape stays readable, matching the content library's own
     asset-row thumbnail (styles/modal.css). */
  background: repeating-conic-gradient(var(--overlay-subtle) 0% 25%, transparent 0% 50%) 0 0 / 6px
    6px;
}

/* Custom checkbox - a flat, sharp box that fills with the accent and draws a tick
   when checked (matches the editor's monochrome-plus-accent chrome). */
.param__checkbox {
  flex: none;
  width: 16px;
  height: 16px;
  margin: 0;
  appearance: none;
  -webkit-appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--surface-deep);
  border: 1px solid var(--border-mid);
  cursor: pointer;
}

.param__checkbox:hover {
  border-color: var(--accent);
}

.param__checkbox:checked {
  background: var(--accent);
  border-color: var(--accent);
}

.param__checkbox:checked::after {
  content: "";
  width: 8px;
  height: 4px;
  border-left: 2px solid var(--on-accent);
  border-bottom: 2px solid var(--on-accent);
  transform: translateY(-1px) rotate(-45deg);
}

.param__checkbox:focus-visible {
  outline: 1px solid var(--accent);
  outline-offset: 1px;
}

.param__placeholder {
  flex: 1;
  min-width: 0;
  font-size: var(--font-size-small);
  color: var(--dim);
}

.param__vector {
  flex: 1;
  min-width: 0;
  display: flex;
  gap: 4px;
}

/* Numeric scrub control (EXAMPLES/ENumberControl, retuned to editor tokens) */

.number-control {
  position: relative;
  /* Auto-size to the shown value (the display span drives the width), with a two-grid-cell
     floor so short values still read and give a comfortable scrub/click target. The width
     tracks the display text; the edit <input> is an absolute overlay so it doesn't disturb it. */
  flex: none;
  width: fit-content;
  min-width: 48px;
  display: flex;
  align-items: center;
  height: 20px;
  background: var(--surface-deep);
  border: 1px solid var(--border-mid);
  overflow: hidden;
  user-select: none;
  font-family: inherit;
  font-size: var(--font-size-small);
  font-variant-numeric: tabular-nums;
  color: var(--panel-foreground);
}

.number-control[data-state="view"] {
  cursor: default;
}

.number-control[data-state="hover"] {
  cursor: ew-resize;
  border-color: var(--border-group);
}

.number-control[data-state="scrubbing"] {
  cursor: ew-resize;
  border-color: var(--accent);
}

.number-control[data-state="edit"] {
  cursor: text;
  border-color: var(--accent);
}

/* Muted: grayed and inert - setDisabled(true) also forces the
   control back to "view" and cancels any in-flight scrub, so this and [data-state="view"] always
   apply together. */
.number-control--disabled {
  cursor: default;
  color: var(--dim);
  background: var(--surface-raised);
  border-color: var(--border-soft);
}

.number-control__display {
  flex: 1;
  min-width: 0;
  text-align: center;
  padding: 0 16px;
  pointer-events: none;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Overlaid on the display (not in flow), so the field's width is set solely by the display
   text and never jumps when entering edit mode. */
.number-control__input {
  position: absolute;
  inset: 0;
  text-align: center;
  padding: 0 16px;
  background: transparent;
  border: none;
  outline: none;
  color: inherit;
  font-family: inherit;
  font-size: inherit;
}

.number-control--compact .number-control__display,
.number-control--compact .number-control__input {
  padding: 0 3px;
}

.number-control__decrement,
.number-control__increment {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 14px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  background: transparent;
  border: none;
  color: var(--muted);
  font-size: 7px;
  cursor: pointer;
}

.number-control__decrement {
  left: 0;
}
.number-control__increment {
  right: 0;
}

.number-control__decrement:hover,
.number-control__increment:hover {
  background: var(--overlay-subtle);
  color: var(--panel-foreground);
}

/* Chevrons only show while hovering the field; hidden in view and edit. */
.number-control[data-state="view"] .number-control__decrement,
.number-control[data-state="view"] .number-control__increment,
.number-control[data-state="edit"] .number-control__decrement,
.number-control[data-state="edit"] .number-control__increment {
  display: none;
}

.number-control[data-state="view"] .number-control__input,
.number-control[data-state="hover"] .number-control__input,
.number-control[data-state="scrubbing"] .number-control__input {
  display: none;
}

/* Kept in flow (just hidden) so it still sizes the field while the overlaid input is shown. */
.number-control[data-state="edit"] .number-control__display {
  visibility: hidden;
}

body.number-control-scrubbing,
body.number-control-scrubbing * {
  cursor: ew-resize !important;
}
/* Color controls
   A swatch button + floating HSV picker, and a gradient (color-ramp) editor.
   Flat, sharp-cornered, accent on interaction (STYLE_GUIDE sections 0/3); popovers are
   screen-space (position: fixed) so pan/zoom never displaces them, like the
   dropdown menu. Alpha reads over a shared checkerboard. */

/* Swatch (the inline trigger) */
.color-swatch {
  flex: 1;
  /* Matches the switch control's fixed track width (switch.css) - the smallest a two-position
     control reads as, so a color control never shrinks below it. */
  min-width: 36px;
  height: 20px;
  /* Overrides .param__input's padding: the fill must span the full content box, flush with
     the border, or the checker behind it leaks out as a ring around the swatch. */
  padding: 0;
  cursor: pointer;
  background: var(--surface-deep) var(--checker);
}

.color-swatch:hover,
.color-swatch--open {
  border-color: var(--accent);
}

.color-swatch__fill {
  display: block;
  width: 100%;
  height: 100%;
}

/* Picker popover: a row of two columns - the HSV/value controls, and (when a palette is wired)
   a full-height palette column stretched to match via the default cross-axis stretch. */
.color-picker {
  position: fixed;
  z-index: var(--z-color-picker);
  display: flex;
  align-items: stretch;
  background: var(--panel-background);
  border: 1px solid var(--border-mid);
}

.color-picker__main {
  width: 200px;
  padding: 8px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.color-picker__sv {
  position: relative;
  width: 100%;
  height: 120px;
  cursor: crosshair;
  touch-action: none;
}

.color-picker__sv-white,
.color-picker__sv-black {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.color-picker__sv-white {
  background: linear-gradient(to right, #fff, rgba(255, 255, 255, 0));
}

.color-picker__sv-black {
  background: linear-gradient(to top, #000, rgba(0, 0, 0, 0));
}

.color-picker__sv-cursor {
  position: absolute;
  width: 12px;
  height: 12px;
  transform: translate(-50%, -50%);
  border: 2px solid #fff;
  border-radius: 50%;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5);
  pointer-events: none;
}

.color-picker__sliders {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.color-picker__hue,
.color-picker__alpha {
  position: relative;
  height: 12px;
  cursor: ew-resize;
  touch-action: none;
}

.color-picker__hue {
  background: linear-gradient(
    to right,
    #f00 0%,
    #ff0 17%,
    #0f0 33%,
    #0ff 50%,
    #00f 67%,
    #f0f 83%,
    #f00 100%
  );
}

.color-picker__alpha {
  background: var(--checker);
}

.color-picker__alpha-fill {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.color-picker__thumb {
  position: absolute;
  top: 50%;
  width: 4px;
  height: 16px;
  transform: translate(-50%, -50%);
  background: #fff;
  border: 1px solid rgba(0, 0, 0, 0.6);
  pointer-events: none;
}

.color-picker__row {
  display: flex;
  align-items: center;
  gap: 6px;
}

.color-picker__preview {
  flex: none;
  width: 22px;
  height: 22px;
  border: 1px solid var(--border-mid);
  background-color: transparent;
  background-image: var(--checker);
}

.color-picker__hex {
  flex: 1;
  min-width: 0;
  height: 22px;
  text-transform: lowercase;
}

/* The Hex/RGB/HSL mode toggle: three equal segments spanning the popover's full width, unlike
   `segmented.css`'s default content-hugging strip. */
.color-picker__mode.segment-group {
  display: flex;
}

.color-picker__mode .segment {
  flex: 1;
  height: 22px;
  padding: 0;
  font-size: var(--font-size-small);
  text-align: center;
}

/* The value row's host: either the hex field above, or (RGB/HSL mode) this row of unlabeled
   compact number fields - same "read the mode toggle above for what each column means" convention
   `.param__vector` uses for a plain vec3/vec4 field. */
.color-picker__value {
  display: flex;
  flex: 1;
  min-width: 0;
}

.color-picker__components {
  display: flex;
  flex: 1;
  min-width: 0;
  gap: 4px;
}

.color-picker__components .number-control {
  flex: 1;
  min-width: 0;
}

/* The palette column: a full-height list of saved colors (row = swatch + label + remove) over a
   full-width save button pinned at the bottom, separated from the HSV column by a border. */
.color-picker__palette {
  width: 140px;
  display: flex;
  flex-direction: column;
  border-left: 1px solid var(--border-mid);
}

.color-picker__palette-list {
  flex: 1;
  /* Lets the list shrink and scroll instead of forcing the popover taller than the HSV column
     once the palette overflows - the default flex-basis (auto/content) would refuse to shrink. */
  min-height: 0;
  overflow-y: auto;
}

.color-picker__palette-row {
  display: flex;
  align-items: center;
  gap: 6px;
  height: 24px;
  padding: 0 6px;
  cursor: pointer;
}

.color-picker__palette-row:hover {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}

.color-picker__palette-swatch {
  flex: none;
  width: 12px;
  height: 12px;
  border: 1px solid var(--border-mid);
  background-color: transparent;
  background-image: var(--checker);
}

.color-picker__palette-label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--font-size-small);
  color: var(--panel-foreground);
}

/* Size only - the guarded danger look (border, hover/counting fill) is the shared
   `.confirm-danger` (countdownConfirm.ts), same pairing as an asset-row/timeline-row remove. */
.color-picker__palette-remove {
  flex: none;
  width: 18px;
  height: 18px;
}

.color-picker__palette-remove svg {
  width: 10px;
  height: 10px;
}

.color-picker__palette-add {
  flex: none;
  height: 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--muted);
  background: transparent;
  border: none;
  border-top: 1px solid var(--border-mid);
  cursor: pointer;
}

.color-picker__palette-add svg {
  width: 11px;
  height: 11px;
}

.color-picker__palette-add:hover {
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}

/* A Texture node's preview: a centred square, its height set by the grid-aligned block
   below the picker and its width matched via aspect-ratio. The chosen texture is a `contain`
   background on the inner layer (so its intrinsic size never widens the card), over the same
   transparency checker the content list uses (see .asset-row__thumb). Empty until a pick. */
.texture-preview {
  align-self: stretch;
  justify-self: stretch;
  min-height: 0;
  margin: 4px 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

.texture-preview__image {
  height: 100%;
  aspect-ratio: 1;
  border: 1px solid var(--border-soft);
  background:
    var(--preview-texture, none) center / contain no-repeat,
    repeating-conic-gradient(var(--overlay-subtle) 0% 25%, transparent 0% 50%) 0 0 / 16px 16px
      var(--surface-deep);
}

/* Color-ramp (gradient) editor */
.param--block {
  height: auto;
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
  padding-top: 4px;
  padding-bottom: 4px;
}

.param--block > .param__label {
  max-width: none;
}

.color-ramp {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.color-ramp__bar-wrap {
  position: relative;
  background: var(--checker);
}

.color-ramp__bar {
  height: 20px;
  cursor: copy;
  border: 1px solid var(--border-mid);
}

.color-ramp__stops {
  position: absolute;
  left: 0;
  right: 0;
  top: 100%;
  height: 10px;
}

.color-ramp__stop {
  position: absolute;
  top: 0;
  width: 10px;
  height: 10px;
  transform: translateX(-50%);
  cursor: ew-resize;
  touch-action: none;
}

/* A downward pin: a small square with a pointer notch toward the bar. */
.color-ramp__stop::before {
  content: "";
  position: absolute;
  top: -4px;
  left: 50%;
  width: 6px;
  height: 6px;
  transform: translateX(-50%) rotate(45deg);
  background: var(--border-mid);
}

.color-ramp__stop-dot {
  position: absolute;
  inset: 0;
  border: 1px solid var(--border-mid);
}

/* Selected: accent ring on the dot (keep its own color visible) + accent pin notch. */
.color-ramp__stop--selected .color-ramp__stop-dot {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent);
}

.color-ramp__stop--selected::before {
  background: var(--accent);
}

.color-ramp__editor {
  display: flex;
  align-items: center;
  gap: 6px;
}

.color-ramp__editor .color-swatch {
  flex: none;
  width: 28px;
}

.color-ramp__editor .number-control {
  flex: none;
  width: 52px;
}

/* The color-ramp stop delete button (the curve anchor delete uses the guarded-trash look). */
.color-ramp__delete {
  flex: none;
  width: 20px;
  height: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--font-size-small);
  color: var(--muted);
  background: var(--surface-deep);
  border: 1px solid var(--border-mid);
  cursor: pointer;
}

.color-ramp__delete:hover:not(:disabled) {
  color: var(--danger);
  border-color: var(--danger);
}

.color-ramp__delete:disabled {
  opacity: 0.4;
  cursor: default;
}
/* Curve editor (inline scalar curve) */
.curve-editor {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

/* The node reserves a whole number of grid rows for the curve block (CURVE_ROW_SPAN). Stretch the
   block to fill that area and let the curve (its plot) take the slack, so the content lands exactly
   on the grid step - no couple-px gap between the last row and the node's bottom border. */
.param--block:has(.curve-editor) {
  align-self: stretch;
}

.param--block:has(.curve-editor) .curve-editor {
  flex: 1;
  min-height: 0;
}

.curve-editor__plot-wrap {
  position: relative;
  /* Grows to absorb the block's leftover height so the content exactly fills the reserved grid
     rows (no empty slack below the last settings row); never shrinks below its resting size. */
  flex: 1;
  min-height: 96px;
  background: var(--surface-deep);
  border: 1px solid var(--border-mid);
  touch-action: none;
}

.curve-editor__plot {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  cursor: copy;
  display: block;
}

.curve-editor__grid line {
  stroke: var(--border-mid);
  stroke-width: 0.5;
  opacity: 0.5;
  vector-effect: non-scaling-stroke;
}

.curve-editor__line {
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.5;
  vector-effect: non-scaling-stroke;
}

.curve-editor__handles {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.curve-editor__handle {
  position: absolute;
  width: 10px;
  height: 10px;
  transform: translate(-50%, -50%);
  background: var(--surface-deep);
  border: 1px solid var(--border-mid);
  cursor: grab;
  pointer-events: auto;
  touch-action: none;
}

/* A sharp anchor is drawn as a diamond to read as a corner; a smooth one is round. */
.curve-editor__handle--smooth {
  border-radius: 50%;
}

.curve-editor__handle--sharp {
  border-radius: 0;
}

.curve-editor__handle--selected {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent);
}

/* The settings host stacks its rows (anchor editor, value range) vertically; each row is a full-width
   flex row below. Without this the host would inherit no direction and the rows would not stack. */
.curve-editor__settings {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* Settings rows: controls left-packed at a uniform gap, starting from the plot's left edge. (An
   edge-to-edge space-between left ugly gaps between the fields and the trailing action buttons.) */
.curve-editor__range,
.curve-editor__editor {
  display: flex;
  align-items: center;
  gap: 6px;
}

.curve-editor__group {
  display: flex;
  align-items: center;
  gap: 6px;
}

.curve-editor__field {
  display: flex;
  align-items: center;
  gap: 4px;
}

.curve-editor__field-label {
  color: var(--muted);
  font-size: 10px;
}

.curve-editor__field .number-control {
  flex: none;
  width: 48px;
}

.curve-editor__interpolation,
.curve-editor__normalize {
  flex: none;
  height: 20px;
  padding: 0 8px;
  font-size: 10px;
  color: var(--muted);
  background: var(--surface-deep);
  border: 1px solid var(--border-mid);
  cursor: pointer;
}

/* Fixed width (border-box) so toggling the label (Smooth <-> Sharp, and their translations) never
   resizes the button and shifts the row. Wide enough for the longest label across locales. */
.curve-editor__interpolation {
  min-width: 64px;
  text-align: center;
}

.curve-editor__interpolation:hover,
.curve-editor__normalize:hover {
  color: var(--panel-foreground);
  border-color: var(--accent);
}

/* The delete button's danger look is the shared `.confirm-danger` (guarded countdown trash); this
   only sizes the cell and its glyph, mirroring `.timeline-row__remove`. */
.curve-editor__delete {
  flex: none;
  width: 20px;
  height: 20px;
  font-size: 11px;
}

.curve-editor__delete svg {
  width: 13px;
  height: 13px;
}

.curve-editor__delete:disabled {
  opacity: 0.4;
  cursor: default;
}
/* Wire layer
   A canvas-sized SVG surface; its inner group carries the camera, so the paths are
   authored in graph coordinates and pan/zoom with the nodes while the surface itself
   stays in screen space and clips to the canvas. Purely visual (never eats pointer
   events - edges are edited by dragging their socket endpoints). */

/* The explicit 100% sizes are load-bearing: an outer `svg` is a replaced element, so offsets
   alone leave it at the 300x150 default box (anchored top-left) that clips away every wire. */
.graph__wires {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.wire {
  fill: none;
  stroke: var(--border-mid);
  stroke-width: 2px;
}

.wire--pending {
  stroke: var(--accent);
  stroke-dasharray: 5 4;
}

/* Route (reroute knot)
   A headerless three-cell node - an input dot, a draggable body, an output dot - used to
   tidy wire layout. It reuses the node drag/select machinery but drops the card's title. */
.node--route {
  min-width: 0;
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  padding: 0 5px;
  background: var(--surface-raised);
  /* Frame is the base `.node` inset box-shadow (--node-frame defaults to --border-mid); no
     `border` here, else it would re-add a layout border and double up with the shadow. */
}

.node--route.node--selected {
  /* --node-frame comes from `.node--selected` (accent); only the fill differs here. */
  background: color-mix(in srgb, var(--accent) 8%, var(--surface-raised));
}

/* Knife stroke (Alt-drag wire cut)
   A screen-space overlay above the nodes; purely visual, never eats pointer events. */
/* Sized like `.graph__wires` above, and for the same replaced-element reason: the stroke is
   drawn in root-relative coordinates and must reach every corner of the canvas. */
.graph__knife {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 6;
  pointer-events: none;
}

.graph__knife path {
  fill: none;
  stroke: var(--danger);
  stroke-width: 2px;
  stroke-dasharray: 4 3;
}

/* Comment boxes
   The back layer carries the camera (so comments pan/zoom with the graph) and stacks behind
   the wires and nodes. The body is click-through - only the header and the corner handles
   capture pointer events - so nodes over a comment and a marquee across it still work. */
.graph__comments {
  position: absolute;
  top: 0;
  left: 0;
  transform-origin: 0 0;
  pointer-events: none;
}

.comment {
  position: absolute;
  box-sizing: border-box;
  background: color-mix(in srgb, var(--muted) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--muted) 40%, transparent);
  pointer-events: none;
}

.comment--selected {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}

.comment--selected .comment__header {
  background: color-mix(in srgb, var(--accent) 18%, transparent);
}

.comment__header {
  pointer-events: auto;
  padding: 3px 8px;
  font-size: 12px;
  font-weight: 600;
  color: var(--panel-foreground);
  background: color-mix(in srgb, var(--muted) 22%, transparent);
  border-bottom: 1px solid color-mix(in srgb, var(--muted) 30%, transparent);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  cursor: grab;
  outline: none;
}

.comment--editing .comment__header {
  cursor: text;
  background: color-mix(in srgb, var(--accent) 12%, var(--panel-background));
}

/* Edge + corner grab handles. The edge strips run the full side and stay invisible (they
   only need to change the cursor and catch a drag); the corner squares sit on top of them. */
.comment__handle {
  position: absolute;
  pointer-events: auto;
}

/* Edge strips: a thin band straddling each side. */
.comment__handle--n,
.comment__handle--s {
  left: 6px;
  right: 6px;
  height: 8px;
  cursor: ns-resize;
}

.comment__handle--e,
.comment__handle--w {
  top: 6px;
  bottom: 6px;
  width: 8px;
  cursor: ew-resize;
}

.comment__handle--n {
  top: -4px;
}

.comment__handle--s {
  bottom: -4px;
}

.comment__handle--w {
  left: -4px;
}

.comment__handle--e {
  right: -4px;
}

/* Corner zones: invisible diagonal-resize catchers (no visible grip - the editor shows no
   handle chrome); they just widen the corner hit area and set the cursor. */
.comment__handle--nw,
.comment__handle--ne,
.comment__handle--sw,
.comment__handle--se {
  width: 12px;
  height: 12px;
}

.comment__handle--nw {
  top: -6px;
  left: -6px;
  cursor: nwse-resize;
}

.comment__handle--ne {
  top: -6px;
  right: -6px;
  cursor: nesw-resize;
}

.comment__handle--sw {
  bottom: -6px;
  left: -6px;
  cursor: nesw-resize;
}

.comment__handle--se {
  bottom: -6px;
  right: -6px;
  cursor: nwse-resize;
}

/* Behavior-sink attribute editor */

.node__attributes {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 6px 8px;
  border-top: 1px solid var(--border-soft);
}

/* The lone "add attribute" row at the foot of a behavior sink: name / type / confirm. */
.attr__row {
  display: flex;
  align-items: center;
  gap: 4px;
}

.attr__new-name {
  flex: 1;
  min-width: 0;
}

.attr__new-name--invalid {
  outline: 1px solid var(--danger);
}

.attr__name--invalid {
  outline: 1px solid var(--danger);
}

.attr__type {
  width: 52px;
}

.attr__add {
  display: inline-flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  padding: 0;
  font-size: 11px;
  color: var(--muted);
  background: var(--overlay-subtle);
  border: 1px solid var(--border-soft);
  cursor: pointer;
}

.attr__add svg {
  width: 12px;
  height: 12px;
}

.attr__add:hover {
  color: var(--panel-foreground);
  border-color: var(--border-mid);
}

/* Delete is guarded by the shared countdown-confirm control (confirm.css); sizing only here. */
.attr__remove {
  flex: none;
  width: 18px;
  height: 18px;
  font-size: 11px;
}

.attr__remove svg {
  width: 12px;
  height: 12px;
}

/* On a sink's `attr:<name>` input row the remove button is a grid cell in the right lane;
   keep it compact and right-aligned rather than stretched across the span. */
.node__body > .attr__remove {
  justify-self: end;
  align-self: center;
}

/* Add-node dialog (STYLE_GUIDE section 4.2) */

.add-node-menu {
  position: fixed;
  z-index: var(--z-add-node-menu);
  min-width: 224px;
  width: max-content;
  display: flex;
  flex-direction: column;
  background: var(--panel-background);
  border: 1px solid var(--border-mid);
}

.add-node-menu__search {
  width: 100%;
  height: 30px;
  flex: none;
  padding: 0 10px;
  font-family: inherit;
  font-size: 12px;
  color: var(--panel-foreground);
  background: var(--surface-deep);
  border: none;
  border-bottom: 1px solid var(--border-soft);
  outline: none;
}

.add-node-menu__search::placeholder {
  color: var(--dim);
}

/* Stepped scrolling: the wheel advances exactly one row (addNodeMenu.ts), so scrollTop
   stays a multiple of the row height and no partial row is ever shown. */
.add-node-menu__list {
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--border-mid) transparent;
}

.add-node-menu__list::-webkit-scrollbar {
  width: 8px;
}

.add-node-menu__list::-webkit-scrollbar-track {
  background: transparent;
}

.add-node-menu__list::-webkit-scrollbar-thumb {
  background: var(--border-mid);
  border: 2px solid var(--panel-background);
}

.add-node-menu__list::-webkit-scrollbar-thumb:hover {
  background: var(--border-group);
}

.add-node-menu__item {
  height: 28px;
  width: 100%;
  display: flex;
  align-items: center;
  padding: 0 10px;
  font-size: 13px;
  text-align: left;
  color: var(--panel-foreground);
  background: transparent;
  border: none;
  cursor: pointer;
  white-space: nowrap;
}

.add-node-menu__item:hover,
.add-node-menu__item--active {
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}

.add-node-menu__empty {
  padding: 8px 10px;
  font-size: var(--font-size-small);
  color: var(--dim);
}

/* Node help card - the shared floating documentation panel (helpCard.ts), shown above the
   add-node menu on hover / keyboard-highlight and above a node header, spanning the full width
   of whichever opened it (width set inline). Non-interactive (never steals the pointer). */
.node-help {
  position: fixed;
  z-index: var(--z-popover);
  padding: 8px 10px;
  background: var(--panel-background);
  border: 1px solid var(--border-mid);
  pointer-events: none;
}

.node-help__title {
  margin-bottom: 4px;
  font-size: 12px;
  font-weight: 600;
  color: var(--panel-foreground);
}

.node-help__body {
  font-size: 12px;
  line-height: 1.4;
  color: var(--muted);
}
/* Timeline shared bits */

/* Empty-state row shown in the timeline grid before any object exists. */
.rows-placeholder {
  height: var(--row-height);
  display: flex;
  align-items: center;
  padding: 0 12px;
  font-size: var(--font-size-small);
  color: var(--dim);
}

/* Timeline: the bottom band - object rows + their keyframe lanes */

/*
 * The panel is a fixed toolbar over a scrolling grid. The grid is split by a
 * fixed-width label column (the object list) and flexible lanes; the head row and
 * every object row share `--timeline-label-width`, so labels and lanes stay aligned.
 */
.timeline {
  --timeline-label-width: 232px;
  --timeline-inspector-width: 240px;
  /* Frame-tick + zebra tints, shared by the lane background layers. The ticks are kept
     faint so they read as a grid, not foreground content. */
  --timeline-zebra: color-mix(in srgb, var(--panel-foreground) 3%, transparent);
  --timeline-tick-line: color-mix(in srgb, var(--panel-foreground) 4%, transparent);
  --timeline-tick-line-major: color-mix(in srgb, var(--panel-foreground) 8%, transparent);
  display: flex;
  flex: 1;
  min-width: 0;
  flex-direction: column;
  background: var(--panel-background);
}

/* Transport + timeline-settings cluster, hosted centre-stage on the middlebar. */
.transport-bar {
  display: flex;
  align-items: center;
  gap: 16px;
}

.timeline__transport {
  display: flex;
  gap: 4px;
}

/* Transport buttons match the middlebar's icon-only cells (28x28) so the whole bar reads as one
   family. */
.timeline__transport-btn {
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--muted);
  background: var(--surface-raised);
  border: 1px solid var(--border-mid);
  cursor: pointer;
}

.timeline__transport-btn svg {
  width: 15px;
  height: 15px;
}

.timeline__transport-btn:hover {
  border-color: var(--accent);
  color: var(--accent);
}

.timeline__transport-btn--active {
  color: var(--accent);
  border-color: var(--accent);
}

/* Icon toggle button - replaces checkboxes. Off reads as a muted icon cell;
   when on it lights up with a filled accent background (STYLE_GUIDE section 4.1 look). */
.toggle-button {
  flex: none;
  width: 24px;
  height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  color: var(--muted);
  background: var(--surface-raised);
  border: 1px solid var(--border-mid);
  cursor: pointer;
}

.toggle-button svg {
  width: 14px;
  height: 14px;
}

.toggle-button:hover {
  border-color: var(--accent);
  color: var(--accent);
}

.toggle-button--active,
.toggle-button--active:hover {
  color: var(--on-accent);
  background: var(--accent);
  border-color: var(--accent);
}

/* Middlebar-sized variant (28x28), matching the transport / action buttons on the middlebar. */
.toggle-button--lg {
  width: 28px;
  height: 28px;
}

.toggle-button--lg svg {
  width: 15px;
  height: 15px;
}

/* A labelled toolbar control cluster (Length, FPS). */
.timeline__group {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: var(--font-size-small);
  color: var(--muted);
}

.timeline__group .number-control {
  width: 68px;
}

.timeline__group-label {
  white-space: nowrap;
}

.timeline__unit {
  color: var(--dim);
}

/* Grid: the scrolling rows + playhead (the stage) beside the item inspector. */
.timeline__grid {
  display: flex;
  flex: 1;
  min-height: 0;
}

.timeline__stage {
  position: relative;
  flex: 1;
  min-width: 0;
  overflow: hidden;
}

.timeline__inspector {
  flex: none;
  width: var(--timeline-inspector-width);
  display: flex;
  flex-direction: column;
  padding: 10px 12px;
  border-left: 1px solid var(--border-soft);
  background: var(--panel-background);
  overflow-y: auto;
}

/*
 * Horizontal panning is programmatic (driven by the custom scrollbar / ctrl-wheel, which set
 * `scrollLeft`), so the native horizontal scrollbar is off (`overflow-x: hidden`); vertical row
 * overflow keeps its native scrollbar.
 */
.timeline__scroll {
  height: 100%;
  overflow-x: hidden;
  overflow-y: auto;
}

/*
 * The rows widen with the horizontal zoom: at zoom 1 they exactly fill the viewport (the calc
 * resolves to 100%); above it the lanes grow past it and `scrollLeft` picks the visible slice. The
 * `100%` here is the scroll viewport width (the rows' containing block), NOT the widened rows.
 */
.timeline__rows {
  width: calc(
    var(--timeline-label-width) + (100% - var(--timeline-label-width)) * var(--timeline-zoom, 1)
  );
  /* Fill the scroll viewport as a column so the trailing filler row can grow into any space left
     below the object rows. Object rows keep their fixed height (flex: none). */
  display: flex;
  flex-direction: column;
  min-height: 100%;
}

/* One object row: a fixed label cell (select / rename / remove) + its keyframe lane. */
.timeline-row {
  display: flex;
  flex: none;
  height: var(--row-height);
}

/* Filler row: grows to fill the space below the last object so the lane visual (ticks +
   gutters + caret) and its click/marquee/pan interactions continue to the bottom of the panel. */
.timeline-row--filler {
  flex: 1 1 auto;
  height: auto;
  min-height: 0;
}

.timeline-row--filler .timeline-row__label {
  cursor: default;
}

/* Each entity type carries its own accent hue via --row-accent, consumed by the icon badge,
   the selected-block tint and this object's keyframes. Defaults to the app accent (untyped rows). */
.timeline-row--emitter {
  --row-accent: var(--accent-emitter);
}

.timeline-row--group {
  --row-accent: var(--accent-group);
}

.timeline-row--mesh {
  --row-accent: var(--accent-mesh);
}

/* Zebra striping stands in for divider lines: every other row is lifted a hair in brightness. It's
   a translucent overlay (background-image), so it layers on top of whatever background-color a row
   already carries (active / sub-row / header tints). The label carries only the zebra; the lane also
   carries the frame-tick gridlines below, so its even-row rule restates them under the zebra. */
.timeline-row:nth-child(even) .timeline-row__label {
  background-image: linear-gradient(var(--timeline-zebra), var(--timeline-zebra));
}

.timeline-row__label {
  flex: none;
  width: var(--timeline-label-width);
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 8px 0 12px;
  font-size: 12px;
  color: var(--muted);
  /* Pinned while the lanes scroll horizontally under the zoom, so the object list stays put
     (Blender-style). Top of the lane stack (markers z 3, caret z 2) so it covers them when
     scrolled. */
  position: sticky;
  left: 0;
  z-index: 4;
  /* An object (header) row's label carries one shade; its sub-rows a greyer one (below), so the
     active element reads apart from its channels while the lanes keep the timeline background. */
  background: var(--surface-raised);
  border-right: 1px solid var(--border-soft);
  cursor: pointer;
}

/* Hover an object's label in the OUTLINE column to highlight its whole block - name + lane;
   hovering the lane (the timeline) does not. Selectable object rows only, excluding the active one
   (already accent-tinted). The label brightens on its own :hover; the lane follows via :has(). */
.timeline-row:not(.timeline-row--track):not(.timeline-row--filler):not(.timeline-row--active)
  .timeline-row__label:hover {
  color: var(--panel-foreground);
  background-color: color-mix(in srgb, var(--panel-foreground) 8%, var(--surface-raised));
}

.timeline-row:not(.timeline-row--track):not(.timeline-row--filler):not(.timeline-row--active):has(
    .timeline-row__label:hover
  )
  .timeline-row__lane {
  background-color: color-mix(in srgb, var(--panel-foreground) 6%, transparent);
}

/* The tinted background alone marks the active row - no left accent stripe. Tinted by the
   entity type's accent. */
.timeline-row--active .timeline-row__label {
  color: var(--panel-foreground);
  background: color-mix(in srgb, var(--row-accent, var(--accent)) 16%, var(--surface-raised));
}

.timeline-row__marker {
  width: 7px;
  height: 7px;
  flex: none;
  background: var(--dim);
}

.timeline-row--active .timeline-row__marker {
  background: var(--row-accent, var(--accent));
}

/* Object badge before an entity's name (emitter gem / VFX cube / mesh). Carries the entity type's
   accent hue so the types read apart at a glance: a muted tint at rest, its full accent
   when hovered or active. */
.timeline-row__icon {
  flex: none;
  width: 14px;
  height: 14px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: color-mix(in srgb, var(--row-accent, var(--dim)) 55%, var(--dim));
}

.timeline-row__icon svg {
  width: 14px;
  height: 14px;
}

.timeline-row__label:hover .timeline-row__icon {
  color: color-mix(in srgb, var(--row-accent, var(--muted)) 80%, var(--muted));
}

.timeline-row--active .timeline-row__icon {
  color: var(--row-accent, var(--accent));
}

/* Value sub-row: an animated track listed under its emitter - indented, with its name at the
   start of the label and only that track's keyframes on its lane. Sub-rows only render for the
   active object, so they carry a *paler* wash of the same accent as their header. */
.timeline-track__label {
  padding-left: 28px;
  font-size: var(--font-size-small);
  background: color-mix(in srgb, var(--row-accent, var(--accent)) 8%, var(--panel-background));
}

.timeline-track__marker {
  width: 4px;
  height: 4px;
  flex: none;
  background: var(--border-mid);
}

.timeline-row__name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Inline rename field, shown in place of the name on double-click. */
.timeline-row__rename {
  flex: 1;
  min-width: 0;
  height: 20px;
  padding: 0 4px;
  font: inherit;
  color: var(--panel-foreground);
  background: var(--surface-raised);
  border: 1px solid var(--accent);
}

/* Remove control: a square-bordered trash button guarded by the countdown-confirm
   component (three clicks to delete). Always shown (on every removable row, active or not). The
   danger-tinted look is the shared `.confirm-danger`; this only sets the row-remove size. */
.timeline-row__remove {
  flex: none;
  width: 20px;
  height: 20px;
  font-size: 11px;
}

.timeline-row__remove svg {
  width: 14px;
  height: 14px;
}

/* Mute control (outline eye): unlike `.timeline-row__action`, always shown (opacity 1) at rest, not
   only on the active/track row - the point is to see at a glance which objects are hidden. Sits
   before Remove. Toggled `is-hidden` dims the row to read as "off"; the icon itself swaps between
   the open/slashed-eye glyphs in script, not CSS. */
.timeline-row__visibility {
  flex: none;
  width: 20px;
  height: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  color: var(--muted);
  background: var(--surface-raised);
  border: 1px solid var(--border-mid);
  cursor: pointer;
}

.timeline-row__visibility svg {
  width: 14px;
  height: 14px;
}

.timeline-row__visibility:hover {
  color: var(--accent);
  border-color: var(--accent);
}

.timeline-row__visibility.is-hidden {
  color: var(--dim);
}

/* Row-action buttons: add-burst / add-play on an emitter header and add-key on a
   transform sub-row. Square-bordered icon cells matching the middlebar's icon buttons. Shown only for
   the selected entity - its active header and its (only-then-rendered) track sub-rows; an inactive
   entity's header hides them (its remove stays), so a resting timeline reads clean. */
.timeline-row__action {
  flex: none;
  width: 20px;
  height: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  color: var(--muted);
  background: var(--surface-raised);
  border: 1px solid var(--border-mid);
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
}

.timeline-row__action svg {
  width: 14px;
  height: 14px;
}

.timeline-row--active .timeline-row__action,
.timeline-row--track .timeline-row__action {
  opacity: 1;
  pointer-events: auto;
}

.timeline-row__action:hover {
  color: var(--accent);
  border-color: var(--accent);
}

/* Fake-track toggle (a `.timeline-row__action` on a channel / value sub-row): `is-live` marks the
   track's keyframes editor-only (excluded from the TS export). The VFX group's own channels are
   always live and the toggle is `is-locked` there - forced on, not interactive. `is-locked` is a
   dimmed look only (never `pointer-events: none` / native `disabled`), so the button stays
   hoverable and its tooltip - which explains WHY it's locked - still shows; the click is a no-op
   in script instead. */
.timeline-row__action.is-live {
  color: var(--accent);
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 16%, var(--surface-raised));
}

.timeline-row__action.is-locked {
  cursor: default;
  opacity: 0.55;
}

/* Lane: the time track for one object; keyframes are positioned by time within it. No divider
   line - rows are told apart by the alternating wash below. It also carries the frame-tick
   gridlines: a 1px line tiled at the minor and major periods that TimelineViewport
   publishes for the current zoom. Each layer is a single-line tile sized to its period (so it
   tiles seamlessly) and shifted by `--timeline-tick-offset` to align a line to frame 0. */
.timeline-row__lane {
  position: relative;
  flex: 1;
  min-width: 0;
  background-repeat: repeat;
  background-position: var(--timeline-tick-offset, 0) 0;
  background-size:
    var(--timeline-major-px, 100000px) 100%,
    var(--timeline-tick-px, 100000px) 100%;
  background-image:
    linear-gradient(90deg, var(--timeline-tick-line-major) 0 1px, transparent 1px),
    linear-gradient(90deg, var(--timeline-tick-line) 0 1px, transparent 1px);
}

/* Even lane: zebra wash (unsized, at position 0) layered over the same two tick tiles. */
.timeline-row:nth-child(even) .timeline-row__lane {
  background-position:
    0 0,
    var(--timeline-tick-offset, 0) 0,
    var(--timeline-tick-offset, 0) 0;
  background-size:
    auto,
    var(--timeline-major-px, 100000px) 100%,
    var(--timeline-tick-px, 100000px) 100%;
  background-image:
    linear-gradient(var(--timeline-zebra), var(--timeline-zebra)),
    linear-gradient(90deg, var(--timeline-tick-line-major) 0 1px, transparent 1px),
    linear-gradient(90deg, var(--timeline-tick-line) 0 1px, transparent 1px);
}

/* Selecting an object tints its whole row, not just the label: the header's lane picks up the
   same accent wash as its label, and the object's sub-row lanes the paler one (matching their
   labels) - so the full selected block reads as highlighted, ticks still showing through. */
.timeline-row--active .timeline-row__lane {
  background-color: color-mix(in srgb, var(--row-accent, var(--accent)) 12%, transparent);
}

.timeline-row--track .timeline-row__lane {
  background-color: color-mix(in srgb, var(--row-accent, var(--accent)) 8%, transparent);
}

/* A baked keyframe: a small diamond centred on its time (left set inline as a %). Above the caret
   (z 3 vs its z 2) so a key under the playhead is still grabbable. Wears the owning entity
   type's accent hue. */
.timeline-key {
  position: absolute;
  top: 50%;
  width: 9px;
  height: 9px;
  transform: translate(-50%, -50%) rotate(45deg);
  background: var(--row-accent, var(--accent));
  border: 1px solid var(--panel-background);
  z-index: 3;
  cursor: grab;
}

.timeline-key:active {
  cursor: grabbing;
}

/* Summary diamonds mirrored onto an object's header row: smaller, dimmed and
   non-interactive - they only indicate that the collapsed object carries keys there. */
.timeline-key--summary {
  width: 6px;
  height: 6px;
  background: var(--dim);
  border-color: transparent;
  pointer-events: none;
  cursor: default;
}

/*
 * The single playhead, drawn over the lanes only. Under horizontal zoom its `left` is set inline in
 * pixels by TimelineViewport (mapping the caret time through the current scroll), and it is hidden
 * when the caret time falls outside the visible window. The `left` fallback below only shows for the
 * first paint before that runs.
 */
/* The caret sits BELOW the keyframes/events in the stack (z 2 vs their z 3, task 4) so a marker
   under the caret takes the press - yet the caret still grabs empty lane for a full-height scrub.
   The sticky label (z 4) stays above both, covering markers that scroll under it. */
.timeline__playhead {
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--timeline-label-width);
  width: 1px;
  background: var(--accent);
  z-index: 2;
  cursor: ew-resize;
}

/* A widened, transparent hit strip so the 1px caret line is easy to grab for scrubbing. Markers
   (z 3) still sit above it, so a press on a key/event under the caret hits the marker. */
.timeline__playhead::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 11px;
  transform: translateX(-50%);
}

/* The padded margins that gray out inactive time before frame 0 and past the authored end:
   translucent washes positioned in px by TimelineViewport. Translucent so each row's zebra +
   frame ticks still read through, keeping the alternation across the gray area. */
.timeline__gutter {
  position: absolute;
  top: 0;
  bottom: 0;
  z-index: 1;
  pointer-events: none;
  background: color-mix(in srgb, #000 20%, transparent);
}

.timeline__gutter--before {
  border-right: 1px solid var(--border-soft);
}

.timeline__gutter--after {
  border-left: 1px solid var(--border-soft);
}

/* Triangle caps at each end of the caret, points aimed at its centre. */
.timeline__playhead-cap {
  position: absolute;
  left: 50%;
  width: 0;
  height: 0;
  transform: translateX(-50%);
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
}

.timeline__playhead-cap--top {
  top: 0;
  border-top: 6px solid var(--accent);
}

.timeline__playhead-cap--bottom {
  bottom: 0;
  border-bottom: 6px solid var(--accent);
}

/* Infinite timeline: the caret (line + caps) switches to the infinite colour. */
.timeline__playhead--infinite {
  background: var(--timeline-infinite);
}

.timeline__playhead--infinite .timeline__playhead-cap--top {
  border-top-color: var(--timeline-infinite);
}

.timeline__playhead--infinite .timeline__playhead-cap--bottom {
  border-bottom-color: var(--timeline-infinite);
}

/* Spawn-event markers on a lane: a burst dot and a play bar (its width spans the duration). Above
   the caret (z 3 vs its z 2) so an event under the playhead is still grabbable. */
.timeline-event {
  position: absolute;
  top: 50%;
  z-index: 3;
  transform: translateY(-50%);
  cursor: grab;
}

.timeline-event:active {
  cursor: grabbing;
}

.timeline-event--burst {
  width: 8px;
  height: 8px;
  margin-left: -4px;
  border-radius: 50%;
  background: var(--timeline-event);
  border: 1px solid var(--panel-background);
}

.timeline-event--play {
  height: 8px;
  min-width: 3px;
  background: color-mix(in srgb, var(--timeline-event) 40%, transparent);
}

/* An infinite play (duration 0): it wears the caret's violet and continues PAST the
   timeline end to the right, reading as unbounded because nothing marks its far edge. */
.timeline-event--infinite {
  background: color-mix(in srgb, var(--timeline-infinite) 40%, transparent);
}

/* A selected keyframe rings on the outside; a selected event rings on the INSIDE so the
   ring never spills past a play bar clipped to the timeline end. Each rings in its OWN color
   (the owning row's accent for a key, the spawn-event amber for a burst/play) rather than the
   generic app accent, so selection reads as "this item" and not just "something is selected". */
.timeline-key.is-selected {
  outline: 2px solid var(--row-accent, var(--accent));
  outline-offset: 1px;
}

.timeline-event.is-selected {
  box-shadow: inset 0 0 0 2px var(--timeline-event);
}

/* An infinite play keeps its violet ring even selected - same rule as its unselected gradient. */
.timeline-event--infinite.is-selected {
  box-shadow: inset 0 0 0 2px var(--timeline-infinite);
}

/* Rubber-band selection rectangle, drawn in client coordinates over the lanes. */
.timeline-marquee {
  position: fixed;
  z-index: var(--z-timeline-marquee);
  pointer-events: none;
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  border: 1px solid var(--accent);
}

/* Item inspector (right column): the selected keyframe / event's editable fields. Group-tier
   caption (tokens.css) - the panel's one heading, naming what is currently being edited. */
.timeline-inspector__title {
  margin-bottom: 8px;
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
}

/* The empty-state hint is centred in the inspector block; `margin: auto` centres it
   both ways in the flex column, so real inspector content still stacks from the top. */
.timeline-inspector__hint {
  margin: auto;
  font-size: var(--font-size-small);
  color: var(--dim);
  text-align: center;
}

.timeline-inspector__row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 6px;
}

.timeline-inspector__label {
  flex: none;
  width: 56px;
  font-size: var(--font-size-small);
  color: var(--muted);
}

/* The scrubbable field(s) fill the rest of the inspector row. */
.timeline-inspector__row > .number-control {
  flex: 1;
  min-width: 0;
}

/* The play event's infinite-duration toggle: sits left of the duration
   NumberControl inside the same control column - the row's [label]|[controls] boundary doesn't
   move, the NumberControl (flex: 1) just shares the remaining space with this fixed-width button.
   Wears the same violet as an infinite play's own marker/caret when active. */
.timeline-inspector__infinite {
  flex: none;
  width: 20px;
  height: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  color: var(--muted);
  background: var(--surface-deep);
  border: 1px solid var(--border-mid);
  cursor: pointer;
}

.timeline-inspector__infinite svg {
  width: 13px;
  height: 13px;
}

.timeline-inspector__infinite:hover {
  color: var(--timeline-infinite);
  border-color: var(--timeline-infinite);
}

.timeline-inspector__infinite.is-active {
  color: var(--timeline-infinite);
  border-color: var(--timeline-infinite);
  background: color-mix(in srgb, var(--timeline-infinite) 16%, var(--surface-deep));
}

/* Vector editor: components stacked one per row (X / Y / Z), each a scrubbable field. */
.timeline-inspector__vector-group {
  margin-bottom: 6px;
}

.timeline-inspector__vector {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-top: 4px;
}

.timeline-inspector__vec-comp {
  display: flex;
  align-items: center;
  gap: 6px;
}

.timeline-inspector__axis {
  flex: none;
  width: 12px;
  font-size: var(--font-size-small);
  color: var(--dim);
}

.timeline-inspector__vec-comp .number-control {
  flex: 1;
  min-width: 0;
}

.timeline-inspector__remove {
  width: 100%;
  height: 24px;
  margin-top: 4px;
  font-size: var(--font-size-small);
  color: var(--danger);
  background: transparent;
  border: 1px solid var(--border-mid);
  cursor: pointer;
}

.timeline-inspector__remove:hover {
  border-color: var(--danger);
  color: var(--on-accent);
  background: var(--danger);
}

/* Floating "add event" menu (right-click a lane) - STYLE_GUIDE section 4.2 floating popover. */
.timeline-menu {
  position: fixed;
  z-index: var(--z-context-menu);
  display: none;
  min-width: 120px;
  padding: 4px;
  background: var(--panel-background);
  border: 1px solid var(--border-hard);
}

.timeline-menu__item {
  display: block;
  width: 100%;
  height: 26px;
  padding: 0 10px;
  text-align: left;
  font-size: 12px;
  color: var(--panel-foreground);
  background: transparent;
  border: none;
  cursor: pointer;
}

.timeline-menu__item:hover {
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  color: var(--accent);
}

/*
 * Bottom scrollbar (Blender-style): a window pill over a track that spans the lane area only - left/
 * right padding clears the fixed label column and the inspector so the track aligns with the lanes.
 * Drag the pill to pan; drag either end handle to zoom; ctrl/cmd + wheel over the lanes zooms to the
 * cursor. The pill's left/width mirror the visible fraction of the timeline.
 */
/*
 * Seconds ruler, docked above the grid. `rulerLane` is a flex sibling between two spacers sized to
 * the label/inspector columns, so its own x origin already lines up with the lanes' below with no
 * extra offset math (the flag's `left` needs none - see TimelineViewport). Border-bottom lives only
 * on the two spacers, the scrollbar's own mirror image on the facing edge (bottom here, top there);
 * the lane itself stays unbordered so the flag sits flush against the caret's triangle right below.
 */
/* Three columns, like the grid below: label/inspector-width spacers flanking a lane with its own
   (darker) fill. The spacers deliberately do NOT reuse the colour of the column they cap - that
   would read as a continuation of the outline/inspector panel rather than the ruler's own corner -
   so they share the scrollbar's own corner tone instead (see .timeline__scrollbar below). */
.timeline__ruler {
  flex: none;
  height: 20px;
  display: flex;
}

/* No border-bottom on the ruler itself - only on these corner spacers. The lane in between stays
   unbordered so the flag sits flush against the caret's top-cap triangle right below it; a border
   there would draw a line between the two instead of one continuous shape. */
.timeline__ruler-labels,
.timeline__ruler-inspector {
  flex: none;
  background: color-mix(in srgb, var(--surface-raised) 88%, var(--panel-foreground));
  border-bottom: 1px solid var(--border-soft);
}

.timeline__ruler-labels {
  width: var(--timeline-label-width);
}

.timeline__ruler-inspector {
  width: var(--timeline-inspector-width);
  border-left: 1px solid var(--border-soft);
}

.timeline__ruler-lane {
  position: relative;
  flex: 1;
  min-width: 0;
  /* Clips here, not on the outer .timeline__ruler: a tick label (or the flag) near either edge is
     an absolutely-positioned descendant, which paints above the plain in-flow spacer backgrounds
     regardless of DOM order - without this, it draws over the label/inspector corners instead of
     stopping at the lane's own edge. */
  overflow: hidden;
  background: color-mix(in srgb, var(--surface-deep) 92%, var(--panel-foreground));
}

.timeline__ruler-tick {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 1px;
  background: var(--border-soft);
}

.timeline__ruler-tick-label {
  position: absolute;
  top: 50%;
  left: 4px;
  transform: translateY(-50%);
  font-size: 10px;
  font-variant-numeric: tabular-nums;
  color: var(--dim);
  white-space: nowrap;
  pointer-events: none;
}

/* The playhead's own flag (Blender-style): the caret's current frame, pinned over its x on the
   ruler. Flush with the ruler's own bottom edge and wider than the caret's top-cap triangle right
   below it (.timeline__playhead-cap--top), so the two read as one continuous shape instead of a
   floating label over a disconnected line - no separate pointer/tail of its own, and no rounded
   corners, matching the rest of the chrome. Colour matches the caret 1:1 (plain accent, or the
   infinite variant), never its own fixed colour. Draggable/clickable like the caret (wired by the
   panel, alongside the caret's own scrub listener) - never `pointer-events: none`. */
.timeline__ruler-flag {
  position: absolute;
  top: 3px;
  bottom: 0;
  /* Reserves room for a 2-digit frame number even when the current one is shorter (e.g. "5"), so
     the box doesn't narrow and re-widen as playback crosses single/double digits; a 3+ digit
     number still grows it past this floor. */
  min-width: calc(2ch + 8px);
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 4px;
  font-size: 10px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--on-accent);
  background: var(--accent);
  z-index: 2;
  cursor: ew-resize;
}

.timeline__ruler-flag--infinite {
  background: var(--timeline-infinite);
}

/* The label column and inspector are cleared with padding (not margin) so the strip's own background
   fills them: the reserved gutter under the object list reads as a tone between the object-list
   column (surface-raised) and the scroll background, not a stark dark rectangle. */
.timeline__scrollbar {
  flex: none;
  height: 14px;
  padding: 0 var(--timeline-inspector-width) 0 var(--timeline-label-width);
  display: flex;
  align-items: center;
  border-top: 1px solid var(--border-soft);
  background: color-mix(in srgb, var(--surface-raised) 88%, var(--panel-foreground));
}

.timeline__scrollbar-track {
  position: relative;
  flex: 1;
  height: 8px;
  background: color-mix(in srgb, var(--panel-foreground) 6%, transparent);
}

.timeline__scrollbar-thumb {
  position: absolute;
  top: 0;
  bottom: 0;
  min-width: 18px;
  background: color-mix(in srgb, var(--panel-foreground) 22%, transparent);
  cursor: grab;
}

.timeline__scrollbar-thumb:hover {
  background: color-mix(in srgb, var(--panel-foreground) 34%, transparent);
}

.timeline__scrollbar-thumb:active {
  cursor: grabbing;
}

/* The pill's editable ends: a grab strip at each end whose visible grip line signals that
   the border can be dragged to zoom (the far end pinned), Blender-style. The grip brightens with the
   thumb and turns accent when its own end is hovered. */
.timeline__scrollbar-handle {
  position: absolute;
  top: -3px;
  bottom: -3px;
  width: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: ew-resize;
}

.timeline__scrollbar-handle::after {
  content: "";
  width: 2px;
  height: 10px;
  background: color-mix(in srgb, var(--panel-background) 55%, transparent);
}

.timeline__scrollbar-thumb:hover .timeline__scrollbar-handle::after {
  background: color-mix(in srgb, var(--panel-background) 80%, transparent);
}

.timeline__scrollbar-handle:hover::after {
  background: var(--accent);
}

.timeline__scrollbar-handle--left {
  left: -1px;
}

.timeline__scrollbar-handle--right {
  right: -1px;
}
/* Segmented control (STYLE_GUIDE section 4.7) */

.segment-group {
  display: inline-flex;
}

/* `position: relative` is unconditional (not just on hover/active) so every segment has a
   stacking context to raise via z-index below - otherwise whichever segment paints later in DOM
   order wins the shared -1px border regardless of which one is actually active/hovered. */
.segment {
  position: relative;
  height: 26px;
  padding: 0 12px;
  font-size: 12px;
  font-weight: 600;
  color: var(--muted);
  background: var(--surface-raised);
  border: 1px solid var(--border-mid);
  cursor: pointer;
}

.segment + .segment {
  margin-left: -1px;
}

.segment:hover,
.segment--active {
  z-index: 1;
}

/* Icon segment (e.g. the gizmo Global/Local space toggle): a square cell holding a centred glyph
   in place of the text label. */
.segment--icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  padding: 0;
}

.segment--icon svg {
  width: 15px;
  height: 15px;
}

.segment:hover {
  border-color: var(--card-accent);
  color: var(--card-accent);
}

.segment--active {
  color: var(--card-accent);
  border-color: var(--card-accent);
  background: color-mix(in srgb, var(--card-accent) 14%, transparent);
}
/* Two-position on/off switch (components/switchControl.ts) */

/* A fixed-size track, never sized by its column - a switch is the same object on every surface.
   Sharp and transition-free like the rest of the chrome (reset.css), so the thumb lands at once. */
.switch {
  flex: none;
  width: 36px;
  height: 20px;
  display: inline-flex;
  align-items: center;
  padding: 2px;
  background: var(--surface-deep);
  border: 1px solid var(--border-mid);
  cursor: pointer;
}

/* The thumb travels inside the track (never over its edge): off parks it left, on pushes it right
   with the free space, so the position alone carries the state even without the lit track. */
.switch__thumb {
  width: 14px;
  height: 14px;
  background: var(--muted);
}

.switch:hover {
  border-color: var(--accent);
}

.switch--on {
  background: color-mix(in srgb, var(--accent) 18%, var(--surface-deep));
  border-color: var(--accent);
}

.switch--on .switch__thumb {
  margin-left: auto;
  background: var(--accent);
}

.switch:focus-visible {
  outline: 1px solid var(--accent);
  outline-offset: 1px;
}

/* Three-position variant (components/triSwitchControl.ts): same track language as .switch, but
   taller (matches the middlebar's 28px controls). Unlike on/off, left/center/right position alone
   doesn't read as unambiguously as two states do, so every zone shows its own icon all the time
   (not just the active one); the thumb slides behind the active zone's icon to pick it out, and
   also drives drag-to-select (`touch-action: none` so a horizontal drag isn't stolen for scrolling). */
.tri-switch {
  position: relative;
  flex: none;
  display: flex;
  width: 72px;
  height: 28px;
  padding: 2px;
  background: var(--surface-deep);
  border: 1px solid var(--border-mid);
  cursor: pointer;
  touch-action: none;
}

.tri-switch:hover {
  border-color: var(--accent);
}

.tri-switch__zone {
  position: relative;
  z-index: 1;
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  padding: 0;
  color: var(--muted);
  cursor: pointer;
}

.tri-switch__zone svg {
  width: 14px;
  height: 14px;
}

.tri-switch__zone--active {
  color: var(--on-accent);
}

.tri-switch__zone:focus-visible {
  outline: 1px solid var(--accent);
  outline-offset: -1px;
}

/* Slides between the three zones via a CSS var the control sets on select (`--tri-switch-index`,
   0/1/2) rather than a JS-computed pixel offset, so the geometry stays declarative in one place.
   Sits behind the zone buttons (which carry the icons) via z-index, not DOM order, so it reads as
   a highlight picking out one zone's icon rather than a second icon layer. */
.tri-switch__thumb {
  position: absolute;
  z-index: 0;
  top: 2px;
  bottom: 2px;
  left: calc(2px + var(--tri-switch-index, 0) * (100% - 4px) / 3);
  width: calc((100% - 4px) / 3);
  background: var(--accent);
  pointer-events: none;
}
/* Content screen (assets + export) - STYLE_GUIDE section 4.10 */

.modal-scrim {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal-scrim);
  display: none;
  align-items: center;
  justify-content: center;
  background: color-mix(in srgb, var(--stage-background) 70%, transparent);
}

.modal-scrim--open {
  display: flex;
}

/* A dialog centred in the viewport (the scrim's flex centring) with a gap on every side. Capped at
   a fixed max-width rather than stretching edge to edge - three fixed/capped-width blocks (Content,
   Project, Help; hence the row layout) never need the full viewport - and tall by default (a high
   min-height floor) so the unified vertical asset list and the Help reference both get room without
   scrolling on typical screens. Beyond max-height each block scrolls internally, not the sheet. */
.content-sheet {
  position: relative;
  z-index: var(--z-modal-sheet);
  display: flex;
  width: calc(100vw - 64px);
  max-width: 1200px;
  min-height: 72vh;
  max-height: calc(100vh - 64px);
  overflow: hidden;
  background: var(--panel-background);
  border: 1px solid var(--border-hard);
}

/* A square close button pinned to the sheet's top-right corner, lifted above the Help block it
   overlays (the rightmost of the three). Danger color/hover come from the shared .confirm-danger
   (same look as a guarded delete/apply button); this only sets its position and size. */
.content-sheet__close {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 2;
  width: 28px;
  height: 28px;
}

.content-sheet__close svg {
  width: 14px;
  height: 14px;
}

/* A subtle accent outline while files are dragged over the sheet. */
.content-sheet--dropping {
  outline: 2px dashed var(--accent);
  outline-offset: -6px;
}

/* flex-shrink (not flex: none) so a narrower viewport compresses all three blocks rather than
   overflowing the sheet's own overflow:hidden - min-width is each block's floor before its own
   content starts truncating awkwardly. */
.content-sheet__main {
  flex: 1 1 340px;
  min-width: 260px;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

/* No divider under the header - the content-screen leans on spacing, not lines (cf. preview tabs). */
.content-sheet__header {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px 16px 6px;
}

.content-sheet__spacer {
  flex: 1;
}

/* Icon + label outline buttons (the upload affordance and the Import/Export controls): not filled,
   highlighting on hover/press - no transition, the highlight lands at once. */
.content-sheet__upload,
.content-export__button {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 28px;
  font-size: var(--font-size-small);
  color: var(--panel-foreground);
  background: transparent;
  border: 1px solid var(--border-mid);
  cursor: pointer;
}

.content-sheet__upload:hover,
.content-export__button:hover {
  color: var(--accent);
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}

.content-sheet__upload:active,
.content-export__button:active {
  background: color-mix(in srgb, var(--accent) 22%, transparent);
}

.content-sheet__upload svg,
.content-export__button svg {
  width: 14px;
  height: 14px;
}

.content-sheet__upload {
  padding: 0 12px 0 10px;
}

/* Native file input is driven by the Upload button - kept out of the layout. */
.content-sheet__file {
  display: none;
}

/* The unified asset list: one scrollbar for the whole Content block, the three asset types
   stacked as sections (content-group) rather than side-by-side columns. */
.content-list {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 16px;
  min-height: 0;
  padding: 4px 16px 16px;
  overflow-y: auto;
}

/* One asset-type section (Images / HDRI / Meshes): a subgroup-tier title over its rows, no
   independent scroll of its own - the parent .content-list is the one scroller. */
.content-group {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.content-group__rows {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

/* The empty-library hint: a single inline line, not a centred placeholder - it now sits inline
   within the unified list rather than filling a whole standalone column. */
.content-group__empty {
  padding: 4px 8px;
  font-size: var(--font-size-small);
  color: var(--muted);
}

/* The Project block: a raised fill (paired with Help, both set apart from the plain Content list)
   so the recessed (surface-deep) input reads against it; a hairline separates it from Content. */
.content-export {
  flex: 1 1 380px;
  min-width: 300px;
  display: flex;
  flex-direction: column;
  padding: 12px 16px 16px;
  background: var(--surface-raised);
  border-left: 1px solid var(--border-mid);
}

/* Extra room under the block's own group-tier title, over the form's row gap. */
.content-export__title {
  margin-bottom: 12px;
}

/* Fills the block under the title; groups sit further apart than the controls within a group. Its
   own scroll (independent of the sheet's fixed max-height) so a locale with longer labels never
   silently clips instead of wrapping/scrolling. */
.content-export__form {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 14px;
  min-height: 0;
  overflow-y: auto;
}

/* Controls that read as one unit (the embed flag over its TypeScript export). */
.content-export__group {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.content-export__field {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

/* Text colour is the shared field-tier caption (preview.css) - only the font-size is set locally,
   since (unlike the viewport N-panel) nothing here establishes an ambient small font-size. */
.content-export__label {
  font-size: var(--font-size-small);
}

/* The block's prominent field - same height as the buttons below it; type size matches every other
   field in the sheet (nodes' own param inputs included), not a one-off larger size. */
.content-export__input {
  width: 100%;
  height: 28px;
  padding: 0 8px;
  font-size: var(--font-size-small);
}

/* The Import/Export JSON pair on one line. */
.content-export__row {
  display: flex;
  gap: 8px;
}

/* Both buttons share the row width. */
.content-export__row .content-export__button {
  flex: 1;
  padding: 0 10px;
}

.content-export__button {
  justify-content: center;
  padding: 0 12px;
}

/* The embed-assets flag: checkbox + label, gating the TypeScript export below it (tooltip has the
   detail). */
.content-export__embed {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: var(--font-size-small);
  color: var(--muted);
  cursor: pointer;
}

/* The Examples list: one .asset-row per bundled preset, no scroll (it's short by design). A preset
   row's preview is a kind glyph (asset-row__preview's own well-background fallback state), like an
   asset library row that never got a pixel thumbnail. */
.content-export__examples {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

/* The per-row apply button: same danger/counting look as a delete (.confirm-danger), sized like
   the outline buttons above it rather than the square per-row action/remove icons. A fixed width
   (not content-sized padding) so the countdown-confirm's digit swap-in (attachCountdownConfirm
   replaces the whole label with a lone number) never visibly resizes the button. */
.content-export__preset-apply {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  width: 88px;
  height: 28px;
  font-size: var(--font-size-small);
}

.content-export__preset-apply svg {
  width: 14px;
  height: 14px;
}

/* A light outline setting an Examples row apart from a plain library asset-row (which carries no
   border of its own, only its fill). */
.content-export__preset-row {
  border: 1px solid var(--border-soft);
}

/* A row: thick enough to hold a preview, no taller than it needs, set off by a fill (no border). */
.asset-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
  background: color-mix(in srgb, var(--surface-raised) 45%, transparent);
}

.asset-row__preview {
  flex: none;
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  color: var(--muted);
  background: var(--well);
  overflow: hidden;
}

.asset-row__thumb {
  width: 100%;
  height: 100%;
  object-fit: contain;
  /* A checker so a transparent PNG's shape stays readable. */
  background: repeating-conic-gradient(var(--overlay-subtle) 0% 25%, transparent 0% 50%) 0 0 / 14px
    14px;
}

.asset-row__title {
  min-width: 0;
  font-size: var(--font-size-small);
  color: var(--panel-foreground);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.asset-row__spacer {
  flex: 1;
}

/* The user count in a boxed chip - the same height as the row's other controls. */
.asset-row__count {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 26px;
  height: 26px;
  padding: 0 6px;
  font-size: var(--font-size-small);
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  background: var(--well);
  border: 1px solid var(--border-soft);
}

/* Row controls: flat square icon buttons carrying their colour at rest (like .middlebar__button), not
   only on hover. The delete button also carries .confirm-danger (countdown-confirm) for its danger
   tint and counting state. */
.asset-row__action,
.asset-row__remove {
  flex: none;
  width: 26px;
  height: 26px;
  padding: 0;
}

.asset-row__action {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--panel-foreground);
  background: var(--surface-raised);
  border: 1px solid var(--border-mid);
  cursor: pointer;
}

.asset-row__action:hover {
  border-color: var(--accent);
  color: var(--accent);
}

.asset-row__action svg,
.asset-row__remove svg {
  width: 14px;
  height: 14px;
}

/* The Help block: a static quick-reference, raised like Project (the pair reads as the sheet's
   settings side) and given room to grow up to a cap - prose reflows better with flexible width
   than the two fixed-width form-driven blocks beside it. */
.content-help {
  flex: 1 1 380px;
  min-width: 260px;
  max-width: 460px;
  display: flex;
  flex-direction: column;
  padding: 12px 16px 16px;
  background: var(--surface-raised);
  border-left: 1px solid var(--border-mid);
}

/* Its own scroll, independent of the sheet's fixed max-height. */
.content-help__body {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-height: 0;
  margin-top: 12px;
  overflow-y: auto;
}

.content-help__intro,
.content-help__misc,
.content-help__shortcuts {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.content-help__line {
  margin: 0;
  font-size: var(--font-size-small);
  line-height: 1.5;
  color: var(--panel-foreground);
}

/* One shortcut row: a key-combo chip (matching the viewport stats readout's own value chip) beside
   its muted description. */
.content-help__shortcut-row {
  display: flex;
  align-items: center;
  gap: 8px;
}

.content-help__shortcut-key {
  flex: none;
  padding: 1px 6px;
  font-size: var(--font-size-small);
  font-variant-numeric: tabular-nums;
  color: var(--panel-foreground);
  background: var(--surface-deep);
  border: 1px solid var(--border-mid);
  white-space: nowrap;
}

.content-help__shortcut-label {
  font-size: var(--font-size-small);
  color: var(--muted);
}
/* Shared hover tooltip (components/tooltip.ts) - a bold accent title over an optional muted
   description, mirrored from the tesselot editor so every hover hint reads identically. Floating
   and non-interactive; positioned in screen space by the helper. */
.tooltip {
  position: fixed;
  z-index: var(--z-tooltip);
  max-width: 240px;
  padding: 7px 9px;
  background: color-mix(in srgb, var(--panel-background) 92%, transparent);
  color: var(--panel-foreground);
  border: 1px solid var(--border-soft);
  font-size: 11px;
  line-height: 1.45;
  pointer-events: none;
  display: none;
}

.tooltip--visible {
  display: block;
}

.tooltip__title {
  font-weight: 600;
  color: var(--accent);
  letter-spacing: 0.02em;
}

.tooltip__desc {
  margin-top: 2px;
  color: var(--muted);
  /* Multi-line hints (e.g. a node's stacked compile-error messages) keep their line breaks. */
  white-space: pre-line;
}
