/* === Stonk Streamer — Broadcast Control UI === */

@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');

:root {
    --bg: #060608;
    --surface: #0c0c10;
    --surface-raised: #121216;
    --surface-hover: #18181e;
    --border: #1a1a22;
    --border-light: #24242e;
    --border-focus: #3a3a4a;
    --text: #e2e2e8;
    --text-muted: #7a7a88;
    --text-dim: #4a4a58;
    --accent: #6aadff;
    --accent-dim: #4a8acc;
    --accent-glow: rgba(106, 173, 255, 0.08);
    --accent-glow-strong: rgba(106, 173, 255, 0.15);
    --green: #34d178;
    --green-dim: #22a85e;
    --green-bg: rgba(34, 209, 120, 0.08);
    --green-border: rgba(34, 209, 120, 0.2);
    --red: #ff5c5c;
    --red-bg: rgba(255, 92, 92, 0.06);
    --red-border: rgba(255, 92, 92, 0.2);
    --yellow: #f0c840;
    --yellow-bg: rgba(240, 200, 64, 0.06);
    --orange: #ff9040;
    --radius: 8px;
    --radius-lg: 12px;
    --font: 'Outfit', system-ui, -apple-system, sans-serif;
    --mono: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace;
    --shadow-sm: 0 1px 2px rgba(0,0,0,0.3);
    --shadow-md: 0 4px 16px rgba(0,0,0,0.3);
    --shadow-lg: 0 8px 32px rgba(0,0,0,0.4);
}

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

body {
    font-family: var(--font);
    background: var(--bg);
    color: var(--text);
    min-height: 100vh;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

/* === Header === */

.app-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0.75rem 2rem;
    background: var(--surface);
    border-bottom: 1px solid var(--border);
    position: sticky;
    top: 0;
    z-index: 100;
    backdrop-filter: blur(16px) saturate(1.2);
    gap: 1.5rem;
}

.header-left {
    display: flex;
    align-items: center;
    gap: 2rem;
}

.app-logo {
    height: 56px;
    display: block;
    flex-shrink: 0;
}

.header-nav {
    display: flex;
    gap: 0.25rem;
}

.nav-btn {
    font-family: var(--font);
    font-size: 0.8rem;
    font-weight: 500;
    color: var(--text-dim);
    background: none;
    border: none;
    padding: 0.55rem 1rem;
    border-radius: var(--radius);
    cursor: pointer;
    transition: all 0.15s ease;
    letter-spacing: 0.01em;
    white-space: nowrap;
}

.nav-btn:hover {
    color: var(--text-muted);
    background: var(--surface-raised);
}

.nav-btn.active {
    color: var(--text);
    background: var(--surface-raised);
    box-shadow: inset 0 -2px 0 var(--accent);
}

.header-status {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    font-size: 0.75rem;
    color: var(--text-dim);
    font-family: var(--mono);
    font-weight: 400;
    background: var(--bg);
    padding: 0.4rem 0.9rem;
    border-radius: 100px;
    border: 1px solid var(--border);
    flex-shrink: 0;
}

/* === Tabs === */

.tab-content {
    display: none;
}

.tab-content.active {
    display: flex;
    flex-direction: column;
    gap: 2rem;
    animation: tab-in 0.2s ease;
}

@keyframes tab-in {
    from { opacity: 0; transform: translateY(4px); }
    to { opacity: 1; transform: none; }
}

.status-dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--green);
    box-shadow: 0 0 8px var(--green-dim);
    animation: pulse-dot 2.5s ease-in-out infinite;
}

@keyframes pulse-dot {
    0%, 100% { opacity: 1; box-shadow: 0 0 8px var(--green-dim); }
    50% { opacity: 0.5; box-shadow: 0 0 3px var(--green-dim); }
}

/* === Main === */

main {
    max-width: 1440px;
    margin: 0 auto;
    padding: 2rem 2.5rem 5rem;
    display: flex;
    flex-direction: column;
    gap: 2rem;
}

/* === Panels === */

.panel {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: 1.5rem 1.75rem;
    box-shadow: var(--shadow-sm);
}

.panel-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 1.5rem;
    padding-bottom: 1.1rem;
    border-bottom: 1px solid var(--border);
}

.panel-header h2 {
    font-size: 0.82rem;
    font-weight: 600;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.12em;
}

/* === Section Bars === */

.section-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 1rem;
}

.section-bar h2 {
    font-size: 0.78rem;
    font-weight: 600;
    color: var(--text-dim);
    text-transform: uppercase;
    letter-spacing: 0.14em;
}

.section-actions {
    display: flex;
    align-items: center;
    gap: 1rem;
}

/* === Buttons === */

.btn {
    font-family: var(--font);
    font-size: 0.8rem;
    font-weight: 500;
    padding: 0.55rem 1.4rem;
    border-radius: var(--radius);
    border: 1px solid var(--border-light);
    background: var(--surface-raised);
    color: var(--text);
    cursor: pointer;
    transition: all 0.15s ease;
    white-space: nowrap;
    box-shadow: var(--shadow-sm);
}

.btn:hover {
    background: var(--surface-hover);
    border-color: var(--border-focus);
    box-shadow: var(--shadow-md);
}

.btn:active {
    transform: translateY(1px);
    box-shadow: none;
}

.btn-start {
    background: linear-gradient(135deg, rgba(34,209,120,0.12) 0%, rgba(34,209,120,0.06) 100%);
    border-color: var(--green-border);
    color: var(--green);
    font-weight: 600;
    letter-spacing: 0.03em;
    padding: 0.6rem 2rem;
}

.btn-start:hover {
    background: linear-gradient(135deg, rgba(34,209,120,0.18) 0%, rgba(34,209,120,0.1) 100%);
    border-color: var(--green-dim);
    box-shadow: 0 0 20px rgba(34, 209, 120, 0.08);
}

.btn-ghost {
    background: transparent;
    border-color: var(--border);
    box-shadow: none;
}

.btn-ghost:hover {
    background: var(--surface-raised);
    box-shadow: none;
}

.btn-stop {
    background: var(--red-bg);
    border-color: var(--red-border);
    color: var(--red);
    font-size: 0.75rem;
    font-weight: 600;
    padding: 0.45rem 1.1rem;
}

.btn-stop:hover {
    background: rgba(255, 92, 92, 0.1);
    border-color: rgba(255, 92, 92, 0.35);
}

.btn-delete {
    background: var(--red-bg);
    border-color: var(--red-border);
    color: var(--red);
    font-size: 0.7rem;
    padding: 0.35rem 0.85rem;
}

.btn-delete:hover {
    background: rgba(255, 92, 92, 0.1);
}

/* === Form Grid === */

.form-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1rem 1.5rem;
    margin-bottom: 1rem;
}

.form-grid.three-col {
    grid-template-columns: 1fr 1fr 1fr;
}

.form-group {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
}

.form-group.span-full {
    grid-column: 1 / -1;
}

.form-group > label {
    font-size: 0.7rem;
    font-weight: 500;
    color: var(--text-dim);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    padding-left: 1px;
}

/* === Inputs === */

input[type="text"],
textarea,
select {
    font-family: var(--font);
    font-size: 0.85rem;
    background: var(--bg);
    border: 1px solid var(--border);
    color: var(--text);
    padding: 0.6rem 0.85rem;
    border-radius: var(--radius);
    outline: none;
    transition: all 0.15s ease;
    width: 100%;
}

input[type="text"]:hover,
textarea:hover,
select:hover {
    border-color: var(--border-light);
}

input[type="text"]:focus,
textarea:focus,
select:focus {
    border-color: var(--accent-dim);
    box-shadow: 0 0 0 3px var(--accent-glow);
}

input::placeholder, textarea::placeholder {
    color: var(--text-dim);
    opacity: 0.7;
}

textarea {
    resize: vertical;
    min-height: 2.5rem;
    font-family: var(--mono);
    font-size: 0.8rem;
    line-height: 1.5;
}

select {
    cursor: pointer;
    appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg width='10' height='6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%234a4a58'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 0.85rem center;
    padding-right: 2.2rem;
}

select option:disabled { color: var(--text-dim); }

/* === Range Sliders === */

input[type="range"] {
    -webkit-appearance: none;
    appearance: none;
    width: 100%;
    height: 3px;
    background: var(--border);
    border-radius: 2px;
    outline: none;
    margin-top: 0.5rem;
    transition: background 0.15s ease;
}

input[type="range"]:hover {
    background: var(--border-light);
}

input[type="range"]::-webkit-slider-thumb {
    -webkit-appearance: none;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: var(--accent);
    cursor: pointer;
    box-shadow: 0 0 0 3px var(--accent-glow), var(--shadow-sm);
    transition: all 0.15s ease;
}

input[type="range"]::-webkit-slider-thumb:hover {
    transform: scale(1.15);
    box-shadow: 0 0 0 5px var(--accent-glow-strong), var(--shadow-md);
}

input[type="range"]::-moz-range-thumb {
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: var(--accent);
    border: none;
    cursor: pointer;
}

