/* ============================================================
   PANEL LINKS: LINK CONFIGURATION, LINK LIST, PANEL VISUALIZATION
   Covers: toggle buttons, fiber type selectors, link config items,
   link params modal overrides, link list, rack filters,
   panel visualization modal, panel hardware, spec badges
   ============================================================ */

/* --- Generic toggle button group --- */

.toggle-btn {
    padding: 8px 14px;
    border: 2px solid var(--dc-n-300);
    background: white;
    border-radius: 6px;
    cursor: pointer;
    font-size: var(--dc-text-md);
    font-weight: 500;
    color: var(--dc-n-800);
    transition: all 0.2s;
    flex-shrink: 0;
}

/* A choice the price list cannot build — dimmed, and it refuses the click. */
.toggle-btn--dead { opacity: .32; cursor: not-allowed; filter: grayscale(1); }
.toggle-btn--dead:hover { background: var(--dc-n-0); }

/* Said when the catalogue carries nothing for the chosen fibre and splice type. */
.lp-variants-empty {
    padding: 14px 16px; margin: 6px 0 10px;
    border: 1px dashed var(--dc-n-200); border-radius: 6px;
    background: #fafbfc; color: var(--dc-n-700); font-size: var(--dc-text-sm); line-height: 1.5;
}

.toggle-btn:hover {
    background: var(--dc-n-50);
    border-color: var(--dc-n-500);
}

.toggle-btn.active {
    background: var(--dc-c-accent);
    border-color: var(--dc-c-accent);
    color: white;
    box-shadow: 0 2px 4px rgba(101, 184, 165, 0.3);
}

.toggle-btn:active {
    transform: scale(0.97);
}

/* --- Panel position / slack preset buttons --- */

.panel-position-btn {
    padding: 3px 8px;
    border: 1px solid var(--dc-n-300);
    background: white;
    border-radius: 3px;
    cursor: pointer;
    font-size: var(--dc-text-xs);
    font-weight: 500;
    color: var(--dc-n-800);
    transition: all 0.2s;
    white-space: nowrap;
}

.panel-position-btn:hover {
    background: var(--dc-n-50);
    border-color: var(--dc-n-500);
}

.panel-position-btn.active {
    background: var(--dc-c-accent);
    border-color: var(--dc-c-accent);
    color: white;
}

/* --- Fiber type toggle colors --- */

.toggle-btn.fiber-sm,
.toggle-btn.fiber-om4 {
    background: var(--dc-n-0);
    border-color: var(--dc-n-150);
    color: var(--dc-n-800);
    transition: border-color 0.15s, background 0.15s;
}

.toggle-btn.fiber-sm:hover,
.toggle-btn.fiber-om4:hover {
    background: #f0f8f6;
    border-color: var(--dc-c-accent);
}

.toggle-btn.fiber-sm.active,
.toggle-btn.fiber-om4.active {
    border-color: var(--dc-c-accent);
    background: var(--dc-c-accent-light);
    color: #2c7a6b;
    box-shadow: none;
}

/* --- Link configuration items (full size) --- */

.link-config-item {
    display: flex;
    align-items: center;
    margin-bottom: 8px;
}

.link-config-radio {
    margin: 0;
    width: 20px;
    height: 20px;
    flex-shrink: 0;
    cursor: pointer;
}

.link-config-label {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 12px;
    background: var(--dc-n-25);
    border: 2px solid var(--dc-n-150);
    border-radius: 4px;
    transition: all 0.2s;
    cursor: pointer;
    flex: 1;
    margin-left: 10px;
}

.link-config-label:hover {
    background: var(--dc-n-50);
    border-color: var(--dc-n-300);
}

.link-config-radio:checked + .link-config-label {
    background: var(--dc-c-accent-light);
    border-color: var(--dc-c-accent);
    box-shadow: 0 0 0 3px rgba(101, 184, 165, 0.1);
}

.link-config-image {
    width: 120px;
    height: 120px;
    object-fit: contain;
    flex-shrink: 0;
    border-radius: 4px;
    background: white;
    padding: 4px;
    border: 1px solid var(--dc-n-150);
}

.link-config-info {
    flex: 1;
    min-width: 0;
    padding: 0 8px;
}

.link-config-title {
    font-size: var(--dc-text-lg);
    font-weight: 600;
    color: var(--dc-c-dark);
    margin-bottom: 4px;
}

.link-config-desc {
    font-size: var(--dc-text-sm);
    color: var(--dc-c-muted);
    line-height: 1.4;
}

/* --- Collapsible sections (left column) --- */

.lp-section-collapse {
    margin-bottom: 8px;
}

.lp-section-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 6px 8px;
    background: var(--dc-n-50);
    border-radius: 5px;
    cursor: pointer;
    font-size: var(--dc-text-xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--dc-c-muted);
    user-select: none;
    transition: background 0.15s;
}

.lp-section-header:hover { background: #dfe6e9; }

.lp-section-chevron {
    font-size: 10px;
    color: var(--dc-n-500);
}

.lp-section-body {
    padding-top: 6px;
    display: flex;
    flex-direction: column;
    gap: 4px;
}

/* --- Favorite items --- */

.lp-fav-item {
    display: flex;
    align-items: center;
    gap: 6px;
    cursor: pointer;
}

.lp-fav-item:hover .lp-fav-type-label {
    background: var(--dc-n-50);
    border-color: var(--dc-n-300);
}

.lp-fav-slot-label {
    font-size: var(--dc-text-2xs);
    font-weight: 700;
    text-transform: uppercase;
    color: var(--dc-n-500);
    width: 30px;
    flex-shrink: 0;
    text-align: center;
}

.lp-fav-type-label {
    flex: 1;
    min-width: 0;
}

.lp-fav-clear-btn {
    background: none;
    border: none;
    cursor: pointer;
    font-size: 16px;
    color: var(--dc-n-300);
    padding: 0 4px;
    line-height: 1;
    flex-shrink: 0;
    transition: color 0.15s;
}

.lp-fav-clear-btn:hover { color: var(--dc-c-error); }

.lp-fav-empty {
    font-size: var(--dc-text-xs);
    color: var(--dc-n-300);
    font-style: italic;
    padding: 4px 8px;
}

/* --- Use as favorite button --- */

.lp-use-fav-btn {
    margin-top: 8px;
    width: 100%;
    padding: 7px 12px;
    background: none;
    border: 1px solid var(--dc-c-accent);
    border-radius: 5px;
    color: var(--dc-c-accent);
    font-size: var(--dc-text-sm);
    font-weight: 600;
    cursor: pointer;
    transition: background 0.15s, border-style 0.15s;
}

.lp-use-fav-btn:hover {
    background: var(--dc-c-accent-light);
}

/* --- Favorite overwrite modal slots --- */

.fav-overwrite-slot {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 12px;
    background: var(--dc-n-25);
    border: 2px solid var(--dc-n-150);
    border-radius: 6px;
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s;
}

.fav-overwrite-slot:hover {
    background: var(--dc-c-accent-light);
    border-color: var(--dc-c-accent);
}

.fav-overwrite-slot-num {
    font-size: var(--dc-text-2xs);
    font-weight: 700;
    text-transform: uppercase;
    color: var(--dc-n-500);
    width: 32px;
    flex-shrink: 0;
    text-align: center;
}

.fav-overwrite-slot-info {
    flex: 1;
    min-width: 0;
}

.fav-overwrite-slot-name {
    font-size: var(--dc-text-md);
    font-weight: 600;
    color: var(--dc-c-dark);
}

.fav-overwrite-slot-desc {
    font-size: var(--dc-text-xs);
    color: var(--dc-c-muted);
    margin-top: 2px;
}

.fav-overwrite-slot-arrow {
    color: var(--dc-n-300);
    font-size: 16px;
    flex-shrink: 0;
}

/* --- Link params modal --- */

.lp-modal-content {
    max-width: 660px;
    width: 94%;
    position: relative;
}

.lp-modal-body {
    padding: 16px 20px;
    user-select: none;
}


.lp-endpoints-info {
    background: var(--dc-n-25);
    border-radius: 8px;
    margin-bottom: 14px;
    overflow: hidden;
}

.lp-ep-row {
    display: flex;
    align-items: stretch;
}

.lp-ep-card {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 3px;
    padding: 10px 14px;
    min-width: 0;
}

.lp-ep-card-label {
    font-size: var(--dc-text-2xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--dc-n-500);
}

.lp-ep-card-info {
    display: flex;
    align-items: center;
    gap: 6px;
    min-width: 0;
    overflow: hidden;
}

.lp-ep-sep {
    color: var(--dc-n-200);
    flex-shrink: 0;
    font-size: var(--dc-text-sm);
}

.lp-ep-card-type {
    font-size: var(--dc-text-sm);
    font-weight: 700;
    color: var(--dc-c-dark);
    white-space: nowrap;
    flex-shrink: 0;
}

.lp-ep-card-zone {
    font-size: var(--dc-text-sm);
    font-weight: 600;
    color: var(--dc-c-muted);
    white-space: nowrap;
    flex-shrink: 0;
}

.lp-ep-card-main {
    display: flex;
    align-items: center;
    gap: 10px;
    min-width: 0;
}

.lp-ep-rack-icon {
    width: 28px;
    height: 28px;
    flex-shrink: 0;
    opacity: 0.85;
}

/* ── Hardware modal: compact, content-sized (single-panel + bulk) ─────────────
   Overrides the big .panel-viz-modal 96vw×94vh box so the modal fits its content:
   narrow for one column, wider for the two-column bulk view, height hugs content
   (no empty space at the bottom). */
#panelHardwareModal .modal-content {
    width: fit-content;
    max-width: 92vw;
    height: auto;
    max-height: 88vh;
    padding: 0;
}
#panelHardwareModal .modal-header { padding: 12px 16px; }
#panelHardwareModal .modal-footer { padding: 12px 16px; }
#panelHardwareModal .modal-body   { padding: 12px 16px; }
/* Single-panel preset list: a comfortable fixed column width. */
#panelHardwareModal .hw-presets-list { width: 440px; max-width: 100%; }

