Files
rollcall/cmd/rollcall/web/style.css
T
Ilker ManapandClaude Opus 5.5 3c7af23be2
CI / agent (push) Successful in 3s
CI / site (push) Successful in 2s
CI / go (push) Successful in 43s
3D rooms: cables as busbars, switches in the rack elevation
Single thin cables were lost at room distance. Per rack and switch the
cables now run as one bus up the back of the rack, thicker the more
cables it carries, with a short stub from each port into it and a comb
under the switch with a tail into each switch port; a bus to a switch in
another rack crosses over the tops as a tray, to one in another room it
rises to the ceiling. Each switch's bus has its own lane behind the
rack. Everything is drawn as a few instanced meshes, picking resolves
the instance (a bus names its cable count, a stub its port).

Switches now show in the side panel's rack elevation at their unit (the
3D room's placement, estimates marked), their body runs deeper into the
rack, and on the selected rack they carry a short name. The panel is
drawn again once the switches are placed, also with a rack selected.
A switch placed by hand mid-rack (place…) gets its bus there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 14:39:54 +01:00

247 lines
23 KiB
CSS

:root {
--bg: #f4f5f7; --panel: #ffffff; --fg: #1c2027; --muted: #687078; --line: #e1e4e8; --line2: #ccd1d6;
--accent: #2563eb; --accent-fg: #ffffff; --warn: #b45309; --danger: #b91c1c; --ok: #15803d;
--pill: #eef1f5; --hover: #f0f4ff; --sel: #e8efff; --side: #1b1f27; --side-fg: #c9ced6; --side-act: #2b3242;
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #0f1216; --panel: #171b21; --fg: #e6e8eb; --muted: #9aa3ad; --line: #2a3139; --line2: #3a424c;
--accent: #3b82f6; --pill: #1f2733; --hover: #1b2230; --sel: #1e2a44; --warn: #f59e0b; --danger: #ef4444; --ok: #22c55e;
--side: #0b0e12; --side-fg: #aeb6c0; --side-act: #1c2430;
}
}
:root[data-theme="dark"] {
--bg: #0f1216; --panel: #171b21; --fg: #e6e8eb; --muted: #9aa3ad; --line: #2a3139; --line2: #3a424c;
--accent: #3b82f6; --pill: #1f2733; --hover: #1b2230; --sel: #1e2a44; --warn: #f59e0b; --danger: #ef4444; --ok: #22c55e;
--side: #0b0e12; --side-fg: #aeb6c0; --side-act: #1c2430;
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body { margin: 0; background: var(--bg); color: var(--fg); font: 13px/1.45 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; }
a { color: var(--accent); }
.muted { color: var(--muted); } .mono { font-family: var(--mono); font-size: 12px; }
.ok { color: var(--ok); } .bad { color: var(--danger); } .warn { color: var(--warn); }
.num { text-align: right; font-variant-numeric: tabular-nums; }
/* layout */
.shell { display: grid; grid-template-columns: 200px 1fr; min-height: 100vh; }
.side { background: var(--side); color: var(--side-fg); padding: 14px 10px; display: flex; flex-direction: column; gap: 2px; position: sticky; top: 0; height: 100vh; }
.brand { font-weight: 700; font-size: 16px; color: #fff; padding: 4px 10px 14px; }
.brand .muted { display: block; font-weight: 400; font-size: 11px; color: var(--side-fg); }
.side a { color: var(--side-fg); text-decoration: none; padding: 7px 10px; border-radius: 6px; display: flex; justify-content: space-between; }
.side a:hover { background: var(--side-act); } .side a.active { background: var(--side-act); color: #fff; }
.side .count { font-size: 11px; opacity: .8; }
.side-foot { margin-top: auto; display: flex; flex-direction: column; gap: 6px; padding: 10px; font-size: 11px; }
main { padding: 18px 22px; max-width: 1500px; width: 100%; min-width: 0; }
.page-head { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; flex-wrap: wrap; }
.page-head h1 { font-size: 18px; margin: 0; } .page-head .sub { color: var(--muted); }
.page-head .right { margin-left: auto; display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
/* controls */
.btn { border: 1px solid var(--line2); background: var(--panel); color: var(--fg); padding: 5px 10px; border-radius: 6px; cursor: pointer; font: inherit; font-size: 12.5px; white-space: nowrap; }
.btn:hover { background: var(--hover); } .btn.primary { background: var(--accent); color: var(--accent-fg); border-color: var(--accent); }
.btn.ghost { border-color: transparent; background: transparent; color: inherit; } .btn.warn { color: var(--warn); } .btn.danger { color: var(--danger); }
.btn:disabled { opacity: .5; cursor: progress; } .btn.sm { padding: 2px 7px; font-size: 11.5px; }
input, select, textarea { padding: 6px 9px; border: 1px solid var(--line2); border-radius: 6px; background: var(--panel); color: var(--fg); font: inherit; }
textarea { font-family: var(--mono); font-size: 12px; width: 100%; }
input[type=search] { min-width: 260px; }
.chips { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
.chip { border: 1px solid var(--line2); border-radius: 999px; padding: 2px 10px; cursor: pointer; background: var(--panel); font-size: 12px; }
.chip:hover { background: var(--hover); } .chip.on { background: var(--accent); border-color: var(--accent); color: var(--accent-fg); }
.chip .n { opacity: .7; margin-left: 4px; }
.menu { position: relative; display: inline-block; }
.menu > .list { display: none; position: absolute; top: 100%; left: 0; z-index: 5; background: var(--panel); border: 1px solid var(--line2); border-radius: 8px; padding: 4px; min-width: 190px; box-shadow: 0 8px 24px rgba(0,0,0,.18); }
.menu.open > .list { display: block; }
.menu .list button { display: block; width: 100%; text-align: left; border: none; background: none; padding: 7px 10px; border-radius: 5px; color: var(--fg); font: inherit; cursor: pointer; }
.menu .list button:hover { background: var(--hover); } .menu .list button.danger { color: var(--danger); } .menu .list hr { border: 0; border-top: 1px solid var(--line); margin: 4px 0; }
/* tables */
.tw { overflow-x: auto; border-radius: 8px; }
.tw table { min-width: 1100px; }
table { width: 100%; border-collapse: collapse; background: var(--panel); border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
th, td { padding: 7px 10px; text-align: left; border-bottom: 1px solid var(--line); white-space: nowrap; vertical-align: middle; }
th { font-weight: 600; color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: .04em; position: sticky; top: 0; background: var(--panel); z-index: 1; }
th.sort { cursor: pointer; } th.sort:hover { color: var(--fg); }
tbody tr.row { cursor: pointer; } tbody tr.row:hover { background: var(--hover); } tbody tr.selected { background: var(--sel); }
td.check, th.check { width: 28px; padding-right: 0; }
.pill { display: inline-block; padding: 1px 8px; border-radius: 999px; background: var(--pill); font-size: 11.5px; line-height: 18px; }
.pill.ready, .pill.booted, .pill.deployed, .pill.done { color: var(--ok); }
.pill.recommission, .pill.new, .pill.stateless, .pill.deploying, .pill.releasing, .pill.running, .pill.pending { color: var(--warn); }
.pill.deploy-failed, .pill.release-failed, .pill.failed { color: var(--danger); }
.pill.tag { background: var(--hover); border: 1px solid var(--line); font-family: var(--mono); font-size: 11px; }
.pill.tag.auto { border-style: dashed; color: var(--muted); } .pill.tag a { color: var(--danger); text-decoration: none; margin-left: 2px; }
.dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--line2); margin-right: 5px; }
.dot.ok { background: var(--ok); } .dot.bad { background: var(--danger); } .dot.warn { background: var(--warn); }
.hw { color: var(--muted); font-size: 12px; } .hw b { color: var(--fg); font-weight: 500; }
.pager { display: flex; gap: 8px; align-items: center; justify-content: flex-end; margin-top: 8px; color: var(--muted); }
.bulk { display: flex; gap: 6px; align-items: center; padding: 8px 10px; background: var(--sel); border: 1px solid var(--line2); border-radius: 8px; margin-bottom: 10px; flex-wrap: wrap; }
.empty { padding: 40px; text-align: center; color: var(--muted); background: var(--panel); border: 1px dashed var(--line2); border-radius: 8px; }
/* cards / grid */
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(380px, 1fr)); gap: 12px; }
.card { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 12px 14px; overflow: auto; }
.card.wide { grid-column: 1 / -1; }
.card h3 { margin: 0 0 8px; font-size: 11.5px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
.card table { border: none; } .card th, .card td { padding: 4px 8px; font-size: 12.5px; } .card th { position: static; }
.kv { display: grid; grid-template-columns: max-content 1fr; gap: 3px 14px; }
.kv dt { color: var(--muted); } .kv dd { margin: 0; word-break: break-all; }
.kv.form { gap: 8px 12px; align-items: center; } .kv.form label { color: var(--muted); } .kv.form input, .kv.form select { width: 100%; }
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; margin-bottom: 14px; }
.stat { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 12px 14px; cursor: pointer; }
.stat:hover { background: var(--hover); } .stat .v { font-size: 24px; font-weight: 600; line-height: 1.1; } .stat .l { color: var(--muted); font-size: 12px; }
.bar { position: relative; height: 8px; background: var(--pill); border-radius: 999px; overflow: hidden; min-width: 120px; }
.bar > span { position: absolute; inset: 0 auto 0 0; background: var(--accent); transition: width .5s; }
/* racks */
.racks { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 12px; }
.rack { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; }
.rack h4 { margin: 0 0 6px; font-size: 13px; display: flex; justify-content: space-between; } .rack h4 .muted { font-weight: 400; }
.elev { display: grid; grid-template-columns: 26px 1fr; gap: 0; border: 1px solid var(--line2); border-radius: 4px; overflow: hidden; font-size: 11px; }
.elev .u { color: var(--muted); text-align: right; padding: 0 4px; border-bottom: 1px solid var(--line); border-right: 1px solid var(--line); line-height: 16px; font-variant-numeric: tabular-nums; }
.elev .slot { border-bottom: 1px solid var(--line); line-height: 16px; padding: 0 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: default; }
.elev .slot.m { cursor: pointer; background: var(--sel); border-left: 3px solid var(--accent); }
.elev .slot.m.deployed, .elev .slot.m.booted, .elev .slot.m.ready { border-left-color: var(--ok); }
.elev .slot.m.deploying, .elev .slot.m.releasing, .elev .slot.m.stateless { border-left-color: var(--warn); }
.elev .slot.m.deploy-failed, .elev .slot.m.release-failed { border-left-color: var(--danger); }
.elev .slot.m:hover { filter: brightness(1.08); }
.elev .u { grid-column: 1; } .elev .slot { grid-column: 2; display: flex; align-items: center; gap: 4px; }
.elev .slot.node { background-image: repeating-linear-gradient(90deg, transparent 0 24%, var(--line) 24% 25%); }
.tree { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
.tree .chip.on { background: var(--accent); color: var(--accent-fg); }
/* detail */
.d-head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 10px; }
.d-head h1 { margin: 0; font-size: 20px; } .d-head .id { color: var(--muted); font-family: var(--mono); font-size: 12px; }
.d-meta { color: var(--muted); margin-bottom: 10px; display: flex; gap: 14px; flex-wrap: wrap; }
.tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--line); margin: 12px 0; }
.tabs a { padding: 7px 12px; color: var(--muted); text-decoration: none; border-bottom: 2px solid transparent; margin-bottom: -1px; }
.tabs a.active { color: var(--fg); border-color: var(--accent); }
pre.json { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 12px; overflow: auto; max-height: 70vh; font-size: 11.5px; }
.act-out { color: var(--muted); font-family: var(--mono); font-size: 12px; white-space: pre-wrap; margin-top: 6px; }
/* dialogs */
dialog { border: 1px solid var(--line2); border-radius: 10px; background: var(--panel); color: var(--fg); padding: 20px; min-width: 420px; max-width: 640px; }
dialog::backdrop { background: rgba(0,0,0,.45); } dialog h2 { margin: 0 0 10px; font-size: 16px; }
dialog menu { padding: 0; margin: 14px 0 0; display: flex; justify-content: flex-end; gap: 6px; }
dialog input { width: 100%; }
#toast { position: fixed; right: 18px; bottom: 18px; background: var(--side); color: #fff; padding: 10px 14px; border-radius: 8px; max-width: 480px; font-size: 12.5px; white-space: pre-wrap; z-index: 20; box-shadow: 0 8px 24px rgba(0,0,0,.3); }
#toast.bad { background: var(--danger); }
@media (max-width: 860px) {
.shell { grid-template-columns: 1fr; }
.side { position: static; height: auto; flex-direction: row; flex-wrap: wrap; align-items: center; }
.brand { padding: 4px 10px; } .brand .muted { display: none; } .side-foot { margin-left: auto; flex-direction: row; }
main { padding: 14px 16px; } .grid { grid-template-columns: 1fr; } input[type=search] { min-width: 0; width: 100%; }
table { display: block; overflow-x: auto; } dialog { min-width: 0; width: calc(100vw - 32px); }
}
/* topology */
.topo-wrap { overflow-x: auto; }
svg.topo { display: block; font: 12px system-ui, -apple-system, Segoe UI, Roboto, sans-serif; }
svg.topo.compact { font-size: 11px; }
svg.topo text { fill: var(--fg); }
svg.topo .t1 { font-weight: 600; } svg.topo .t2, svg.topo .t3, svg.topo .n2 { fill: var(--muted); font-size: 11px; }
svg.topo.compact .n2, svg.topo.compact .t3 { font-size: 10px; }
svg.topo .seg line { stroke: var(--line2); stroke-width: 3; stroke-linecap: round; }
svg.topo .seg.dhcp line { stroke: var(--accent); stroke-width: 4; }
svg.topo .seg.p-storage line, svg.topo .seg.p-ib line { stroke-dasharray: 8 5; }
svg.topo .seg { cursor: pointer; } svg.topo .seg:hover line { stroke-width: 6; }
svg.topo .wire { stroke: var(--line2); stroke-width: 1.5; fill: none; }
svg.topo .wire.ctl { stroke: var(--accent); stroke-width: 1.5; }
svg.topo .wire.ctl.down { stroke: var(--danger); stroke-dasharray: 5 4; }
svg.topo .wire.ext { stroke: var(--muted); stroke-dasharray: 3 3; }
svg.topo .tap { fill: var(--panel); stroke: var(--fg); stroke-width: 1.5; }
svg.topo .tap.rack { fill: var(--accent); stroke: var(--accent); } svg.topo .wire.rackdrop { stroke: var(--accent); stroke-opacity: .55; stroke-width: 2; }
svg.topo .node rect { fill: var(--panel); stroke: var(--line2); stroke-width: 1.2; }
svg.topo .node .n1 { font-weight: 600; }
svg.topo .region rect { stroke: var(--accent); stroke-width: 2; fill: var(--sel); }
svg.topo .ipam rect { stroke-dasharray: 4 3; } svg.topo .ipam.bad rect { stroke: var(--danger); }
svg.topo .rack.ok .led { fill: var(--ok); } svg.topo .rack.bad .led { fill: var(--danger); } svg.topo .rack.bad rect { stroke: var(--danger); }
svg.topo .mach { cursor: pointer; } svg.topo .mach:hover rect { fill: var(--hover); }
svg.topo .mach .stripe { stroke: none; fill: var(--line2); }
svg.topo .mach.ok .stripe { fill: var(--ok); } svg.topo .mach.warn .stripe { fill: var(--warn); } svg.topo .mach.bad .stripe { fill: var(--danger); }
svg.topo .mach.group rect { stroke-width: 1.2; } svg.topo .mach.group .stack { fill: none; stroke: var(--line2); }
svg.topo .mach.group.ok rect:first-child { stroke: var(--ok); } svg.topo .mach.group.warn rect:first-child { stroke: var(--warn); } svg.topo .mach.group.bad rect:first-child { stroke: var(--danger); }
.legend { display: flex; gap: 14px; flex-wrap: wrap; color: var(--muted); font-size: 11.5px; margin-top: 8px; align-items: center; }
.legend i { display: inline-block; width: 22px; height: 0; border-top: 4px solid var(--accent); vertical-align: middle; margin-right: 5px; border-radius: 2px; }
.legend i.drop { border-top: 2px solid var(--accent); opacity: .55; } .legend b.sq { display: inline-block; width: 8px; height: 8px; background: var(--accent); margin-right: 5px; } .legend b.ci { display: inline-block; width: 7px; height: 7px; border: 1.5px solid var(--fg); border-radius: 50%; margin-right: 5px; }
.legend i.plain { border-top: 3px solid var(--line2); } .legend i.dash { border-top: 3px dashed var(--line2); } .legend i.ctl { border-top: 2px solid var(--accent); } .legend i.down { border-top: 2px dashed var(--danger); }
/* overview */
.ov { display: grid; grid-template-columns: repeat(12, 1fr); gap: 12px; }
.ov > .card { grid-column: span 4; } .ov > .card.w6 { grid-column: span 6; } .ov > .card.w8 { grid-column: span 8; } .ov > .card.wide { grid-column: 1 / -1; }
.big { display: grid; grid-template-columns: repeat(auto-fit, minmax(90px, 1fr)); gap: 10px; }
.big div b { display: block; font-size: 20px; font-weight: 600; line-height: 1.15; font-variant-numeric: tabular-nums; } .big div span { color: var(--muted); font-size: 11.5px; }
.hbar { display: grid; grid-template-columns: minmax(90px, max-content) 1fr 64px; gap: 4px 10px; align-items: center; font-size: 12.5px; }
.hbar .bar { min-width: 0; } .hbar .n { text-align: right; color: var(--muted); font-variant-numeric: tabular-nums; }
.statebar { display: flex; height: 12px; border-radius: 999px; overflow: hidden; background: var(--pill); margin: 4px 0 10px; }
.statebar span { height: 100%; } .statebar .ok { background: var(--ok); } .statebar .warn { background: var(--warn); } .statebar .bad { background: var(--danger); } .statebar .idle { background: var(--line2); }
@media (max-width: 1100px) { .ov > .card, .ov > .card.w6, .ov > .card.w8 { grid-column: 1 / -1; } }
/* network page for large sites (netview.js) */
.nv-bar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-bottom: 10px; }
.nv-bar #nv-q { width: 240px; } .nv-bar .right { margin-left: auto; }
.seg-btns { display: inline-flex; } .seg-btns .btn { border-radius: 0; } .seg-btns .btn:first-child { border-radius: 6px 0 0 6px; } .seg-btns .btn:last-child { border-radius: 0 6px 6px 0; margin-left: -1px; }
.seg-btns .btn.on { background: var(--accent); border-color: var(--accent); color: var(--accent-fg); }
.nv-issues { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 6px 12px; margin-bottom: 12px; }
.nv-issues.ok { color: var(--muted); padding: 9px 12px; }
.nv-issue { padding: 4px 0; } .nv-issue[data-ips], .nv-issue[data-nvrack] { cursor: pointer; } .nv-issue[data-ips]:hover, .nv-issue[data-nvrack]:hover { text-decoration: underline; }
.nv-issue.bad { color: var(--danger); } .nv-issue.warn { color: var(--warn); }
.nv-issues > a { display: inline-block; margin: 2px 0 4px; font-size: 12px; }
table.nv-matrix { min-width: 0; } .nv-matrix td, .nv-matrix th { padding: 5px 8px; }
.nv-matrix th.nv-rackh { height: 92px; vertical-align: bottom; padding: 4px 2px; width: 26px; min-width: 26px; text-align: center; }
.nv-matrix th.nv-rackh span { writing-mode: vertical-rl; transform: rotate(180deg); white-space: nowrap; font-family: var(--mono); text-transform: none; letter-spacing: 0; font-size: 11.5px; }
.nv-matrix th.nv-rackh.down span { color: var(--danger); }
.nv-matrix td.nv-cell { text-align: center; padding: 5px 2px; font-size: 14px; color: var(--muted); border-left: 1px solid var(--line); }
.nv-matrix td.nv-cell.serve { color: var(--ok); } .nv-matrix td.nv-cell.second { opacity: .65; } .nv-matrix td.nv-cell.down { color: var(--danger); opacity: 1; }
.nv-matrix tr.nv-fab td { background: var(--hover); font-weight: 600; font-size: 12px; padding: 5px 8px; }
.nv-matrix tr.nv-row { cursor: pointer; } .nv-matrix tr.nv-row:hover td { background: var(--hover); }
.nv-matrix tr.nv-row.hl td { background: var(--sel); }
.nv-matrix td.num, .nv-matrix th.num { text-align: right; }
.nv-states { display: flex; height: 4px; border-radius: 2px; overflow: hidden; margin-top: 2px; min-width: 60px; background: var(--pill); }
.nv-states i { display: block; } .nv-states i.ok { background: var(--ok); } .nv-states i.warn { background: var(--warn); } .nv-states i.bad { background: var(--danger); } .nv-states i.idle { background: var(--line2); }
.nv-matrix .bar { min-width: 70px; }
.bar.warn > span { background: var(--warn); } .bar.bad > span { background: var(--danger); }
.legend b.g { color: var(--ok); font-weight: 400; } .legend b.r { color: var(--danger); font-weight: 400; }
.nv-pathcard { margin-bottom: 12px; } .nv-path { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; padding: 4px 0; }
.nv-path .arr { color: var(--muted); }
/* sites, map, floor plans (sites.js) */
#site-map { height: 440px; background: var(--bg); }
.room-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 12px; }
.room-card { display: block; text-decoration: none; color: inherit; } .room-card:hover { background: var(--hover); }
.room-mini { margin: 4px 0 8px; }
.floor-wrap { display: grid; grid-template-columns: minmax(0, 1fr) 340px; gap: 12px; align-items: start; }
.floor-card { overflow: auto; } .floor-side { position: sticky; top: 12px; max-height: calc(100vh - 40px); overflow: auto; }
@media (max-width: 1100px) { .floor-wrap { grid-template-columns: 1fr; } }
svg.floor { display: block; touch-action: none; user-select: none; }
svg.floor .room { fill: var(--bg); stroke: var(--line2); }
svg.floor .tile { stroke: var(--line); stroke-width: 0.5; }
svg.floor .rk { cursor: pointer; } svg.floor .rk .body { fill: var(--pill); stroke: var(--line2); }
svg.floor .rk.ok .body { fill: color-mix(in srgb, var(--ok) 28%, var(--panel)); stroke: var(--ok); }
svg.floor .rk.warn .body { fill: color-mix(in srgb, var(--warn) 28%, var(--panel)); stroke: var(--warn); }
svg.floor .rk.bad .body { fill: color-mix(in srgb, var(--danger) 32%, var(--panel)); stroke: var(--danger); }
svg.floor .rk.idle .body { fill: var(--pill); stroke: var(--muted); }
svg.floor .rk.auto .body { stroke-dasharray: 3 2; }
svg.floor .rk.sel .body { stroke: var(--accent); stroke-width: 2.5; }
svg.floor .rk .front { stroke: var(--fg); stroke-width: 3; stroke-linecap: round; }
svg.floor.mini .rk .front { stroke-width: 1.5; }
svg.floor .rk .fill { fill: var(--accent); }
svg.floor .rk text { font-size: 9px; fill: var(--fg); pointer-events: none; font-family: var(--mono); }
.legend b.lg { display: inline-block; width: 12px; height: 12px; border-radius: 2px; margin-right: 5px; vertical-align: middle; border: 1px solid var(--line2); }
.legend b.lg.ok { background: color-mix(in srgb, var(--ok) 28%, var(--panel)); border-color: var(--ok); }
.legend b.lg.warn { background: color-mix(in srgb, var(--warn) 28%, var(--panel)); border-color: var(--warn); }
.legend b.lg.bad { background: color-mix(in srgb, var(--danger) 32%, var(--panel)); border-color: var(--danger); }
.legend b.lg.vacant { background: var(--pill); }
.room3d { position: relative; height: min(72vh, 680px); min-height: 360px; border-radius: 6px; overflow: hidden; }
.room3d canvas { display: block; cursor: grab; } .room3d canvas:active { cursor: grabbing; }
.r3-tip { position: absolute; pointer-events: none; background: var(--panel); border: 1px solid var(--line2); border-radius: 6px; padding: 4px 8px; font-size: 12px; white-space: nowrap; box-shadow: 0 4px 14px rgba(0,0,0,.18); }
.sw-list { display: grid; gap: 6px; } .sw-row { display: grid; grid-template-columns: 12px minmax(0, 1fr) auto; gap: 8px; align-items: center; }
.sw-row > div { min-width: 0; overflow-wrap: anywhere; } .sw-dot { width: 10px; height: 10px; border-radius: 2px; display: inline-block; }
.nb-link { font-size: 12px; font-weight: 400; text-transform: none; letter-spacing: 0; white-space: nowrap; align-self: center; }
.page-head .right .nb-link { margin-right: 6px; }
.cab-m { margin-top: 8px; border-top: 1px solid var(--line); padding-top: 6px; }
.cab-p { display: grid; grid-template-columns: 58px minmax(0, 1fr) 14px; gap: 6px; font-size: 12px; padding: 1px 0; } .cab-p > span { overflow-wrap: anywhere; }
.elev .slot.sw { background: var(--hover); border-left: 3px solid var(--accent); padding: 0 6px; font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }