/* GX-12q: per-OS tab bar inside the certificate-install modal.
   Five small pills the user toggles between Windows / macOS /
   iOS / Android / Linux. Active tab gets the accent fill so
   the user always knows which set of instructions they're
   reading. */
.cert-modal-tabs {
    display: flex;
    flex-wrap: wrap;
    gap: 2px;
    margin: 8px 0 12px;
    border-bottom: 1px solid var(--border);
    padding-bottom: 0;
}
.cert-modal-tab {
    background: transparent;
    border: none;
    border-bottom: 2px solid transparent;
    color: var(--text-secondary);
    padding: 7px 12px;
    margin-bottom: -1px;
    border-radius: 0;
    font-size: 12px;
    cursor: pointer;
    transition: color 0.12s, border-color 0.12s;
}
.cert-modal-tab:hover {
    color: var(--text-primary);
}
.cert-modal-tab.active {
    color: var(--accent, #5b8def);
    border-bottom-color: var(--accent, #5b8def);
    font-weight: 600;
}
/* .cert-modal-content holds the per-OS tab body. Typography rules
   come from .cert-modal-frame above, only the pre block needs
   its own dark-background treatment for the Linux certutil snippet. */
.cert-modal-content pre {
    background: rgba(0, 0, 0, 0.35);
    padding: 10px;
    border-radius: 4px;
    overflow-x: auto;
    font-size: 11.5px;
    line-height: 1.4;
}

/* GX-12p: Strength sliders in the Denoise / Decon modals.
   Replaces the type=number stepper that was too small to hit
   accurately on iPhone (and triggered the iOS soft keyboard,
   which hides half the modal). Slider sits in a flex row with
   the numeric value to the right so the user always sees the
   exact float they're committing to. */
.graxpert-strength-row {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
}
.graxpert-strength-slider {
    flex: 1 1 auto;
    min-width: 0;        /* let flex shrink instead of overflowing */
    height: 28px;        /* generous touch target on mobile (Apple HIG ≥ 44pt total, padding bumps it there) */
    accent-color: var(--accent, #5b8def);
    cursor: pointer;
}
.graxpert-strength-value {
    flex: 0 0 auto;
    min-width: 3em;
    text-align: right;
    font-variant-numeric: tabular-nums;   /* number doesn't dance as you drag */
    font-size: 13px;
    color: var(--text-primary);
}

.graxpert-compare-handle {
    /* GX-11c: the handle is a 40-px-wide invisible HIT TARGET
       centred on the split column. The visible 2-px bar + the
       36-px circular grip live inside it but are pointer-events:
       none so the whole 40-px column reliably receives mousedown.
       Previously the handle was only the 2-px bar, which meant
       clicking the obvious-looking grip missed by ~17 px and
       fell through to the wrap (which had no listener), drag
       was effectively broken on every screen. */
    position: absolute;
    top: 0;
    bottom: 0;
    left: var(--split);
    width: 40px;
    transform: translateX(-20px);
    cursor: ew-resize;
    z-index: 2;
    background: transparent;
}
.graxpert-compare-handle-bar {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 50%;
    width: 2px;
    transform: translateX(-1px);
    background: rgba(255, 255, 255, 0.85);
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.35);
    pointer-events: none;
}
.graxpert-compare-handle-grip {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 36px;
    height: 36px;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.92);
    color: #0f172a;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 700;
    font-size: 13px;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
    pointer-events: none;
}
.graxpert-compare-tag {
    position: absolute;
    top: 10px;
    padding: 3px 9px;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.6px;
    color: #fff;
    background: rgba(0, 0, 0, 0.55);
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
    pointer-events: none;
}
.graxpert-compare-tag-src { left: 12px; }
.graxpert-compare-tag-out { right: 12px; }
.editor-export-row {
    display: grid;
    grid-template-columns: 80px 1fr auto;
    align-items: center;
    gap: 10px;
    margin-bottom: 12px;
    font-size: 12px;
}
.editor-export-row select,
.editor-export-row input[type="number"] { width: 100%; }
.editor-export-row .muted { white-space: nowrap; }

/* Theme the Export modal's dropdowns + number input so they match the
   dark UI instead of falling back to the OS white combobox. Mirrors the
   Settings-card select styling: dark surface, custom chevron, focus ring,
   and color-scheme:dark so the option popup renders dark too. */
.editor-export-row select,
.editor-export-row input[type="number"] {
    background: var(--bg-input);
    color: var(--text-primary);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 9px 10px;
    min-height: 38px;
    box-sizing: border-box;
    font-size: 12px;
    outline: none;
    color-scheme: dark;
    transition: border-color var(--transition), box-shadow var(--transition);
}
.editor-export-row select {
    padding-right: 32px;
    cursor: pointer;
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'><path d='M2 4l4 4 4-4' fill='none' stroke='%23a8b3bf' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/></svg>");
    background-repeat: no-repeat;
    background-position: right 10px center;
}
.editor-export-row select:hover {
    border-color: var(--text-secondary);
}
.editor-export-row select:focus,
.editor-export-row input[type="number"]:focus {
    border-color: var(--accent);
    box-shadow: 0 0 0 2px rgba(var(--accent-rgb), 0.25);
}
.editor-export-row select option {
    background: var(--bg-secondary);
    color: var(--text-primary);
}

/* ──────────────────────── /EDITOR ──────────────────────── */

.tonight-panel,
.milkyway-panel {
    padding: 16px;
    overflow-y: auto;
    gap: 14px;
    display: flex;
    flex-direction: column;
}

.tonight-filters {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
}
/* Provenance of the comet elements. Deliberately quiet: a footnote under the
   filters, not a banner competing with the object list. */
.tonight-comet-note {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    margin-top: 6px;
    font-size: 11px;
    color: var(--text-secondary);
}
/* Bundled, or old enough that a new discovery could be missing. A tint, not a
   red alert: an out-of-date comet list is a limitation, not a fault. */
.tonight-comet-note .stale {
    color: var(--warning, #d9a441);
}
.tonight-filter-chip {
    background: rgba(255, 255, 255, 0.04);
    color: var(--text-secondary);
    border: 1px solid var(--border);
    padding: 4px 10px;
    border-radius: 12px;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.4px;
    cursor: pointer;
    user-select: none;
    transition: background 0.12s ease, color 0.12s ease;
}
.tonight-filter-chip:hover { background: rgba(255, 255, 255, 0.08); }
.tonight-filter-chip.active {
    background: rgba(var(--accent-rgb), 0.18);
    color: var(--accent-text);
    border-color: rgba(var(--accent-rgb), 0.5);
}
.tonight-filter-chip.disabled {
    opacity: 0.4;
    cursor: not-allowed;
}
.tonight-filter-toggle {
    margin-left: 6px;
}

.tonight-list {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.tonight-card {
    display: grid;
    grid-template-columns: 80px minmax(220px, 1fr) minmax(180px, 2fr) 78px auto;
    gap: 12px;
    align-items: center;
    background: var(--bg-secondary);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 10px 12px;
    transition: border-color 0.12s ease;
}
.tonight-card:hover { border-color: rgba(79, 195, 247, 0.4); }
/* Coloured left edge to distinguish categories at a glance */
.tonight-card--moon   { border-left: 3px solid #c8e6c9; }
.tonight-card--planet { border-left: 3px solid #ffe082; }
.tonight-card--dso    { border-left: 3px solid #90caf9; }
.tonight-card--comet  { border-left: 3px solid #ce93d8; }

.tonight-thumb {
    width: 80px;
    height: 80px;
    border-radius: 4px;
    overflow: hidden;
    background: #0c1226;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}
.tonight-thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}
.tonight-thumb-placeholder {
    color: var(--text-muted);
    font-size: 20px;
}

.tonight-info {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
}
.tonight-name-row {
    display: flex;
    align-items: baseline;
    gap: 6px;
    flex-wrap: wrap;
}
.tonight-name {
    color: var(--accent2);
    text-decoration: none;
    font-weight: 700;
    font-size: 13px;
}
.tonight-name:hover { text-decoration: underline; }
.tonight-common {
    color: var(--text-secondary);
    font-size: 12px;
}
.tonight-score {
    margin-left: auto;
    color: var(--text-muted);
    font-size: 10px;
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.5px;
}
.tonight-coords {
    display: flex;
    gap: 12px;
    flex-wrap: wrap;
    font-size: 11px;
    color: var(--text-secondary);
    font-variant-numeric: tabular-nums;
}
.tonight-badges {
    display: flex;
    gap: 6px;
    flex-wrap: wrap;
}
.tonight-badge {
    font-size: 10px;
    padding: 2px 6px;
    border-radius: 3px;
    font-weight: 600;
    letter-spacing: 0.3px;
}
.tonight-badge--fits  { background: rgba(var(--success-rgb), 0.22); color: #c8e6c9; }
.tonight-badge--big   { background: rgba(var(--warning-rgb), 0.22); color: #ffe0b2; }
.tonight-badge--peak  { background: rgba(255, 255, 255, 0.06); color: var(--text-secondary); }

.tonight-chart-cell {
    height: 78px;          /* bumped to fit the x-axis time ticks */
    min-width: 200px;
    position: relative;
}
.tonight-chart {
    width: 100% !important;
    height: 78px !important;
}

.tonight-compass {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1px;
}
.tonight-compass-svg {
    width: 60px;
    height: 60px;
}
.tonight-compass-label {
    font-size: 10px;
    color: var(--accent2);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}
.tonight-alt-now {
    font-size: 10px;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

.tonight-actions {
    display: flex;
    align-items: center;
}

@media (max-width: 900px) {
    .tonight-card {
        grid-template-columns: 64px 1fr auto;
        grid-template-areas:
            "thumb info compass"
            "thumb chart  chart"
            "thumb actions actions";
    }
    .tonight-thumb { width: 64px; height: 64px; grid-area: thumb; }
    .tonight-info { grid-area: info; }
    .tonight-chart-cell { grid-area: chart; height: 48px; min-width: 0; }
    .tonight-chart { height: 48px !important; }
    .tonight-compass { grid-area: compass; }
    .tonight-actions { grid-area: actions; justify-content: flex-end; }
}

/* Observatory location helpers, address search + Use my location */
.obs-location-tools {
    display: flex;
    flex-direction: column;
    gap: 6px;
    margin-bottom: 10px;
}
.obs-location-row {
    display: flex;
    gap: 6px;
    align-items: center;
    flex-wrap: wrap;
}
.obs-location-error {
    font-size: 11px;
    color: #ffb74d;
}
.obs-location-results {
    display: flex;
    flex-direction: column;
    gap: 2px;
    background: rgba(255, 255, 255, 0.03);
    border: 1px solid var(--border);
    border-radius: 4px;
    padding: 4px;
}
.obs-location-result {
    text-align: left;
    background: transparent;
    border: none;
    color: var(--text);
    padding: 6px 8px;
    border-radius: 3px;
    cursor: pointer;
    font-size: 12px;
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.obs-location-result:hover {
    background: rgba(var(--accent-rgb), 0.12);
}

/* ─── STUDIO tab, frame browser (ST-1) ─────────────────────────────── */
.studio-panel {
    padding: 16px;
    overflow: hidden;             /* the layout below owns scrolling */
    gap: 12px;
    display: flex;
    flex-direction: column;
}

/* Two-column STUDIO workspace: tree on the left, single-frame
   preview on the right. The wrapper takes whatever vertical
   space the panel has after header + filters + selection-bar,
   and each column owns its own scroll. min-height:0 + the
   parent flex:1 are both required for the inner scrolls to
   actually engage instead of pushing the whole page. */
.studio-layout {
    display: flex;
    flex-direction: row;
    gap: 12px;
    flex: 1;
    min-height: 0;
}
.studio-tree-col {
    flex: 0 0 320px;
    overflow-y: auto;
    padding-right: 4px;
    border-right: 1px solid var(--border);
}
.studio-preview-col {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    display: flex;
    flex-direction: column;
}

/* Native <details>/<summary> drive expand/collapse — no JS,
   no state to persist, and keyboard nav (Space/Enter on the
   summary) comes free. The chevron is the default disclosure
   triangle; we just style spacing + colours around it. */
.studio-tree-target,
.studio-tree-type,
.studio-tree-filter {
    margin: 0;
}
.studio-tree-target > summary,
.studio-tree-type > summary,
.studio-tree-filter > summary {
    list-style: none;
    cursor: pointer;
    user-select: none;
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 4px 6px;
    border-radius: 3px;
    font-size: 12px;
    color: var(--text-primary);
}
.studio-tree-target > summary::-webkit-details-marker,
.studio-tree-type > summary::-webkit-details-marker,
.studio-tree-filter > summary::-webkit-details-marker { display: none; }
.studio-tree-target > summary::before,
.studio-tree-type > summary::before,
.studio-tree-filter > summary::before {
    content: '▸';
    color: var(--text-muted);
    transition: transform 0.1s ease;
    font-size: 10px;
}
.studio-tree-target[open] > summary::before,
.studio-tree-type[open] > summary::before,
.studio-tree-filter[open] > summary::before {
    transform: rotate(90deg);
}
.studio-tree-target > summary:hover,
.studio-tree-type > summary:hover,
.studio-tree-filter > summary:hover {
    background: rgba(255, 255, 255, 0.05);
}

.studio-tree-target > summary { font-weight: 600; font-size: 13px; }
.studio-tree-type > summary,
.studio-tree-filter > summary { padding-left: 18px; }
.studio-tree-filter > summary { padding-left: 32px; }

.studio-tree-label { flex: 1; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.studio-tree-count {
    background: rgba(255, 255, 255, 0.06);
    color: var(--text-muted);
    border-radius: 999px;
    padding: 1px 7px;
    font-size: 10px;
    font-variant-numeric: tabular-nums;
}
.studio-tree-filter-name { flex: 1; }

/* Per-type colour tag inside the tree summary. Matches the
   AUTORUN sequence-card pill convention so the eye picks the
   same colour cue across both panels. */
.studio-tree-typetag {
    font-size: 10px;
    font-weight: 600;
    padding: 1px 7px;
    border-radius: 3px;
    text-transform: uppercase;
    letter-spacing: 0.3px;
}
.studio-tt-light  { background: rgba(var(--success-rgb), 0.18); color: #a5e1a8; }
.studio-tt-bias   { background: rgba(var(--accent-rgb), 0.18); color: var(--accent-text); }
.studio-tt-flat   { background: rgba(255, 167, 38, 0.18); color: #ffcc80; }
.studio-tt-dark   { background: rgba(229, 57, 53, 0.18); color: #ef9a9a; }
.studio-tt-master { background: rgba(156, 39, 176, 0.18); color: #ce93d8; }
.studio-tt-other  { background: rgba(255, 255, 255, 0.08); color: var(--text-muted); }

/* Leaf row (a single frame). active = currently in the right
   preview pane (single). selected = part of the multi-select
   batch (an item can be both — that's the typical case when
   the user clicks ONE thing). */
.studio-tree-frame {
    display: flex;
    align-items: baseline;
    gap: 6px;
    padding: 3px 6px 3px 48px;
    border-radius: 3px;
    cursor: pointer;
    font-size: 11px;
    color: var(--text-secondary);
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
}
.studio-tree-frame:hover { background: rgba(255, 255, 255, 0.04); }
.studio-tree-frame.selected {
    background: rgba(var(--accent-rgb), 0.12);
    color: var(--text-primary);
}
.studio-tree-frame.active {
    background: rgba(var(--accent-rgb), 0.25);
    color: var(--text-primary);
    border-left: 2px solid var(--accent);
    padding-left: 46px;
}
.studio-tree-frame-name { flex: 1; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.studio-tree-frame-meta { color: var(--text-muted); font-size: 10px; flex-shrink: 0; }

/* Right preview pane */
.studio-preview-empty {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    color: var(--text-muted);
    text-align: center;
    padding: 32px;
}
.studio-preview-empty p { margin: 6px 0; }
.studio-preview-empty .hint { font-size: 11px; }
.studio-preview-body {
    flex: 1;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    gap: 12px;
}
.studio-preview-image {
    flex: 1;
    min-height: 0;
    background: #000;
    border-radius: 6px;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    cursor: zoom-in;
}
.studio-preview-image img {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
}
.studio-preview-meta {
    flex: 0 0 auto;
    padding: 8px 12px;
    background: var(--bg-secondary);
    border: 1px solid var(--border);
    border-radius: 6px;
}
.studio-preview-meta h3 {
    margin: 0 0 8px;
    font-size: 13px;
    font-weight: 600;
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
    color: var(--text-primary);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.studio-preview-dl {
    margin: 0 0 10px;
    display: grid;
    grid-template-columns: 80px 1fr;
    gap: 2px 12px;
    font-size: 11px;
}
.studio-preview-dl dt { color: var(--text-muted); }
.studio-preview-dl dd { margin: 0; color: var(--text-primary); }
.studio-preview-actions {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
}

/* Mobile: stack vertically. Threshold matches .live-pane etc. */
@media (max-width: 900px) {
    .studio-layout { flex-direction: column; }
    .studio-tree-col {
        flex: 0 0 auto;
        max-height: 40vh;
        border-right: none;
        border-bottom: 1px solid var(--border);
    }
}
.studio-filters {
    display: flex;
    gap: 8px;
    align-items: center;
    flex-wrap: wrap;
}
.studio-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
    gap: 12px;
}
.studio-card {
    background: var(--bg-secondary);
    border: 1px solid var(--border);
    border-radius: 6px;
    overflow: hidden;
    cursor: pointer;
    display: flex;
    flex-direction: column;
    transition: border-color 0.1s ease;
}
.studio-card:hover { border-color: rgba(79, 195, 247, 0.5); }
.studio-card.selected {
    border-color: var(--accent2);
    box-shadow: 0 0 0 1px var(--accent2) inset;
}
.studio-thumb {
    width: 100%;
    aspect-ratio: 4 / 3;
    background: #000;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
}
.studio-thumb img {
    width: 100%;
    height: 100%;
    object-fit: contain;
}
.studio-card-meta {
    padding: 6px 8px;
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.studio-card-name {
    font-size: 11px;
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
    color: var(--text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.studio-card-sub {
    font-size: 10px;
    color: var(--text-secondary);
}
.studio-card-sub.muted { color: var(--text-muted); }
.studio-selection-bar {
    position: sticky;
    bottom: 0;
    background: var(--bg-secondary);
    border-top: 1px solid var(--border);
    padding: 8px 12px;
    margin: 0 -16px -16px;
    display: flex;
    align-items: center;
    gap: 12px;
    z-index: 5;
}

/* ────────── ST-2: Frame viewer modal ────────── */
.studio-viewer-modal {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.78);
    z-index: 100;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 24px;
}
.studio-viewer {
    width: 100%;
    height: 100%;
    max-width: 1600px;
    background: var(--bg-primary);
    border: 1px solid var(--border);
    border-radius: 10px;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}
.studio-viewer-header {
    display: flex;
    align-items: center;
    gap: 16px;
    padding: 12px 16px;
    border-bottom: 1px solid var(--border);
    background: var(--bg-secondary);
}
.studio-viewer-title { flex: 1; display: flex; flex-direction: column; gap: 2px; }
.studio-viewer-title .muted { font-size: 11px; color: var(--text-muted); }
.studio-viewer-body {
    flex: 1;
    display: grid;
    grid-template-columns: 1fr 340px;
    min-height: 0;
}
.studio-viewer-canvas {
    background: #000;
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
}
.studio-viewer-img {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
    image-rendering: pixelated;
}

/* Pixel-exact rendering for every astro-image inspection surface: editor
   preview, crop picker, the before/after comparer, and the Auto Workflow
   detail preview. Astro pixels matter — the browser must NOT bilinear-
   smooth when the image is scaled up. `crisp-edges` first as the older
   Firefox spelling, then `pixelated` (Chrome/Edge/Safari + modern FF).
   The main PREVIEW/LIVE viewers use OpenSeadragon, disabled separately
   via imageSmoothingEnabled:false in app.js. */
/* VIEWFILT: .editor-preview-img is no longer in this list. The editor now
   carries an inline `image-rendering` from viewFilterCss(), so the operator
   picks nearest or smooth per session; nearest is still the default
   and still what these other surfaces get unconditionally. */
.crop-picker-img,
.graxpert-compare-src,
.graxpert-compare-out,
.awf-detail-preview {
    image-rendering: crisp-edges;
    image-rendering: pixelated;
}
/* Fallback for the editor layers before Alpine binds (and if it never does):
   the inline style from viewFilterCss() wins over this whenever it is set. */
.editor-preview-img {
    image-rendering: crisp-edges;
    image-rendering: pixelated;
}
.studio-viewer-stars {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
}
.studio-viewer-sidebar {
    background: var(--bg-secondary);
    border-left: 1px solid var(--border);
    padding: 16px;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 18px;
}
.studio-viewer-section h4 {
    margin: 0 0 8px;
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-muted);
}
.studio-slider {
    display: grid;
    grid-template-columns: 80px 1fr 56px;
    align-items: center;
    gap: 8px;
    font-size: 12px;
    margin-bottom: 6px;
}
.studio-slider input[type="range"] { width: 100%; }
.studio-slider-val {
    font-family: var(--font-mono, monospace);
    font-size: 11px;
    color: var(--text-muted);
    text-align: right;
}
.studio-histogram-wrap {
    height: 80px;
    background: var(--bg-primary);
    border: 1px solid var(--border);
    border-radius: 4px;
    padding: 4px;
}
.studio-stats {
    width: 100%;
    font-size: 12px;
    border-collapse: collapse;
}
.studio-stats td {
    padding: 3px 0;
    border-bottom: 1px solid var(--border);
}
.studio-stats td:first-child { color: var(--text-muted); }
.studio-stats td:last-child  {
    text-align: right;
    font-family: var(--font-mono, monospace);
}

@media (max-width: 900px) {
    .studio-viewer-body { grid-template-columns: 1fr; grid-template-rows: 1fr auto; }
    .studio-viewer-sidebar { border-left: 0; border-top: 1px solid var(--border); max-height: 50%; }
}

/* ────────── ST-3: Master frame modal ────────── */
.studio-master-modal {
    background: var(--bg-primary);
    border: 1px solid var(--border);
    border-radius: 10px;
    width: min(560px, 90vw);
    max-height: 90vh;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
}
.studio-master-body {
    padding: 16px;
    display: flex;
    flex-direction: column;
    gap: 12px;
}
.studio-master-progress {
    margin-top: 14px;
    padding: 12px;
    background: var(--bg-secondary);
    border-radius: 6px;
    display: flex;
    flex-direction: column;
    gap: 6px;
}
.studio-master-bar {
    height: 8px;
    background: var(--bg-primary);
    border: 1px solid var(--border);
    border-radius: 4px;
    overflow: hidden;
}
.studio-master-bar-fill {
    height: 100%;
    /* Progress fill in the new icy-cyan palette (matches the brand
       wordmark gradient). */
    background: linear-gradient(90deg, var(--accent2-deep), var(--accent2-light));
    transition: width 0.3s ease;
}

/* CC-6: combine modal tab strip + tab body. Three modes share the
   same modal shell + the common controls section below, so each
   tab body collapses to a compact area between header + actions. */
.combine-tabs {
    display: flex;
    gap: 0;
    border-bottom: 1px solid var(--border);
    margin-bottom: 10px;
}
.combine-tab {
    flex: 1;
    padding: 8px 12px;
    background: transparent;
    border: none;
    border-bottom: 2px solid transparent;
    color: var(--text-secondary);
    font-size: 13px;
    cursor: pointer;
    transition: color 0.15s, border-color 0.15s;
}
.combine-tab:hover:not(:disabled) {
    color: var(--text-primary);
}
.combine-tab.active {
    color: var(--accent);
    border-bottom-color: var(--accent);
}
.combine-tab:disabled {
    cursor: not-allowed;
    opacity: 0.5;
}
.combine-tab-body {
    display: flex;
    flex-direction: column;
    gap: 4px;
}

/* ===========================================================
   External tools (Siril + GraXpert), Settings section + Siril
   run modal. Each "row" is a card per tool with status pill +
   detail body.
   ----------------------------------------------------------- */
.external-tool-row {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 12px 14px;
    margin-bottom: 12px;
    background: rgba(0, 0, 0, 0.18);
}
.external-tool-head {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 8px;
}
.external-tool-head strong {
    font-size: 13px;
    color: var(--text-primary);
}
.external-tool-status {
    font-size: 12px;
    padding: 2px 10px;
    border-radius: 999px;
    font-weight: 600;
}
.external-tool-status.ok   { background: rgba(34, 197, 94, 0.18); color: #86efac; }
.external-tool-status.warn { background: rgba(245, 158, 11, 0.18); color: #fcd34d; }
.external-tool-status.idle { background: rgba(148, 163, 184, 0.18); color: #cbd5e1; }
.external-tool-status.error{ background: rgba(239, 68, 68, 0.20); color: #fca5a5; }
.external-tool-body {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

/* Siril run modal: dropdown of scripts, frame counts, progress
   bar, log tail. Reuses the location-setup-modal backdrop pattern. */
.siril-modal {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.85);
    z-index: 1000;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
}
.siril-modal-frame {
    background: var(--bg-secondary);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 22px;
    max-width: 640px;
    width: 100%;
    color: var(--text-primary);
}
.siril-modal-frame h2 {
    margin: 0 0 14px;
    font-size: 18px;
}
.siril-modal-row {
    display: flex;
    gap: 10px;
    margin-bottom: 10px;
    flex-wrap: wrap;
}
.siril-modal-row label {
    display: flex;
    flex-direction: column;
    gap: 4px;
    flex: 1;
    min-width: 180px;
}
/* GX-9 (UX): checkbox labels in the modal need a row layout, the
   default column-stack (designed for "title above input" form rows)
   floated the tick mark above its descriptor instead of next to it.
   Constrain to the .checkbox-label class so non-checkbox labels keep
   their stacked layout. */
.siril-modal-row label.checkbox-label {
    flex-direction: row;
    align-items: center;
    gap: 8px;
    /* GX-12e: breathing room above the checkbox row in the GraXpert
       modal. The "1 file(s) selected" line above and the "Run in
       browser" line were stacked with no separation, looked cramped. */
    margin-top: 10px;
}
.siril-modal-row label.checkbox-label input[type="checkbox"] {
    flex: 0 0 auto;
    margin: 0;
}
.siril-modal-counts {
    display: flex;
    gap: 12px;
    font-size: 12px;
    color: var(--text-muted);
    margin-bottom: 10px;
}
.siril-modal-counts strong { color: var(--text-primary); }
.siril-modal-progress {
    margin: 14px 0;
}
.siril-modal-progress-bar {
    width: 100%;
    height: 10px;
    background: var(--bg-primary);
    border-radius: 5px;
    overflow: hidden;
    border: 1px solid var(--border);
}
.siril-modal-progress-fill {
    height: 100%;
    /* Progress fill in the new icy-cyan palette (matches the brand
       wordmark gradient). */
    background: linear-gradient(90deg, var(--accent2-deep), var(--accent2-light));
    transition: width 0.4s ease;
}
.siril-modal-progress-meta {
    display: flex;
    justify-content: space-between;
    margin-top: 6px;
    font-size: 11px;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

/* AI-TOOLS-DISCOVERY: on-demand model download strip inside the AI tool
   modals. Shown when the selected model isn't on the host yet, or while a
   download is running (host fetch, or this device fetching for an offline
   host). Reuses .siril-modal-progress-* for the bar. */
.onnx-model-dl {
    margin: 10px 0 4px;
    padding: 10px 12px;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--bg-elev, rgba(255, 255, 255, 0.03));
}
.onnx-model-dl-offer {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
}
.onnx-model-dl-offer .hint { flex: 1 1 220px; }

/* Live Siril console — the stdout/stderr stream rendered as a
   terminal-style block inside the run modal so the user can watch
   the job progress instead of staring at an opaque progress bar.
   Fixed height + internal scroll so a 500-line log doesn't push
   the action buttons off-screen on a small viewport. */
.siril-console {
    margin: 12px 0 4px;
    border: 1px solid var(--border);
    border-radius: 6px;
    overflow: hidden;
    background: #050810;
}
.siril-console-head {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 10px;
    background: rgba(255, 255, 255, 0.03);
    border-bottom: 1px solid var(--border);
    font-size: 11px;
    color: var(--text-secondary);
}
.siril-console-body {
    margin: 0;
    padding: 8px 10px;
    height: 220px;
    overflow-y: auto;
    background: #050810;
    color: #c8d4e6;
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
    font-size: 11px;
    line-height: 1.45;
    white-space: pre-wrap;
    word-break: break-all;
}
.siril-console-err {
    /* stderr lines get a soft red so they pop without screaming. */
    color: #fca5a5;
}
/* CROP modal picker. The <img> sizes naturally so the aspect ratio
   stays right; the overlay <div> tracks the user-drawn rectangle in
   DISPLAY pixels (cropOnPointerMove computes left/top/w/h against
   the picker's bounding rect). Cursor is a crosshair so the
   "drag a rectangle" affordance is obvious.

   max-height is capped so even a 4K master doesn't push the modal
   off-screen on a laptop — the img scales down via the natural
   width / height ratio. */
.crop-picker {
    position: relative;
    /* inline-block so the picker shrink-wraps the image exactly (it is
       never smaller than the image, so the pointer-capture area always
       covers the whole photo). An earlier width:fit-content attempt made
       the picker NARROWER than the image, so the drag area — and thus the
       drawn rectangle — was smaller than the photo. */
    display: inline-block;
    margin: 12px auto;
    max-width: 100%;
    cursor: crosshair;
    user-select: none;
    -webkit-user-select: none;
    /* The crop rectangle is a touch-drag. Without touch-action:none the
       browser treats the gesture as a scroll/pan (the @touchmove.prevent
       is ignored on a passive listener), so the page scrolled under the
       finger and the rectangle no longer tracked the touch. none = this
       element owns all touch gestures. */
    touch-action: none;
    background: #000;
    /* Frame with an OUTLINE, not a border: a 1px border shifted the
       overlay's coordinate origin (border box vs padding box) and the old
       overflow:hidden + border-radius clipped the dashed rectangle right at
       the edges, so it never visually met the image border. outline takes
       no layout space and doesn't clip, so the selection reaches every
       edge. */
    outline: 1px solid var(--border);
    outline-offset: -1px;
}
.crop-picker-img {
    display: block;
    max-width: 100%;
    max-height: 60vh;
    object-fit: contain;
    pointer-events: none;
    user-select: none;
    -webkit-user-drag: none;
}
.crop-picker-overlay {
    position: absolute;
    border: 2px dashed var(--success);
    background: rgba(var(--success-rgb), 0.15);
    pointer-events: none;
    box-sizing: border-box;
}

/* ── STUDIO Analyze modal (tilt + aberration) ───────────────── */
.analyze-tabs {
    display: flex;
    gap: 2px;
    margin: 10px 0 4px;
    border-bottom: 1px solid var(--border);
}
.analyze-tabs button {
    background: transparent;
    color: var(--text-secondary);
    border: none;
    border-bottom: 2px solid transparent;
    border-radius: 0;
    padding: 7px 16px;
    margin-bottom: -1px;
    font-size: 13px;
    cursor: pointer;
    transition: color 0.12s, border-color 0.12s;
}
.analyze-tabs button:hover { color: var(--text-primary); }
.analyze-tabs button.active {
    background: transparent;
    color: var(--accent);
    border-bottom-color: var(--accent);
    font-weight: 600;
}
.analyze-body {
    display: flex;
    gap: 16px;
    flex-wrap: wrap;
    align-items: flex-start;
}
.analyze-stage {
    /* reuses .crop-picker (position:relative + border) */
    flex: 1 1 380px;
    min-width: 300px;
    cursor: default;
}
.analyze-overlay {
    position: absolute;
    left: 0;
    top: 0;
    pointer-events: none;
}
/* Aberration Inspector mosaic — a plain centred canvas (sized in JS). */
.analyze-inspector-canvas {
    display: block;
    max-width: 100%;
    max-height: calc(94vh - 170px);
}
.analyze-zoom {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-top: 10px;
    font-size: 13px;
    color: var(--text-secondary);
}
.analyze-zoom .btn {
    min-width: 30px;
    padding: 4px 8px;
    font-size: 16px;
    line-height: 1;
}
.analyze-side {
    flex: 1 1 260px;
    min-width: 240px;
    display: flex;
    flex-direction: column;
    gap: 10px;
}
.analyze-zone-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 3px;
    aspect-ratio: 3 / 2;
    max-width: 320px;
}
.analyze-zone-cell {
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 3px;
    font-size: 13px;
    font-weight: 600;
    color: #fff;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
    min-height: 38px;
}
.analyze-verdict {
    font-size: 13.5px;
    line-height: 1.5;
    margin: 2px 0;
    padding: 10px 12px;
    border-radius: var(--radius);
    background: var(--bg-input);
    border-left: 3px solid var(--border);
}
.analyze-verdict.sev-good    { border-left-color: var(--success); }
.analyze-verdict.sev-mild    { border-left-color: #e0a800; }
.analyze-verdict.sev-strong  { border-left-color: var(--danger); }
.analyze-verdict.sev-unknown { border-left-color: var(--text-secondary); }

/* Near-fullscreen frame (like the image viewer) so the analyzed frame
   + overlay get as much room as possible. Flex column: title + tabs on
   top, body grows to fill, Close pinned at the bottom. */
.analyze-modal-frame {
    max-width: none;
    width: 96vw;
    height: 94vh;
    display: flex;
    flex-direction: column;
    position: relative;   /* positioning context for the loading spinner */
}
/* Centred loading overlay shown while the server analysis runs or the
   (hi-res) image is still loading. */
.analyze-spinner {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 14px;
    font-size: 13px;
    color: var(--text-secondary);
    background: rgba(11, 14, 20, 0.35);
    z-index: 5;
    pointer-events: none;
}
.analyze-modal-frame .analyze-body {
    flex: 1 1 auto;
    min-height: 0;          /* let the flex children shrink, not the frame */
    overflow: hidden;       /* no scrollbar — content is sized to fit */
    align-items: stretch;
}
.analyze-modal-frame .analyze-side {
    flex: 0 0 340px;        /* give the picture the lion's share */
    overflow: auto;         /* safety on short screens; rarely needed */
}
/* Override .crop-picker's shrink-to-fit (inline-block): that made a wide
   image ignore max-width:100% (the parent width was the image's own),
   lock to max-height, overflow the column and force a scrollbar. As a
   block flex item with a definite column width, the image is bounded by
   BOTH the column width and the body height, so a wide frame renders
   short and nothing scrolls. The overlay canvas stays aligned because
   the image sits at the stage's top-left. */
.analyze-modal-frame .analyze-stage {
    /* Centre the (letterboxed) image in the column. min-width/height:0
       on the img is what lets max-width/height:100% actually shrink it
       inside a flex parent (a flex item defaults to min-*:auto, which
       otherwise keeps it at intrinsic size and gets it cropped). The
       overlay canvas is offset to the image's position in JS so it
       stays aligned even though the image is centred. */
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    background: transparent;
    border: 0;
}
.analyze-modal-frame .analyze-stage .crop-picker-img {
    display: block;
    /* Bound by the column width AND the viewport height directly. The vh
       cap (frame is 94vh minus title/tabs/actions/padding chrome ≈ 170px)
       doesn't depend on the flex parent resolving a % height, so a frame
       taller than the screen is always shown whole (letterboxed), never
       clipped. */
    max-width: 100%;
    max-height: calc(94vh - 170px);
    width: auto;
    height: auto;
    min-width: 0;
    min-height: 0;
}

.siril-modal-actions {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
    margin-top: 14px;
}

/* ===========================================================
   Activity bar, docked at the bottom of the body flex column
   showing active operations on the left and host CPU/RAM on
   the right. Used to be position:fixed and overlaid content
   (Settings rows + Sky controls disappeared underneath it);
   now it's a regular flow element so content stops above it.
   ----------------------------------------------------------- */

.activity-bar {
    height: 36px;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 0 14px;
    background: rgba(5, 8, 16, 0.78);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    border-top: 1px solid var(--border);
    font-size: 11px;
    color: var(--text-secondary);
    user-select: none;
}

/* Collapse handle at the right end of the bar. Faint on purpose: it is
   housekeeping, not something anyone needs to find in a hurry. */
.activity-collapse {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 20px;
    margin-left: 2px;
    padding: 0;
    background: none;
    border: 0;
    border-radius: 4px;
    color: var(--text-muted);
    opacity: 0.55;
    cursor: pointer;
    transition: opacity 0.12s ease, color 0.12s ease;
}
.activity-collapse:hover { opacity: 1; color: var(--accent); }

/* Collapsed: a few pixels of the bar remain, with a handle in the middle so
   the strip reads as something to press rather than a stray border. The whole
   sliver is the hit target (see the footer's @click). */
.activity-bar.activity-bar-collapsed {
    height: 7px;
    min-height: 7px;
    padding: 0;
    gap: 0;
    overflow: hidden;
    cursor: pointer;
    justify-content: center;
}
.activity-bar.activity-bar-collapsed > * { display: none; }
.activity-bar.activity-bar-collapsed::after {
    content: '';
    width: 46px;
    height: 3px;
    margin: 2px auto;
    border-radius: 2px;
    background: color-mix(in srgb, var(--accent) 55%, transparent);
}
/* On mobile the bar is fixed, so the content's bottom cushion has to shrink
   with it, otherwise collapsing buys a gap instead of screen. */
@media (max-width: 768px) {
    body:has(.activity-bar.activity-bar-collapsed) .content { margin-bottom: 63px; }
}

/* Telemetry tray wrappers. Inert on desktop: the items render as the same
   row they always were, and the toggle is hidden. Declared BEFORE the media
   query below, which is what lets that query win: a media block adds no
   specificity, so these rules would otherwise override it. */
.activity-tray {
    position: relative;
    display: flex;
    align-items: center;
    flex-shrink: 0;
}
.activity-bar-tray-items {
    display: flex;
    align-items: center;
    gap: 10px;
}
.activity-tray-toggle { display: none; }

/* On mobile the sidebar reverts to a position:fixed bottom strip
   (56px tall). The activity bar would otherwise sit under it, so
   on small screens we float the bar just above the sidebar. */
@media (max-width: 768px) {
    .activity-bar {
        position: fixed;
        left: 0;
        right: 0;
        bottom: var(--nav-strip-h);
        z-index: 50;
    }
    /* The bar is taken out of the flex column on mobile, so the
       content needs to leave room for both the sidebar (56px plus the
       operator's inset) and the floating activity bar (36px). */
    .content {
        margin-bottom: calc(var(--nav-strip-h) + 36px);
    }

    /* Collapse the host telemetry behind the ••• button: on a phone those
       readouts squeeze the activity chips to nothing and truncate anyway.
       The panel opens UPWARD, since this bar lives at the bottom. */
    .activity-tray-toggle { display: inline-flex; align-items: center; }
    .activity-bar-tray-items {
        display: none;
        position: absolute;
        bottom: calc(100% + 8px);
        right: 0;
        flex-direction: column;
        align-items: stretch;
        gap: 10px;
        padding: 10px 12px;
        min-width: 170px;
        /* Cap the width or the host group, a wide row of stats, drags the
           panel off the left edge of the screen. */
        max-width: min(88vw, 320px);
        box-sizing: border-box;
        max-height: 60vh;
        overflow-y: auto;
        background: var(--bg-secondary);
        border: 1px solid var(--border);
        border-radius: 8px;
        box-shadow: 0 10px 28px rgba(0, 0, 0, 0.45);
        z-index: 200;
    }
    .activity-bar-tray-items.activity-tray-open { display: flex; }
    /* Readouts are laid out for a row; in the panel they are list entries,
       and the right margins that separated them there only add ragged edges
       here. */
    .activity-bar-tray-items > * {
        margin-right: 0;
        width: 100%;
        justify-content: flex-start;
    }
    /* The host group is a row of stats in its own right; stack it too, one
       label + value per line. */
    .activity-bar-tray-items .activity-bar-host {
        flex-direction: column;
        align-items: stretch;
        gap: 8px;
    }
    .activity-bar-tray-items .activity-host-stat {
        justify-content: space-between;
        white-space: nowrap;
        gap: 10px;
    }
    /* The panel is exactly the room those readouts lacked in the strip, so
       undo the narrow-screen thinning here: the Client and Polaris chips come
       back and the numbers show used AND total again. */
    .activity-bar-tray-items .activity-host-stat.activity-host-client,
    .activity-bar-tray-items .activity-host-stat.activity-host-polaris {
        display: inline-flex;
    }
    .activity-bar-tray-items .activity-host-value.host-val-full { display: inline; }
    .activity-bar-tray-items .activity-host-value.host-val-compact { display: none; }
    /* The divider after the device name separated it from the stats in a row;
       in a column it is just a line down the right edge. */
    .activity-bar-tray-items .activity-host-device {
        padding-right: 0;
        margin-right: 0;
        border-right: none;
        max-width: 100%;
    }
    /* Undo the yield-to-the-chips fold inside the open tray. There the
       readouts are a stacked list, not competitors for the strip, and they are
       the reason the operator opened the panel: folding them away would answer
       a request to see them by hiding them. */
    .activity-bar.stats-folded .activity-bar-tray-items.activity-tray-open
        .activity-host-stat:not(.activity-host-device):not(.activity-host-clock) {
        max-width: 100%;
        margin-left: 0;
        opacity: 1;
        transform: none;
        pointer-events: auto;
    }
    /* The ••• panel already is the manual override on a phone, and it wins
       over the fold (rule just above), so the arrow would be a second control
       for the same thing pointing at readouts that are not in the strip. */
    .activity-stats-toggle { display: none; }
}

/* Operations row: horizontally scrollable when chips overflow,
   so adding the 8th chip doesn't push the CPU/RAM block off-screen. */
.activity-bar-ops {
    display: flex;
    gap: 6px;
    flex-wrap: nowrap;
    overflow-x: auto;
    overflow-y: hidden;
    /* Own the leftover middle space and scroll internally — basis 0 so the
       chips' total width never dictates the bar layout and pushes the fixed
       net / CPU / RAM blocks off the right edge. */
    flex: 1 1 0;
    min-width: 0;
    /* Hide the scrollbar, most users have <5 chips at once. */
    scrollbar-width: thin;
    scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
}
.activity-bar-ops::-webkit-scrollbar { height: 4px; }
.activity-bar-ops::-webkit-scrollbar-thumb {
    background: rgba(255, 255, 255, 0.2); border-radius: 2px;
}

.activity-chip {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 3px 9px;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.06);
    border: 1px solid var(--border);
    color: var(--text-primary);
    white-space: nowrap;
    font-size: 11px;
    line-height: 1.2;
    cursor: default;
    /* Keep the natural content width — the parent .activity-bar-ops row
       scrolls when crowded. Without this the chips are flex-shrunk below
       their nowrap text, which then spills past the rounded pill edge. */
    flex: 0 0 auto;
}
.activity-chip-warn { border-color: rgba(245, 158, 11, 0.55); }
.activity-chip-error { border-color: rgba(239, 68, 68, 0.6); }
/* REFSUG-2: clickable chips (refocus suggestion routes to FOCUS tab).
   Hover lifts slightly so the user can tell the difference from
   purely informational chips. */
.activity-chip-clickable {
    cursor: pointer;
    transition: background-color 0.15s ease, transform 0.1s ease;
}
.activity-chip-clickable:hover {
    background: rgba(255, 255, 255, 0.12);
    transform: translateY(-1px);
}

.activity-chip-icon { font-size: 12px; }
.activity-chip-label {
    max-width: 260px;
    overflow: hidden;
    text-overflow: ellipsis;
    /* Operator preference: activity-bar badges read in all-caps. */
    text-transform: uppercase;
}

/* FIELD-6: dismiss × for chips that opt in (under-voltage today).
   Small, low-contrast by default, brightens on hover so the
   operator doesn't accidentally swat it while scanning the bar. */
.activity-chip-dismiss {
    margin-left: 6px;
    padding: 0 5px;
    background: transparent;
    border: none;
    color: var(--text-muted);
    font-size: 13px;
    line-height: 1;
    cursor: pointer;
    border-radius: 4px;
    opacity: 0.7;
}
.activity-chip-dismiss:hover {
    background: rgba(255, 255, 255, 0.12);
    color: var(--text-primary);
    opacity: 1;
}
.activity-chip-progress { margin-left: 2px; }
.activity-chip-progress-bar {
    display: inline-block;
    width: 48px;
    height: 4px;
    background: rgba(0, 0, 0, 0.4);
    border-radius: 2px;
    overflow: hidden;
    vertical-align: middle;
}
.activity-chip-progress-bar > span {
    display: block;
    height: 100%;
    background: var(--accent);
    transition: width 0.3s ease;
}

/* ─── XFER-2: per-transfer progress chips ─────────────────────────
   ASIAIR-style "I'm transferring N MB right now" indicator. Each
   chip is a separate visual unit from the activity ops chips
   above, because transfers can land at any moment (not tied to a
   captured operation) and benefit from a slightly more prominent
   bar so the user can read the bytes/total at a glance.
   --------------------------------------------------------------- */
.activity-bar-transfers {
    display: flex;
    gap: 6px;
    flex-wrap: nowrap;
    overflow-x: auto;
    overflow-y: hidden;
    flex: 0 1 auto;
    min-width: 0;
    margin-left: 8px;
    scrollbar-width: thin;
}
.activity-bar-transfers::-webkit-scrollbar { height: 3px; }
.activity-bar-transfers::-webkit-scrollbar-thumb {
    background: rgba(255, 255, 255, 0.15);
    border-radius: 2px;
}
.activity-transfer {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 3px 9px;
    border-radius: 999px;
    background: rgba(var(--accent-rgb), 0.10);
    border: 1px solid rgba(var(--accent-rgb), 0.4);
    color: var(--text-primary);
    white-space: nowrap;
    font-size: 11px;
    line-height: 1.2;
    /* No fixed max-width: the pill shrink-wraps its content. The label
       child is capped + ellipsised (140px) so the whole pill stays
       bounded; a hard cap here clipped the trailing "x MB / y MB" past
       the rounded right edge when label + bar + bytes exceeded it. */
    max-width: none;
    /* Same as .activity-chip: don't let the flex row shrink the pill
       below its nowrap text (which spills past the right edge). */
    flex: 0 0 auto;
    transition: background 0.2s ease, border-color 0.2s ease;
}
.activity-transfer-up {
    background: rgba(var(--success-rgb), 0.10);
    border-color: rgba(var(--success-rgb), 0.4);
}
.activity-transfer-down {
    background: rgba(var(--accent-rgb), 0.10);
    border-color: rgba(var(--accent-rgb), 0.4);
}
.activity-transfer-done {
    /* Brief "✓ done" fade — same colour family but dimmer +
       slightly muted so the eye recognises completion without
       being startled by a colour flip. */
    opacity: 0.65;
}
.activity-transfer-failed {
    background: rgba(244, 67, 54, 0.10);
    border-color: rgba(244, 67, 54, 0.5);
}
.activity-transfer-arrow {
    font-weight: 700;
    font-size: 12px;
}
.activity-transfer-up .activity-transfer-arrow { color: #86efac; }
.activity-transfer-down .activity-transfer-arrow { color: #93c5fd; }
.activity-transfer-failed .activity-transfer-arrow { color: #fca5a5; }
.activity-transfer-label {
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 140px;
    color: var(--text-secondary);
}
.activity-transfer-bar {
    display: inline-block;
    width: 60px;
    height: 5px;
    background: rgba(0, 0, 0, 0.45);
    border-radius: 3px;
    overflow: hidden;
    vertical-align: middle;
    flex-shrink: 0;
}
.activity-transfer-fill {
    display: block;
    height: 100%;
    background: var(--accent);
    transition: width 0.2s linear;
}
.activity-transfer-up .activity-transfer-fill {
    background: var(--success);
}
.activity-transfer-down .activity-transfer-fill {
    background: var(--accent);
}
.activity-transfer-failed .activity-transfer-fill {
    background: var(--danger);
}
/* Indeterminate bar (server didn't send Content-Length) cycles a
   short fill across the track so the user sees activity without a
   percent number. */
.activity-transfer-fill.is-indeterminate {
    width: 30% !important;
    background: linear-gradient(90deg,
        transparent,
        var(--accent) 50%,
        transparent);
    animation: xfer-indeterminate 1.1s linear infinite;
}
.activity-transfer-up .activity-transfer-fill.is-indeterminate {
    background: linear-gradient(90deg,
        transparent,
        var(--success) 50%,
        transparent);
}
@keyframes xfer-indeterminate {
    0%   { transform: translateX(-100%); }
    100% { transform: translateX(333%); }
}
.activity-transfer-bytes {
    font-variant-numeric: tabular-nums;
    font-size: 10px;
    color: var(--text-secondary);
    min-width: 0;
}

/* NET-1: client↔server throughput readout. Two rows stacked tight
   so the pair occupies a single bar-height slot. Pulse animation
   brightens + scales the arrow briefly when bytes arrive, giving
   an LED-style activity hint the eye picks up faster than the
   changing number. */
.activity-net {
    display: inline-flex;
    flex-direction: column;
    justify-content: center;
    gap: 1px;
    margin-right: 6px;
    flex-shrink: 0;
    font-size: 10px;
    line-height: 1.1;
    font-variant-numeric: tabular-nums;
    color: var(--text-muted);
}
/* The host's own leg, immediately after the browser pair. Deliberately dimmer
   and separated by a hairline: these are two different measurements standing
   side by side, and reading them as one four-line block would be worse than
   not showing the second at all. The double arrows (⇓⇑ against ↓↑) carry the
   distinction where colour cannot, night mode included. */
.activity-net-host {
    opacity: 0.72;
    padding-left: 6px;
    border-left: 1px solid var(--border);
}

/* Auto-push to network storage (SMB/SFTP) chip in the activity bar. */
.activity-storage {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    margin-right: 10px;
    padding: 1px 7px;
    flex-shrink: 0;
    font-size: 11px;
    line-height: 1.4;
    font-variant-numeric: tabular-nums;
    color: var(--text-muted);
    border: 1px solid var(--border);
    border-radius: 10px;
    white-space: nowrap;
}
.activity-storage-icon { font-size: 11px; }
.activity-storage-label { font-weight: 600; letter-spacing: 0.3px; }
/* Uploading: tint accent + gentle pulse so it reads as "active". */
.activity-storage.activity-storage-busy {
    color: var(--accent);
    border-color: var(--accent);
    animation: activity-storage-pulse 1.4s ease-in-out infinite;
}
/* A real problem (a file gave up, or the connection dropped on an error):
   red. Note "not connected" alone is NOT an error, it is the neutral idle
   state before the first image is pushed (see storagePushState in app.js). */
.activity-storage.activity-storage-error {
    color: var(--danger, #e25555);
    border-color: var(--danger, #e25555);
}
@keyframes activity-storage-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.55; } }
.activity-net-row {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    white-space: nowrap;
}
.activity-net-arrow {
    width: 10px;
    text-align: center;
    transition: filter 80ms ease-out, transform 80ms ease-out;
    will-change: filter, transform;
}
.activity-net-rx .activity-net-arrow { color: #86efac; }   /* green */
.activity-net-tx .activity-net-arrow { color: #93c5fd; }   /* blue */
.activity-net-row.net-pulse .activity-net-arrow {
    filter: drop-shadow(0 0 4px currentColor) brightness(1.5);
    transform: scale(1.25);
}
/* Reserve just enough for the widest realistic rate ("1023 KB/s") so the
   number can change every second without shoving the WiFi chip sideways.
   The reservation used to sit on the whole block as a min-width, which also
   padded the RIGHT of the readout and left an obvious hole before the WiFi
   bars whenever the rate was short. */
.activity-net-rate {
    color: var(--text-primary);
    display: inline-block;
    min-width: 48px;
}

/* WiFi signal indicator, sits next to the net rx/tx block. */
.activity-wifi {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    margin-right: 10px;
    flex-shrink: 0;
    font-size: 10px;
    line-height: 1.1;
    font-variant-numeric: tabular-nums;
    color: var(--text-muted);
}
.activity-wifi .wifi-bars { width: 15px; height: 11px; display: block; }
/* Unlit bars: subtle base fill. Lit bars get a strength colour below. */
.activity-wifi .wifi-bar { fill: var(--border, rgba(255, 255, 255, 0.16)); }
.activity-wifi .wifi-bar.wifi-strong { fill: #86efac; }   /* green  */
.activity-wifi .wifi-bar.wifi-medium { fill: #fcd34d; }   /* amber  */
.activity-wifi .wifi-bar.wifi-weak   { fill: #fca5a5; }   /* red    */
.activity-wifi .activity-wifi-val { color: var(--text-primary); }

/* Host metrics block on the right side.
   Spacing lives on the children rather than on the flex `gap`: a gap keeps
   reserving its space for a child that has collapsed to nothing, and these
   children do collapse (see the stats-folded rule further down). */
.activity-bar-host {
    display: flex;
    gap: 0;
    align-items: center;
    flex-shrink: 0;
}
.activity-host-stat {
    display: inline-flex;
    align-items: center;
    gap: 5px;
}
.activity-host-stat + .activity-host-stat { margin-left: 14px; }
.activity-host-label {
    color: var(--text-muted);
    text-transform: uppercase;
    font-size: 10px;
    letter-spacing: 0.5px;
}
.activity-host-value {
    font-variant-numeric: tabular-nums;
    color: var(--text-primary);
}
.host-green { color: #86efac; }
.host-amber { color: #fcd34d; }
.host-red   { color: #fca5a5; font-weight: 600; }

/* Narrow screens: the host stats row ate the whole activity bar
   (field report). Drop the Client + Polaris chips and swap RAM/Disk
   to their free-only compact values (the markup carries both spans;
   only one renders per breakpoint). */
.activity-host-value.host-val-compact { display: none; }
@media (max-width: 900px) {
    .activity-host-stat.activity-host-client,
    .activity-host-stat.activity-host-polaris { display: none; }
    .activity-host-value.host-val-full { display: none; }
    .activity-host-value.host-val-compact { display: inline; }
    .activity-host-stat + .activity-host-stat { margin-left: 10px; }
}

/* ---- Yield the strip to the activity chips -------------------------------
   The chips are the thing being watched: an exposure counting down, a solve
   running, a sequence step. They share the bar with the host readouts, and on
   anything narrower than a desktop the two fight for the same pixels, so the
   chips arrived squeezed into a few characters.

   While any chip is up, the numbers after the board name fold away and give
   the room back; they unfold when the last chip clears. What stays is the
   board name (which says WHICH machine is talking) and the clock chip, which
   only ever appears when the server clock is actually wrong.

   That rule alone was wrong for the case it mattered most in. A live stack
   keeps a chip on the bar for the whole session, so the readouts folded when
   stacking began and stayed folded all night — the operator lost CPU, RAM and
   disk during the one activity that runs for hours and fills the disk. The
   fold now keys off .stats-folded, which is the automatic rule OR whatever the
   arrow at the end of the bar was last told (see activityStatsMode in app.js).

   The fold is a real animation rather than display:none because the bar is
   permanently on screen at the edge of vision: something vanishing there reads
   as a glitch, something folding reads as the interface making room. Each
   readout leaves a beat after the one before it, and they come back in the
   same order, nearest the board name first. */
.activity-host-stat {
    overflow: hidden;
    white-space: nowrap;
    /* Comfortably past the longest readout ("Polaris 100% · 1234 MB"), so the
       cap only ever exists to give the fold something to animate. */
    max-width: 200px;
    transition:
        max-width  260ms cubic-bezier(0.4, 0, 0.2, 1),
        opacity    160ms ease-out,
        margin-left 260ms cubic-bezier(0.4, 0, 0.2, 1),
        transform  260ms cubic-bezier(0.4, 0, 0.2, 1);
}
.activity-bar.stats-folded
    .activity-host-stat:not(.activity-host-device):not(.activity-host-clock) {
    max-width: 0;
    margin-left: 0;
    opacity: 0;
    transform: translateX(8px);
    pointer-events: none;
}
/* Folding away, the far end goes first, so the row reads as retreating
   towards the board name rather than collapsing all at once. Unfolding
   reverses it, which is why the delays are declared for both states.

   Counted with nth-of-type over the spans in source order: 1 clock,
   2 device, 3 CPU, 4 RAM, 5 Client, 6 Disk, 7 Polaris. The clock and device
   spans are counted even while x-show has them display:none, so the numbering
   does not shift when the clock chip is absent, which it usually is. */
.activity-bar.stats-folded .activity-host-stat:nth-of-type(7) { transition-delay: 0ms; }
.activity-bar.stats-folded .activity-host-stat:nth-of-type(6) { transition-delay: 40ms; }
.activity-bar.stats-folded .activity-host-stat:nth-of-type(5) { transition-delay: 80ms; }
.activity-bar.stats-folded .activity-host-stat:nth-of-type(4) { transition-delay: 120ms; }
.activity-bar.stats-folded .activity-host-stat:nth-of-type(3) { transition-delay: 160ms; }
.activity-host-stat:nth-of-type(3) { transition-delay: 0ms; }
.activity-host-stat:nth-of-type(4) { transition-delay: 40ms; }
.activity-host-stat:nth-of-type(5) { transition-delay: 80ms; }
.activity-host-stat:nth-of-type(6) { transition-delay: 120ms; }
.activity-host-stat:nth-of-type(7) { transition-delay: 160ms; }

@media (prefers-reduced-motion: reduce) {
    .activity-host-stat { transition-duration: 1ms; transition-delay: 0ms !important; }
    .activity-bar.stats-folded
        .activity-host-stat:not(.activity-host-device):not(.activity-host-clock) {
        transform: none;
    }
    .activity-stats-toggle svg { transition: none; }
}

/* The fold handle. Sits at the end of the bar, beside the collapse chevrons,
   because it has to hold still: the readouts it controls are what moves, and a
   button that travelled with them would be chasing its own effect.
   Faint until hovered, like its neighbour. */
.activity-stats-toggle {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 20px;
    margin-left: 6px;
    padding: 0;
    background: none;
    border: 0;
    border-radius: 4px;
    color: var(--text-muted);
    opacity: 0.55;
    cursor: pointer;
    transition: opacity 0.12s ease, color 0.12s ease;
}
.activity-stats-toggle:hover { opacity: 1; color: var(--accent); }
.activity-stats-toggle:focus-visible {
    opacity: 1;
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}
/* One glyph, turned around, rather than two that swap: the arrow is the same
   object changing its mind, and the turn lands with the readouts it moved. */
.activity-stats-toggle svg {
    display: block;
    transition: transform 260ms cubic-bezier(0.4, 0, 0.2, 1);
}
.activity-stats-toggle.is-open svg { transform: rotate(180deg); }

/* Hardware identification chip, sits leftmost in the host group
   so the eye lands on "this is Polaris on a Pi 5" before the live
   numbers. Slightly muted compared to CPU/RAM since it's static. */
.activity-host-device {
    padding-right: 10px;
    margin-right: 2px;
    border-right: 1px solid var(--border);
    max-width: 240px;
    overflow: hidden;
}
.activity-host-device .activity-host-value {
    color: var(--text-secondary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.activity-host-icon {
    font-size: 12px;
    line-height: 1;
}
/* CPU info chip, same visual weight as the device chip + same
   right-border separator. Caps width so a verbose CPU name doesn't
   push the live metrics off-screen on narrow viewports. */
.activity-host-cpu-info {
    padding-right: 10px;
    margin-right: 2px;
    border-right: 1px solid var(--border);
    max-width: 280px;
    overflow: hidden;
}
.activity-host-cpu-info .activity-host-value {
    color: var(--text-secondary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* The activity bar now docks in the body flex column, so panels
   no longer need to reserve space for a floating overlay. Padding
   stays zero (default) on desktop. Mobile keeps its own clearance
   via .content margin-bottom inside the @media block above. */

/* ===========================================================
   RIGS tab, connection strip + role-based grid + accessories
   ----------------------------------------------------------- */

/* Compact connection strip shown at the top of the RIGS tab
   when INDI is connected. Wears the same glass treatment as the
   home cards so the page reads as a single visual family. */
.equip-connection-strip {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 14px;
    margin: 0 0 12px;
    background: rgba(var(--panel-glass-rgb), 0.65);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    font-size: 13px;
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
}
.equip-conn-status {
    font-weight: 600;
    padding: 2px 10px;
    border-radius: 999px;
}
.equip-conn-status.ok {
    background: rgba(34, 197, 94, 0.18);
    color: #86efac;
}
.equip-conn-detail {
    color: var(--text-secondary);
    font-size: 12px;
}
.equip-conn-detail code {
    background: rgba(0, 0, 0, 0.3);
    padding: 1px 6px;
    border-radius: 3px;
    font-size: 12px;
}

/* Role-based grid replacing the old equip-scroll. auto-fit
   collapses to fewer columns on narrow viewports without media
   queries; minmax floors at 280px so cards never get too narrow
   for their content. */
.equip-grid {
    display: grid;
    /* Column count follows the grid's ACTUAL width, not the viewport: pack as
       many columns as fit at a ~440px floor (so 2 cols appear as soon as there
       is room, even with the sidebar + density/zoom eating viewport width — the
       old viewport breakpoints stayed 1-col in that case). The 440px floor
       keeps the Camera card's cooler chart + sensor metadata legible; on a
       genuinely huge screen it tops out around 3–4 readable cols. */
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 440px), 1fr));
    gap: 12px;
    padding: 0;
}

/* Metadata-only card variant (Main Telescope, Guidescope, Guide
   Camera). Visually distinguished from hardware cards with a
   subtle accent border so the user knows there's no Connect
   button to look for. */
.equip-card-metadata {
    border-left: 3px solid var(--accent);
}

/* Two-column grid of label/input pairs used inside the metadata
   cards. Wraps on narrow viewports. */
.equip-meta-row {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    align-items: end;
    margin-bottom: 8px;
}
.equip-meta-row label {
    display: flex;
    flex-direction: column;
    gap: 3px;
    font-size: 11px;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.5px;
}
/* Selects inside metadata-card rows need to fill the label width
   (otherwise inherit input-sm's 60-90px fixed width and clip
   ", Manual entry," / ", Pick a model," labels). Numeric inputs
   inside the same row keep their explicit inline width attr.
   max-width: none clears the 220px cap from .input-sm — that cap
   is right for free-floating compact inputs but inside metadata
   rows it was the actual reason the Accessory dropdown stopped
   way before the Model dropdown's right edge ('[reducer] SVBony
   SV209 0.8x Re…'). Lifting it lets the accessory + brand +
   model selects stretch to their label's flex-allocated width
   instead of clamping at 220px. */
.equip-meta-row label select.input-sm {
    width: 100%;
    min-width: 160px;
    max-width: none;
}
/* Only stretch labels that *wrap a select*, labels around numeric
   inputs keep their intrinsic narrow width (set via inline style on
   the input). Uses :has() (Chromium 105+, Safari 15.4+, Firefox 121+
  , fine for this app's target browsers). */
.equip-meta-row > label:has(select.input-sm) {
    flex: 1 1 180px;
    min-width: 0;
}
/* Accessory row has a single, often very long option label like
   '[reducer] SVBony SV209 0.8x Reducer/Flattener (0.8×)' that gets
   cropped at the card's narrow 280px width inherited from the role
   grid. Bumping just THIS row's select min-width — without touching
   the Brand+Model row above (which has two side-by-side selects
   that must share the same row width) — gives the accessory text
   room to breathe. Card is in an auto-fit grid so on viewports
   wider than the minimum it'll happily expand and the wider
   accessory select absorbs the extra space. */
.equip-meta-row.accessory-row > label:has(select.input-sm) {
    flex: 1 1 360px;
}
.equip-meta-row.accessory-row label select.input-sm {
    min-width: 280px;
    /* Bigger font specifically here so the long, dense accessory
       string ('[reducer] SVBony SV209 0.8x...') is easier to read
       at a glance on a sidebar-heavy layout. */
    font-size: 13px;
}
.equip-meta-derived {
    /* The 'f/X.Y' badge that sits next to Focal length + Aperture.
       Previously aligned center with a 13px font: it visually sank
       between the inputs (which sit at the bottom of their column-
       direction label cells) and read smaller than every other
       value in the row. Now aligns to the input baseline + matches
       the 14px input font + bumps to the primary text color so the
       computed value reads as a first-class field. nowrap keeps
       'f/X.Y' from breaking onto a second line when the row gets
       tight, and the small horizontal padding gives it breathing
       room from the neighbouring Aperture input. */
    align-self: flex-end;
    margin-bottom: 9px;
    padding: 0 6px;
    font-size: 13px;
    line-height: 1.2;
    color: var(--text-primary);
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}
.equip-meta-derived strong {
    font-weight: 600;
}

/* Status dot variants used by the new metadata cards. */
.equip-status-dot.configured {
    background: rgba(var(--accent2-rgb), 0.6);
}

/* Accessories collapsible. Open state shows the embedded grid;
   collapsed state is just the summary row with a count. */
.equip-accessories {
    margin-top: 18px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: rgba(var(--panel-glass-rgb), 0.45);
}
.equip-accessories-summary {
    cursor: pointer;
    padding: 10px 14px;
    display: flex;
    align-items: center;
    gap: 10px;
    font-size: 13px;
    user-select: none;
    list-style: none;
}
.equip-accessories-summary::-webkit-details-marker { display: none; }
.equip-accessories-summary::marker { content: ''; }
.equip-accessories-summary strong { color: var(--text-primary); }
.equip-accessories-caret {
    display: inline-block;
    transition: transform 0.15s ease;
}
.equip-accessories[open] .equip-accessories-caret {
    transform: rotate(90deg);
}
.equip-accessories-grid {
    padding: 0 14px 14px;
}

/* ==========================================================================
   PH2X: PHD2 deep-integration UI (Guide tab tabstrip + xpra GUI + presets)
   ========================================================================== */


/* ==========================================================================
   STUDIO batch-rename modal (🏷). Token chips + old→new preview table.
   Reuses the siril-modal chrome + files-list table; only the chips, the
   arrow column and the status badges are bespoke.
   ========================================================================== */
.br-tokens {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin: 8px 0 4px;
}
.br-token {
    background: var(--bg-elevated, rgba(255, 255, 255, .05));
    color: var(--text-secondary);
    border: 1px solid var(--border);
    border-radius: 999px;
    padding: 3px 10px;
    font-size: 12px;
    font-family: var(--font-mono, monospace);
    cursor: pointer;
    transition: color .12s, border-color .12s, background .12s;
}
.br-token:hover {
    color: var(--text-primary);
    border-color: var(--accent);
}
.br-preview {
    max-height: 280px;
    overflow: auto;
    border: 1px solid var(--border);
    border-radius: var(--radius, 6px);
    margin: 8px 0;
}
.br-preview-table {
    width: 100%;
    font-size: 12px;
}
.br-preview-table td, .br-preview-table th {
    padding: 4px 8px;
    vertical-align: middle;
}
.br-preview-table .br-old {
    color: var(--text-secondary);
    word-break: break-all;
}
.br-preview-table .br-new {
    color: var(--text-primary);
    font-weight: 600;
    word-break: break-all;
}
.br-preview-table .br-arrow {
    color: var(--text-muted);
    text-align: center;
    width: 1.5em;
}
.br-col-status { width: 96px; }
.br-badge {
    display: inline-block;
    padding: 1px 8px;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 600;
    white-space: nowrap;
    border: 1px solid transparent;
}
.br-badge-preview, .br-badge-renamed {
    color: var(--accent);
    border-color: var(--accent);
}
.br-badge-unchanged {
    color: var(--text-muted);
    border-color: var(--border);
}
.br-badge-skipped {
    color: var(--text-secondary);
    border-color: var(--border);
}
.br-badge-error {
    color: var(--danger);
    border-color: var(--danger);
}

/* White Balance summary (PCC/SPCC): two scatter panels + line fit, shown on
   the shared .graxpert-compare-backdrop overlay. */
.wb-modal {
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 14px;
    width: min(1100px, 94vw);
    max-height: 92vh;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
}
.wb-head {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 12px 16px;
    border-bottom: 1px solid var(--border);
}
.wb-body {
    display: flex;
    gap: 16px;
    padding: 16px;
    overflow: auto;
    flex-wrap: wrap;
}
.wb-panel {
    flex: 1 1 460px;
    min-width: 320px;
    display: flex;
    flex-direction: column;
}
.wb-caption { font-size: 12px; color: var(--text-muted); line-height: 1.5; margin-bottom: 8px; }
.wb-cap-title { color: var(--text); font-weight: 700; font-size: 13px; }
.wb-canvas-wrap { position: relative; height: 340px; }
.wb-foot { padding: 12px 16px; border-top: 1px solid var(--border); font-size: 12px; }

/* Touch tooltip bubble (js/touch-tooltips.js): tap-to-reveal for the "?" help
   icons on touch devices, where hover `title` tooltips never show. Above modals
   so tips inside dialogs are readable; pointer-events:none so it never eats a
   tap. */
.touch-tip {
    position: fixed;
    z-index: 1200;
    left: -9999px;
    top: 0;
    max-width: 340px;
    padding: 8px 11px;
    border-radius: 8px;
    background: rgba(20, 26, 38, 0.97);
    color: #e6edf6;
    border: 1px solid rgba(255, 255, 255, 0.14);
    box-shadow: 0 6px 22px rgba(0, 0, 0, 0.5);
    font-size: 12.5px;
    line-height: 1.4;
    white-space: normal;
    pointer-events: none;
    opacity: 0;
    transform: translateY(-2px);
    transition: opacity .12s ease, transform .12s ease;
}
.touch-tip.show { opacity: 1; transform: translateY(0); }

/* ---- WAVE-3: wavelet layers modal ----------------------------------
   Two columns: the preview carries the decision, the sliders are the hand on
   the knob. Wider than a normal modal because judging sharpening on a
   thumbnail is judging nothing. Collapses to one column on a phone, preview
   first, since that is the part you look at while dragging. */
/* .modal-card.wave-card, not .wave-card alone. `.modal-card` caps every dialog
   at 440px and lives in 08-plan-misc.css, which loads AFTER this file; at equal
   specificity the later rule wins, so the 1040px here was silently discarded
   and the modal rendered at 440px. With a fixed sliders column that left the
   preview a ~60px strip, and the absolutely-positioned A/B button wider than
   the box it sits in, hanging off the left edge.
   Two classes (0-2-0) beat the single-class cap wherever the files sit in the
   cascade. The other wide dialogs in this app dodge the same cap with inline
   style="max-width:...", which works but cannot be restyled from a stylesheet. */
.modal-card.wave-card {
    width: min(1040px, 96vw);
    max-width: min(1040px, 96vw);
}
.wave-body {
    display: grid;
    /* The sliders column has to fit label + [-]slider[+] + value +
       [-]slider[+]: app.js wraps every range input in stepper buttons, so a
       row needs far more than the 320px this used to reserve, and the
       overflow pushed the buttons out past the card edge. */
    grid-template-columns: minmax(0, 1fr) minmax(360px, 400px);
    gap: 14px;
    align-items: start;
}
.wave-preview {
    position: relative;
    min-height: 260px;
    max-height: 70vh;
    display: flex;
    align-items: center;
    justify-content: center;
    background: #000;
    border: 1px solid var(--border);
    border-radius: 8px;
    overflow: hidden;
}
.wave-preview img {
    max-width: 100%;
    max-height: 62vh;
    display: block;
}
.wave-preview-empty {
    color: var(--text-muted);
    font-size: 12px;
}
.wave-ab {
    position: absolute;
    right: 8px;
    bottom: 8px;
    opacity: 0.85;
    /* Never wider than the box it floats in. Anchored on the right, an
       oversized button grows leftwards and walks out of the frame. */
    max-width: calc(100% - 16px);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.wave-sliders {
    display: flex;
    flex-direction: column;
    gap: 8px;
}
.wave-row {
    display: grid;
    /* Both sliders carry stepper buttons (28px each plus gaps), so neither can
       be given a fixed width narrow enough to fit the old 74px. Proportional
       tracks with a floor keep the sharpen slider the wider of the two, which
       matches which one gets dragged more. */
    grid-template-columns: 58px minmax(96px, 1.35fr) 42px minmax(84px, 1fr);
    align-items: center;
    gap: 6px;
}
.wave-lab {
    font-size: 11.5px;
    color: var(--text-muted);
}
.wave-val {
    font-family: var(--font-mono);
    font-size: 11.5px;
    text-align: right;
}
/* The denoise slider is deliberately smaller and quieter: it is the second
   decision, taken after the sharpening one. */
.wave-dn { opacity: 0.75; }
.wave-presets {
    display: flex;
    gap: 6px;
    margin-top: 4px;
}

@media (max-width: 900px) {
    .wave-body { grid-template-columns: 1fr; }
    .wave-preview img { max-height: 40vh; }
}

/* Dust-mote modal: a bigger canvas than the wavelet one. A mote is judged by
   whether a faint disc is still there, so the preview wants room — up to ~80%
   of the screen — while still collapsing sanely on a tablet or phone. Its own
   class so widening it never touches the Wavelets dialog. Beats the 440px
   `.modal-card` cap the same 0-2-0 way `.wave-card` does. */
.modal-card.dust-card {
    width: min(1600px, 84vw);
    max-width: min(1600px, 84vw);
    max-height: 92vh;
    overflow-y: auto;
}
/* Phones: 84vw wastes the screen on a narrow device; take almost all of it. */
@media (max-width: 700px) {
    .modal-card.dust-card { width: 96vw; max-width: 96vw; }
}
/* The stage shrink-wraps the preview image so the circle overlay, pinned to
   inset:0, lands exactly on the pixels the motes were measured in. */
.dust-stage {
    position: relative;
    display: inline-block;
    line-height: 0;
    max-width: 100%;
}
.dust-stage img {
    display: block;
    max-width: 100%;
    max-height: 66vh;
}
.dust-overlay {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
}
@media (max-width: 900px) {
    .dust-stage img { max-height: 44vh; }
}