/* Bulk hardware selector — Front / Rear columns (colour-coded like panel badges). */
.hw-bulk-selector {
    display: flex;
    gap: 16px;
    align-items: flex-start;
}
.hw-bulk-col {
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
}
/* Column header sticks to the top of the scroll area so it's always visible. */
.hw-bulk-col-title {
    position: sticky;
    top: 0;
    z-index: 2;
    font-weight: 700;
    font-size: var(--dc-text-sm);
    letter-spacing: 0.04em;
    text-transform: uppercase;
    text-align: center;
    padding: 6px 10px;
    margin-bottom: 10px;
    border-radius: 5px;
}
/* Front = blue, Rear = red — same palette as .lp-ep-panel-badge--front/--rear. */
.hw-bulk-col--front .hw-bulk-col-title { background: #e8f4fd; color: var(--dc-c-info); }
.hw-bulk-col--rear  .hw-bulk-col-title { background: #fdeaea; color: var(--dc-c-error-hover); }

.hw-bulk-col-list { display: flex; flex-direction: column; }

/* Option rows reuse the .hw-preset-row card layout, but compact (BoM-like cards).
   Compound selectors beat the base .hw-preset-row rules (e.g. .lp-cards { flex:1 }). */
.hw-preset-row.hw-bulk-opt { width: fit-content; margin-bottom: 8px; padding: 8px 10px; gap: 8px; }
.hw-preset-row.hw-bulk-opt .lp-cards { flex: 0 0 auto; gap: 6px; }
.hw-preset-row.hw-bulk-opt .lp-card { flex: 0 0 96px; width: 96px; }
.hw-preset-row.hw-bulk-opt .lp-card-photo { height: 56px; }
.hw-preset-row.hw-bulk-opt .lp-card-arrow { padding-bottom: 20px; }
.hw-bulk-col--front .hw-bulk-opt:hover           { border-color: var(--dc-c-info); background: #f4f9fd; }
.hw-bulk-col--front .hw-bulk-opt--selected       { border-color: var(--dc-c-info); background: #e8f4fd; }
.hw-bulk-col--rear  .hw-bulk-opt:hover           { border-color: var(--dc-c-error-hover); background: #fdf5f5; }
.hw-bulk-col--rear  .hw-bulk-opt--selected       { border-color: var(--dc-c-error-hover); background: #fdeaea; }

/* Scope note pinned under the modal title when applying to multiple racks. */
.hw-bulk-scope-note {
    flex-shrink: 0;
    margin: 0;
    padding: 8px 16px;
    font-size: var(--dc-text-sm);
    font-weight: 600;
    color: #2a8068;
    background: #eaf6f3;
    border-bottom: 1px solid #cfe9e0;
}

/* Select-all button in the Created Links list (below filters, above the list). */
.link-select-all-btn {
    display: block;
    width: 100%;
    margin: 4px 0 6px;
    padding: 6px 10px;
    font-size: var(--dc-text-sm);
    font-weight: 600;
    color: #2a8068;
    background: #eaf6f3;
    border: 1px solid #b2dfd4;
    border-radius: 5px;
    cursor: pointer;
    transition: background 0.15s, color 0.15s;
}

.link-select-all-btn:hover {
    background: #d8efe8;
}

.link-select-all-btn.is-active {
    color: var(--dc-n-0);
    background: var(--dc-c-accent);
    border-color: #4fa892;
}


.lp-ep-card-details {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.lp-ep-card-name {
    font-size: var(--dc-text-md);
    font-weight: 700;
    color: var(--dc-c-dark);
    display: flex;
    align-items: center;
    gap: 5px;
    min-width: 0;
}

.lp-ep-card-sub {
    font-size: var(--dc-text-xs);
    color: var(--dc-n-500);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.lp-ep-panel-badge {
    display: inline-block;
    padding: 1px 6px;
    border-radius: 3px;
    font-size: var(--dc-text-2xs);
    font-weight: 700;
    letter-spacing: 0.04em;
    flex-shrink: 0;
}

.lp-ep-panel-badge--front {
    background: #e8f4fd;
    color: var(--dc-c-info);
}

.lp-ep-panel-badge--rear {
    background: #fdeaea;
    color: var(--dc-c-error-hover);
}

/* The rail could not be named - see panelTone() in js/ui-templates.js. Grey on purpose:
   front is blue and rear is red, and which of the two this is, is exactly what is not
   known. A colour here would answer a question the code cannot answer. */
.lp-ep-panel-badge--unknown {
    background: var(--dc-n-50);
    color: var(--dc-n-700);
}

.lp-ep-arrow {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0 10px;
    color: var(--dc-n-300);
    font-size: 18px;
    flex-shrink: 0;
    border-left: 1px solid var(--dc-n-100);
    border-right: 1px solid var(--dc-n-100);
    background: white;
}

/* Slide-in animation for step transitions */
@keyframes lpSlideFromRight {
    from { transform: translateX(32px); opacity: 0; }
    to   { transform: translateX(0);    opacity: 1; }
}
@keyframes lpSlideFromLeft {
    from { transform: translateX(-32px); opacity: 0; }
    to   { transform: translateX(0);     opacity: 1; }
}

.lp-step-current.lp-animate-in  { animation: lpSlideFromLeft  0.22s ease; }
.lp-step-choose.lp-animate-in   { animation: lpSlideFromRight 0.22s ease; }
.lp-step-settings.lp-animate-in { animation: lpSlideFromRight 0.22s ease; }

/* Step 0: current link summary */
.lp-step-current { display: block; }

.lp-step0-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 14px;
}

.lp-change-type-btn { flex-shrink: 0; white-space: nowrap; }

.lp-current-badges {
    display: flex;
    align-items: center;
    justify-content: space-between;
    background: #f0f7ff;
    border: 1px solid #c8dff8;
    border-radius: 6px;
    padding: 5px 10px;
    margin-bottom: 10px;
    font-size: var(--dc-text-sm);
}

.lp-s0-icons {
    display: flex;
    align-items: center;
    gap: 6px;
}

.lp-s0-icon { display: block; border-radius: 3px; }

.lp-current-cards { margin-bottom: 8px; }

.lp-edit-settings-btn {
    display: block;
    width: 100%;
    padding: 9px 14px;
    background: var(--dc-n-25);
    border: 2px solid var(--dc-n-150);
    border-radius: 8px;
    font-size: var(--dc-text-md);
    font-weight: 600;
    color: var(--dc-c-dark);
    cursor: pointer;
    text-align: left;
    transition: background 0.15s, border-color 0.15s;
}

.lp-edit-settings-btn:hover {
    background: var(--dc-c-accent-light);
    border-color: var(--dc-c-accent);
}

.lp-section-label {
    font-size: var(--dc-text-2xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--dc-n-500);
    margin-bottom: 8px;
}

/* Left column — type list */
.lp-type-list {
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.lp-type-item {
    display: flex;
    align-items: center;
    gap: 0;
}

.lp-type-item .link-config-radio-compact {
    display: none;
}

.lp-type-label {
    flex: 1;
    display: flex;
    flex-direction: column;
    padding: 7px 10px;
    background: var(--dc-n-25);
    border: 2px solid var(--dc-n-150);
    border-radius: 8px;
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s;
    min-width: 0;
}

.lp-type-label:hover {
    background: var(--dc-n-50);
    border-color: var(--dc-n-300);
}

.link-config-radio-compact:checked + .lp-type-label {
    background: var(--dc-c-accent-light);
    border-color: var(--dc-c-accent);
    box-shadow: 0 0 0 3px rgba(101,184,165,0.12);
}

.lp-type-item-copper .link-config-radio-compact:checked + .lp-type-label {
    background: #fdf6ee;
    border-color: #c07a2e;
    box-shadow: 0 0 0 3px rgba(192,122,46,0.12);
}

.lp-type-name {
    font-size: var(--dc-text-md);
    font-weight: 600;
    color: var(--dc-c-dark);
}

.lp-type-name-copper { color: #7d5a2e; }

/* The fibre count is .badge .badge--info .badge--chip (js/links.js). What stays here
   is where it sits in the column — placement belongs to the row, not to the badge. */
.lp-type-fiber-badge { margin-top: 2px; align-self: flex-start; }

.lp-type-desc {
    font-size: var(--dc-text-sm);
    color: var(--dc-c-muted);
    margin-top: 1px;
}

.lp-fiber-total-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    background: #f0f7ff;
    border: 1px solid #c8dff8;
    border-radius: 6px;
    padding: 5px 10px;
    margin-bottom: 8px;
    font-size: var(--dc-text-sm);
}

.lp-fiber-per-link {
    color: var(--dc-n-800);
}

.lp-fiber-total {
    font-weight: 600;
    color: var(--dc-c-info);
}

/* Step 1: choose link type */
.lp-step-choose {
    display: block;
}

/* Step 2: configure selected type */
.lp-step-settings {
    display: block;
}

/* Inline row group for qty + fiber + splice */
.lp-settings-inline {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px 20px;
    margin-bottom: 12px;
}

/* Read-only config display in left panel */
.lp-cur-row {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 10px;
}

.lp-cur-label {
    font-size: var(--dc-text-2xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--dc-n-500);
    min-width: 64px;
}

.lp-cur-value {
    font-size: var(--dc-text-md);
    font-weight: 600;
    color: var(--dc-c-dark);
}

.lp-cur-warn {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    font-size: var(--dc-text-sm);
    font-weight: 600;
    color: var(--dc-c-warning);
}

.lp-cur-badge {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 2px 8px;
    border-radius: 20px;
    font-size: var(--dc-text-sm);
    font-weight: 600;
    background: var(--dc-n-50);
    color: var(--dc-c-dark);
}

.lp-cur-title {
    font-size: var(--dc-text-lg);
    font-weight: 700;
    color: var(--dc-c-dark);
    margin-bottom: 3px;
}

.lp-cur-desc {
    font-size: var(--dc-text-sm);
    color: var(--dc-c-muted);
    margin-bottom: 14px;
}

.lp-selected-title {
    font-size: var(--dc-text-lg);
    font-weight: 700;
    color: var(--dc-c-dark);
    margin-bottom: 2px;
}

.lp-selected-desc {
    font-size: var(--dc-text-sm);
    color: var(--dc-c-muted);
}


.lp-back-btn {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 5px 10px;
    font-size: var(--dc-text-sm);
    font-weight: 600;
    color: var(--dc-c-muted);
    background: transparent;
    border: 1px solid var(--dc-n-200);
    border-radius: 4px;
    cursor: pointer;
    margin-bottom: 12px;
    transition: background 0.15s, color 0.15s;
}

.lp-back-btn:hover {
    background: var(--dc-n-50);
    color: var(--dc-c-dark);
}

.lp-setting-row {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 0;
}

/* Inside .lp-settings-inline, rows sit side by side */
.lp-settings-inline .lp-setting-row {
    margin-bottom: 0;
}

.lp-setting-label {
    font-size: var(--dc-text-sm);
    color: var(--dc-n-800);
    width: 76px;
    flex-shrink: 0;
}

.lp-qty-input {
    width: 60px;
    max-width: 60px;
    flex-shrink: 0;
    padding: 5px 8px;
    border: 1px solid var(--dc-n-200);
    border-radius: 5px;
    font-size: var(--dc-text-md);
    font-weight: 600;
    text-align: center;
}

.lp-qty-input:focus {
    outline: none;
    border-color: var(--dc-c-accent);
}

.lp-stepper {
    display: inline-flex;
    align-items: center;
    border: 2px solid var(--dc-n-300);
    border-radius: 8px;
    overflow: hidden;
    height: 34px;
}

.lp-stepper-input {
    width: 44px;
    border: none;
    border-left: 1px solid var(--dc-n-150);
    border-right: none;
    text-align: center;
    font-size: var(--dc-text-md);
    font-weight: 700;
    background: var(--dc-n-0);
    height: 100%;
    padding: 0;
    -moz-appearance: textfield;
}

.lp-stepper-input::-webkit-outer-spin-button,
.lp-stepper-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

.lp-stepper-unit {
    font-size: var(--dc-text-sm);
    font-weight: 600;
    color: var(--dc-n-600);
    padding: 0 5px 0 2px;
    background: var(--dc-n-0);
    height: 100%;
    display: flex;
    align-items: center;
    border-right: 1px solid var(--dc-n-150);
}

.lp-toggle-group {
    display: flex;
    gap: 5px;
}

.lp-toggle-group .toggle-btn {
    padding: 5px 8px;
    min-width: unset;
    border-radius: 5px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

.lp-image-section {
    margin-top: 8px;
    border-radius: 8px;
    overflow: hidden;
    background: var(--dc-n-25);
}

.lp-cards-title {
    font-size: var(--dc-text-2xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--dc-n-500);
    padding: 8px 6px 0;
    background: var(--dc-n-25);
}

.lp-cards {
    display: flex;
    flex-wrap: wrap;
    align-items: stretch;
    padding: 8px 6px;
    gap: 6px 0;
    background: var(--dc-n-25);
    border-radius: 8px;
}

.lp-card {
    flex: 1;
    display: flex;
    flex-direction: column;
    border: 1px solid var(--dc-n-150);
    border-radius: 5px;
    overflow: hidden;
    background: white;
    min-width: 0;
}

.lp-card-photo {
    height: 80px;
    background: white;
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
    position: relative;
}

.lp-card-qty-badge {
    position: absolute;
    bottom: 4px;
    right: 4px;
    background: var(--dc-c-accent);
    color: white;
    font-size: var(--dc-text-2xs);
    font-weight: 700;
    padding: 1px 5px;
    border-radius: 4px;
    line-height: 1.5;
    pointer-events: none;
}

.lp-card-badge {
    position: absolute;
    bottom: 24px;
    right: 4px;
    background: var(--dc-c-warning);
    color: white;
    font-size: var(--dc-text-2xs);
    font-weight: 700;
    padding: 1px 5px;
    border-radius: 4px;
    line-height: 1.5;
    pointer-events: none;
}

.lp-card-photo-img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
}

.lp-card-photo-placeholder {
    width: 100%;
    height: 100%;
    background: white;
}

.lp-card-label {
    padding: 4px 5px;
    background: var(--dc-n-25);
    border-top: 1px solid var(--dc-n-100);
}

.lp-card-role {
    font-size: var(--dc-text-2xs);
    font-weight: 700;
    text-transform: uppercase;
    color: var(--dc-n-500);
    letter-spacing: 0.05em;
    margin-bottom: 1px;
}

.lp-card-name {
    font-size: var(--dc-text-xs);
    color: var(--dc-c-dark);
    line-height: 1.3;
    overflow: hidden;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
}

.lp-card-qty-hint {
    display: block;
    font-size: var(--dc-text-2xs);
    color: var(--dc-c-warning);
    margin-top: 2px;
}

.lp-insertion-loss-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 6px 6px 2px;
    background: var(--dc-n-25);
}

.lp-insertion-loss-label {
    font-size: var(--dc-text-2xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--dc-n-500);
}

.lp-insertion-loss-value {
    font-size: var(--dc-text-xs);
    color: var(--dc-n-300);
    font-style: italic;
}

.lp-card-arrow {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0 3px;
    color: var(--dc-n-300);
    font-size: 14px;
    font-weight: 600;
    flex-shrink: 0;
    padding-bottom: 28px;
}

/* Arrow between groups (Side A → Cable → Side B) */
.lp-card-arrow--sep {
    font-size: 18px;
    font-weight: 400;
    padding: 0 6px;
}

/* --- Variant rows (Variant B UI) --- */

.lp-variants {
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding: 4px 0;
}

.lp-variant-row {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    padding: 8px 10px;
    border: 1.5px solid var(--dc-n-150);
    border-radius: 8px;
    cursor: pointer;
    transition: border-color 0.15s, background 0.15s;
    background: var(--dc-n-25);
}

.lp-variant-row:hover {
    border-color: var(--dc-c-accent);
    background: #f0faf8;
}

.lp-variant-row--selected {
    border-color: var(--dc-c-accent);
    background: #eaf6f3;
}

.lp-variant-body {
    flex: 1;
    min-width: 0;
}

.lp-variant-label {
    font-size: var(--dc-text-2xs);
    font-weight: 700;
    color: var(--dc-c-dark);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    margin-bottom: 5px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
}

.lp-variant-cost {
    font-size: var(--dc-text-sm);
    font-weight: 700;
    color: #2c7a5c;
    text-transform: none;
    letter-spacing: 0;
    white-space: nowrap;
    flex-shrink: 0;
}

.lp-variant-cost--unknown {
    color: var(--dc-n-400);
    font-weight: 400;
}

.lp-variant-price-wrap {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    flex-shrink: 0;
}

.lp-variant-warn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: var(--dc-c-error);
    color: white;
    font-size: var(--dc-text-xs);
    font-weight: 700;
    line-height: 1;
    cursor: default;
    flex-shrink: 0;
}

/* --- Hardware preset rows --- */

.hw-presets-list {
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding-right: 12px;
}

.hw-preset-row {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 12px;
    border: 2px solid var(--dc-n-150);
    border-radius: 10px;
    cursor: pointer;
    transition: border-color 0.15s, background 0.15s;
    background: var(--dc-n-0);
}

.hw-preset-row:hover {
    border-color: var(--dc-c-accent);
    background: #f7fdfc;
}

.hw-preset-row--selected {
    border-color: var(--dc-c-accent);
    background: #eafaf4;
}

.hw-preset-row--pending {
    border-color: var(--dc-c-accent);
    border-style: dashed;
    background: #f7fdfc;
}

.hw-preset-row .lp-cards {
    flex: 1;
    border: none;
    background: none;
    padding: 0;
    border-radius: 0;
}

.hw-preset-row .lp-cards-section {
    background: none;
}

.hw-preset-check {
    width: 18px;
    height: 18px;
    border-radius: 50%;
    border: 2px solid var(--dc-n-200);
    flex-shrink: 0;
    transition: border-color 0.15s, background 0.15s;
    position: relative;
}

.hw-preset-row--selected .hw-preset-check {
    border-color: var(--dc-c-accent);
    background: var(--dc-c-accent);
}

.hw-preset-row--selected .hw-preset-check::after {
    content: '';
    position: absolute;
    top: 3px; left: 3px;
    width: 8px; height: 8px;
    border-radius: 50%;
    background: var(--dc-n-0);
}

.hw-preset-750plus {
    display: none;
}

.hw-presets-list--wide .hw-preset-750plus {
    display: flex;
}

/* --- Link configuration items (compact) --- */

.link-config-item-compact {
    display: flex;
    align-items: center;
    margin-bottom: 4px;
    overflow: hidden;
}

.link-config-radio-compact {
    margin: 0;
    width: 16px;
    height: 16px;
    flex-shrink: 0;
    cursor: pointer;
}

.link-config-label-compact {
    display: grid;
    grid-template-columns: 1fr auto;
    align-items: center;
    column-gap: 10px;
    padding: 5px 10px;
    background: var(--dc-n-25);
    border: 2px solid var(--dc-n-150);
    border-radius: 4px;
    transition: all 0.2s;
    cursor: pointer;
    flex: 1;
    margin-left: 8px;
}

.link-config-label-compact:hover {
    background: var(--dc-n-50);
    border-color: var(--dc-n-300);
}

.link-config-radio-compact:checked + .link-config-label-compact {
    background: var(--dc-c-accent-light);
    border-color: var(--dc-c-accent);
    box-shadow: 0 0 0 3px rgba(101, 184, 165, 0.1);
}

.link-config-title-compact {
    grid-column: 1; grid-row: 1;
    font-size: var(--dc-text-md);
    font-weight: 600;
    color: var(--dc-c-dark);
}

.link-config-desc-compact {
    grid-column: 1; grid-row: 2;
    font-size: var(--dc-text-xs);
    color: var(--dc-c-muted);
    line-height: 1.3;
}

/* --- Per-row link count + fiber config --- */

.link-row-config {
    grid-column: 2;
    grid-row: 1 / 3;
    display: grid;
    grid-template-columns: 28px 52px 24px 44px 60px 60px;
    align-items: center;
    column-gap: 5px;
    justify-self: end;
}

.link-row-qty-label {
    font-size: var(--dc-text-xs);
    color: var(--dc-c-muted);
    white-space: nowrap;
}

.link-row-count {
    width: 36px;
    padding: 2px 3px;
    border: 1px solid var(--dc-n-300);
    border-radius: 3px;
    font-size: var(--dc-text-sm);
    font-weight: 600;
    text-align: center;
}

/* --- Link params modal overrides --- */

#linkParamsModal .modal-body {
    max-height: calc(100vh - 200px);
    overflow-y: auto;
}

#imageModal {
    background: rgba(0, 0, 0, 0.9);
}

#imageModal .modal-content {
    display: flex;
    align-items: center;
    justify-content: center;
}

/* --- Panel link list --- */

.link-list {
    margin-top: 15px;
    border-top: 1px solid var(--dc-n-150);
    padding-top: 15px;
}

.link-list.tray-edit-locked {
    opacity: 0.4;
    pointer-events: none;
    user-select: none;
}

.rack-filter-buttons {
    display: flex;
    flex-wrap: wrap;
    gap: 5px;
}

/* One filter group in the Created Links step: a label over a row of buttons. Five of
   these carried the same handful of declarations inline in index.html, so a sixth was
   a copy-paste rather than a class. Built by _renderLinkFilterGroup(). */
.link-filter-group {
    margin: 10px 0;
}

.link-filter-label {
    font-size: var(--dc-text-sm);
    color: var(--dc-c-muted);
    margin-bottom: 5px;
}

.rack-filter-btn {
    padding: 4px 10px;
    font-size: var(--dc-text-xs);
    border: 1px solid var(--dc-n-300);
    background: white;
    border-radius: 4px;
    cursor: pointer;
    transition: all 0.2s;
    font-weight: 500;
    color: var(--dc-n-800);
}

.rack-filter-btn:hover {
    background: var(--dc-n-50);
    border-color: var(--dc-n-500);
}

.rack-filter-btn.active {
    background: var(--dc-c-accent);
    border-color: var(--dc-c-accent);
    color: white;
}

.rack-filter-btn.all {
    background: #3498db;
    border-color: #3498db;
    color: white;
}

.rack-filter-btn.all:hover {
    background: var(--dc-c-info);
    border-color: var(--dc-c-info);
}

.link-item {
    padding: 8px 10px;
    margin: 4px 0;
    background: var(--dc-n-25);
    border-radius: 4px;
    cursor: pointer;
    transition: background 0.2s;
    display: flex;
    align-items: flex-start;
    font-size: var(--dc-text-md);
    line-height: 1.4;
    gap: 8px;
}

.link-item > span:first-child {
    flex: 1;
}

.link-item-text {
    flex: 1;
    min-width: 0;
}

.link-item-first-line {
    font-weight: 600;
    color: var(--dc-c-dark);
    margin-bottom: 4px;
    display: flex;
    align-items: center;
    gap: 5px;
}

.link-item-badges {
    display: flex;
    gap: 6px;
    flex-wrap: wrap;
}

.link-item-third-line {
    font-size: var(--dc-text-xs);
    color: var(--dc-c-muted);
    margin-top: 2px;
}

/* ── The two ends of a link ───────────────────────────────────────────────────
   One flex row of two unbreakable units: [A] and [→ B]. Each unit is nowrap, so
   when the room runs out the far end drops to the next line WHOLE. Before this the
   pair sat in a plain span with " → " between, and a 280 px sidebar broke the arrow,
   the panel badge and the second name onto three separate lines — one link read as
   three. The cabinet NAME is what gives way inside a unit: .cl-name ellipsizes.
   Every width, no breakpoint; the container query further down is a different
   question. Built by buildLinkListItemCard() in js/panellinks.js. */
.link-item-eps {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    column-gap: 5px;
    row-gap: 2px;
    flex: 1;
    min-width: 0;
}

.link-item-ep {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    white-space: nowrap;
    min-width: 0;
    max-width: 100%;
}

/* Search over the list: cabinet name or coordinate, either end. Full width of the
   step, the .field primitive for everything else. */
.link-search-input {
    width: 100%;
}

/* ── Compact card in a narrow sidebar ─────────────────────────────────────────
   The sidebar is dragged between 280 and 700 px (js/init.js), which no media query
   can see — the window has not changed size. So the list is a container and the card
   answers to IT.

   Where 340 comes from: on the wide card the text area is the list width minus
   148 px (8 stripe + 20 card padding + 98 of buttons + the gaps between them).
   At a 340 px list that leaves 192 px of text, and the card is already four lines
   tall; narrower still and the buttons keep the width while the only part worth
   reading loses it. Below 340 the card becomes one grid: the three buttons join the
   chip row on the right, and the id gives up its disc for small muted text at the
   head of that row. The text then has the whole width — 290 px instead of 192 at the
   same 340 — and the buttons cost a row they were going to cost in wrapping anyway.

   The three levels of the card (item → text box → first line) are flattened with
   `display: contents` for exactly one reason: only as ONE grid can the buttons share
   a row with the chips, which live two boxes deeper in the tree. */
#panelLinkListItems {
    container-type: inline-size;
    container-name: linklist;
}

@container linklist (max-width: 340px) {
    .link-item {
        display: grid;
        grid-template-columns: auto auto 1fr auto auto auto;
        grid-template-areas:
            "stripe warn  eps    eps   eps  eps"
            "stripe id    badges color edit del"
            "stripe third third  third third third";
        /* Top-aligned, not centred: the chips wrap to two or three lines in here, and
           centring floats the id and the buttons in the middle of that block instead
           of at the head and the end of the row they belong to. */
        align-items: start;
        column-gap: 5px;
        row-gap: 4px;
        /* One knob for both icon buttons — see .icon-btn in css/ui-primitives.css */
        --icon-btn-size: 28px;
    }

    .link-item-text,
    .link-item-text > .link-item-first-line { display: contents; }

    .link-color-stripe    { grid-area: stripe; }
    .link-no-route-icon   { grid-area: warn; }
    .link-item-eps        { grid-area: eps; }
    .spec-link-id         { grid-area: id; }
    .link-item-badges     { grid-area: badges; }
    .link-color-btn       { grid-area: color; }
    .link-item-edit       { grid-area: edit; }
    .link-item-delete     { grid-area: del; }
    .link-item-third-line { grid-area: third; }

    /* 26 px of green circle for two digits is the widest thing on the line, and here
       the id only has to be findable.
       Qualified with .link-item on purpose: a container query adds no specificity, and
       the disc is declared further down this file, so a bare .spec-link-id would lose
       to it on source order alone. */
    .link-item .spec-link-id {
        background: none;
        color: var(--dc-c-muted);
        font-size: var(--dc-text-2xs);
        font-weight: 600;
        border-radius: 0;
        min-width: 0;
        height: auto;
        justify-self: start;
    }

    .link-item.highlighted .spec-link-id {
        color: var(--dc-n-0);
    }

    /* 28 px like the two beside it: 20 px picker, 2 px padding, 2 px border.
       Qualified for the same reason as the id above — source order, not specificity. */
    .link-item .link-color-btn {
        padding: 2px;
    }
}

.link-section-separator {
    margin: 8px 0 4px;
    font-size: var(--dc-text-2xs);
    font-weight: 700;
    color: var(--dc-c-muted);
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

.link-list-count {
    font-size: var(--dc-text-xs);
    color: var(--dc-c-muted);
    margin-top: 5px;
}

.link-context-label {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: var(--dc-text-xs);
    font-weight: 600;
    color: #3a5bbf;
    background: #eef3ff;
    border: 1px solid #c0cef5;
    border-radius: 6px;
    padding: 4px 8px;
    margin: 4px 0 6px;
}

.link-ctx-icon {
    font-size: 12px;
    line-height: 1;
}

.link-item:hover {
    background: var(--dc-n-50);
}

.link-item.highlighted {
    background: var(--dc-c-accent);
    color: white;
}

.link-item.highlighted div {
    color: white !important;
}

/* .link-item-edit / .link-item-delete use the .icon-btn--settings/--delete
   primitives (look and hover live in ui-primitives.css) */

.link-item--no-route {
    background: #fff5f5;
}

.link-item--no-route:hover {
    background: #ffe8e8;
}

.link-item--no-route.highlighted {
    background: var(--dc-c-accent);
}

/* Same glyph and pill as the no-route marker in the links overview report — see
   _LO_ICON and WARN_CHIP in js/export.js. Change all three together. */
.link-no-route-icon {
    display: inline-block;
    box-sizing: border-box;
    width: 19px;
    height: 17px;
    background: #fff3cd url('../images/icon_warning.svg') center / 12px 12px no-repeat;
    border: 1px solid #ffc107;
    border-radius: 4px;
    flex-shrink: 0;
}

/* --- Panel visualization modal (Cabinet View) --- */

.panel-viz-modal.modal-content,
.panel-viz-modal .modal-content,
#panelCabinetZoomModal .modal-content {
    width: 96vw;
    max-width: 96vw;
    height: 94vh;
    max-height: 94vh;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    position: relative;
    padding: 15px;
}



#panelEnlargedModal .modal-content {
    max-width: 95vw;
    width: 95vw;
    max-height: 92vh;
    display: flex;
    flex-direction: column;
    background: var(--dc-n-0);
    position: relative;
}

/* Header bar */
.cabinet-modal-header {
    display: flex;
    align-items: center;
    gap: 16px;
    flex-shrink: 0;
    padding: 8px 16px;
    border-bottom: 1px solid var(--dc-n-100);
    min-height: 0;
    margin-bottom: 0;
}

.cabinet-modal-title {
    font-size: var(--dc-text-lg);
    font-weight: 600;
    color: #374151;
    white-space: nowrap;
    flex-shrink: 0;
}

/* Rack info card inline in header */
.cabinet-modal-rack-info {
    flex: 1;
    min-width: 0;
}

.cabinet-modal-rack-info .lp-ep-row {
    align-items: center;
}

.cabinet-modal-rack-info .lp-ep-card {
    flex-direction: row;
    align-items: center;
    gap: 10px;
    padding: 4px 10px;
    background: #f4f6f8;
    border-radius: 6px;
}

.cabinet-modal-rack-info .lp-ep-card-main {
    gap: 8px;
}

.cabinet-modal-rack-info .lp-ep-rack-icon {
    width: 20px;
    height: 20px;
}

.cabinet-modal-rack-info .lp-ep-card-name {
    font-size: var(--dc-text-md);
    font-weight: 600;
}

.cabinet-modal-rack-info .lp-ep-card-sub {
    font-size: var(--dc-text-xs);
    color: var(--dc-n-600);
}

.cabinet-modal-btns {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-shrink: 0;
    margin-left: auto;
}

/* Body = flex row: cabinet column + optional sidebar */
.cabinet-modal-body {
    flex: 1;
    display: flex;
    flex-direction: row;
    overflow: hidden;
    min-height: 0;
}

/* Left column: header info + hint + cabinet container */
.cabinet-view-main {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    overflow: hidden;
    padding: 12px 16px 0;
    min-width: 0;
}

.cabinet-zoom-hint {
    font-size: var(--dc-text-xs);
    color: var(--dc-n-600);
    text-align: center;
    margin: 6px 0 10px;
}

#panelCabinetZoomContainer {
    width: 100%;
    flex: 1;
    min-height: 0;
    overflow: hidden;
    border: 1px solid var(--dc-n-1000);
    position: relative;
}

