/* Fixtures Tab (KL-135, extracted to features/). Form-row shared
   classes stay in style.css since govee + DMX modals also use them. */
@layer features {

/* Fixtures Tab */
.fixtures-panel {
    background: var(--surface);
    border-radius: 0.5rem;
    padding: 1rem;
    margin-bottom: 1rem;
}

.fixtures-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 0.6rem;
    flex-wrap: wrap;
    margin-bottom: 0.75rem;
}

.fixtures-header-actions {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
}

.fixture-filter {
    background: var(--surface-2);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: 0.3rem;
    padding: 0.35rem 0.6rem;
    font-size: 0.8rem;
    font-family: inherit;
    min-width: 14rem;
}

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

.fixtures-header h2 {
    margin: 0;
    font-size: 1rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-dim);
}

.restart-banner {
    background: #3a2a10;
    border: 1px solid #7a5a20;
    border-radius: 0.4rem;
    padding: 0.6rem 0.8rem;
    margin-bottom: 0.75rem;
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
    font-size: 0.8rem;
}

.restart-banner strong { color: #f4c770; letter-spacing: 0.04em; }
.restart-banner span { color: var(--text-dim); }

.fixture-list {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
}

.fixtures-empty {
    padding: 1.5rem 1rem;
    text-align: center;
    color: var(--text-dim);
    font-style: italic;
    background: var(--surface-2);
    border-radius: 0.4rem;
}

/* KL-157: role-color tokens shared between the universe-map blocks and
   the fixture-row left stripes. Single source of truth — change here
   to update both surfaces simultaneously.
   bass_mover was #ff4a3a (KL-175 swap): too close to --danger, so it
   washed into error/collision states on the Fixtures tab. Switched to
   a warm sunset orange that keeps the "warm, energetic" bass cue
   without colliding with error red. */
:root {
    --role-bass-mover: #ff7a18;
    --role-mid-wash: #2ec27e;
    --role-high-accent: #4aa3ff;
    --role-ambient: #b48cff;
}

.fixture-row {
    display: grid;
    grid-template-columns: 1fr auto auto auto auto;
    gap: 0.75rem;
    align-items: center;
    padding: 0.55rem 0.75rem 0.55rem calc(0.75rem - 3px);
    background: var(--surface-2);
    border-radius: 0.4rem;
    font-size: 0.85rem;
    /* KL-157: 3px transparent default so the layout doesn't shift when
       a role-color stripe lands. Per-role rules below paint it. The
       errored state at the bottom overrides with red. */
    border-left: 3px solid transparent;
}
/* KL-157: per-role left-stripe color. Reuses the same hex as the
   universe-map blocks above so map ↔ list link is visually exact. */
.fixture-row.role-bass_mover  { border-left-color: var(--role-bass-mover); }
.fixture-row.role-mid_wash    { border-left-color: var(--role-mid-wash); }
.fixture-row.role-high_accent { border-left-color: var(--role-high-accent); }
.fixture-row.role-ambient     { border-left-color: var(--role-ambient); }
/* KL-157: also color the role-text label so all three role expressions
   (stripe, map block, label) match. Default .fx-role color was generic
   accent-purple — visually disconnected from the new stripe + map. */
.fixture-row.role-bass_mover  .fx-role { color: var(--role-bass-mover); }
.fixture-row.role-mid_wash    .fx-role { color: var(--role-mid-wash); }
.fixture-row.role-high_accent .fx-role { color: var(--role-high-accent); }
.fixture-row.role-ambient     .fx-role { color: var(--role-ambient); }

.fixture-row .fx-name { font-weight: 600; color: var(--text); }
.fixture-row .fx-badge {
    font-size: 0.7rem;
    color: var(--text-dim);
    padding: 0.1rem 0.5rem;
    border: 1px solid var(--border);
    border-radius: 0.8rem;
}
.fixture-row .fx-badge.fx-custom { color: var(--accent); border-color: var(--accent); opacity: 0.7; }
.fixture-row .fx-addr { font-family: inherit; color: var(--text-dim); font-size: 0.8rem; }
.fixture-row .fx-role { color: var(--accent); font-size: 0.75rem; letter-spacing: 0.04em; }

.fx-delete {
    background: transparent;
    color: var(--danger);
    border: 1px solid transparent;
    padding: 0.25rem 0.55rem;
    border-radius: 0.3rem;
    cursor: pointer;
    font-size: 0.75rem;
    opacity: 0.7;
    transition: opacity 0.15s, border-color 0.15s;
}
.fx-delete:hover { opacity: 1; border-color: var(--danger); }

/* Add Fixture Modal form rows */
.form-row {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    margin-bottom: 0.85rem;
}

.form-row label {
    font-size: 0.75rem;
    color: var(--text-dim);
    letter-spacing: 0.04em;
    text-transform: uppercase;
}

.form-row input,
.form-row select {
    background: var(--surface-2);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: 0.3rem;
    padding: 0.4rem 0.6rem;
    font-family: inherit;
    font-size: 0.85rem;
}

.form-row input:focus,
.form-row select:focus {
    outline: none;
    border-color: var(--accent);
}

.form-row-inline {
    flex-direction: row;
    gap: 0.6rem;
}
.form-row-inline > div { flex: 1; display: flex; flex-direction: column; gap: 0.3rem; }

.form-hint {
    font-size: 0.7rem;
    color: var(--text-dim);
    margin-top: 0.15rem;
    min-height: 1em;
}

.modal-warning {
    margin-top: 0.5rem;
    padding: 0.5rem 0.75rem;
    background: #3a1a1a;
    border: 1px solid #6a2020;
    border-radius: 0.3rem;
    color: #f88;
    font-size: 0.8rem;
}

.fixture-row { cursor: pointer; }
.fixture-row:hover { background: var(--surface); }
.fixture-row .fx-delete { cursor: pointer; }

/* Errored fixture row — red-tint the card + spill the validation
   message beneath the address line so it's obvious which one to fix. */
.fixture-row.errored {
    background: rgba(120, 30, 30, 0.22);
    border-left: 3px solid #ff6b6b;
    padding-left: calc(0.75rem - 3px);
}
.fixture-row.errored:hover { background: rgba(120, 30, 30, 0.35); }
.fixture-row.errored .fx-name { color: #ffb0b0; }
.fixture-row .fx-error-line {
    grid-column: 1 / -1;
    font-size: 0.72rem;
    color: #ffb0b0;
    opacity: 0.95;
    padding-top: 0.25rem;
}

.channel-editor {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    max-height: 38vh;
    overflow-y: auto;
    padding: 0.3rem;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: 0.3rem;
}

.channel-row {
    display: grid;
    /* number | type | default | invert | delete */
    grid-template-columns: 2.2rem 1fr 5rem auto 1.6rem;
    gap: 0.4rem 0.4rem;
    align-items: center;
}

.channel-row .ch-hint {
    /* Span all columns under the row; was 2/4 when there were fewer cols. */
    grid-column: 2 / -1;
    font-size: 0.7rem;
    color: var(--text-dim);
    font-style: italic;
    margin-top: -0.15rem;
    min-height: 1em;
}

/* KL-146: per-channel invert toggle. Visible only for direction-flippable
   types; takes its grid slot but collapses out of layout when hidden so
   non-invertible rows still align. */
.channel-row .ch-invert {
    display: flex;
    align-items: center;
    gap: 0.25rem;
    font-size: 0.7rem;
    color: var(--text-dim);
    cursor: pointer;
    white-space: nowrap;
    user-select: none;
}
.channel-row .ch-invert input[type="checkbox"] {
    margin: 0;
    cursor: pointer;
}
.channel-row .ch-invert[style*="display: none"] {
    /* Alpine x-show toggles inline display:none; the auto column collapses
       gracefully because empty grid cells don't claim width. */
}

.channel-row .ch-num {
    font-size: 0.75rem;
    color: var(--text-dim);
    text-align: right;
    font-variant-numeric: tabular-nums;
}

.channel-row input[type="text"],
.channel-row input[type="number"] {
    background: var(--bg);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: 0.25rem;
    padding: 0.25rem 0.4rem;
    font-size: 0.8rem;
    font-family: inherit;
    width: 100%;
}

.channel-row input[type="text"]:focus,
.channel-row input[type="number"]:focus {
    outline: none;
    border-color: var(--accent);
}

.channel-row .ch-delete {
    background: transparent;
    border: 1px solid transparent;
    color: var(--danger);
    border-radius: 0.2rem;
    cursor: pointer;
    font-size: 0.9rem;
    line-height: 1;
    padding: 0.1rem 0.3rem;
    opacity: 0.6;
}
.channel-row .ch-delete:hover { opacity: 1; border-color: var(--danger); }

.channel-add {
    align-self: flex-start;
    margin-top: 0.4rem;
    font-size: 0.75rem;
    padding: 0.3rem 0.6rem;
}

/* KL-149: role-lock UX. The dropdown sits next to a small Override
   button when the role is locked (templated fixtures only). Unlocking
   surfaces a warning hint that role changes can hurt show feel.
   Custom fixtures skip the lock pattern and render the dropdown
   alone. */
.role-row {
    display: flex;
    align-items: stretch;
    gap: 0.4rem;
}
.role-row select { flex: 1; }
.role-override-btn {
    flex: 0 0 auto;
    font-size: 0.7rem;
    padding: 0.25rem 0.55rem;
    letter-spacing: 0.04em;
}
.role-override-warn {
    color: var(--text-dim);
    font-style: italic;
    margin-top: 0.25rem;
}
/* Disabled select still needs to read clearly — the lock is
   intentional, not "broken." Lighten the typical browser-disabled
   washed-out look with a subtle background tint instead. */
.role-row select:disabled {
    opacity: 1;
    color: var(--text);
    background: var(--surface-2);
    cursor: not-allowed;
}

/* KL-154: visual universe map. One strip per universe in use, each
   strip is a flex row of segments (fixture blocks + empty gaps) sized
   proportionally to their channel count. Click a fixture block → Edit
   modal; click a gap → Add modal pre-filled with that start address.

   Role colors are tuned to be readable on the dark surface and to
   intuitively map to band semantics: bass = warm red, mids = green,
   highs = blue, ambient = soft violet. */
.universe-map {
    margin: 0.5rem 0 1rem;
    padding: 0.6rem 0.75rem 0.75rem;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: 0.4rem;
}
.universe-map-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 0.4rem;
    flex-wrap: wrap;
    gap: 0.5rem;
}
.universe-map-title {
    font-size: 0.75rem;
    color: var(--text-dim);
    letter-spacing: 0.08em;
    text-transform: uppercase;
}
.universe-map-legend {
    display: inline-flex;
    align-items: center;
    gap: 0.6rem;
    font-size: 0.7rem;
    color: var(--text-dim);
}
.universe-legend-swatch {
    display: inline-block;
    width: 0.6rem;
    height: 0.6rem;
    border-radius: 2px;
    margin-right: 0.2rem;
    vertical-align: -0.05rem;
}
.universe-strip {
    display: flex;
    align-items: stretch;
    gap: 0.5rem;
    margin-bottom: 0.35rem;
}
.universe-strip:last-child { margin-bottom: 0; }
/* KL-175: when this strip has at least one collision bubble, the bubble
   renders absolute-positioned below the segment it's pinned to. Bump the
   strip's bottom margin so the next universe row doesn't overlap. */
.universe-strip:has(.collision-bubble) {
    margin-bottom: calc(0.35rem + 2rem);
}
.universe-label {
    flex: 0 0 1.6rem;
    font-size: 0.7rem;
    color: var(--text-dim);
    align-self: center;
    text-align: right;
    font-variant-numeric: tabular-nums;
}
/* KL-196: vertical stack inside the strip. The channels bar sits on
   top, the tick scale renders directly underneath at the same width
   so tick positions map 1:1 to bar positions. */
.universe-bar-stack {
    flex: 1;
    display: flex;
    flex-direction: column;
}
.universe-channels {
    display: flex;
    height: 1.4rem;
    background: var(--bg);
    border-radius: 3px;
    /* KL-175: overflow visible so collision bubbles inside segments can
       render below the strip. Segment text is clipped by .universe-
       segment-label's own overflow:hidden, so the strip itself doesn't
       need to clip. */
    overflow: visible;
    border: 1px solid var(--border);
}
/* KL-196: tick-scale row beneath the channels bar. */
.universe-ticks {
    position: relative;
    height: 0.95rem;
    margin-top: 1px;
}
.universe-tick {
    position: absolute;
    top: 0;
    transform: translateX(-50%);
    pointer-events: none;
    display: flex;
    flex-direction: column;
    align-items: center;
    line-height: 1;
}
/* Endpoints align flush with bar edges so labels don't overflow. */
.universe-tick-endpoint-start { transform: translateX(0); align-items: flex-start; }
.universe-tick-endpoint-end   { transform: translateX(-100%); align-items: flex-end; }
.universe-tick-line {
    width: 1px;
}
.universe-tick-minor .universe-tick-line {
    height: 2px;
    background: var(--text-muted);
}
.universe-tick-major .universe-tick-line,
.universe-tick-endpoint-start .universe-tick-line,
.universe-tick-endpoint-end   .universe-tick-line {
    height: 4px;
    background: var(--text-dim);
}
.universe-tick-label {
    font-family: var(--font-mono, ui-monospace, Menlo, Consolas, monospace);
    font-size: 9px;
    color: var(--text-muted);
    margin-top: 1px;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
/* Responsive: at narrow widths drop minors to avoid crowding. The
   majors + endpoints alone still give a usable scale. */
@media (max-width: 720px) {
    .universe-tick-minor { display: none; }
}
.universe-segment {
    /* Reset button defaults so we can use semantic <button> for
       clicks/keyboard while keeping a flat colored block visually. */
    border: none;
    margin: 0;
    padding: 0;
    cursor: pointer;
    color: inherit;
    font: inherit;
    /* KL-175: overflow visible so collision bubbles can extend below
       the strip. The segment-label has its own overflow:hidden +
       text-overflow:ellipsis so the text label still gets clipped to
       the segment width. */
    overflow: visible;
    position: relative;
    transition: filter 0.12s, transform 0.12s;
    min-width: 0;
}
.universe-segment:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
    z-index: 2;
}
.universe-gap {
    background: transparent;
    cursor: cell;  /* hint that clicking adds a fixture here */
}
.universe-gap:hover {
    background: rgba(180, 140, 255, 0.18);
}
.universe-block {
    color: rgba(255, 255, 255, 0.95);
    font-size: 0.65rem;
    line-height: 1.4rem;
    padding: 0 0.3rem;
    white-space: nowrap;
    text-align: left;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
    border-right: 1px solid rgba(0, 0, 0, 0.25);
}
.universe-block:hover {
    filter: brightness(1.18);
}
.universe-block:active {
    transform: translateY(1px);
}
.universe-segment-label {
    display: inline-block;
    text-overflow: ellipsis;
    overflow: hidden;
    max-width: 100%;
}
/* Role colors. Saturated enough to read on dark, distinct enough that
   a quick glance tells you the role distribution across a universe.
   KL-157: backed by --role-* tokens so the fixture-row stripes use the
   same colors. */
.universe-block.role-bass_mover,
.universe-legend-swatch.role-bass_mover  { background: var(--role-bass-mover); }
.universe-block.role-mid_wash,
.universe-legend-swatch.role-mid_wash    { background: var(--role-mid-wash); }
.universe-block.role-high_accent,
.universe-legend-swatch.role-high_accent { background: var(--role-high-accent); }
.universe-block.role-ambient,
.universe-legend-swatch.role-ambient     { background: var(--role-ambient); }

/* KL-175: overlap visual flag. Stripes layered over the role color
   keep the role hint readable; the bright outline + saturated stripe
   makes a collision unmistakable at a glance. Both fixtures involved
   in any pairwise collision get this treatment (universemap.js sets
   `overlap: true` on each side). */
.universe-block.is-overlap {
    background-image: repeating-linear-gradient(
        45deg,
        rgba(255, 60, 60, 0.55) 0,
        rgba(255, 60, 60, 0.55) 4px,
        transparent 4px,
        transparent 8px
    );
    outline: 2px solid var(--danger);
    outline-offset: -2px;
    z-index: 1;
}
.universe-block.is-overlap .universe-segment-label {
    font-weight: 600;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.7);
}