input[type="range"]:disabled {
    opacity: 0.5;
}

input[type="range"]:disabled::-webkit-slider-thumb {
    background: var(--text-dim);
    box-shadow: none;
    cursor: default;
}

.slider-readout {
    font-family: var(--mono);
    font-size: 0.65rem;
    color: var(--accent);
    font-weight: 500;
    margin-left: 0.3rem;
}

/* === Checkboxes === */

input[type="checkbox"] {
    accent-color: var(--accent);
    width: 15px;
    height: 15px;
    cursor: pointer;
}

.check-label {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    font-size: 0.8rem;
    color: var(--text-muted);
    cursor: pointer;
    user-select: none;
    transition: color 0.15s ease;
}

.check-label:hover {
    color: var(--text);
}

.record-check {
    height: 100%;
    display: flex;
    align-items: center;
    padding: 0.55rem 0.85rem;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    transition: all 0.15s ease;
}

.record-check:hover {
    border-color: var(--border-light);
}

/* === Input with unit === */

.input-with-unit {
    display: flex;
    align-items: stretch;
}

.input-with-unit input[type="number"] {
    font-family: var(--mono);
    font-size: 0.85rem;
    background: var(--bg);
    border: 1px solid var(--border);
    color: var(--text);
    padding: 0.6rem 0.85rem;
    border-radius: var(--radius) 0 0 var(--radius);
    outline: none;
    flex: 1;
    min-width: 0;
    transition: all 0.15s ease;
}

.input-with-unit input[type="number"]:hover {
    border-color: var(--border-light);
}

.input-with-unit input[type="number"]:focus {
    border-color: var(--accent-dim);
    box-shadow: 0 0 0 3px var(--accent-glow);
    z-index: 1;
}

.input-unit {
    font-family: var(--mono);
    font-size: 0.72rem;
    font-weight: 500;
    color: var(--text-dim);
    background: var(--surface-raised);
    border: 1px solid var(--border);
    border-left: none;
    padding: 0.6rem 0.7rem;
    border-radius: 0 var(--radius) var(--radius) 0;
    display: flex;
    align-items: center;
}

/* === Preset Row === */

.preset-row {
    display: flex;
    gap: 0.4rem;
}

.preset-row select { flex: 1; }

.btn-sm {
    padding: 0.45rem 0.65rem;
    font-size: 0.85rem;
    line-height: 1;
}

/* === Save Preset Button (on stream cards) === */

.btn-save-preset {
    font-family: var(--font);
    font-size: 0.65rem;
    font-weight: 600;
    padding: 0.3rem 0.75rem;
    border-radius: var(--radius);
    border: 1px solid var(--border-light);
    background: var(--surface-raised);
    color: var(--accent);
    cursor: pointer;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    transition: all 0.15s ease;
}

.btn-save-preset:hover {
    background: var(--accent-glow-strong);
    border-color: var(--accent-dim);
    box-shadow: 0 0 12px var(--accent-glow);
}

/* === Source Input === */

.source-input {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

/* === Stereo Sources === */

.stereo-grid {
    display: flex;
    align-items: center;
    gap: 1rem;
    flex-wrap: wrap;
}

.eye-picker {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex: 1;
    min-width: 200px;
}

.eye-label {
    font-size: 0.72rem;
    font-weight: 600;
    color: var(--text-muted);
    white-space: nowrap;
    text-transform: uppercase;
    letter-spacing: 0.06em;
}

/* === Streams Grid === */

.streams {
    display: grid;
    gap: 1.25rem;
    grid-template-columns: repeat(auto-fill, minmax(520px, 1fr));
}

.stream-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    overflow: hidden;
    transition: all 0.2s ease;
    box-shadow: var(--shadow-sm);
}

.stream-card:hover {
    border-color: var(--border-light);
    box-shadow: var(--shadow-md);
}

.stream-header {
    padding: 0.85rem 1.15rem;
    display: flex;
    justify-content: space-between;
    align-items: center;
    border-bottom: 1px solid var(--border);
    background: linear-gradient(180deg, var(--surface-raised) 0%, var(--surface) 100%);
}

.stream-name {
    font-weight: 600;
    font-size: 0.92rem;
    letter-spacing: 0.01em;
}

.stream-state {
    font-family: var(--mono);
    font-size: 0.62rem;
    font-weight: 600;
    padding: 0.25rem 0.7rem;
    border-radius: 100px;
    text-transform: uppercase;
    letter-spacing: 0.08em;
}

.stream-state.running { background: var(--green-bg); color: var(--green); border: 1px solid var(--green-border); }
.stream-state.stopped { background: var(--red-bg); color: var(--red); border: 1px solid var(--red-border); }
.stream-state.failed { background: var(--red-bg); color: var(--orange); border: 1px solid rgba(255,144,64,0.2); }
.stream-state.stopping { background: rgba(255,144,64,0.06); color: var(--orange); border: 1px solid rgba(255,144,64,0.2); }
.stream-state.reconnecting { background: var(--yellow-bg); color: var(--yellow); border: 1px solid rgba(240,200,64,0.2); }

/* Tier 2 reclock badge — only present on streams running through the new
   placement-buffer + master-clock pipeline. Sits next to the state pill
   so an operator can tell at a glance which streams are on the new path. */
.stream-reclock-badge {
    font-size: 0.65rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    padding: 0.2rem 0.45rem;
    border-radius: 4px;
    background: rgba(106, 173, 255, 0.12);
    color: var(--accent);
    border: 1px solid rgba(106, 173, 255, 0.3);
    text-transform: uppercase;
    user-select: none;
    margin-left: 0.4rem;
}

/* Reclock opt-in toggle in the profile editor modal. Accent-coloured left
   border to flag "this is the experimental Tier 2 flag" so it reads as
   distinct from the everyday vignette/convergence knobs. */
.reclock-toggle-row {
    border-left: 2px solid var(--accent);
    padding: 0.55rem 0.7rem;
    background: rgba(106, 173, 255, 0.04);
    border-radius: 0 4px 4px 0;
}

/* Override the .form-group > label uppercase/letter-spacing default — the
   reclock toggle uses sentence-case label text inside a check-label. */
.reclock-toggle-row > .check-label {
    font-size: 0.8rem;
    color: var(--text-muted);
    text-transform: none;
    letter-spacing: normal;
    font-weight: 500;
    padding-left: 0;
    align-items: flex-start;
}

/* Glyph prefix on each badge so the state is legible without colour —
   running and reconnecting otherwise rely solely on green-vs-yellow which is
   a known deuteranomaly trap. */
.stream-state::before {
    margin-right: 0.35em;
    display: inline-block;
}
.stream-state.running::before      { content: '▶'; }
.stream-state.reconnecting::before { content: '↻'; }
.stream-state.stopping::before     { content: '◼'; }
.stream-state.stopped::before      { content: '◻'; }
.stream-state.failed::before       { content: '⚠'; }

video {
    width: 100%;
    display: block;
    background: #000;
}

/* Restore HTML `hidden` semantics on video. The bare `video {}` rule
   above sets `display: block` at author-CSS specificity (0,0,1) — and
   because *author origin* beats UA origin, the UA `[hidden] {
   display: none }` is overridden even though it has higher specificity.
   Net effect without this re-assertion: `<video hidden>` still
   participates in layout, which on `.preview-blend` means a
   black, 50%-opacity, position:absolute overlay always sits over the
   main video — fine when there's nothing to see, ugly when the blend
   element happens to size or position over the main image. Re-state
   `display: none` for hidden videos at author scope so the attribute
   does what every reader expects. */
video[hidden] {
    display: none;
}

.preview-stage {
    position: relative;
    overflow: hidden;
    background: #000;
    aspect-ratio: 16/9;
    cursor: default;
    user-select: none;
}

.preview-stage.panning {
    cursor: grabbing;
}

.preview-pan {
    position: relative;
    width: 100%;
    height: 100%;
    transform-origin: center center;
    transition: transform 0.04s linear;
    will-change: transform;
}

.preview-pan > video {
    width: 100%;
    height: 100%;
    display: block;
    object-fit: contain;
    aspect-ratio: auto;
}

.preview-blend {
    position: absolute;
    inset: 0;
    opacity: 0.5;
    pointer-events: none;
    width: 100%;
    height: 100%;
    object-fit: contain;
}

/* In blend mode the operator drags the blend video to align with the main one,
   so it must receive mouse events. Outside blend mode it stays click-through
   so the underlying canvas zoom/pan handlers on the stage work. */
.preview-stage.blend-on .preview-blend { pointer-events: auto; }

/* Blend mode: each video is rendered at 2× container width with object-fit: fill,
   then positioned so one eye fills the visible container. Aspect ratios of the two
   eyes are identical, so any distortion is uniform across both → offsets stay accurate. */
.preview-stage.blend-on .preview-pan {
    overflow: hidden;
}