.panel-viz-modal .modal-body {
    flex: 1;
    overflow: hidden;
    padding-right: 0;
    max-height: none;   /* override the base .modal-body { max-height: 60vh } cap so the
                           body fills the flex column instead of stopping at 60vh */
    margin-bottom: 0;
}

#panelHardwareModal .modal-content,
#panelLinksModal .modal-content {
    display: flex;
    flex-direction: column;
    max-height: 90vh;
}

#panelHardwareModal .modal-body,
#panelLinksModal .modal-body {
    flex: 1;
    overflow-y: auto;
    min-height: 0;
}

/* Connected Links modal fills the available height so the scroll area (list)
   occupies the whole modal instead of leaving empty space at the bottom. */
#panelLinksModal .modal-content {
    height: 90vh;
}

#panelLinksModal .modal-body {
    display: flex;
    flex-direction: column;
    padding-top: 0;
}

#panelLinksModal #panelLinksContainer {
    flex: 1;
    min-height: 0;
}

/* Prevent text selection while shift/ctrl multi-selecting link cards. */
#panelLinksModal .panel-links-pinned-header,
#panelLinksModal #panelLinksContainer {
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
}

/* Panel info row pinned between the title and the scrolling list. */
.panel-links-pinned-header {
    flex-shrink: 0;
    padding: 10px 4px 6px;
    border-bottom: 1px solid #eef1f4;
    margin-bottom: 4px;
}