/* KL-175: per-universe header pill. Sits at the right end of the strip
   and shows the count of distinct collision pairs on this universe, so
   the universe list itself signals which strips need attention. */
.universe-collisions-pill {
    display: inline-flex;
    align-items: center;
    /* align with the channel strip's vertical center, matching .universe-label. */
    align-self: flex-start;
    padding: 0 0.45rem;
    font-size: 0.65rem;
    font-weight: 600;
    color: var(--danger);
    background: rgba(255, 0, 0, 0.12);
    border: 1px solid var(--danger);
    border-radius: 3px;
    white-space: nowrap;
    line-height: 1.2;
    height: 1.4rem;
}

/* KL-175: collision bubbles are rendered inside their fixture segment,
   absolute-positioned below the strip. Anchoring to the segment dodges
   the flex-basis:auto distortion that breaks address-based percentage
   math when buttons have text content — the bubble simply sits under
   whatever pixel width the flex algorithm gave its segment. */
.collision-bubble {
    position: absolute;
    /* top is set inline (per-bubble row index for multi-collision
       fixtures); horizontally centered on the segment. */
    left: 50%;
    transform: translateX(-50%);
    background: var(--danger, #ff4d4d);
    color: white;
    font-size: 0.65rem;
    font-weight: 500;
    padding: 0.2rem 0.45rem;
    border-radius: 3px;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35);
    cursor: pointer;
    line-height: 1.15;
    white-space: nowrap;
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    max-width: 18rem;
    z-index: 5;
}
.collision-bubble::before {
    content: '';
    position: absolute;
    top: -0.3rem;
    left: 50%;
    width: 0.5rem;
    height: 0.5rem;
    background: var(--danger, #ff4d4d);
    transform: translateX(-50%) rotate(45deg);
    box-shadow: -1px -1px 2px rgba(0, 0, 0, 0.15);
}
.collision-bubble:hover {
    filter: brightness(1.1);
}
.collision-bubble-range {
    background: rgba(0, 0, 0, 0.25);
    padding: 0.05rem 0.3rem;
    border-radius: 2px;
    font-variant-numeric: tabular-nums;
    flex: 0 0 auto;
}
.collision-bubble-label {
    overflow: hidden;
    text-overflow: ellipsis;
}

/* KL-155: red border on a DMX address input when its value overlaps an
   existing fixture's range. The accompanying inline hint names the
   conflicting fixture; the Save button is disabled by Alpine. */
input[type="number"].address-conflict {
    border-color: var(--danger);
    outline-color: var(--danger);
}

/* KL-143: inline spinner for the "Loading templates…" hint shown in
   the Add Fixture modal while the catalog fetch is in flight. Sized
   to sit beside text without pushing layout. */
.catalog-spinner {
    display: inline-block;
    width: 0.7rem;
    height: 0.7rem;
    border: 2px solid var(--border);
    border-top-color: var(--accent);
    border-radius: 50%;
    animation: catalog-spinner-rotate 0.8s linear infinite;
    vertical-align: -0.1rem;
    margin-right: 0.35rem;
}
@keyframes catalog-spinner-rotate {
    to { transform: rotate(360deg); }
}


} /* end @layer features */