/* SBS: each video is rendered at 200% width and positioned so one eye fills
   the visible container — left video shows the left eye (left:0) and the
   blend video is slid by -100% to show the right eye in the same area. */
.preview-stage.blend-on.layout-sbs .preview-pan > video:first-of-type,
.preview-stage.blend-on.layout-sbs .preview-blend {
    position: absolute;
    top: 0;
    width: 200%;
    height: 100%;
    object-fit: fill;
}
.preview-stage.blend-on.layout-sbs .preview-pan > video:first-of-type {
    left: 0;
    /* Main video is the LEFT eye stretched 200% wide, anchored at left:0.
       Visible portion is the left half of the element, so the visible
       centre sits at 25% of the element's own width. Rotate/scale must
       pivot there or the operator's "rotate" feels like an arc. */
    transform-origin: 25% center;
}
.preview-stage.blend-on.layout-sbs .preview-blend {
    left: -100%;
    /* Visible portion is the right half of the 200%-wide element, so the
       visible center sits at 75% of the element's own width. Rotate/scale
       must pivot there or the operator's "rotate" feels like an arc. */
    transform-origin: 75% center;
    cursor: grab;
}

/* TAB: same trick on the vertical axis — 200% height, blend slides by -100%. */
.preview-stage.blend-on.layout-tab .preview-pan > video:first-of-type,
.preview-stage.blend-on.layout-tab .preview-blend {
    position: absolute;
    left: 0;
    width: 100%;
    height: 200%;
    object-fit: fill;
}
.preview-stage.blend-on.layout-tab .preview-pan > video:first-of-type {
    top: 0;
    /* TAB main video: 200% tall, anchored top:0. Visible portion is the top
       half of the element, so the visible centre sits at 25% height. */
    transform-origin: center 25%;
}
.preview-stage.blend-on.layout-tab .preview-blend {
    top: -100%;
    transform-origin: center 75%;
    cursor: grab;
}

.preview-stage.calibrating .preview-blend { cursor: grabbing; }

/* Close button only visible in expanded mode. Sits in the top-right corner
   of the expanded card; click or Esc collapses. */