#panelEnlargedModal .modal-body {
    overflow-y: auto;
    overflow-x: auto;
}

.panel-viz-body {
    display: flex;
    gap: 24px;
    align-items: stretch;
    flex: 1;
    min-height: 0;
}

.panel-viz-info {
    flex-shrink: 0;
    width: 240px;
}

.panel-viz-cabinet-col {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 10px;
    min-width: 0;
    min-height: 0;
    overflow: hidden;
}

/* Panel Configuration modal fills its height so the canvas can too (no bottom gap). */
#panelVizModal .modal-body {
    display: flex;
    flex-direction: column;
    overflow: hidden;
}
#panelVizModal #panelVizHeader { flex-shrink: 0; }

/* Interactive panel canvas — a pan/zoom viewport (floorplan-style). */
.panel-viz-cabinet-wrap {
    width: 100%;
    flex: 1;
    min-height: 0;
    position: relative;
    overflow: hidden;
    border: 1px solid var(--dc-n-150);
    background: var(--dc-n-25);   /* same canvas look as the floor plan */
    cursor: grab;
}
.panel-viz-cabinet-wrap:active { cursor: grabbing; }

/* Transformed content inside the viewport. `will-change: transform` is intentionally
   NOT used: it would promote the content to a GPU layer rasterized once at scale 1 and
   then upscaled (blurry until the DOM is rebuilt). Without it the browser re-rasterizes
   at the actual scale each paint, keeping the panels sharp. */
.panel-viz-cabinet-wrap .panel-hardware-viz {
    position: absolute;
    top: 0;
    left: 0;
    transform-origin: 0 0;
}

/* Hint above the interactive panel view in the config modal. */
.panel-viz-drag-hint {
    flex-shrink: 0;
    font-size: var(--dc-text-xs);
    color: var(--dc-n-600);
    text-align: center;
}

.panel-viz-cabinet-wrap .rack-cabinet-view {
    transform-origin: top center;
}

/* Custom scrollbar for modal body */
.panel-viz-modal .modal-body::-webkit-scrollbar {
    width: 8px;
}

.panel-viz-modal .modal-body::-webkit-scrollbar-track {
    background: var(--dc-n-50);
    border-radius: 4px;
}

.panel-viz-modal .modal-body::-webkit-scrollbar-thumb {
    background: var(--dc-c-accent);
    border-radius: 4px;
}

.panel-viz-modal .modal-body::-webkit-scrollbar-thumb:hover {
    background: #5aa896;
}

.panel-viz-info {
    padding: 0;
    margin-bottom: 0;
}

.panel-viz-info h3 {
    font-size: var(--dc-text-lg);
    color: var(--dc-c-dark);
    margin-bottom: 8px;
}

.panel-viz-info-row {
    display: flex;
    justify-content: space-between;
    font-size: var(--dc-text-sm);
    margin-bottom: 4px;
}

.panel-viz-info-row:last-child {
    margin-bottom: 0;
}

/* Sized here rather than inherited: the label is used both inside .panel-viz-info-row,
   which is 12px, and beside the slack guide button in a plain flex row, where it was
   taking whatever body gave it - 16px. The same label read at two sizes depending on
   which box it landed in. */
.panel-viz-info-label {
    color: var(--dc-c-muted);
    font-weight: 500;
    font-size: var(--dc-text-sm);
}

.panel-viz-info-value {
    color: var(--dc-c-dark);
    font-weight: 600;
}

.panel-visualization {
    border: 2px solid var(--dc-n-150);
    border-radius: 6px;
    padding: 20px;
    background: var(--dc-n-0);
    min-height: 200px;
}

/* --- Panel hardware visualization --- */

/* ── Rack cabinet sprite view ─────────────────────────────────────────────── */
.rack-cabinet-view {
    display: inline-block;
    line-height: 0;
}
.rack-units-area {
    overflow: hidden;
}
.rack-panels-overlay {
    position: absolute;
    top: 0;
}
.rack-unit-label {
    position: absolute;
    left: 15px;
    height: 28px;
    line-height: 28px;
    font-size: 8px;
    font-weight: 600;
    color: var(--dc-n-1000);
    text-shadow: none;
    user-select: none;
    pointer-events: none;
    z-index: 1;
    white-space: nowrap;
}


.panel-links-count-btn {
    cursor: pointer;
    text-decoration: underline dotted;
    text-underline-offset: 3px;
}
.panel-links-count-btn:hover {
    color: var(--dc-c-info);
}

.panel-overcapacity-warning {
    display: flex;
    align-items: flex-start;
    gap: 14px;
    margin-bottom: 20px;
    padding: 18px 20px;
    background: #fff5f5;
    border: 2px solid var(--dc-c-error);
    border-radius: 8px;
}
.panel-overcapacity-icon {
    flex-shrink: 0;
    width: 32px;
    height: 32px;
    border-radius: 50%;
    background: var(--dc-c-error);
    color: var(--dc-n-0);
    font-size: 20px;
    font-weight: bold;
    display: flex;
    align-items: center;
    justify-content: center;
    line-height: 1;
}
.panel-overcapacity-text {
    color: var(--dc-c-error-hover);
    font-size: var(--dc-text-md);
}
.panel-overcapacity-text strong {
    display: block;
    font-size: var(--dc-text-md);
    margin-bottom: 4px;
}
.panel-overcapacity-text p {
    margin: 0;
    opacity: 0.85;
}

.panel-odf-recommendation {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    margin-top: 14px;
    padding: 12px 14px;
    background: #eaf4fb;
    border: 1.5px solid #3498db;
    border-radius: 8px;
}
.panel-odf-recommendation-icon {
    flex-shrink: 0;
    width: 28px;
    height: 28px;
    border-radius: 50%;
    background: #3498db;
    color: var(--dc-n-0);
    font-size: 15px;
    font-weight: bold;
    display: flex;
    align-items: center;
    justify-content: center;
    line-height: 1;
}
.panel-odf-recommendation-text {
    color: #1a5276;
    font-size: var(--dc-text-sm);
    line-height: 1.45;
}
.panel-odf-recommendation-text strong {
    display: block;
    font-size: var(--dc-text-md);
    margin-bottom: 3px;
}

.panel-hardware-viz {
    margin: 0;
    padding: 0;
    background: none;
    border: none;
}

.panel-hardware-title-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 15px;
}

.panel-hardware-title {
    font-size: var(--dc-text-md);
    font-weight: 600;
    color: var(--dc-c-dark);
}

.rack-cabinet-view {
    display: flex;
    flex-direction: column;
    flex-shrink: 0;
    margin-bottom: 12px;
}

.rack-units-area {
    flex-shrink: 0;
    overflow: hidden;
}

.rack-panels-overlay {
    /* horizontal position set inline per rack width */
}

.panel-stack {
    display: flex;
    flex-direction: column-reverse;
    gap: 0px;
    align-items: center;
    transition: opacity 0.2s;
}

.panel-stack:hover {
    opacity: 0.9;
}

.panel-stack-enlarged {
    display: flex;
    flex-direction: column-reverse;
    gap: 0px;
    align-items: flex-start;
}

.enlarged-panel-row {
    display: flex;
    align-items: center;
    gap: 10px;
}

.enlarged-panel-zoom {
    /* zoom applied dynamically via JS based on container width */
}

.enlarged-panel-label {
    font-size: var(--dc-text-md);
    font-weight: 700;
    color: var(--dc-n-950);
    white-space: nowrap;
    min-width: 42px;
    pointer-events: none;
    user-select: none;
}

.panel-unit {
    position: relative;
    width: 300px;
    height: 28px;
}

.panel-unit-label {
    position: absolute;
    left: 308px;
    top: 50%;
    transform: translateY(-50%);
    font-size: 10px;
    font-weight: 600;
    color: var(--dc-n-800);
    white-space: nowrap;
    pointer-events: none;
    user-select: none;
}

.panel-base-image {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    z-index: 1;
}

.panel-module {
    position: absolute;
    z-index: 2;
}

.panel-module-double {
    width: 129px;
    height: 8px;
}

.panel-module-single {
    width: 63px;
    height: 8px;
}

.panel-module-copper {
    width: 59px;
    height: 10px;
}

/* The caption over a module in the cabinet zoom. The same bar as .panel-module-dest and
   the same rules below it — it differs only in being shown outright, since that view has
   no destination mode to switch, and in sitting above the equipment overlays. Its box and
   its colour come from the layout entry and are set inline. */
.cabinet-label-overlay {
    position: absolute;
    z-index: 3;
    display: flex;
    height: 8px;
    overflow: visible;
    white-space: nowrap;
}

.panel-module-dest,
.cabinet-label-overlay {
    align-items: center;
    justify-content: flex-start;
    font-size: 3.6px;
    font-weight: 600;
    letter-spacing: -0.4px;
    color: var(--dc-n-1000);
    border-radius: 1px;
    padding: 0 1px;
    box-sizing: border-box;
}

.panel-module-dest {
    position: absolute;
    z-index: 2;
    display: none;
    overflow: hidden;
    white-space: normal;
}

.panel-module-dest .ml-dest {
    overflow: hidden;
    text-overflow: ellipsis;
}

/* The cabinet inside a module caption. Captions are drawn at 3.6px because the whole
   panel is then blown up by the viewer's zoom, so the meta floor — which is measured in
   real screen pixels — has to come off: at this scale only the proportion is right.

   The caption reads "slot  CABINET coord/zone · address", so the cabinet needs air on
   both sides: it is an inline-flex box, and the plain space around it collapses against
   its edges. */
.panel-module-dest .cl,
.cabinet-label-overlay .cl,
.odf-cassette-label .cl {
    display: inline-flex;
    gap: 0.25em;
    margin: 0 0.35em;
}

.panel-module-dest .cl,
.cabinet-label-overlay .cl {
    --cl-meta-min: 0px;
}

/* The caption tracks at a flat -0.4px. Against the cabinet name that is about a tenth of
   an em and reads as tight-but-normal; against the coordinate-and-zone column beside it,
   whose type is 0.4 of that again, it is nearly a fifth — enough to close the space in
   "Zone 1" and run the words together. The column is small already and needs no help
   fitting, so it keeps its own spacing. */
.panel-module-dest .cl-meta,
.cabinet-label-overlay .cl-meta { letter-spacing: normal; }

/* A cassette caption is shown at its own 8px and never blown up, so the proportion
   alone would put the coordinate at 3.2px. It keeps a floor, just a lower one than
   the app-wide 9px, which would out-size the 8px name next to it. */
.odf-cassette-label .cl {
    --cl-meta-min: 6px;
}

.panel-module-dest.panel-module-double {
    width: 129px;
    height: 8px;
}

.panel-module-dest.panel-module-single {
    width: 63px;
    height: 8px;
}

.panel-module-dest.panel-module-copper {
    width: 59px;
    height: 10px;
}

.panel-hardware-viz.dest-mode .panel-module {
    display: none;
}

.panel-hardware-viz.dest-mode .panel-module-dest {
    display: flex;
}

/* --- Link detail items in panel viz --- */

.links-section {
    margin-bottom: 8px;
}

.links-section:last-child {
    margin-bottom: 0;
}

.links-section-header {
    font-size: var(--dc-text-2xs);
    font-weight: 700;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    color: var(--dc-n-500);
    padding: 0 0 8px 0;
    margin-bottom: 10px;
    border-bottom: 1px solid var(--dc-n-50);
}

.link-detail-item {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 12px;
    margin-bottom: 8px;
    background: var(--dc-n-0);
    border-radius: 8px;
    border: 1px solid var(--dc-n-100);
    transition: border-color 0.15s, background 0.15s;
}

.link-detail-item:hover {
    background: #f7fdfc;
    border-color: var(--dc-c-accent);
}

.link-detail-item:last-child {
    margin-bottom: 0;
}

.link-number {
    background: var(--dc-c-accent);
    color: white;
    font-weight: 700;
    font-size: var(--dc-text-xs);
    border-radius: 50%;
    min-width: 26px;
    height: 26px;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}

.link-details {
    flex: 1;
    min-width: 0;
    display: flex;
    align-items: center;
    gap: 10px;
    overflow: hidden;
}

.link-direction {
    font-size: 14px;
    color: var(--dc-n-400);
    flex-shrink: 0;
    width: 16px;
    text-align: center;
}

.link-destination {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-shrink: 1;
    min-width: 0;
    overflow: hidden;
}

