Files
DarkflameServer/dDashboardServer/static/css/dashboard.css
Aaron Kimbrell 9bbdc45f4f feat(world3d): follow a player across worlds, live and in capture playback
Live: when the position feed reports the followed player only in another world,
load its scene, keep following and toast where they went. Capture playback: load
every zone's movement, switch the scene at the followed character's world change
(forward and when seeking back), follow a lone captured character from the start,
and mark world changes on the replay slider.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 03:21:53 -05:00

265 lines
12 KiB
CSS

.sidebar {
width: 250px;
background-color: var(--bs-tertiary-bg);
}
@media (min-width: 992px) {
nav.sidebar {
position: sticky;
top: 0;
height: 100vh;
overflow-y: auto;
}
}
/* Narrow screens: the sidebar slides out (offcanvas-lg), and wide tables scroll sideways */
@media (max-width: 991.98px) {
nav.sidebar.offcanvas-lg { width: min(280px, 85vw); overflow-y: auto; }
.dt-container .dt-layout-table, .dt-container .dt-layout-row:has(table) { overflow-x: auto; }
}
/* .table-stack: on phones every row becomes a small card, each cell labelled with its column */
@media (max-width: 575.98px) {
.table-stack thead { display: none; }
.table-stack, .table-stack tbody, .table-stack tr, .table-stack td { display: block; width: 100% !important; }
.table-stack tr { border: 1px solid var(--bs-border-color); border-radius: .5rem; margin-bottom: .5rem; padding: .25rem 0; }
.table-stack td { border: 0; padding: .15rem .6rem; text-align: left !important; }
.table-stack td[data-label]:not([data-label=""])::before { content: attr(data-label) ": "; color: var(--bs-secondary-color); font-size: .8rem; }
.table-stack td:last-child { padding-top: .4rem; }
h2 { font-size: 1.4rem; }
.card-body { padding: .75rem; }
.table { font-size: .875rem; }
.table .btn-sm { padding: .15rem .4rem; }
}
.list-group-item {
border: none;
background-color: transparent;
}
.dataTables_wrapper .dataTables_paginate .paginate_button.current,
.dataTables_wrapper .dataTables_paginate .paginate_button.current:hover {
background: var(--bs-primary) !important;
border-color: var(--bs-primary) !important;
color: #fff !important;
}
.item-icon {
width: 32px;
height: 32px;
object-fit: contain;
}
pre.log-output {
max-height: 70vh;
overflow: auto;
white-space: pre-wrap;
word-break: break-all;
font-size: 0.8rem;
}
.item-tooltip {
--bs-tooltip-bg: #11141a;
--bs-tooltip-color: #e9ecef;
--bs-tooltip-opacity: 0.97;
}
.item-tooltip .tooltip-inner {
max-width: 320px;
border: 1px solid #3a3f4b;
text-align: left;
}
.rarity-2 { color: #4fc948; }
.rarity-3 { color: #3e8ee8; }
.rarity-4 { color: #b44be1; }
.item-equipped { color: #d16f05; }
/* ---- Reports charts (palette validated against the dark card surface #212529) ---- */
:root {
--viz-surface: #212529;
--viz-up: #3987e5;
--viz-down: #d95926;
--viz-grid: #2c2c2a;
--viz-baseline: #6c757d;
--viz-tick: #898781;
--viz-hover: rgba(255, 255, 255, 0.06);
}
.viz { width: 100%; min-height: 60px; }
.viz svg { display: block; }
.viz-grid { stroke: var(--viz-grid); stroke-width: 1; }
.viz-baseline { stroke: var(--viz-baseline); stroke-width: 1; }
.viz-tick { fill: var(--viz-tick); font-size: 11px; font-variant-numeric: tabular-nums; }
.viz-up { fill: var(--viz-up); background: var(--viz-up); }
.viz-down { fill: var(--viz-down); background: var(--viz-down); }
.viz-hit { fill: transparent; cursor: crosshair; }
.viz-hit.viz-hover { fill: var(--viz-hover); }
.viz-num { font-variant-numeric: tabular-nums; }
.viz-hero { font-size: 1.75rem; font-weight: 600; line-height: 1.2; }
.viz-legend { display: flex; gap: 1rem; font-size: 0.85rem; color: var(--bs-secondary-color); }
.viz-key { display: inline-flex; align-items: center; gap: 0.4rem; }
.viz-swatch { display: inline-block; width: 10px; height: 10px; border-radius: 2px; flex: none; }
.viz-tooltip {
position: fixed;
z-index: 1080;
pointer-events: none;
min-width: 160px;
padding: 0.5rem 0.65rem;
font-size: 0.85rem;
color: var(--bs-body-color);
background: #11141a;
border: 1px solid #3a3f4b;
border-radius: 6px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
}
.viz-tip-row { display: flex; align-items: center; gap: 0.4rem; }
.viz-map { position: relative; width: 100%; }
.viz-map canvas { display: block; max-width: 100%; border-radius: 4px; cursor: crosshair; }
.viz-scale { display: inline-flex; align-items: center; gap: 0.5rem; }
.viz-ramp {
display: inline-block;
width: 120px;
height: 8px;
border-radius: 4px;
background: linear-gradient(to right, #184f95, #2a78d6, #6da7ec, #cde2fb);
}
.viz-scroll { max-height: 22rem; overflow-y: auto; }
/* In a card that fills the window (.panel-fill) the map takes the room left and reports.js fits the canvas, centred, into it */
@media (min-width: 992px) {
.panel-fill .viz-map { flex: 1 1 auto; min-height: 0; }
.panel-fill .viz-map canvas { position: absolute; inset: 0; margin: auto; }
}
@media (min-width: 1200px) { .panel-stack-xl .viz-scroll { max-height: none; } }
#itemLot { min-width: 18rem; }
#traceId { min-width: 16rem; }
.qr-box { width: 180px; height: 180px; background: #fff; border-radius: 6px; padding: 0; }
.qr-box svg { width: 100%; height: 100%; display: block; }
/* Sidebar groups that fold away; the dot shows while a closed group holds a waiting badge */
.nav-group { margin-top: .25rem; }
.nav-group-toggle {
display: flex; align-items: center; gap: .4rem; width: 100%;
padding: .45rem 1rem; border: 0; background: none;
color: var(--bs-secondary-color); font-size: .75rem; font-weight: 600; letter-spacing: .05em; text-transform: uppercase; text-align: left;
}
.nav-group-toggle:hover { color: var(--bs-body-color); }
.nav-group-toggle::after {
content: ""; margin-left: auto; width: .45rem; height: .45rem;
border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
transform: rotate(45deg); transition: transform .15s;
}
.nav-group-toggle[aria-expanded="false"]::after { transform: rotate(-45deg); }
.nav-group-dot { display: none; width: .45rem; height: .45rem; border-radius: 50%; background: var(--bs-danger); }
.nav-group:has(.collapse:not(.show) [data-badge]:not(.d-none)) .nav-group-dot { display: inline-block; }
.sidebar .list-group-item { padding: .4rem 1rem; border-radius: .375rem; }
.sidebar .nav-group .list-group-item { padding-left: 1.5rem; }
/* Top bar: the signed-in user's menu sits at the right */
.topbar { z-index: 1020; min-height: 3rem; }
.user-avatar {
display: inline-flex; align-items: center; justify-content: center;
width: 1.9rem; height: 1.9rem; border-radius: 50%;
background: var(--bs-primary); color: #fff; font-weight: 600; font-size: .85rem;
}
/* The page column: clip wide content sideways without becoming a scroll container (that would stop position: sticky) */
.main-column { min-width: 0; overflow-x: clip; }
/* ---- Layout helpers that save page scrolling (dashboard.js keeps --topbar-h and --fill-top current) ---- */
:root { --topbar-h: 3.5rem; }
/* .fill-viewport[-lg|-xl]: reaches from where it starts to the bottom of the window, so the page needn't scroll while you
work in it (a map, a list with its details). The -lg/-xl forms only do so from that width up; below it they size to their content. */
.fill-viewport { height: calc(100dvh - var(--fill-top, 12rem)); min-height: var(--fill-min, 20rem); }
@media (min-width: 992px) { .fill-viewport-lg { height: calc(100dvh - var(--fill-top, 12rem)); min-height: var(--fill-min, 20rem); } }
@media (min-width: 1200px) { .fill-viewport-xl { height: calc(100dvh - var(--fill-top, 12rem)); min-height: var(--fill-min, 20rem); } }
/* A filling .row passes its height on to its columns */
.row.fill-viewport > * { height: 100%; }
@media (min-width: 992px) { .row.fill-viewport-lg > * { height: 100%; } }
@media (min-width: 1200px) { .row.fill-viewport-xl > * { height: 100%; } }
/* .panel-fill: a card whose body takes the height left under its header and scrolls inside (.panel-scroll does the same for any box) */
.panel-fill { display: flex; flex-direction: column; min-height: 0; }
.panel-fill > .card-body, .panel-fill > .list-group, .panel-scroll { flex: 1 1 auto; min-height: 0; overflow: auto; }
.panel-fill > .card-header, .panel-fill > .card-footer { flex: none; }
/* .panel-cap: a .panel-fill card that grows with its content up to most of a window, then scrolls inside (lists that can get long) */
.panel-cap { max-height: min(36rem, 70dvh); }
/* Table headers stay at the top of a scrolling panel (a .table-responsive inside would otherwise become the scroll box) */
.panel-fill > .card-body .table-responsive, .panel-scroll .table-responsive { overflow: visible; }
.panel-fill thead th, .panel-scroll thead th, .sticky-head thead th {
position: sticky; top: 0; z-index: 2;
background: var(--bs-card-bg, var(--bs-body-bg)); box-shadow: inset 0 -1px 0 var(--bs-border-color);
}
/* .panel-stack-xl: a side column whose cards share the height of the row's main content (its tallest neighbour) from xl up,
each .panel-fill card scrolling inside; below xl the cards just stack. Wrap the cards in .panel-stack-xl > .panel-stack-inner. */
@media (min-width: 1200px) {
.panel-stack-xl { position: relative; height: 100%; min-height: 20rem; }
.panel-stack-xl > .panel-stack-inner { position: absolute; inset: 0; display: flex; flex-direction: column; gap: 1rem; }
.panel-stack-xl > .panel-stack-inner > * { margin: 0 !important; }
.panel-stack-xl > .panel-stack-inner > .panel-fill { flex: 1 1 0; }
.panel-stack-xl > .panel-stack-inner > .panel-fit { flex: 0 1 auto; max-height: 50%; }
}
/* .sticky-toolbar: filters and buttons that stay under the top bar while the content below scrolls */
.sticky-toolbar { position: sticky; top: var(--topbar-h); z-index: 5; background: var(--bs-body-bg); padding-top: .5rem; padding-bottom: .5rem; }
/* Long lists in DataTables keep their column headers in view under the top bar (on narrow screens the table scrolls sideways instead) */
@media (min-width: 992px) {
table.dataTable > thead > tr > th {
position: sticky; top: var(--topbar-h); z-index: 2;
background: var(--bs-card-bg, var(--bs-body-bg)); box-shadow: inset 0 -1px 0 var(--bs-border-color);
}
.panel-fill table.dataTable > thead > tr > th, .panel-scroll table.dataTable > thead > tr > th { top: 0; }
.modal table.dataTable > thead > tr > th { position: static; }
}
/* details.about-text: a page's longer explanation, folded to one line until asked for (Ctrl+F still finds and opens it) */
details.about-text > summary {
display: inline-flex; align-items: center; gap: .35rem; cursor: pointer; list-style: none;
color: var(--bs-secondary-color); font-size: .875rem;
}
details.about-text > summary::-webkit-details-marker { display: none; }
details.about-text > summary::before {
content: "?"; display: inline-flex; align-items: center; justify-content: center; width: 1.1rem; height: 1.1rem; border-radius: 50%;
border: 1px solid currentColor; font-size: .7rem; font-weight: 600;
}
details.about-text > summary:hover { color: var(--bs-body-color); }
details.about-text[open] > summary { margin-bottom: .35rem; }
details.about-text > :not(summary) { color: var(--bs-secondary-color); font-size: .875rem; }
/* Wide screens: the menu can be hidden (the top bar's menu button; header.jinja2 keeps the choice) */
@media (min-width: 992px) {
html.sidebar-hidden nav.sidebar { display: none !important; }
html.sidebar-hidden .topbar-brand { display: inline !important; }
}
/* Changing pages without a reload (nav.js): a thin bar at the top while the next page loads */
main:focus { outline: none; }
.nav-progress {
position: fixed; top: 0; left: 0; z-index: 1100; width: 100%; height: 3px;
background: var(--bs-primary); transform-origin: left; transform: scaleX(0); opacity: 0; pointer-events: none;
}
html.nav-loading .nav-progress { opacity: 1; animation: nav-progress 10s cubic-bezier(.08, .7, .2, 1) forwards; }
html.nav-loading main { opacity: .65; transition: opacity .2s; }
@keyframes nav-progress { from { transform: scaleX(.08); } to { transform: scaleX(.92); } }
@media (prefers-reduced-motion: reduce) { html.nav-loading .nav-progress { animation: none; transform: scaleX(.5); } }
/* A playback slider with world change markers above it (World 3D capture replay, Packet Captures): one tick per change,
labelled with the zone; clicking one seeks to it */
.timeline-track { position: relative; min-width: 10rem; padding-top: 1.1rem; }
.timeline-track > input[type=range] { width: 100%; display: block; }
.timeline-markers { position: absolute; left: .5rem; right: .5rem; top: 0; height: 100%; pointer-events: none; }
.timeline-marker { position: absolute; top: 0; bottom: .35rem; width: 0; padding: 0; border: 0; border-left: 2px solid var(--bs-warning);
background: none; pointer-events: auto; cursor: pointer; }
.timeline-marker > span { position: absolute; left: 2px; top: 0; font-size: .68rem; line-height: 1; white-space: nowrap; padding: .05rem .25rem;
border-radius: .2rem; background: var(--bs-warning-bg-subtle); color: var(--bs-warning-text-emphasis); max-width: 9rem; overflow: hidden;
text-overflow: ellipsis; }
.timeline-marker:hover > span, .timeline-marker:focus-visible > span { max-width: none; z-index: 1; }