.expand-close {
    display: none;
    position: absolute;
    top: 0.65rem;
    right: 0.65rem;
    z-index: 10;
    width: 2rem;
    height: 2rem;
    border-radius: 50%;
    border: 1px solid var(--border);
    background: var(--bg, #0e0e10);
    color: var(--text);
    font-size: 1.4rem;
    line-height: 1;
    cursor: pointer;
    padding: 0;
    transition: background 0.12s ease, border-color 0.12s ease;
}
.expand-close:hover {
    background: var(--hover, rgba(255, 255, 255, 0.06));
    border-color: var(--accent, #6aadff);
}
.stream-card.expanded .expand-close {
    display: flex;
    align-items: center;
    justify-content: center;
}

/* Expand: pop the card out as a fixed overlay so the operator can dial in
   alignment in a much larger preview. Layout splits into a big canvas on
   the left and a scrolling sidebar on the right that holds every param
   control (preview-tools, convergence, alignment, log, actions). Tester
   asked for this — DatGUI-style — because the previous expand stacked
   everything below the canvas and was "almost impossible" to edit. */
.stream-card.expanded {
    position: fixed;
    inset: 1rem;
    margin: 0 !important;
    z-index: 1000;
    max-width: none;
    background: var(--bg, #0e0e10);
    border-color: var(--accent, #6aadff);
    box-shadow: 0 8px 60px rgba(0, 0, 0, 0.7);

    display: grid;
    grid-template-columns: 1fr min(420px, 38vw);
    grid-template-rows: auto 1fr;
    grid-template-areas:
        "header  sidebar"
        "preview sidebar";
    column-gap: 1rem;
    overflow: hidden;
    padding: 0.75rem 1rem 1rem 1rem;
}

.stream-card.expanded > .stream-header {
    grid-area: header;
}
.stream-card.expanded > .preview-stage {
    grid-area: preview;
    /* Override the default 16:9 — fill whatever the grid track gives. The
       inner `.preview-pan` and videos use `object-fit: contain` so the
       composite letterboxes inside if the aspect ratios disagree. */
    aspect-ratio: auto;
    height: 100%;
    width: 100%;
    min-width: 0;   /* allow flex/grid shrink */
    min-height: 0;
}
.stream-card.expanded > .stream-card-sidebar {
    grid-area: sidebar;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    padding-right: 0.25rem;  /* tiny breathing room from the scroll edge */
}

/* In default (non-expanded) mode the sidebar is just a passthrough
   container — its children stack naturally below the preview-stage. */
.stream-card-sidebar {
    display: contents;
}
/* When expanded, switch the sidebar from `display: contents` to flex so
   children layout as actual children of the sidebar (column-stacked with
   gap). `display: contents` is invisible to layout, which is fine for
   non-expanded mode but breaks the grid-area assignment. */
.stream-card.expanded > .stream-card-sidebar {
    display: flex;
}

/* Mobile / narrow viewports — fall back to single-column stack so the
   sidebar doesn't squeeze content. */
@media (max-width: 720px) {
    .stream-card.expanded {
        grid-template-columns: 1fr;
        grid-template-rows: auto auto 1fr;
        grid-template-areas:
            "header"
            "preview"
            "sidebar";
        overflow-y: auto;
    }
    .stream-card.expanded > .preview-stage {
        height: auto;
        aspect-ratio: 16 / 9;
    }
}

body.has-expanded-card::before {
    content: '';
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.6);
    z-index: 999;
    pointer-events: none;
}

/* Locked: visually dim the calibration controls and signal "no drag" on the
   overlay. The actual disabling is the `disabled` attribute set in JS — this
   is just affordance. */
/* Sliders and number inputs are always live — only drag/wheel on the blend
   overlay is gated by the "Drag / scroll on preview" toggle. The cursor
   hint here is the only affordance: when the toggle is off we show
   not-allowed so the operator knows clicking won't tweak anything. */
.stream-card.locked .preview-blend { cursor: not-allowed; }
.stream-card.locked.preview-stage.blend-on .preview-blend { cursor: not-allowed; }

.preview-crosshair {
    position: absolute;
    pointer-events: none;
    /* Default: cover the full stage. Overridden per layout below so each
       crosshair sits inside its own eye region. */
    inset: 0;
    /* Hidden by default — `.preview-stage.crosshair-on` flips it on. We use a
       CSS class on the parent (mirroring `.profile-preview-pane.crosshair-on`)
       instead of the HTML `hidden` attribute on the SVG, because Safari
       silently ignores `hidden` on SVG elements (it works on HTML but the
       implementation is inconsistent across engines). The class-based path
       works everywhere. */
    display: none;
}
.preview-stage.crosshair-on .preview-crosshair { display: block; }

/* SBS non-blend: left crosshair on the left half, right on the right half.
   Each SVG fills its half exactly; the cross at viewBox 50/50 lands at the
   center of that half = center of its eye region in the combined frame. */
.preview-stage.layout-sbs:not(.blend-on) .preview-crosshair-left {
    left: 0; top: 0; right: auto; bottom: auto;
    width: 50%; height: 100%;
}
.preview-stage.layout-sbs:not(.blend-on) .preview-crosshair-right {
    left: 50%; top: 0; right: auto; bottom: auto;
    width: 50%; height: 100%;
}

/* TAB non-blend: top half, bottom half. */
.preview-stage.layout-tab:not(.blend-on) .preview-crosshair-left {
    left: 0; top: 0; right: auto; bottom: auto;
    width: 100%; height: 50%;
}
.preview-stage.layout-tab:not(.blend-on) .preview-crosshair-right {
    left: 0; top: 50%; right: auto; bottom: auto;
    width: 100%; height: 50%;
}

/* Flat: only show the left (single) crosshair across the whole frame. */
.preview-stage.layout-flat .preview-crosshair-right { display: none; }
.preview-stage.layout-flat .preview-crosshair-left {
    left: 0; top: 0; width: 100%; height: 100%;
}

/* Blend mode: both crosshairs cover the full container so they overlay the
   same screen area (where each eye is rendered). The right one inherits the
   alignment CSS transform via JS so it tracks its eye image. */
.preview-stage.blend-on .preview-crosshair-left,
.preview-stage.blend-on .preview-crosshair-right {
    left: 0; top: 0; width: 100%; height: 100%;
}

/* Distinct L/R colors so the operator can tell at a glance which crosshair
   is which (especially in blend mode where they overlay). Red (left) + cyan
   (right) pair: high contrast against typical scene content, similar
   luminance so neither hogs attention, and they don't clash with the
   existing accent palette. */
.preview-crosshair-left line { stroke: rgba(255, 80, 80, 0.85); }
.preview-crosshair-right line { stroke: rgba(80, 220, 255, 0.85); }
.preview-crosshair-left circle { fill: rgba(255, 80, 80, 0.95); }
.preview-crosshair-right circle { fill: rgba(80, 220, 255, 0.95); }
.preview-crosshair line {
    stroke-width: 1.2;
    vector-effect: non-scaling-stroke;
}
.preview-crosshair circle {
    stroke: rgba(255, 255, 255, 0.7);
    stroke-width: 1;
    vector-effect: non-scaling-stroke;
}

.preview-tools {
    display: flex;
    align-items: center;
    gap: 1rem;
    padding: 0.55rem 1.15rem;
    border-top: 1px solid var(--border);
    background: var(--surface-raised);
    flex-wrap: wrap;
    font-size: 0.75rem;
}

.preview-tools .check-label {
    font-size: 0.72rem;
}

.preview-zoom {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex: 1;
    min-width: 200px;
}

.preview-zoom > label {
    font-size: 0.68rem;
    font-weight: 500;
    color: var(--text-dim);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    white-space: nowrap;
}

.preview-zoom input[type="range"] {
    flex: 1;
    margin: 0;
}

/* === Per-eye Alignment Controls === */

.alignment-controls {
    padding: 0.75rem 1.15rem 0.85rem;
    border-top: 1px solid var(--border);
    background: linear-gradient(180deg, transparent 0%, rgba(106, 173, 255, 0.04) 100%);
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
}

.align-banner {
    font-size: 0.78rem;
    color: var(--text-dim);
    padding: 0.4rem 0.55rem;
    border-radius: 4px;
    background: rgba(255, 175, 64, 0.12);
    border: 1px solid rgba(255, 175, 64, 0.3);
}

/* Dirty-state banner: shown when the operator's local slider state has
   diverged from what the server reports as applied to the running ffmpeg.
   Two visual variants — `align-dirty-live` (HLS-buffer-lag, less alarming)
   and `align-dirty-pending` (restart needed, slightly more attention-
   grabbing yellow). Hidden by default via the `hidden` attribute. */
.align-dirty-banner {
    font-size: 0.74rem;
    padding: 0.35rem 0.55rem;
    border-radius: 4px;
    line-height: 1.35;
}
.align-dirty-banner.align-dirty-live {
    color: var(--text-muted);
    background: rgba(106, 173, 255, 0.10);
    border: 1px solid rgba(106, 173, 255, 0.28);
}
.align-dirty-banner.align-dirty-pending {
    color: var(--text);
    background: rgba(255, 175, 64, 0.16);
    border: 1px solid rgba(255, 175, 64, 0.45);
}

/* Suggested-offset banner — Phase 5. Sits next to the dirty banner in
   the alignment block. Subtler than dirty since it's a "here's how to
   improve" hint, not a "something is wrong" alert. Green-tinted to
   read as helpful suggestion rather than warning. */
.align-suggest-banner {
    font-size: 0.74rem;
    padding: 0.35rem 0.55rem;
    border-radius: 4px;
    line-height: 1.35;
    color: var(--text-muted);
    background: rgba(93, 211, 158, 0.10);
    border: 1px solid rgba(93, 211, 158, 0.28);
}
.align-suggest-banner code {
    background: rgba(93, 211, 158, 0.15);
    padding: 0.06rem 0.32rem;
    border-radius: 3px;
    font-family: var(--mono);
    font-size: 0.93em;
}

.align-eye-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.9rem;
}

.align-eye {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    padding: 0.55rem 0.7rem;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.align-eye-label {
    font-size: 0.65rem;
    font-weight: 600;
    color: var(--accent);
    text-transform: uppercase;
    letter-spacing: 0.1em;
    margin-bottom: 0.15rem;
}

.align-row {
    display: grid;
    grid-template-columns: 2.2rem 1fr 4.2rem 1.2rem;
    align-items: center;
    gap: 0.4rem;
}

.align-row label {
    font-family: var(--mono);
    font-size: 0.66rem;
    color: var(--text-dim);
    text-transform: uppercase;
    letter-spacing: 0.05em;
}

.align-row input[type="range"] {
    margin: 0;
    height: 3px;
}

.align-num {
    width: 100%;
    font-family: var(--mono);
    font-size: 0.72rem;
    background: var(--bg-input, rgba(255,255,255,0.04));
    border: 1px solid var(--border);
    color: var(--text);
    padding: 0.18rem 0.32rem;
    border-radius: 4px;
    text-align: right;
    /* hide the number-input spinner — the slider is the +/- affordance */
    -moz-appearance: textfield;
}
.align-num::-webkit-outer-spin-button,
.align-num::-webkit-inner-spin-button {
    -webkit-appearance: none;
    margin: 0;
}
.align-num:focus {
    outline: 1px solid var(--accent, #6aadff);
    border-color: var(--accent, #6aadff);
}

.align-unit {
    font-size: 0.66rem;
    color: var(--text-dim);
    font-family: var(--mono);
}

.align-actions {
    display: flex;
    gap: 0.4rem;
    justify-content: flex-end;
}

@media (max-width: 900px) {
    .align-eye-row { grid-template-columns: 1fr; }
}

/* --- Tier 2 Phase E: live sync-offset block (below alignment on card) ---
   Mirrors `.alignment-disclosure` / `.alignment-controls` visually so the two
   blocks read as siblings, with a small status pill at the end of each row
   (green = applied live, amber = will-apply-on-restart, red = rejected). */
.sync-offset-disclosure {
    border-top: 1px solid var(--border);
    background: linear-gradient(180deg, transparent 0%, rgba(106, 173, 255, 0.04) 100%);
}
.sync-offset-disclosure > summary {
    list-style: none;
    cursor: pointer;
    padding: 0.55rem 1.15rem;
    font-size: 0.78rem;
    color: var(--text-dim);
    user-select: none;
    display: flex;
    align-items: center;
    gap: 0.4rem;
}
.sync-offset-disclosure > summary::-webkit-details-marker { display: none; }
.sync-offset-disclosure > summary::before {
    content: '\25B6';
    font-size: 0.55rem;
    color: var(--text-muted);
    transition: transform 0.15s ease;
}
.sync-offset-disclosure[open] > summary::before { transform: rotate(90deg); }

.sync-offset-block {
    padding: 0.5rem 1.15rem 0.85rem;
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
}

.sync-offset-hint {
    font-size: 0.72rem;
    color: var(--text-muted);
    line-height: 1.45;
    margin-bottom: 0.25rem;
}

.sync-offset-row {
    display: grid;
    grid-template-columns: 3.5rem 1fr 4.5rem 1.4rem 8rem;
    align-items: center;
    gap: 0.5rem;
}

.sync-offset-row label {
    font-family: var(--mono);
    font-size: 0.7rem;
    color: var(--text-dim);
    text-transform: uppercase;
    letter-spacing: 0.05em;
}

.sync-offset-row input[type="range"] {
    margin: 0;
    height: 3px;
}

.sync-offset-row input[disabled] {
    opacity: 0.4;
    cursor: not-allowed;
}

.sync-offset-num {
    font-family: var(--mono);
    font-size: 0.72rem;
    background: var(--bg-input, rgba(255,255,255,0.04));
    border: 1px solid var(--border);
    color: var(--text);
    padding: 0.18rem 0.32rem;
    border-radius: 4px;
    text-align: right;
    width: 100%;
    -moz-appearance: textfield;
}
.sync-offset-num::-webkit-outer-spin-button,
.sync-offset-num::-webkit-inner-spin-button {
    -webkit-appearance: none;
    margin: 0;
}
.sync-offset-num:focus {
    outline: 1px solid var(--accent);
    border-color: var(--accent);
}

.sync-offset-unit {
    font-size: 0.66rem;
    color: var(--text-dim);
    font-family: var(--mono);
}

.sync-offset-status {
    font-size: 0.68rem;
    font-family: var(--mono);
    padding: 0.18rem 0.45rem;
    border-radius: 3px;
    text-align: center;
    line-height: 1.3;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.sync-offset-status-live {
    color: var(--green);
    background: var(--green-bg);
    border: 1px solid var(--green-border);
}
.sync-offset-status-pending {
    color: var(--text);
    background: rgba(255, 175, 64, 0.16);
    border: 1px solid rgba(255, 175, 64, 0.45);
}
.sync-offset-status-error {
    color: var(--red);
    background: var(--red-bg);
    border: 1px solid var(--red-border);
}

@media (max-width: 900px) {
    .sync-offset-row {
        grid-template-columns: 3rem 1fr 4rem 1.2rem;
    }
    .sync-offset-status {
        grid-column: 1 / -1;
        justify-self: end;
        max-width: 100%;
    }
}

.stream-meta {
    padding: 0.6rem 1.15rem;
    font-size: 0.72rem;
    color: var(--text-dim);
    font-family: var(--mono);
    line-height: 1.7;
}

.stream-meta a { color: var(--accent); text-decoration: none; }
.stream-meta a:hover { text-decoration: underline; color: var(--accent-dim); }

/* Negotiated per-source capture detail row. Sits directly under .stream-meta,
   slightly tighter to read as a sub-line. Same font family / size so it
   visually belongs to the meta block. */
.stream-sources-detail {
    padding: 0 1.15rem 0.4rem;
    margin-top: -0.3rem;
    font-size: 0.72rem;
    color: var(--text-dim);
    font-family: var(--mono);
    line-height: 1.5;
    opacity: 0.85;
}

/* Audio level meter (VU) — small RMS+peak bar emitted in renderHealthLineInner.
   --vu-rms: 0–100% width of the green/amber/red fill (RMS, average loudness)
   --vu-peak: 0–100% horizontal position of the thin peak indicator line
   --vu-fill: fill colour selected by app.js based on peak dBFS zone */
.audio-vu {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    vertical-align: middle;
}

.audio-vu-bar {
    position: relative;
    display: inline-block;
    width: 60px;
    height: 8px;
    background: rgba(255, 255, 255, 0.08);
    border-radius: 2px;
    overflow: hidden;
}

.audio-vu-bar::before {
    content: '';
    position: absolute;
    inset: 0 auto 0 0;
    width: var(--vu-rms, 0%);
    background: var(--vu-fill, #5dd39e);
    transition: width 120ms linear;
}

.audio-vu-bar::after {
    content: '';
    position: absolute;
    top: 0;
    bottom: 0;
    left: var(--vu-peak, 0%);
    width: 2px;
    background: rgba(255, 255, 255, 0.85);
    transition: left 120ms linear;
}

.audio-vu-label {
    font-family: var(--mono);
    font-size: 0.68rem;
    color: var(--text-dim);
    min-width: 3.2em;
    text-align: right;
}

.stream-actions {
    padding: 0.6rem 1.15rem;
    border-top: 1px solid var(--border);
    display: flex;
    gap: 0.5rem;
    justify-content: flex-end;
    align-items: center;
}

.stream-log {
    border-top: 1px solid var(--border);
    background: var(--surface);
}

.stream-log pre {
    margin: 0;
    padding: 0.6rem 1.15rem;
    font-family: var(--mono);
    font-size: 0.68rem;
    color: var(--text-muted);
    max-height: 14rem;
    overflow-y: auto;
    white-space: pre-wrap;
    word-break: break-all;
    line-height: 1.5;
    /* Make the panel feel anchored to its card even on long tails. */
    scroll-behavior: smooth;
}

.stream-log pre:empty::before {
    content: 'Loading…';
    color: var(--text-dim);
    font-style: italic;
}

.stream-log.is-error pre {
    color: var(--red);
    background: var(--red-bg);
}

/* Friendly classification banner above raw stderr — orange to read as a
   warning without screaming red, since for reconnecting streams the hint
   is informational ("source dropped, will retry") rather than terminal. */
.stream-log-hint {
    margin: 0;
    padding: 0.55rem 1.15rem;
    font-family: var(--font);
    font-size: 0.78rem;
    color: var(--orange);
    background: rgba(255, 144, 64, 0.08);
    border-top: 1px solid rgba(255, 144, 64, 0.18);
    border-bottom: 1px solid rgba(255, 144, 64, 0.18);
    line-height: 1.45;
}

.stream-log-hint::before {
    content: '⚠ ';
    margin-right: 0.25em;
}

/* === Calibration disclosure (start-stream form) ===
   Reuses the .align-* shapes so the form panel and the running-card panel
   feel the same; only the chrome (disclosure summary, padding) differs. */

.calibration-disclosure {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 0.6rem 0.85rem;
}

.calibration-disclosure > summary {
    cursor: pointer;
    font-size: 0.72rem;
    font-weight: 600;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    list-style: none;
    user-select: none;
    padding: 0.15rem 0;
}

.calibration-disclosure > summary::-webkit-details-marker {
    display: none;
}

.calibration-disclosure > summary::before {
    content: '▸';
    display: inline-block;
    margin-right: 0.4rem;
    transition: transform 0.15s ease;
    color: var(--text-dim);
}

.calibration-disclosure[open] > summary::before {
    transform: rotate(90deg);
}

.cal-summary-hint {
    color: var(--text-dim);
    font-weight: 400;
    text-transform: none;
    letter-spacing: 0;
    font-size: 0.7rem;
    margin-left: 0.4rem;
}

.calibration-controls {
    margin-top: 0.7rem;
}

/* === Sync Calibration (POST /api/v1/calibrate) ===
   Sits at the top of the calibration disclosure: trigger button + in-flight
   status + result panel with confidence pill + plain-language summary +
   Apply-to-profile action. Visual family matches .align-suggest-banner
   (subtle, hint-coloured) since this is a "here's what to do" UI, not an
   alert. */

.calibrate-block {
    margin-top: 0.7rem;
    margin-bottom: 0.6rem;
    display: flex;
    flex-direction: column;
    gap: 0.55rem;
}

.calibrate-row {
    display: flex;
    align-items: center;
    gap: 0.65rem;
    flex-wrap: wrap;
}

.calibrate-hint {
    font-size: 0.72rem;
    color: var(--text-dim);
    line-height: 1.35;
}

.calibrate-status {
    font-size: 0.75rem;
    color: var(--text-muted);
    padding: 0.4rem 0.55rem;
    border-radius: 4px;
    background: var(--accent-glow);
    border: 1px solid rgba(106, 173, 255, 0.22);
    display: flex;
    align-items: center;
    gap: 0.45rem;
}

/* The browser's default `[hidden] { display: none }` loses specificity to
   the `display: flex/column` rules above, leaving stale status / result
   panels visible after the JS toggles `el.hidden = true`. Re-assert it. */
.calibrate-status[hidden],
.calibrate-result[hidden],
.calibrate-error[hidden] {
    display: none !important;
}

.calibrate-spinner {
    width: 0.7rem;
    height: 0.7rem;
    border: 2px solid rgba(106, 173, 255, 0.25);
    border-top-color: var(--accent);
    border-radius: 50%;
    display: inline-block;
    animation: calibrate-spin 0.8s linear infinite;
}

@keyframes calibrate-spin {
    to { transform: rotate(360deg); }
}

.calibrate-result {
    background: var(--surface-raised);
    border: 1px solid var(--border-light);
    border-radius: var(--radius);
    padding: 0.65rem 0.75rem;
    display: flex;
    flex-direction: column;
    gap: 0.55rem;
}

.calibrate-error {
    font-size: 0.78rem;
    color: var(--red);
    padding: 0.4rem 0.55rem;
    border-radius: 4px;
    background: var(--red-bg);
    border: 1px solid var(--red-border);
}

.calibrate-section {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
}

.calibrate-section + .calibrate-section {
    padding-top: 0.5rem;
    border-top: 1px dashed var(--border);
}

.calibrate-section-head {
    display: flex;
    align-items: center;
    gap: 0.55rem;
    flex-wrap: wrap;
}

.calibrate-section-label {
    font-size: 0.7rem;
    font-weight: 600;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.06em;
}

.calibrate-confidence {
    font-size: 0.68rem;
    font-weight: 600;
    padding: 0.15rem 0.55rem;
    border-radius: 100px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}

.calibrate-confidence-strong {
    background: var(--green-bg);
    color: var(--green);
    border: 1px solid var(--green-border);
}

.calibrate-confidence-usable {
    background: var(--yellow-bg);
    color: var(--yellow);
    border: 1px solid rgba(240, 200, 64, 0.3);
}

.calibrate-confidence-low {
    background: var(--red-bg);
    color: var(--red);
    border: 1px solid var(--red-border);
}

.calibrate-summary {
    font-size: 0.85rem;
    color: var(--text);
    line-height: 1.4;
}

.calibrate-summary strong {
    color: var(--accent);
}

.calibrate-summary code {
    background: var(--surface);
    padding: 0.06rem 0.32rem;
    border-radius: 3px;
    font-family: var(--mono);
    font-size: 0.92em;
    color: var(--text);
}

.calibrate-detail {
    font-size: 0.72rem;
    color: var(--text-dim);
    display: flex;
    flex-wrap: wrap;
    gap: 0.45rem;
    line-height: 1.4;
}

.calibrate-detail-tag {
    font-family: var(--mono);
    background: var(--surface);
    padding: 0.1rem 0.4rem;
    border-radius: 3px;
    border: 1px solid var(--border);
    color: var(--text-muted);
}

.calibrate-warnings {
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    margin: 0;
    padding: 0.4rem 0.55rem;
    border-radius: 4px;
    background: rgba(255, 175, 64, 0.10);
    border: 1px solid rgba(255, 175, 64, 0.25);
}

.calibrate-warnings li {
    font-size: 0.72rem;
    color: var(--text-muted);
    line-height: 1.4;
}

.calibrate-warnings li::before {
    content: '⚠';
    margin-right: 0.35rem;
    color: var(--orange);
}

.calibrate-actions {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    flex-wrap: wrap;
    margin-top: 0.2rem;
}

.calibrate-applied-ok {
    color: var(--green);
    font-weight: 600;
}

/* On-card alignment disclosure: same chrome family as the form-side
   calibration so the eye glances naturally between them. The summary line
   shows the current alignment at a glance ("L: X+5 Y-2 · R: ×1.02") so the
   operator doesn't have to expand to know what's set. */
.alignment-disclosure {
    background: var(--surface);
    border-top: 1px solid var(--border);
    padding: 0.5rem 1.15rem;
}

.alignment-disclosure > summary {
    cursor: pointer;
    font-size: 0.7rem;
    font-weight: 600;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    list-style: none;
    user-select: none;
    padding: 0.15rem 0;
}

.alignment-disclosure > summary::-webkit-details-marker {
    display: none;
}

.alignment-disclosure > summary::before {
    content: '▸';
    display: inline-block;
    margin-right: 0.4rem;
    transition: transform 0.15s ease;
    color: var(--text-dim);
}

.alignment-disclosure[open] > summary::before {
    transform: rotate(90deg);
}

.alignment-disclosure > .alignment-controls {
    margin-top: 0.6rem;
    padding: 0;
    border: none;
    background: transparent;
}

/* Advanced-feature disclosure shares the calibration disclosure's chrome —
   same chevron, same summary type — so the form gets a consistent "click to
   expand" affordance for the rarely-touched bits. */
.advanced-disclosure {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 0.6rem 0.85rem;
}

.advanced-disclosure > summary {
    cursor: pointer;
    font-size: 0.72rem;
    font-weight: 600;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    list-style: none;
    user-select: none;
    padding: 0.15rem 0;
}

.advanced-disclosure > summary::-webkit-details-marker {
    display: none;
}

.advanced-disclosure > summary::before {
    content: '▸';
    display: inline-block;
    margin-right: 0.4rem;
    transition: transform 0.15s ease;
    color: var(--text-dim);
}

.advanced-disclosure[open] > summary::before {
    transform: rotate(90deg);
}

.advanced-disclosure > .form-grid {
    margin-top: 0.7rem;
}

/* The .align-* classes inside the form share the same look as the on-card
   alignment block. Only delta is the box already has the panel chrome from
   the disclosure, so suppress the second-level border. */

.calibration-controls .alignment-controls,
.calibration-controls > .align-eye-row {
    background: transparent;
    border: none;
    padding: 0;
}

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

/* === CDN-disabled checkbox label === */

.check-label.disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

.check-label.disabled span {
    color: var(--text-dim);
}

.cdn-link {
    color: var(--accent);
}

/* === Saved scenes (one-click rig setups) === */

.scenes-bar {
    margin: 0 0 1rem 0;
    padding: 0.85rem 1.15rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
}

.scenes-bar-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 0.6rem;
}

.scenes-bar-header h3 {
    font-size: 0.72rem;
    font-weight: 600;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.08em;
}

.scenes-bar-hint {
    font-weight: 400;
    text-transform: none;
    letter-spacing: 0;
    font-size: 0.7rem;
    color: var(--text-dim);
    margin-left: 0.4rem;
}

.scenes-bar-hint em {
    color: var(--text-muted);
    font-style: normal;
    font-weight: 500;
}

.scenes-list {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
}

.scene-card {
    position: relative;
    padding: 0.6rem 0.85rem;
    padding-right: 1.8rem;
    background: var(--surface-raised);
    border: 1px solid var(--border-light);
    border-radius: var(--radius);
    cursor: pointer;
    min-width: 160px;
    max-width: 240px;
    transition: border-color 0.15s ease, transform 0.1s ease;
}

.scene-card:hover {
    border-color: var(--border-focus);
    transform: translateY(-1px);
}

.scene-card:active {
    transform: translateY(0);
}

.scene-name {
    font-size: 0.78rem;
    font-weight: 600;
    color: var(--text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.scene-meta {
    font-size: 0.65rem;
    color: var(--text-dim);
    font-family: var(--mono);
    margin-top: 0.2rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.scene-delete {
    position: absolute;
    top: 0.35rem;
    right: 0.35rem;
    width: 1.1rem;
    height: 1.1rem;
    border-radius: 50%;
    background: transparent;
    border: none;
    color: var(--text-dim);
    cursor: pointer;
    font-size: 0.95rem;
    line-height: 1;
    opacity: 0;
    transition: opacity 0.15s ease, color 0.15s ease;
}

.scene-card:hover .scene-delete {
    opacity: 1;
}

.scene-delete:hover {
    color: var(--red);
}

/* === Device preview thumbnails === */

.device-thumb {
    position: relative;
    margin-top: 0.5rem;
    aspect-ratio: 16 / 9;
    width: 100%;
    max-width: 320px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    color: var(--text-dim);
    font-size: 0.7rem;
    font-family: var(--mono);
    text-align: center;
    padding: 0.4rem;
}

.device-thumb[data-state="loading"],
.device-thumb[data-state="error"],
.device-thumb[data-state="idle"] {
    flex-direction: column;
    gap: 0.4rem;
}

.device-thumb img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
}

.device-thumbs {
    display: flex;
    flex-wrap: wrap;
    gap: 0.6rem;
}

.thumb-hint {
    color: var(--text-dim);
}

.thumb-error {
    color: var(--red);
    word-break: break-word;
    line-height: 1.4;
    max-width: 100%;
}

.thumb-spinner {
    width: 18px;
    height: 18px;
    border-radius: 50%;
    border: 2px solid var(--border-light);
    border-top-color: var(--accent);
    animation: thumb-spin 0.7s linear infinite;
}

@keyframes thumb-spin {
    to { transform: rotate(360deg); }
}

/* Refresh / retry button is overlaid in the bottom-right of an image thumb,
   and stacked underneath the message in the error state. */
.device-thumb[data-state="image"] .thumb-retry {
    position: absolute;
    bottom: 0.3rem;
    right: 0.3rem;
    opacity: 0;
    transition: opacity 0.15s ease;
}

.device-thumb[data-state="image"]:hover .thumb-retry {
    opacity: 1;
}

.eye-picker {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
}

/* === Convergence Controls === */

.convergence-controls {
    padding: 0.85rem 1.15rem;
    border-top: 1px solid var(--border);
    font-size: 0.8rem;
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    background: linear-gradient(180deg, transparent 0%, var(--accent-glow) 100%);
}

.convergence-controls label {
    font-size: 0.68rem;
    font-weight: 500;
    color: var(--text-dim);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    min-width: 58px;
}

.convergence-controls input[type="range"] {
    flex: 1;
    height: 3px;
    margin: 0;
}

.conv-row {
    display: flex;
    align-items: center;
    gap: 0.75rem;
}

.convergence-controls input[type="number"] {
    width: 5rem;
    background: var(--bg);
    border: 1px solid var(--border);
    color: var(--text);
    padding: 0.3rem 0.55rem;
    border-radius: var(--radius);
    font-family: var(--mono);
    font-size: 0.75rem;
    outline: none;
    transition: border-color 0.15s ease;
}

.convergence-controls input[type="number"]:focus {
    border-color: var(--accent-dim);
}

.conv-num {
    width: 4.5rem !important;
    font-family: var(--mono);
    font-size: 0.8rem;
}

.conv-unit {
    color: var(--text-dim);
    font-size: 0.68rem;
    font-family: var(--mono);
}

.conv-result {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding-top: 0.5rem;
    border-top: 1px solid var(--border);
    gap: 0.75rem;
}

.conv-shift-label {
    font-family: var(--mono);
    font-size: 0.82rem;
    color: var(--accent);
    font-weight: 600;
}

.conv-eye-label {
    font-family: var(--mono);
    font-size: 0.68rem;
    color: var(--text-dim);
}

/* === Empty States === */

.empty-state {
    color: var(--text-dim);
    text-align: center;
    padding: 3.5rem 2rem;
    font-size: 0.85rem;
    font-weight: 300;
    letter-spacing: 0.02em;
}

.empty-state p {
    margin: 0;
}

.empty-state-hint {
    margin-top: 0.6rem !important;
    font-size: 0.78rem;
    color: var(--text-muted);
    max-width: 36rem;
    margin-left: auto !important;
    margin-right: auto !important;
}

.empty-state strong {
    color: var(--text);
    font-weight: 500;
}

.scenes-empty-hint {
    color: var(--text-dim);
    font-size: 0.78rem;
    padding: 0.4rem 0.2rem;
    line-height: 1.5;
}

.scenes-empty-hint strong {
    color: var(--text-muted);
    font-weight: 500;
}

/* === Recordings === */

.rec-list {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.rec-item {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 0.7rem 1.15rem;
    display: flex;
    justify-content: space-between;
    align-items: center;
    font-size: 0.85rem;
    transition: all 0.15s ease;
    box-shadow: var(--shadow-sm);
}

.rec-item:hover {
    border-color: var(--border-light);
    box-shadow: var(--shadow-md);
}

.rec-item a {
    color: var(--accent);
    text-decoration: none;
    font-weight: 500;
}

.rec-item a:hover { text-decoration: underline; }

.rec-meta {
    color: var(--text-dim);
    font-family: var(--mono);
    font-size: 0.72rem;
}

.rec-link {
    color: var(--accent);
    text-decoration: none;
    font-size: 0.8rem;
}

.rec-link:hover { text-decoration: underline; }

/* === Toast Notifications === */

#toast-container {
    position: fixed;
    top: 1.5rem;
    right: 1.5rem;
    z-index: 9999;
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    pointer-events: none;
}

.toast {
    font-family: var(--font);
    padding: 0.7rem 1.1rem;
    border-radius: var(--radius);
    font-size: 0.8rem;
    min-width: 300px;
    max-width: 440px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    animation: toast-in 0.3s cubic-bezier(0.22, 1, 0.36, 1);
    pointer-events: auto;
    backdrop-filter: blur(12px);
    box-shadow: var(--shadow-lg);
}

.toast-error {
    background: rgba(30, 8, 8, 0.95);
    border: 1px solid var(--red-border);
    color: var(--red);
}

.toast-success {
    background: rgba(8, 30, 16, 0.95);
    border: 1px solid var(--green-border);
    color: var(--green);
}

.toast .close-btn {
    background: none;
    border: none;
    color: inherit;
    cursor: pointer;
    font-size: 1.1rem;
    padding: 0 0 0 0.85rem;
    opacity: 0.5;
    transition: opacity 0.15s ease;
}

.toast .close-btn:hover { opacity: 1; }

@keyframes toast-in {
    from { opacity: 0; transform: translateY(-0.75rem) scale(0.97); }
    to { opacity: 1; transform: none; }
}

/* === Profiles Grid === */

.profiles-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
    gap: 1rem;
}

.profile-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: 1.1rem 1.25rem;
    transition: all 0.15s ease;
    box-shadow: var(--shadow-sm);
}

.profile-card:hover {
    border-color: var(--border-light);
    box-shadow: var(--shadow-md);
}

.profile-card-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 0.75rem;
}

.profile-card-name {
    font-weight: 600;
    font-size: 0.9rem;
}

.profile-card-codec {
    font-family: var(--mono);
    font-size: 0.62rem;
    font-weight: 600;
    padding: 0.2rem 0.6rem;
    border-radius: 100px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    background: var(--accent-glow);
    color: var(--accent);
    border: 1px solid rgba(106, 173, 255, 0.15);
}

.profile-card-details {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.4rem 1rem;
}

.profile-detail {
    display: flex;
    justify-content: space-between;
    font-size: 0.75rem;
}

.profile-detail-label {
    color: var(--text-dim);
}

.profile-detail-value {
    font-family: var(--mono);
    color: var(--text-muted);
    font-size: 0.72rem;
}

.profile-card-desc {
    margin-top: 0.6rem;
    padding-top: 0.6rem;
    border-top: 1px solid var(--border);
    font-size: 0.75rem;
    color: var(--text-dim);
    font-style: italic;
}

.profile-card-actions {
    display: flex;
    gap: 0.4rem;
    margin-top: 0.75rem;
    padding-top: 0.6rem;
    border-top: 1px solid var(--border);
}

/* === Modal === */

.modal-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.7);
    backdrop-filter: blur(4px);
    z-index: 1000;
    display: flex;
    align-items: center;
    justify-content: center;
    animation: fade-in 0.15s ease;
}

@keyframes fade-in {
    from { opacity: 0; }
    to { opacity: 1; }
}

.modal {
    background: var(--surface);
    border: 1px solid var(--border-light);
    border-radius: var(--radius-lg);
    width: 90%;
    max-width: 600px;
    box-shadow: var(--shadow-lg);
    animation: modal-in 0.2s cubic-bezier(0.22, 1, 0.36, 1);
}

@keyframes modal-in {
    from { opacity: 0; transform: scale(0.96) translateY(8px); }
    to { opacity: 1; transform: none; }
}

.modal-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 1.1rem 1.5rem;
    border-bottom: 1px solid var(--border);
}

.modal-header h2 {
    font-size: 0.9rem;
    font-weight: 600;
    color: var(--text);
}

.modal-body {
    padding: 1.25rem 1.5rem;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    max-height: calc(85vh - 8rem);
    overflow-y: auto;
}

.modal-body .form-grid { margin-bottom: 0.25rem; }

.modal.modal-wide { max-width: 720px; }

/* "Already streaming this composite live — jump to the card" banner that
   replaces the pre-flight preview pane when the form's profile + sources
   match a running stream. Sized to the same width footprint as the pane so
   the form layout doesn't jump when it appears. */
.composite-preview-matched {
    align-self: center;
    width: 100%;
    max-width: 480px;
    background: var(--surface-raised);
    border: 1px solid var(--border-light);
    border-left: 3px solid var(--green, #5dd39e);
    border-radius: var(--radius);
    padding: 0.75rem 1rem;
    margin-bottom: 0.75rem;
    color: var(--text-muted);
    font-size: 0.78rem;
    line-height: 1.4;
}
.composite-preview-matched a {
    color: var(--accent, #6aadff);
    text-decoration: none;
}
.composite-preview-matched a:hover { text-decoration: underline; }
.composite-preview-matched .matched-stream-icon {
    color: var(--green, #5dd39e);
    margin-right: 0.25rem;
}

.profile-preview-pane {
    align-self: center;
    width: 100%;
    max-width: 480px;
    aspect-ratio: 16 / 9;
    background: var(--surface-raised);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-direction: column;
    gap: 0.4rem;
    color: var(--text-dim);
    font-size: 0.7rem;
    font-family: var(--mono);
    text-align: center;
    padding: 0.5rem;
    margin-bottom: 0.75rem;
    /* Anchor for the absolutely-positioned blend overlay. Without this the
       `width: 200%` rule sizes against the viewport instead of the pane. */
    position: relative;
}

.profile-preview-pane img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
}

.profile-preview-pane[data-state="image"] {
    padding: 0;
    background: #000;
}

/* Composite-pane preview tools row (Crosshair + 50% Blend toggles). Sits
   above the pane so adjustments are visible while iterating in the form. */

.composite-preview-tools {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 1rem;
    margin: 0.4rem 0 0.5rem;
    font-size: 0.78rem;
    color: var(--text-muted);
}

.composite-preview-tools .check-label {
    cursor: pointer;
}

.composite-tools-hint {
    color: var(--text-dim);
    font-size: 0.7rem;
    font-style: italic;
}

/* Click cursor on the pane only when the crosshair toggle invites placement. */
.profile-preview-pane.crosshair-on { cursor: crosshair; }

/* Brief expanding ring at the click point so the operator knows the click
   registered — the crosshair SVG redraw alone is too instantaneous to notice. */
.composite-click-pulse {
    position: absolute;
    width: 16px;
    height: 16px;
    margin-left: -8px;
    margin-top: -8px;
    border-radius: 50%;
    border: 2px solid rgba(255, 80, 80, 0.95);
    pointer-events: none;
    animation: composite-click-pulse 0.6s ease-out forwards;
    z-index: 5;
}

@keyframes composite-click-pulse {
    0%   { transform: scale(0.4); opacity: 1; }
    100% { transform: scale(2.6); opacity: 0; }
}

/* Two <img> overlay model for blend mode. Mirror of `.preview-blend` /
   `.preview-stage.blend-on …` for the on-card stage but sized to the static
   composite pane and using `<img>` instead of `<video>`. */

.profile-preview-pane .composite-img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
}

.profile-preview-pane .composite-blend {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: contain;
    opacity: 0.5;
    pointer-events: none;
    /* Hidden until blend is on — `display:none` so it doesn't compete for
       layout when not in use. */
    display: none;
}

.profile-preview-pane.blend-on.layout-sbs .composite-img,
.profile-preview-pane.blend-on.layout-sbs .composite-blend {
    position: absolute;
    top: 0;
    width: 200%;
    height: 100%;
    object-fit: fill;
    display: block;
}

.profile-preview-pane.blend-on.layout-sbs .composite-img { left: 0; }
.profile-preview-pane.blend-on.layout-sbs .composite-blend { left: -100%; }

.profile-preview-pane.blend-on.layout-tab .composite-img,
.profile-preview-pane.blend-on.layout-tab .composite-blend {
    position: absolute;
    left: 0;
    width: 100%;
    height: 200%;
    object-fit: fill;
    display: block;
}

.profile-preview-pane.blend-on.layout-tab .composite-img { top: 0; }
.profile-preview-pane.blend-on.layout-tab .composite-blend { top: -100%; }

/* Crosshair overlay — two SVGs, one per eye, each centred on its own eye
   region so the cross sits where the operator is actually trying to align.
   Mirrors the on-card pattern; in blend mode both crosshairs cover the full
   pane (eye images overlay there). */

.profile-preview-pane .composite-crosshair {
    position: absolute;
    pointer-events: none;
    display: none;
    inset: 0;
}

.profile-preview-pane.crosshair-on .composite-crosshair {
    display: block;
}

/* SBS non-blend: left half / right half. */
.profile-preview-pane.layout-sbs:not(.blend-on) .composite-crosshair-left {
    left: 0; top: 0; right: auto; bottom: auto;
    width: 50%; height: 100%;
}
.profile-preview-pane.layout-sbs:not(.blend-on) .composite-crosshair-right {
    left: 50%; top: 0; right: auto; bottom: auto;
    width: 50%; height: 100%;
}

/* TAB non-blend: top half / bottom half. */
.profile-preview-pane.layout-tab:not(.blend-on) .composite-crosshair-left {
    left: 0; top: 0; right: auto; bottom: auto;
    width: 100%; height: 50%;
}
.profile-preview-pane.layout-tab:not(.blend-on) .composite-crosshair-right {
    left: 0; top: 50%; right: auto; bottom: auto;
    width: 100%; height: 50%;
}

/* Flat: only the single (left) crosshair across the whole pane. */
.profile-preview-pane.layout-flat .composite-crosshair-right { display: none; }
.profile-preview-pane.layout-flat.crosshair-on .composite-crosshair-left {
    left: 0; top: 0; width: 100%; height: 100%;
}

/* Blend mode: both crosshairs cover the full pane to match where each eye
   image now lands (200%-scaled and stacked at the same screen rect). */
.profile-preview-pane.blend-on .composite-crosshair-left,
.profile-preview-pane.blend-on .composite-crosshair-right {
    left: 0; top: 0; width: 100%; height: 100%;
}

/* Same L=red / R=cyan convention as the on-card crosshairs so the operator
   doesn't have to context-switch between the form pane and the running
   stream's preview. */
.profile-preview-pane .composite-crosshair-left line { stroke: rgba(255, 80, 80, 0.85); }
.profile-preview-pane .composite-crosshair-right line { stroke: rgba(80, 220, 255, 0.85); }
.profile-preview-pane .composite-crosshair-left circle { fill: rgba(255, 80, 80, 0.95); }
.profile-preview-pane .composite-crosshair-right circle { fill: rgba(80, 220, 255, 0.95); }
.profile-preview-pane .composite-crosshair line {
    stroke-width: 1.2;
    vector-effect: non-scaling-stroke;
}
.profile-preview-pane .composite-crosshair circle {
    stroke: rgba(255, 255, 255, 0.7);
    stroke-width: 1;
    vector-effect: non-scaling-stroke;
}

.modal-footer {
    display: flex;
    justify-content: flex-end;
    gap: 0.5rem;
    padding: 1rem 1.5rem;
    border-top: 1px solid var(--border);
}

/* Help button in the header. Compact "?" icon with optional "Sync help"
   label that hides on narrow viewports. Sits left of the status pill so
   the eye lands on it after reading the build SHA. */
.help-btn {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    margin-right: 0.6rem;
    padding-inline: 0.6rem;
}
.help-btn-label {
    font-size: 0.78rem;
}
@media (max-width: 720px) {
    .help-btn-label { display: none; }
}

/* Help walkthrough modal — wider than the default modal because the
   content has lots of structured prose + code spans. Same chrome as
   profile-edit / scene-load modals (handled by .modal-overlay /
   .modal). */
.modal-overlay {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.65);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 1100;
    padding: 1rem;
}
.modal-overlay[hidden] { display: none; }
.modal {
    background: var(--surface, #14141a);
    border: 1px solid var(--border, #2a2a32);
    border-radius: var(--radius-lg, 8px);
    box-shadow: 0 12px 60px rgba(0, 0, 0, 0.7);
    max-width: 520px;
    width: 100%;
    max-height: 90vh;
    display: flex;
    flex-direction: column;
}
.modal-help {
    max-width: 760px;
}
.modal-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 1rem 1.5rem;
    border-bottom: 1px solid var(--border);
}
.modal-header h2 {
    margin: 0;
    font-size: 1.05rem;
}
.modal-close {
    background: none;
    border: none;
    color: var(--text-muted);
    font-size: 1.4rem;
    line-height: 1;
    cursor: pointer;
    padding: 0.2rem 0.5rem;
}
.modal-close:hover { color: var(--text); }
.modal-body {
    overflow-y: auto;
    padding: 1.25rem 1.5rem;
    color: var(--text);
    font-size: 0.85rem;
    line-height: 1.55;
}
.modal-body section + section { margin-top: 1.4rem; }
.modal-body h3 {
    margin: 0 0 0.5rem;
    font-size: 0.92rem;
    color: var(--accent, #6aadff);
}
.modal-body p { margin: 0.4rem 0; }
.modal-body ul, .modal-body ol {
    margin: 0.4rem 0;
    padding-left: 1.4rem;
}
.modal-body li { margin: 0.35rem 0; }
.modal-body code {
    background: rgba(106, 173, 255, 0.10);
    border: 1px solid rgba(106, 173, 255, 0.18);
    padding: 0.05rem 0.32rem;
    border-radius: 3px;
    font-family: var(--mono);
    font-size: 0.92em;
}

/* === Settings Grid === */

.settings-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1.5rem;
}

.settings-section h3 {
    font-size: 0.72rem;
    font-weight: 600;
    color: var(--text-dim);
    text-transform: uppercase;
    letter-spacing: 0.1em;
    margin-bottom: 0.75rem;
}

.settings-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 0.45rem 0;
    border-bottom: 1px solid var(--border);
}

.settings-row:last-child {
    border-bottom: none;
}

.settings-label {
    font-size: 0.8rem;
    color: var(--text-muted);
}

.settings-value {
    font-family: var(--mono);
    font-size: 0.78rem;
    color: var(--text);
}

.settings-badge {
    font-family: var(--mono);
    font-size: 0.65rem;
    padding: 0.15rem 0.55rem;
    border-radius: 100px;
    font-weight: 600;
}

.settings-badge.available {
    background: var(--green-bg);
    color: var(--green);
    border: 1px solid var(--green-border);
}

.settings-badge.unavailable {
    background: var(--red-bg);
    color: var(--text-dim);
    border: 1px solid var(--border);
}

/* === Responsive === */

@media (max-width: 900px) {
    main { padding: 1rem; }
    .app-header { padding: 0.75rem 1rem; flex-wrap: wrap; }
    .header-left { flex-wrap: wrap; gap: 0.75rem; }
    .app-logo { height: 40px; }
    .header-nav { overflow-x: auto; }
    .form-grid, .form-grid.three-col { grid-template-columns: 1fr; }
    .streams { grid-template-columns: 1fr; }
    .profiles-grid { grid-template-columns: 1fr; }
    .settings-grid { grid-template-columns: 1fr; }
    .panel { padding: 1rem; }
}

/* v0.4.0 per-source telemetry rows. One row per cam: state pill,
 * label, URI tail, fps, age, optional respawn badge, and the
 * enable/swap action buttons. Compact + monospace for the metric
 * columns so the numbers line up tick-to-tick. */
.stream-captures {
    margin: 0.5rem 0 0.25rem;
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    font-size: 0.85rem;
}
.cap-row {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.25rem 0.5rem;
    background: var(--surface-2, rgba(255, 255, 255, 0.03));
    border-radius: 4px;
    flex-wrap: wrap;
}
.cap-pill {
    display: inline-block;
    width: 0.7rem;
    height: 0.7rem;
    line-height: 0.7rem;
    text-align: center;
    font-size: 1.2rem;
    border-radius: 50%;
    flex-shrink: 0;
}
.cap-pill-live { color: var(--green, #4ade80); }
.cap-pill-stalled { color: var(--orange, #fb923c); }
.cap-pill-reconnecting {
    color: var(--blue, #60a5fa);
    animation: cap-blink 1s ease-in-out infinite;
}
.cap-pill-disabled { color: var(--text-dim, #9ca3af); opacity: 0.5; }
@keyframes cap-blink { 50% { opacity: 0.3; } }
.cap-label { font-weight: 600; min-width: 2.5rem; }
.cap-uri {
    color: var(--text-dim, #9ca3af);
    font-family: var(--mono, ui-monospace, monospace);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    flex: 1 1 8rem;
    min-width: 4rem;
}
.cap-fps, .cap-age {
    font-family: var(--mono, ui-monospace, monospace);
    color: var(--text-dim, #9ca3af);
    min-width: 4.5rem;
    text-align: right;
}
.cap-recon {
    color: var(--orange, #fb923c);
    font-size: 0.8rem;
    padding: 0 0.4rem;
    border-radius: 3px;
    background: rgba(251, 146, 60, 0.1);
}
.cap-toggle, .cap-swap {
    font-size: 0.75rem;
    padding: 0.15rem 0.5rem;
    border: 1px solid var(--border, rgba(255,255,255,0.1));
    background: transparent;
    color: var(--text, #e5e7eb);
    border-radius: 3px;
    cursor: pointer;
}
.cap-toggle:hover, .cap-swap:hover {
    background: var(--surface-3, rgba(255, 255, 255, 0.05));
}
.cap-toggle-off {
    background: rgba(251, 146, 60, 0.1);
    border-color: rgba(251, 146, 60, 0.3);
}
.cap-toggle:disabled, .cap-swap:disabled {
    opacity: 0.5;
    cursor: wait;
}

@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
    }
}