.link-dest-name {
    font-size: var(--dc-text-md);
    font-weight: 600;
    color: var(--dc-c-dark);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.link-dest-info {
    display: flex;
    flex-direction: column;
    gap: 1px;
    min-width: 0;
    overflow: hidden;
}

.link-dest-row {
    display: flex;
    align-items: center;
    gap: 5px;
    font-size: var(--dc-text-md);
    font-weight: 600;
    color: var(--dc-c-dark);
    white-space: nowrap;
}

.link-dest-zone {
    font-size: var(--dc-text-xs);
    color: var(--dc-n-500);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.link-specs {
    display: flex;
    gap: 5px;
    flex-shrink: 0;
    flex-wrap: nowrap;
}

/* --- Spec badges --- */

.link-row-badge {
    text-align: center;
    justify-self: stretch;
}

/* Link id badge — same look as .link-number (green circle). Placed after the
   :first-child rule so it wins at equal specificity when it leads the line. */
.spec-link-id {
    background: var(--dc-c-accent);
    color: white;
    font-weight: 700;
    font-size: var(--dc-text-xs);
    border: none;
    border-radius: 50%;
    padding: 0;
    min-width: 26px;
    height: 26px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}

/* Same glyph and pill as the waypoint marker in the links overview report. */
.link-color-filter-swatch {
    display: inline-block;
    width: 12px;
    height: 12px;
    border-radius: 3px;
    border: 1px solid rgba(0, 0, 0, 0.25);
    vertical-align: middle;
    margin-right: 2px;
}

/* --- Link action buttons --- */

/* Zone delete button now uses the .icon-btn--delete primitive */

/* --- Slot outlines for drag-and-drop reordering --- */

.slot-outline {
    position: absolute;
    z-index: 3;
    box-sizing: border-box;
    border: 1px dashed transparent;
    border-radius: 1px;
    pointer-events: none;
    display: none;
}

.slot-outline.active {
    display: block;
    border-color: rgba(41, 128, 185, 0.55);
    background: rgba(41, 128, 185, 0.05);
}

.slot-outline.snap-target {
    border: 2px solid var(--dc-c-info);
    background: rgba(41, 128, 185, 0.18);
}

/* ── Cabinet view — panel block drag ───────────────────────────────────────── */

.cabinet-slot-target {
    position: absolute;
    box-sizing: border-box;
    border: 1px dashed rgba(41, 128, 185, 0.55);
    background: rgba(41, 128, 185, 0.05);
    border-radius: 2px;
    pointer-events: none;
    display: none;
    z-index: 4;
}
.cabinet-slot-target.active { display: block; }
.cabinet-slot-target.snap-target {
    border: 2px solid var(--dc-c-info);
    background: rgba(41, 128, 185, 0.18);
}

.rack-panel-slot-wrapper { cursor: grab; }
.rack-panel-slot-wrapper:active { cursor: grabbing; }

/* ── ODF Panel Visualization ─────────────────────────────────────────────── */

/* ODF panel — unified table with rowspan chassis column */
.odf-panel-table {
    width: 100%;
    border-collapse: collapse;
    border: 1px solid var(--dc-n-200);
    border-radius: 4px;
    overflow: hidden;
    font-size: 8px;
}

.odf-slot-row { height: 15px; }
.odf-slot-row td { border-bottom: 1px solid var(--dc-n-50); padding: 0; vertical-align: middle; }
.odf-slot-row:last-child td { border-bottom: none; }
.odf-slot-row:nth-child(even) .odf-slot-num-cell,
.odf-slot-row:nth-child(even) .odf-slot-content-cell { background: #fafbfc; }
.odf-chassis-first-row td { border-top: 3px solid var(--dc-n-400); }
.odf-chassis-first-row:first-child td { border-top: none; }

/* Chassis label cell (rowspan) */
.odf-chassis-cell {
    background: #f0f2f5;
    border-right: 1px solid var(--dc-n-200);
    width: 36px;
    min-width: 36px;
    max-width: 36px;
    padding: 0;
    vertical-align: middle;
}
.odf-chassis-cell-inner {
    writing-mode: vertical-lr;
    transform: rotate(180deg);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 6px;
    padding: 6px 0;
    min-height: 20px;
}
.odf-chassis-cell-name {
    font-size: 8px;
    font-weight: 700;
    color: var(--dc-n-800);
    white-space: nowrap;
}
.odf-chassis-cell-info {
    font-size: 7px;
    color: var(--dc-n-600);
    white-space: nowrap;
}

/* Slot number cell */
.odf-slot-num-cell {
    width: 20px;
    min-width: 20px;
    font-size: 8px;
    color: var(--dc-n-400);
    text-align: right;
    padding-right: 3px;
    font-family: monospace;
}

/* Cassette content (image + label) */
.odf-cassette-content {
    display: flex;
    align-items: center;
    gap: 3px;
    padding: 0 3px;
    overflow: hidden;
}

.odf-cassette-img {
    height: 11px;
    width: auto;
    max-width: 100px;
    flex-shrink: 0;
    display: block;
    object-fit: contain;
}

.odf-cassette-label {
    flex: 1;
    font-size: 8px;
    white-space: nowrap;
    overflow: hidden;
    display: flex;
    align-items: center;
    gap: 2px;
    min-width: 0;
}

/* Two characters, not five: a cassette slot is a bare number and the label is set at
   8px, where a five-character floor would push the destination off the end. */
.odf-cassette-label .ml-slot {
    color: var(--dc-n-500);
    min-width: 2ch;
    font-family: monospace;
}

/* The columns here are separated by gap on the row, so the base padding comes off -
   with both, the destination would sit five pixels from a two-character slot. */
.odf-cassette-label .ml-dest {
    color: var(--dc-n-900);
    padding-left: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Right-aligned panel — chassis col on right, content reversed */
.odf-panel-right .odf-chassis-cell {
    border-right: none;
    border-left: 1px solid var(--dc-n-200);
}
.odf-panel-right .odf-chassis-cell-inner {
    transform: rotate(0deg); /* mirror: reads top to bottom */
}
.odf-panel-right .odf-slot-num-cell {
    text-align: left;
    padding-right: 0;
    padding-left: 3px;
}
.odf-panel-right .odf-cassette-content { flex-direction: row-reverse; }
.odf-panel-right .odf-cassette-label   { flex-direction: row-reverse; }

/* Enlarged modal variant */
.odf-enlarged {
    width: 100%;
    max-width: 640px;
}
.odf-enlarged .odf-slot-row { height: 28px; }
.odf-enlarged .odf-slot-row td { border-bottom: 1px solid var(--dc-n-100); }
.odf-enlarged .odf-chassis-cell {
    width: 52px;
    min-width: 52px;
    max-width: 52px;
}
.odf-enlarged .odf-chassis-cell-inner { gap: 8px; padding: 8px 0; }
.odf-enlarged .odf-chassis-cell-name { font-size: 11px; }
.odf-enlarged .odf-chassis-cell-info { font-size: 10px; }
.odf-enlarged .odf-slot-num-cell { width: 32px; min-width: 32px; font-size: 11px; }
.odf-enlarged .odf-cassette-content { gap: 6px; padding: 0 8px; }
.odf-enlarged .odf-cassette-img { height: 18px; max-width: 220px; }
.odf-enlarged .odf-cassette-label { font-size: 11px; gap: 4px; }
.odf-enlarged .odf-cassette-label .ml-slot { font-size: 11px; min-width: 3ch; color: var(--dc-n-400); }
.odf-enlarged .odf-cassette-label .ml-dest { font-size: 11px; color: var(--dc-n-950); }

/* ── ODF front elevation ────────────────────────────────────────────────────
   A render of the actual cabinet with the cassettes drawn over it. Everything
   inside is positioned in per cent of the picture, because the same markup is
   shown at whatever width its container happens to be — the panel view, the
   enlarged modal, a cabinet standing in a row — and per cent follows the image
   without anyone recomputing anything. The millimetre → per cent conversion is
   in _odfFrontChassisBoxes() (js/panels.js). */

/* The wrapper is laid out at the render's own pixel size and then scaled by a
   transform. That is what makes pan and zoom possible, and it keeps every per cent
   inside a per cent of the picture at any magnification — fitting the image with
   max-height instead does neither, and does not even fit: a percentage height
   against a shrink-wrapped parent resolves to nothing. _odfFrontAttachZoom()
   (js/panels.js) sets the width and drives the transform. */
.odf-front           { position: absolute; top: 0; left: 0; line-height: 0;
                       transform-origin: 0 0; }
.odf-front-img       { display: block; width: 100%; height: auto; }

/* The box the picture is panned inside. */
.odf-front-viewport  { position: relative; overflow: hidden; cursor: grab; }
.odf-front-viewport--dragging { cursor: grabbing; }
.odf-front-chassis   { position: absolute; }
.odf-front-slot      { position: absolute; left: 0; right: 0; }

/* The block that stands for one cassette. It covers the rectangle the cassette
   really occupies — the tray band of the chassis across, the slot down, less the
   gap set by ODF_FRONT_CASSETTE_GAP_PX — and both the sprite and the fill live
   inside it, so the mark does not change shape as the zoom changes what is drawn.
   The slot itself is left unpainted: filling the whole width of it turned a loaded
   chassis into a painted panel, with no cassettes to be seen in it. */
.odf-front-cassette { position: absolute; cursor: grab; }
.odf-front-cassette-img { display: block; width: 100%; height: 100%; }

/* A taken slot is shown two ways, and they take turns with the zoom: the fill,
   which reads when the whole cabinet is in view, and the cassette sprite over it —
   the only thing here that shows the ports — once there is room for it.

   The fill is the adapter colour of the sprite it stands in for, read off the
   sprites themselves: #00b0f0 for single mode, #d15b8f for OM4. Any other colour
   makes the mark change hue as the picture is zoomed, and the app's own teal sits
   at the lightness of the grey cabinet, so from far out a full chassis and an empty
   one read the same. Nearly opaque for the same reason — a tint of the cabinet is
   the cabinet. A triple cassette is called out by an edge because it fills the slot
   differently. */
.odf-front-slot--taken .odf-front-cassette          { background: rgba(0, 176, 240, 0.92); }
.odf-front-slot--taken.odf-front-slot--om4 .odf-front-cassette
                                                    { background: rgba(209, 91, 143, 0.92); }
.odf-front-slot--triple .odf-front-cassette { box-shadow: inset 0 0 0 1px rgba(44, 62, 122, 0.75); }

/* Dragging a cassette to another slot of the same panel. */
.odf-front-slot--dragging .odf-front-cassette { opacity: 0.45; cursor: grabbing; }
.odf-front-slot--drop {
    outline: 2px solid rgba(44, 62, 122, 0.9);
    outline-offset: -1px;
    background: rgba(44, 62, 122, 0.16);
}
.odf-front--no-cassettes .odf-front-cassette-img { display: none; }

/* …but a module needs no stand-in for its front. The fill above is the colour a cassette
   is drawn as, and it is what reads when the picture is too small to draw the sprite; a
   module sprite is a photograph of the ports with clear air round them, so the fill under
   it shows only as a rim and a bar down the middle in a colour nothing there is. Kept
   for the size at which the sprite is not drawn at all, which is the size it was for. */
.odf-front-chassis--ianos .odf-front-slot--taken .odf-front-cassette {
    background: none;
}
.odf-front--no-cassettes .odf-front-chassis--ianos .odf-front-slot--taken .odf-front-cassette {
    background: rgba(0, 176, 240, 0.92);
}
.odf-front--no-cassettes .odf-front-chassis--ianos
                         .odf-front-slot--taken.odf-front-slot--om4 .odf-front-cassette {
    background: rgba(209, 91, 143, 0.92);
}

/* Free capacity: a position the cabinet has and nothing has claimed. Marked over
   the same rectangle, faintly, so the eye reads the taken ones and still sees where
   the free ones are. */
.odf-front-free {
    position: absolute;
    background: rgba(255, 255, 255, 0.34);
}

.odf-front-label {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    /* font-size comes from the slot's own height, set in _buildOdfFrontElement():
       the shared cabinet label stacks the coordinate over the zone, and a fixed size
       makes that stack taller than a cassette. Ems below, so the box follows it. */
    line-height: 1.15;
    white-space: nowrap;
    /* As wide as the name needs, not as wide as what is left of the slot. The label
       starts past the cassette, so shrink-to-fit would give it only the remainder of
       the chassis to grow into and clip the zone off the end. max-content is the one
       width the containing block does not cap. */
    width: max-content;
    color: #1a1a2e;
    background: rgba(255, 255, 255, 0.82);
    border-radius: 0.3em;
    padding: 0.1em 0.4em;
    pointer-events: none;
}
/* The slot number, on the hinge side of the cassette so it does not sit over the
   ports. Placed by the same constants as the sprite, in _buildOdfFrontElement(). */
.odf-front-slotnum {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    font-family: monospace;
    line-height: 1;
    color: var(--dc-n-800);
    background: rgba(255, 255, 255, 0.82);
    border-radius: 0.3em;
    padding: 0.1em 0.3em;
    pointer-events: none;
}

/* The caption over an IANOS module on a front elevation is the 19" view's own
   .panel-module-dest — the same bar, the same fibre-type colour, the same "slot, then
   where it lands" order — because it is the same module, and one module wearing two
   different labels is one label too many. What has to be said again here is only what
   differs between a field drawn once and magnified and one drawn at its final size.

   It is shown outright. In a cabinet the bar waits for .dest-mode on an ancestor, which
   is how that view's own toggle works; this view has a toggle of its own and simply does
   not draw the bar when it is off.

   Its gaps are in ems. That view is laid out once in a 300 px field and then magnified,
   so a padding of a pixel is magnified with everything else; this one is laid out at
   whatever size the render is currently shown at, where a fixed pixel would swallow the
   type when the picture is small and disappear when it is large. The type size itself
   comes from the same proportion, set in _odfFrontDrawIanosChassis() — see
   MODULE_DEST_TYPE_SCALE, which is this rule's 3.6px over the 8px module it sits on.
   The tracking is deliberately NOT restated in ems: it would come to -0.11em, and this
   caption carries a zone and a far-end address that the cabinet one does not, so at that
   tightness the space before "PP-01" closes up and the words run together.

   Not clickable: dragging a module looks for the .odf-front-cassette under the pointer,
   and a bar covering the whole rectangle would take every mousedown before it got there. */
.odf-front .panel-module-dest {
    display: flex;
    white-space: nowrap;
    padding: 0 0.35em;
    border-radius: 0.28em;
    pointer-events: none;
}
/* A 6-port module is half the width of a 12-port one and carries a caption of the same
   length — longer, when it faces two modules and lists both addresses. min-width:0 lets
   the flex item give way, and the ellipsis it already carries then fires: a name that
   trails off says it was cut, a letter sliced by the edge of the bar says nothing. */
.panel-module-dest .ml-dest,
.cabinet-label-overlay .ml-dest { min-width: 0; }
.odf-front .panel-module-dest .ml-dest { padding-left: 0.5em; }

/* The address inside the cabinet at the far end — "PP-02 A1/A2", or the cassette number
   when that end is another ODF. Written after the cabinet and quieter than it: the
   cabinet answers where the fibres go, this answers where in it, and a link filling
   twelve positions names the same cabinet twelve times over. Part of every module
   caption the app writes — see UITpl.moduleCaption(). */
.ml-address { opacity: 0.72; white-space: nowrap; }
.ml-address::before { content: '·'; margin: 0 0.3em; opacity: 0.6; }

.odf-front--no-labels .odf-front-slotnum { display: none; }

/* Cleared of the sprite by the same constants that place it — the side and the
   offset are set in _buildOdfFrontElement(). */

/* Set by _odfFrontFitLabels() when a slot is drawn too short to write on. Fifteen
   cassettes fill a 7U chassis, so at the sizes this view is actually shown at a slot is
   a few pixels tall and the labels would overlap into a smear. */
.odf-front--no-labels .odf-front-label,
.odf-front--no-labels .panel-module-dest { display: none; }

/* The floor under the meta size comes off here. Everywhere else it stops a coordinate
   shrinking to nothing beside a name of a fixed size; here the label's own size is set
   from the height of the slot it has to fit in, and the picture is then scaled — so a
   floor in screen pixels does not shrink with the rest and the coordinate-over-zone
   column grows taller than the cassette it belongs to. The proportion is enough,
   because zooming in enlarges everything together. */
.odf-front-label .cl { --cl-meta-min: 0px; }

/* Nothing here trims itself. The coordinate and the zone clip to their line box and
   ellipsis a long zone away, which is right in a table cell fighting for width — but
   this label is sized to its content and has all the width it wants, so the machinery
   has no work to do and only does damage: the type ends up a few pixels tall before
   the picture is scaled up, and a line box a hair shorter than the glyphs slices their
   bottoms off in a way the magnification then makes obvious. */
.odf-front-label .cl-meta,
.panel-module-dest .cl-meta,
.cabinet-label-overlay .cl-meta { max-width: none; }
.odf-front-label .cl-zone,
.odf-front-label .cl-coord,
.panel-module-dest .cl-zone,
.panel-module-dest .cl-coord,
.cabinet-label-overlay .cl-zone,
.cabinet-label-overlay .cl-coord {
    overflow: visible;
    text-overflow: clip;
    line-height: 1.35;
}

/* Marks the chassis this panel is about when the picture shows more than one
   rail — a composite ODF carries the other rail's chassis in the same image. */
.odf-front-chassis--active { outline: 1px solid rgba(44, 62, 122, 0.55); }

/* Everything on the picture that is not that rail. A composite ODF is several
   rails in one render and the panel modal works on one of them, so the rest are
   washed over — the cabinet stays recognisable, but the column being edited is
   the only part of it that reads. One rectangle each side of the rail rather
   than a mark per foreign chassis: the frame, the ducts and the gaps between
   rails belong here no more than the other rails do, and marking only the
   chassis leaves those bright and turns the cabinet into stripes.

   A flat fill, not a backdrop blur. The render is panned and zoomed by a
   transform on .odf-front, and a blur would be recomputed over the whole picture
   on every frame of that — and the PDF report, which draws the same view, has no
   way to reproduce one.

   The colour is also written down in ODF_FRONT_PAINT (js/panels.js), where the
   PDF reads it from — jsPDF has no stylesheet. Same for the taken and free fills
   above. Change both or neither. */
.odf-front-dim {
    position: absolute;
    top: 0;
    height: 100%;
    background: rgba(150, 150, 150, 0.55);
    pointer-events: none;
}

/* ── Link colour picker ─────────────────────────────────────────────────── */

.link-color-stripe {
    width: 8px;
    flex-shrink: 0;
    align-self: stretch;
    border-radius: 4px 0 0 4px;
    margin: -8px 0 -8px -10px;
}

.link-color-btn {
    flex-shrink: 0;
    cursor: pointer;
    display: flex;
    align-items: center;
    background: var(--dc-n-0);
    border: 2px solid var(--dc-n-150);
    border-radius: 6px;
    padding: 3px;
    transition: border-color 0.15s, background 0.15s;
}

.link-color-btn:hover {
    background: #f0f8f6;
    border-color: var(--dc-c-accent);
}

#link-color-popover {
    position: fixed;
    z-index: var(--dc-z-popover);
    background: var(--dc-n-0);
    border: 1px solid var(--dc-n-200);
    border-radius: 10px;
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.16);
    padding: 10px;
    display: none;
    min-width: 148px;
}

.link-color-swatches {
    display: grid;
    grid-template-columns: repeat(6, 1fr);
    gap: 5px;
    margin-bottom: 8px;
}

.link-color-swatch {
    width: 20px;
    height: 20px;
    border-radius: 50%;
    cursor: pointer;
    border: 2px solid rgba(0, 0, 0, 0.12);
    transition: transform 0.12s, box-shadow 0.12s;
}

.link-color-swatch:hover {
    transform: scale(1.22);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.28);
}

.link-color-swatch.active {
    border-color: var(--dc-n-950);
    box-shadow: 0 0 0 2px #fff, 0 0 0 4px #333;
}

.link-color-reset {
    font-size: var(--dc-text-xs);
    color: var(--dc-n-750);
    text-align: center;
    cursor: pointer;
    padding: 3px 6px;
    border-radius: 5px;
    user-select: none;
}

.link-color-reset:hover {
    background: var(--dc-n-50);
    color: var(--dc-n-950);
}

/* ── Equipment Catalog ──────────────────────────────────────────────────────── */

/* Sidebar inside cabinet modal */
.equipment-catalog-sidebar {
    width: 260px;
    flex-shrink: 0;
    border-right: 1px solid var(--dc-n-150);
    display: flex;
    flex-direction: column;
    overflow-y: auto;
    background: var(--dc-n-50);
}

.eq-sidebar-step {
    display: flex;
    flex-direction: column;
    min-height: 0;
    flex-shrink: 0;
}

.eq-sidebar-step-main {
    flex: none;
}

.eq-sidebar-step + .eq-sidebar-step {
    border-top: 1px solid var(--dc-n-150);
}

.eq-sidebar-step .step-header {
    padding: 10px 12px;
    margin-bottom: 0;
    flex-shrink: 0;
    background: var(--dc-n-50);
    border-bottom: 1px solid var(--dc-n-150);
}

.eq-sidebar-step.collapsed .step-header {
    border-bottom: none;
}

.eq-sidebar-step.collapsed .step-header::after {
    transform: rotate(-90deg);
}

.eq-sidebar-step .step-content {
    display: flex;
    flex-direction: column;
}

.eq-sidebar-step.collapsed .step-content {
    display: none;
}

.eq-catalog-toolbar {
    padding: 8px;
    border-bottom: 1px solid var(--dc-n-100);
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.eq-catalog-search,
.eq-catalog-filter {
    width: 100%;
    padding: 5px 8px;
    border: 1px solid var(--dc-n-200);
    border-radius: 4px;
    font-size: var(--dc-text-sm);
    box-sizing: border-box;
}

.eq-catalog-add-btn {
    font-size: var(--dc-text-sm);
    padding: 4px 8px;
    align-self: flex-start;
}

.eq-catalog-list {
    overflow-y: auto;
    max-height: calc(94vh - 300px);
}

.eq-catalog-empty,
.eq-catalog-loading {
    padding: 16px;
    text-align: center;
    font-size: var(--dc-text-sm);
    color: var(--dc-n-600);
}

.eq-catalog-card {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 8px;
    cursor: default;
    user-select: none;
    border-bottom: 1px solid var(--dc-n-50);
    transition: background 0.12s;
}

.eq-catalog-card:hover {
    background: #edf2ff;
}

.eq-catalog-card img {
    width: 40px;
    height: 20px;
    object-fit: contain;
    flex-shrink: 0;
}

.eq-card-thumb-ph {
    width: 40px;
    height: 20px;
    border-radius: 2px;
    flex-shrink: 0;
}

.eq-card-info {
    flex: 1;
    min-width: 0;
}

.eq-name {
    font-size: var(--dc-text-sm);
    font-weight: 500;
    color: var(--dc-n-1000);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.eq-meta {
    font-size: var(--dc-text-xs);
    color: var(--dc-n-600);
    white-space: nowrap;
}

/* .eq-card-edit uses the .icon-btn--sm.icon-btn--settings primitive */

/* Placed equipment units inside rack-units-area */
.equipment-unit {
    position: absolute;
    z-index: 3;
    display: flex;
    align-items: center;
    padding: 0;              /* no horizontal padding — image fills the slot, aligned with the rails */
    gap: 6px;
    font-size: var(--dc-text-xs);
    box-sizing: border-box;
    overflow: hidden;
    border-radius: 1px;
    cursor: grab;
    color: var(--dc-n-0);
}

/* 1px edge drawn as an overlay (above the image) instead of a real border, so it
   doesn't steal content height — a real border shrinks a 1U image's height (and thus
   its width, aspect-preserved) by ~7%, which looked like the image was narrowed. */
.equipment-unit::after {
    content: '';
    position: absolute;
    inset: 0;
    box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.35);
    border-radius: 1px;
    pointer-events: none;
    z-index: 1;
}

.equipment-unit img {
    position: absolute;      /* fill the whole slot, independent of the flex label/remove button */
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: contain;     /* keep the saved aspect — no squish, no clip */
    pointer-events: none;
    z-index: 0;
}

/* Highlight the equipment unit while its right-click context menu is open */
.equipment-unit--ctx {
    outline: 2px solid var(--dc-c-warning);
    outline-offset: -1px;
    z-index: 5;
}

.equipment-placeholder {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    padding: 0 8px;
    font-size: var(--dc-text-xs);
    color: rgba(255, 255, 255, 0.85);
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

.eq-label {
    flex: 1;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.eq-remove {
    position: absolute;      /* overlay top-right so a full-width image can't push it out */
    top: 1px;
    right: 3px;
    z-index: 6;
    cursor: pointer;
    opacity: 0.7;
    padding: 0 2px;
    font-size: var(--dc-text-sm);
    line-height: 1;
    color: var(--dc-n-0);
    text-shadow: 0 0 2px #000, 0 0 2px #000;
}

.eq-remove:hover { opacity: 1; }

.eq-name-overlay {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(0, 0, 0, 0.52);
    color: var(--dc-n-0);
    font-size: 10px;
    font-weight: 600;
    text-align: center;
    padding: 0 6px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    pointer-events: none;
    z-index: 5;
    border-radius: 1px;
}

.equipment-unit.eq-switch,
.equipment-unit.eq-server,
.equipment-unit.eq-ups,
.equipment-unit.eq-router,
.equipment-unit.eq-firewall,
.equipment-unit.eq-storage,
.equipment-unit.eq-kvm,
.equipment-unit.eq-console,
.equipment-unit.eq-other    { background: #035293; }

/* Gray placeholder — opposite-side shallow equipment (doesn't reach this side) */
.equipment-unit-gray {
    position: absolute;
    left: 0;
    right: 0;
    z-index: 2;
    background: repeating-linear-gradient(
        45deg,
        rgba(0,0,0,0.06) 0px,
        rgba(0,0,0,0.06) 4px,
        rgba(0,0,0,0.02) 4px,
        rgba(0,0,0,0.02) 8px
    );
    border: 1px dashed rgba(0,0,0,0.25);
    pointer-events: none;
    box-sizing: border-box;
}

/* Equipment Catalog management modal grid */
.eq-modal-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 12px;
    padding: 4px 2px;
}

.eq-grid-loading,
.eq-grid-empty {
    grid-column: 1 / -1;
    padding: 32px;
    text-align: center;
    color: var(--dc-n-600);
    font-size: var(--dc-text-md);
}

.eq-modal-card {
    border: 1px solid var(--dc-n-150);
    border-radius: 6px;
    overflow: hidden;
    background: var(--dc-n-0);
    display: flex;
    flex-direction: column;
    transition: box-shadow 0.15s;
}

.eq-modal-card:hover { box-shadow: 0 2px 10px rgba(0,0,0,0.1); }

.eq-modal-card-img {
    height: 80px;
    background: var(--dc-n-25);
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
}

.eq-modal-card-img img {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
}

.eq-modal-card-ph {
    width: 100%;
    height: 100%;
}

.eq-modal-card-info {
    padding: 8px 10px 6px;
    flex: 1;
}

.eq-modal-card-name {
    font-size: var(--dc-text-md);
    font-weight: 600;
    color: var(--dc-n-1000);
    margin-bottom: 3px;
}

.eq-modal-card-meta {
    font-size: var(--dc-text-xs);
    color: var(--dc-n-600);
}

.eq-modal-card-actions {
    padding: 6px 10px 8px;
    display: flex;
    gap: 6px;
    border-top: 1px solid var(--dc-n-50);
}

.eq-modal-card-actions .secondary-btn,
.eq-modal-card-actions .danger-btn {
    font-size: var(--dc-text-xs);
    padding: 3px 10px;
}

/* Edit form */
.eq-edit-form-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px 16px;
}

.eq-edit-form-grid .full-width {
    grid-column: 1 / -1;
}

.eq-image-previews {
    display: flex;
    gap: 16px;
}

.eq-image-preview-box {
    flex: 1;
    border: 1px solid var(--dc-n-150);
    border-radius: 4px;
    padding: 8px;
    min-height: 60px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--dc-n-25);
    font-size: var(--dc-text-sm);
    color: var(--dc-n-400);
    overflow: hidden;
}

.eq-image-preview-box img {
    max-width: 100%;
    max-height: 80px;
    object-fit: contain;
}

.eq-no-img {
    font-size: var(--dc-text-xs);
    color: var(--dc-n-300);
}

/* Ports table */
.eq-ports-table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--dc-text-sm);
}

.eq-ports-table th {
    background: var(--dc-n-25);
    padding: 5px 8px;
    text-align: left;
    font-weight: 500;
    border-bottom: 1px solid var(--dc-n-150);
}

.eq-ports-table td {
    padding: 4px 6px;
    border-bottom: 1px solid var(--dc-n-50);
}

.eq-ports-table input,
.eq-ports-table select {
    width: 100%;
    padding: 3px 5px;
    border: 1px solid var(--dc-n-200);
    border-radius: 3px;
    font-size: var(--dc-text-sm);
    box-sizing: border-box;
}

.eq-port-del {
    background: none;
    border: none;
    color: var(--dc-c-error-hover);
    cursor: pointer;
    font-size: 14px;
    padding: 0 4px;
    line-height: 1;
}

.eq-port-del:hover { color: var(--dc-c-error); }

/* Modal toolbar for catalog search */
.eq-modal-toolbar {
    display: flex;
    gap: 10px;
    align-items: center;
    flex-wrap: wrap;
}

.eq-modal-toolbar input,
.eq-modal-toolbar select {
    padding: 6px 10px;
    border: 1px solid var(--dc-n-200);
    border-radius: 5px;
    font-size: var(--dc-text-md);
}

.eq-modal-toolbar input { flex: 1; min-width: 160px; }

/* Edit form 2-column grid */
.eq-edit-grid {
    display: grid;
    grid-template-columns: 130px 1fr;
    gap: 8px 12px;
    align-items: start;
}

.eq-edit-grid label {
    font-size: var(--dc-text-sm);
    font-weight: 500;
    color: var(--dc-n-800);
    padding-top: 5px;
}

.eq-edit-grid input,
.eq-edit-grid select,
.eq-edit-grid textarea {
    width: 100%;
    padding: 5px 8px;
    border: 1px solid var(--dc-n-200);
    border-radius: 4px;
    font-size: var(--dc-text-md);
    box-sizing: border-box;
    font-family: inherit;
}

.eq-edit-grid textarea { resize: vertical; }

/* Image preview blocks */
.eq-img-block {
    flex: 1 1 0;
    min-width: 0;      /* allow shrinking below the image's intrinsic width — otherwise
                          a wide 1U image forces the panel wider than the modal and the
                          overflow:hidden clips the Save/Cancel footer off-screen */
    max-width: 420px;  /* keep cards from stretching huge on wide screens */
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.eq-img-label {
    font-size: var(--dc-text-sm);
    font-weight: 500;
    color: var(--dc-n-800);
}

.eq-img-preview {
    border: 1px solid var(--dc-n-150);
    border-radius: 4px;
    padding: 8px;
    min-height: 60px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--dc-n-25);
    overflow: hidden;
}

.eq-img-preview--ro { cursor: default; }

.eq-img-preview img {
    max-width: 100%;
    max-height: 80px;
    object-fit: contain;
}

.eq-img-hint {
    font-size: var(--dc-text-2xs);
    color: var(--dc-n-400);
    line-height: 1.4;
}

.eq-img-hint code {
    font-size: var(--dc-text-2xs);
    background: var(--dc-n-50);
    padding: 1px 4px;
    border-radius: 3px;
    word-break: break-all;
}

/* ── Equipment port editor ───────────────────────────────── */


/* Slide wrapper */
.eq-slide-outer {
    flex: 1;
    min-height: 0;
    overflow: hidden;  /* reliable clipping viewport */
}
.eq-slide-wrap {
    display: flex;
    height: 100%;
    transition: transform 0.28s cubic-bezier(0.4,0,0.2,1);
}
.eq-slide-panel {
    min-width: 100%;   /* each panel = 100% of eq-slide-outer */
    flex-shrink: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    min-height: 0;
}

/* Image / crop editor (panel 3) */
.eq-crop-editor-body {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-height: 0;
    overflow: hidden;
}
.eq-crop-stage {
    flex: 1;
    min-height: 0;
    position: relative;
    overflow: hidden;
    background: var(--dc-n-50);
    cursor: pointer;
    user-select: none;
}
.eq-crop-world {
    position: absolute;
    top: 0; left: 0;
    width: 100%; height: 100%;
    transform-origin: 0 0;   /* viewport zoom/pan applied here */
    pointer-events: none;    /* the stage handles all pointer events */
}
.eq-crop-img {
    position: absolute;
    top: 0; left: 0;
    -webkit-user-drag: none;
    user-select: none;
    pointer-events: none;   /* drags/clicks handled by the stage */
}
.eq-crop-frame {
    position: absolute;
    border: 2px solid #2196F3;
    box-shadow: 0 0 0 100000px rgba(0,0,0,0.45);  /* dim outside crop (large so it still covers when zoomed out) */
    pointer-events: none;
    box-sizing: border-box;
    z-index: 1;
}
.eq-crop-hint {
    position: absolute;
    top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    color: #546e7a;
    font-size: var(--dc-text-md);
    pointer-events: none;
    z-index: 2;
}
/* Body width without mounting ears (450 mm) — two dashed vertical lines */
.eq-crop-guide {
    position: absolute;
    border-left: 2px dashed #ffb300;
    border-right: 2px dashed #ffb300;
    pointer-events: none;
    box-sizing: border-box;
    z-index: 2;
}
.eq-crop-guide-label {
    position: absolute;
    top: 2px; left: 50%;
    transform: translateX(-50%);
    padding: 1px 5px;
    background: rgba(255,179,0,0.9);
    color: #3a2c00;
    font-size: 10px;
    font-weight: 600;
    border-radius: 0 0 3px 3px;
    white-space: nowrap;
}
.eq-crop-handle {
    position: absolute;
    width: 12px;
    height: 12px;
    margin: -6px 0 0 -6px;      /* centre on the corner */
    background: #2196F3;
    border: 2px solid var(--dc-n-0);
    border-radius: 50%;
    box-shadow: 0 1px 3px rgba(0,0,0,0.4);
    z-index: 3;                 /* above the frame's dim overlay */
    box-sizing: border-box;
}
.eq-crop-handle[data-corner="nw"] { cursor: nwse-resize; }
.eq-crop-handle[data-corner="se"] { cursor: nwse-resize; }
.eq-crop-handle[data-corner="ne"] { cursor: nesw-resize; }
.eq-crop-handle[data-corner="sw"] { cursor: nesw-resize; }
.eq-crop-toolbar {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 12px;
    border-top: 1px solid var(--dc-n-50);
    flex-shrink: 0;
}
.eq-crop-dims {
    margin-left: auto;
    font-size: var(--dc-text-xs);
    color: var(--dc-n-500);
}

/* Port editor header */
.eq-port-editor-header {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 9px 14px 9px 10px;
    border-bottom: 1px solid var(--dc-n-100);
    flex-shrink: 0;
}
.eq-back-btn {
    background: none;
    border: 1px solid var(--dc-n-200);
    border-radius: 5px;
    font-size: var(--dc-text-md);
    color: #374151;
    cursor: pointer;
    padding: 4px 10px;
    white-space: nowrap;
}
.eq-back-btn:hover { background: var(--dc-n-25); }

.eq-port-side-tabs {
    display: flex;
    border: 1px solid var(--dc-n-200);
    border-radius: 5px;
    overflow: hidden;
}
.eq-port-side-tab {
    padding: 4px 14px;
    font-size: var(--dc-text-sm);
    font-weight: 500;
    border: none;
    background: none;
    cursor: pointer;
    color: var(--dc-n-700);
}
.eq-port-side-tab + .eq-port-side-tab { border-left: 1px solid var(--dc-n-200); }
.eq-port-side-tab.active { background: #2c3e7a; color: var(--dc-n-0); }

.eq-port-editor-hint {
    font-size: var(--dc-text-xs);
    color: var(--dc-n-400);
    margin-left: auto;
}

/* Port editor body */
.eq-port-editor-body {
    display: flex;
    flex: 1;
    min-height: 0;
    overflow: hidden;
}

/* Connector palette */
.eq-port-palette {
    width: 210px;
    flex-shrink: 0;
    border-right: 1px solid var(--dc-n-100);
    overflow-y: auto;
    padding: 8px 6px;
    display: flex;
    flex-direction: column;
    gap: 0;
}

/* Form-factor group */
.eq-ff-group { margin-bottom: 10px; }
.eq-ff-group-label {
    font-size: 10px;
    font-weight: 700;
    color: var(--dc-n-700);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    padding: 6px 4px 4px;
    border-bottom: 1px solid var(--dc-n-100);
    margin-bottom: 4px;
}

/* Palette item row */
.eq-ff-item {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 5px 4px;
    border-radius: 5px;
    cursor: grab;
    user-select: none;
    border: 1px solid transparent;
}
.eq-ff-item:hover  { background: var(--dc-n-25); border-color: var(--dc-n-100); }
.eq-ff-item:active { cursor: grabbing; }

/* Proportional rectangle (palette) */
.eq-ff-rect {
    border-radius: 3px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 9px;
    font-weight: 700;
    color: rgba(255,255,255,0.95);
    letter-spacing: 0.02em;
    flex-shrink: 0;
    box-shadow: 0 1px 3px rgba(0,0,0,0.25);
}

/* Text next to rectangle */
.eq-ff-meta {
    display: flex;
    flex-direction: column;
    gap: 1px;
    min-width: 0;
}
.eq-ff-label {
    font-size: 11px;
    font-weight: 600;
    color: #1f2937;
    white-space: nowrap;
}
.eq-ff-speed {
    font-size: 9px;
    color: var(--dc-n-400);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Canvas area */
.eq-port-canvas-wrap {
    flex: 1;
    min-height: 0;
    overflow: hidden;
    background: var(--dc-n-50);
    box-sizing: border-box;
    position: relative;
}
.eq-port-canvas {
    position: absolute;
    top: 12px; right: 12px; bottom: 12px; left: 12px;
    cursor: grab;
}
.eq-port-canvas img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: contain;
    pointer-events: none;
    user-select: none;
}
.eq-port-canvas-ph {
    width: 100%;
    height: 100%;
    background: var(--dc-c-dark);
    border-radius: 4px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--dc-n-700);
    font-size: 13px;
    box-sizing: border-box;
}

/* Placed port rectangles on canvas */
.eq-port-placed {
    position: absolute;
    transform: translate(-50%, -50%);
    z-index: 10;
    border-radius: 3px;
    border: 1.5px solid rgba(255,255,255,0.85);
    box-shadow: 0 1px 4px rgba(0,0,0,0.45);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 8px;
    font-weight: 700;
    color: rgba(255,255,255,0.95);
    letter-spacing: 0.02em;
    transition: filter 0.1s, box-shadow 0.1s;
}
.eq-port-placed:hover {
    filter: brightness(1.15);
    box-shadow: 0 2px 8px rgba(0,0,0,0.5);
}

/* Drag ghost from palette */
.eq-port-ghost {
    position: fixed;
    pointer-events: none;
    z-index: var(--dc-z-cursor);
    border-radius: 3px;
    border: 1.5px solid rgba(255,255,255,0.85);
    box-shadow: 0 2px 10px rgba(0,0,0,0.4);
    transform: translate(-50%, -50%);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 8px;
    font-weight: 700;
    color: rgba(255,255,255,0.95);
    opacity: 0.85;
}

/* Port dot popup */
.eq-port-dot-menu {
    position: absolute;
    z-index: 50;
    background: var(--dc-n-0);
    border: 1px solid var(--dc-n-200);
    border-radius: 7px;
    padding: 8px;
    box-shadow: 0 4px 16px rgba(0,0,0,0.14);
    display: flex;
    flex-direction: column;
    gap: 5px;
    min-width: 168px;
}
.eq-port-dot-menu input {
    width: 100%;
    padding: 4px 7px;
    border: 1px solid var(--dc-n-200);
    border-radius: 4px;
    font-size: var(--dc-text-sm);
    box-sizing: border-box;
    outline: none;
}
.eq-port-dot-menu input:focus { border-color: #2c3e7a; }
.eq-port-dot-menu-type {
    font-size: var(--dc-text-xs);
    color: var(--dc-n-700);
    padding: 0 2px;
    font-weight: 500;
}
.eq-port-dot-del {
    padding: 4px 8px;
    border: none;
    border-radius: 4px;
    background: #fee2e2;
    color: #dc2626;
    font-size: var(--dc-text-sm);
    cursor: pointer;
    text-align: left;
    font-weight: 500;
}
.eq-port-dot-del:hover { background: #fecaca; }

/* ── Patch cord gallery & list (sidebar Step 2) ─────────────────────────── */

.pc-hint {
    padding: 6px 8px;
    font-size: var(--dc-text-xs);
    color: var(--dc-n-500);
    text-align: center;
    border-bottom: 1px solid var(--dc-n-50);
}

.pc-gallery {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 8px;
    padding: 8px;
}

.pc-gallery-card {
    cursor: pointer;
    user-select: none;
    transition: outline 0.1s, box-shadow 0.1s;
}

.pc-gallery-card.pc-card-active {
    outline: 2px solid var(--dc-c-warning);
    box-shadow: 0 0 0 4px rgba(230,126,34,0.12);
}

.pc-gallery-card .ps-card-img {
    aspect-ratio: 3/2;
    padding: 6px;
    pointer-events: none;
}

.pc-gallery-card .ps-card-body {
    padding: 6px 8px 8px;
    gap: 4px;
}

.pc-gallery-card .ps-card-name {
    font-size: var(--dc-text-xs);
}

.pc-gallery-card .ps-filter-label {
    font-size: var(--dc-text-2xs);
}

.pc-gallery-card .ps-pill {
    padding: 3px 7px;
    font-size: var(--dc-text-2xs);
}

/* ── Fiber type pill colors (override ps-pill.selected green default) ─── */

.ps-pill.pc-fiber-sm:hover    { border-color: #ece343; color: #6b6100; }
.ps-pill.pc-fiber-sm.selected { background: #fdfbe0; border-color: #ece343; color: #6b6100; }

.ps-pill.pc-fiber-om4:hover    { border-color: #df829d; color: #8c3050; }
.ps-pill.pc-fiber-om4.selected { background: #fce8ef; border-color: #df829d; color: #8c3050; }

/* Drop-zone highlight when a patch cord is dragged over the rack view */
.cabinet-view-main.pc-drop-active {
    outline: 2px dashed var(--dc-c-warning);
    outline-offset: -3px;
}

/* Added patch cord list */
.pc-list {
    border-top: 1px solid var(--dc-n-100);
    padding-bottom: 4px;
}

.pc-list-header {
    padding: 6px 8px 3px;
    font-size: var(--dc-text-xs);
    font-weight: 600;
    color: var(--dc-n-800);
}

.pc-list-item {
    display: flex;
    align-items: center;
    padding: 4px 8px;
    gap: 6px;
    border-bottom: 1px solid var(--dc-n-50);
}

.pc-list-item--invalid {
    background: #fdecec;
}

.pc-list-item-invalid {
    font-size: var(--dc-text-xs);
    color: var(--dc-c-error-hover);
    margin-top: 2px;
}

.pc-list-item-name {
    flex: 1;
    font-size: var(--dc-text-sm);
    color: var(--dc-n-950);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* .pc-list-item-del uses the .icon-btn--delete primitive */

/* ── Patch cord SVG overlay ─────────────────────────────── */

#pcCordOverlay {
    position: absolute;
    top: 0; left: 0;
    width: 100%; height: 100%;
    overflow: visible;
    z-index: 10;
}

#pcGrabber {
    pointer-events: all;
    cursor: grab;
    filter: drop-shadow(0 1px 3px rgba(0,0,0,0.4));
}

#pcGrabber:active { cursor: grabbing; }

/* ── Magnifier ───────────────────────────────────────────── */

#pcMagnifier {
    position: fixed;
    z-index: var(--dc-z-cursor);
    pointer-events: none;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    filter: drop-shadow(0 4px 12px rgba(0,0,0,0.45));
}

/* ── Endpoint labels in the cord list ───────────────────── */

.pc-list-item-endpoints {
    font-size: var(--dc-text-2xs);
    color: var(--dc-c-muted);
    margin-top: 2px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* ── Fiber color dot (cord list) ───────────────────────── */

.pc-fiber-dot {
    display: inline-block;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    flex-shrink: 0;
    vertical-align: middle;
}

/* ── Fiber type label in cord list ─────────────────────── */

.pc-list-fiber-label {
    font-size: var(--dc-text-2xs);
    font-weight: 600;
    margin-left: 2px;
    vertical-align: middle;
}

/* ── Cord length label in cord list ─────────────────────── */

.pc-list-length {
    font-size: var(--dc-text-2xs);
    font-weight: 600;
    color: var(--dc-n-600);
    margin-left: 5px;
    vertical-align: middle;
}

/* A cord past CORD_MAX_PATCH_M — warning colour, not the error red: the cord is
   valid, it is just long enough that a tray link is the better answer. */
.pc-list-length--over {
    color: var(--dc-c-warning);
}

.pc-list-item-toolong {
    font-size: var(--dc-text-xs);
    color: var(--dc-c-warning);
    margin-top: 2px;
}

/* ── Click-click add mode status bar ────────────────────── */

.pc-add-status {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 5px 8px;
    min-height: 28px;
}

.pc-add-hint {
    font-size: var(--dc-text-xs);
    color: var(--dc-n-400);
}

.pc-add-step {
    flex: 1;
    font-size: var(--dc-text-xs);
    font-weight: 600;
    color: var(--dc-c-warning);
}

.pc-add-step2 {
    color: var(--dc-c-success);
}

.pc-add-cancel-btn {
    background: none;
    border: 1px solid var(--dc-n-150);
    border-radius: 4px;
    padding: 1px 6px;
    font-size: var(--dc-text-xs);
    color: var(--dc-n-600);
    cursor: pointer;
    line-height: 1.5;
    flex-shrink: 0;
}

.pc-add-cancel-btn:hover { border-color: var(--dc-c-error); color: var(--dc-c-error); }

/* Port hover ring pulse */
@keyframes pc-port-pulse {
    0%, 100% { opacity: 1; }
    50%       { opacity: 0.35; }
}

.pc-nearest-ring {
    animation: pc-port-pulse 0.8s ease-in-out infinite;
}

/* Port 1 anchor pulse animation */
@keyframes pc-port1-pulse {
    0%, 100% { opacity: 1; }
    50%       { opacity: 0.45; }
}

.pc-port1-anchor {
    animation: pc-port1-pulse 1s ease-in-out infinite;
}

/* ── Cabinet row view ─────────────────────────────────────────────────────────
   The cabinet view holds the whole row (js/cabinet-row.js). The strip is what the
   zoom transform is applied to, so everything inside it — card type included — is
   sized for the scale the strip is first fitted at, not in screen pixels. */

.cabinet-row-strip {
    position: relative;      /* offsetParent for the slots: the opener reads their
                                offsets to centre the cabinet being edited */
    display: flex;
    align-items: flex-end;   /* one floor line. Cabinets differ in height, and the
                                overhead tray region above them differs with it */
    width: max-content;
}

.cabinet-row-slot {
    display: flex;
    flex-direction: column;
    align-items: center;
    flex-shrink: 0;
}

/* Cabinets in a row stand edge to edge, exactly as on the plan. The space under each
   one is the floor band its tray overlay draws there — the depth comes from JS as
   --cab-row-floor on the strip, and EVERY slot leaves it, including the ones with no
   overlay of their own: the slots are aligned along their bottoms, and a slot that did
   not leave the room would stand a band's depth below the rest of the row. */
.cabinet-row-slot .rack-cabinet-view { margin-bottom: var(--cab-row-floor, 0); }

/* A cabinet that is only being shown. Nothing in it may be grabbed: its panels and
   equipment carry their own handlers, and acting on them would edit a cabinet the
   user never opened. */
.cab-row-idle {
    pointer-events: none;
}

/* The fading is on the cabinet, not on the element — the element also holds the tray
   overlay, and the trays belong to the ZONE. Fading them with the cabinet under them
   made one level run across the row in patches, light over the cabinets being shown
   and full strength over the one being edited, and put the height marked at one end of
   the row in a different ink from the one at the other. */
.cab-row-idle > .rack-units-area,
.cab-row-idle > .rack-sprite {
    opacity: 0.55;
}

/* An ODF in a row — its own render. Across, it fills the box the row gave it; down,
   _cabRowOdf() (js/cabinet-row.js) sets its height and its distance off the floor from
   the millimetres the picture actually covers, so every shelf sits at its true height
   above the row's floor line. The marks on it are positioned in per cent of the
   picture, so the picture and its box have to be one rectangle — which is why the
   height is set on .odf-front and not on the image alone. The picture itself takes
   no pointer — a row is for looking at, and a click here belongs to the cabinet
   underneath. */
.cab-row-odf { position: relative; }

/* The cabinet body. Its height is the box; the element around it is left to grow,
   because the tray overlay puts the headroom for the levels above the cabinet in
   as a first child and a fixed height would let that spill over the row. */
.cab-row-odf-body { width: 100%; }

/* pointer-events sits on the picture rather than on the element: the tray overlay
   draws into the same element, and its cables answer the pointer over every other
   cabinet in the row. */
/* top:auto matters. The base .odf-front rule pins the picture to the top, and an
   absolutely positioned box given top, bottom and a height of its own is
   over-constrained — the browser drops `bottom` and the picture stays at the top of
   the box. Harmless while the box was exactly the picture; the moment the tray
   overlay adds its headroom above the cabinet, the picture floats up with it and
   every shelf leaves the floor. */
.cab-row-odf .odf-front     { position: absolute; top: auto; left: 0; width: 100%;
                              pointer-events: none; }
.cab-row-odf .odf-front-img { position: absolute; inset: 0; width: 100%; height: 100%; }

/* A position with no cabinet sprite of its own — an in-row unit */
.cab-row-blank {
    background: repeating-linear-gradient(
        45deg, var(--dc-n-150) 0, var(--dc-n-150) 6px, var(--dc-n-200) 6px, var(--dc-n-200) 12px);
    border: 1px solid var(--dc-n-400);
    box-sizing: border-box;
    border-radius: 2px;
    opacity: 0.55;
    pointer-events: none;
}

.cab-row-card {
    display: flex;
    align-items: center;
    gap: 8px;
    height: 56px;            /* fixed: an uneven card would tilt the floor line */
    max-width: 100%;
    padding: 0 10px;
    margin-top: 10px;
    overflow: hidden;
    line-height: 1.2;
    font-family: 'Golos Text', sans-serif;
}

/* The card holds the shared cabinet label (.cl, css/ui-templates.css). Its type is
   sized here rather than by a cl--sm/md/lg class: the strip carries a zoom transform,
   so these are the pixels of the unzoomed strip, not what ends up on screen. The
   floor under the meta size is lifted for the same reason — at this scale the pure
   0.4 proportion is what keeps the label honest when the strip is zoomed out. */
.cab-row-card .cl {
    font-size: 26px;
    --cl-meta-min: 0px;
}

.cab-row-arrow {
    display: block;
    width: 52px;
    height: 30px;
    margin: 4px 0 8px;
    padding: 0;
    border: none;
    background: none;
    cursor: pointer;
}

.cab-row-arrow.is-active { cursor: default; }

.cab-row-arrow svg { display: block; width: 100%; height: 100%; }

.cab-row-arrow:not(.is-active):hover svg { opacity: 0.75; }

/* Keeps the slot the same height where there is no arrow to draw */
.cab-row-arrow--none { cursor: default; }

/* ── Suspended panels above the row ───────────────────────────────────────────
   The panels hang in the tray region the cabinets draw above themselves, so they
   sit in a layer of their own over the strip rather than in a slot. Every position
   is set from measured geometry in placeCabinetRowPanels(); the CSS only says that
   they float and how the caption sits. */

.cab-row-panel-layer {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    /* The layer covers the whole strip, so it must not swallow clicks meant for the
       cabinets under it. Only the panel blocks re-enable the mouse, below: they drag
       between units like a cabinet's do. The frame itself is not selectable — a
       suspended panel is not something the row view hands the editing to. */
    pointer-events: none;
    z-index: 6;             /* above the tray bands and drop-outs (z-index 5) */
}

.cab-row-panel .rack-panel-slot-wrapper { pointer-events: auto; }

.cab-row-panel {
    position: absolute;
    visibility: hidden;     /* until placed, so it never flashes at 0,0 */
}

.cab-row-panel .rack-cabinet-view { margin: 0; }

/* Hangs under no tray: the plan paints this red, so it is marked here too */
.cab-row-panel--offtray .rack-cabinet-view {
    outline: 2px dashed #ef5350;
    outline-offset: 1px;
}

/* Sits under the frame without widening it — the caption is free to be longer
   than 500 mm of panel */
.cab-row-panel-cap {
    position: absolute;
    top: 100%;
    left: 50%;
    transform: translateX(-50%);
    margin-top: 4px;
    font-family: 'Golos Text', sans-serif;
    font-size: 20px;
    font-weight: 600;
    color: #2c3e50;
    white-space: nowrap;
}

/* Same reasoning as .cab-row-card .cl — unzoomed pixels, pure proportion. */
.cab-row-panel-cap .cl {
    font-size: 20px;
    --cl-meta-min: 0px;
}

.cab-row-panel-cap-u {
    font-weight: 400;
    color: var(--dc-n-600);
}

/* The hanging frame is 500 mm wide against a cabinet's 600, so a 19" panel leaves it
   about 5 px of rail either side where a cabinet has 36. The U numbers do not fit in
   that, and printed inside they land on the modules — so for this frame alone they
   hang outside it, which needs the units area to stop clipping. Nothing else in it
   overflows: the panel wrappers are inset by the same 5 px, and no equipment is ever
   drawn in a suspended panel. */

.rack-cabinet-view--overhead .rack-units-area { overflow: visible; }

.rack-cabinet-view--overhead .rack-unit-label {
    left: -15px;
    width: 13px;
    text-align: right;
}

/* ── Reserved positions in the row ────────────────────────────────────────────
   A Reserved position is a cabinet that has not been installed yet, so it is drawn
   more faintly than an ordinary cabinet that merely stands empty, and carries the
   same dashed outline and RESERVED caption the floor plan gives it. It can never be
   the cabinet being edited: _cabRowArrow() gives it a spacer instead of an arrow, so
   there is nothing to click, and every equipment drop targets the active cabinet. */

/* Only the cabinet itself fades and gets the dash — not .cabinet-tray-overhead, which
   shares the element. The tray band is a property of the ZONE and runs the length of
   the row at one height; fading it over one slot makes the tray read as interrupted,
   and outlining the whole element would draw a dashed box round the empty air above
   the cabinet. This opacity multiplies the 0.55 the idle cabinet already carries. */
/* 0.28 is what this used to come to — 0.5 of the 0.55 the idle cabinet carried when
   that fade was still on the whole element. Stated outright now that the two no longer
   multiply. */
.cab-row-reserved > .rack-sprite { opacity: 0.28; }

/* The dash is the thing that has to be read, so it must not be faded along with what
   it encloses — an outline is painted in the same opacity as its element. The mounting
   space therefore keeps full opacity and is washed out by an overlay instead, which
   leaves the frame crisp: outlines paint above all descendants, so the wash dims the
   rails and the U numbers underneath it and never touches the dash. */
.cab-row-reserved > .rack-units-area {
    position: relative;
    outline: 3px dashed var(--dc-n-800);
    outline-offset: -2px;
}

.cab-row-reserved > .rack-units-area::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 2;             /* over the U numbers, which sit at 1 */
    background: rgba(255, 255, 255, 0.55);
    pointer-events: none;
}

.cab-row-card--reserved { opacity: 0.55; }

/* ── Tray levels, dragged to a new height ─────────────────────────────────────
   The band is the one thing in the overlay SVG that takes the mouse: the SVG as a
   whole is pointer-events:none so it never swallows a click meant for a cabinet, and
   an idle cabinet is inert as well — but the tray height belongs to the ZONE, so its
   level can be grabbed over any cabinet, editable or not. */

.cabinet-tray-svg .cabinet-tray-band {
    pointer-events: all;
    cursor: ns-resize;
}

.cabinet-tray-svg .cabinet-tray-band:hover {
    filter: brightness(0.94);
}

/* While a level is being dragged, everything it carries — the sections, the drop-out
   fittings, the cables — is left where it was rather than rebuilt on every frame, so
   it is dimmed to say plainly that it is stale. Redrawn in place on release. */
.cabinet-tray-svg.tray-dragging > *:not([data-tray-band]) {
    opacity: 0.15;
}

/* ── The floor under the row ──────────────────────────────────────────────────
   One band for the whole row, not a share per cabinet: an in-row unit draws no tray
   overlay, and a floor built cabinet by cabinet came out with a hole under it. Placed
   by placeCabinetRowFloor() once the strip has been laid out, since the line the
   cabinets stand on is only known then; the room for it under each slot comes from
   --cab-row-floor above. */

.cab-row-floor {
    position: absolute;
    /* Run it far past both ends of the row: the floor is the room's, not the row's, and
       it should reach the edges of the canvas however far the view is panned or zoomed
       out. Being inside the strip, it pans and scales with everything else, so it stays
       welded to the line the cabinets stand on — which positioning it against the
       container instead would cost a recompute on every frame of a pan. It is out of
       flow, so this width does not widen the strip, and the container clips it. */
    left: -50000px;
    width: 100000px;
    pointer-events: none;
    z-index: 4;              /* below the tray overlay (5) and the panels above it (6) */
    background-color: var(--dc-n-50);
    /* A fine square hatch: two hairline grids, one each way. */
    background-image:
        linear-gradient(to right,  #cfcfcf 0.8px, transparent 0.8px),
        linear-gradient(to bottom, #cfcfcf 0.8px, transparent 0.8px);
    background-size: 7px 7px;
}
