mirror of
https://github.com/knutwurst/patchdl.git
synced 2026-10-06 08:00:21 +02:00
A sticky-feeling banner appears whenever any job is queued or active.
It shows the eyebrow (Downloading / Queued), the current title's name,
a "3 of 9" chip when more than one job is in this batch, plus
percent, speed, and ETA. A thin gradient bar runs edge-to-edge along
the bottom so the at-a-glance state matches the per-card progress.
Batch counting: jobs with state in {queued, active, done} make up the
batch; done count + 1 is the current position. When all queued jobs
finish and roll out of the pool list the banner hides on the next poll.
While the active job's manifest is still being fetched (no total_bytes
yet) the bar runs an indeterminate sweep so the user isn't staring at
a frozen 0%. Speed comes from the existing per-job smoothed estimate
in reconcileFromJobs so the banner shares one source of truth with
the per-game tiles.
Render hooks: renderGames() and applyDownloadProgress() both call
renderGlobalStatus(), so every refresh path keeps it in sync without
adding a separate timer.
670 lines
19 KiB
CSS
670 lines
19 KiB
CSS
:root {
|
|
color-scheme: dark;
|
|
--bg: #0b0d10;
|
|
--surface: #14181d;
|
|
--surface-2: #1b2027;
|
|
--ink: #edf2f7;
|
|
--muted: #9aa6b2;
|
|
--border: #2a313a;
|
|
--green: #38c172;
|
|
--green-dark: #63d994;
|
|
--green-soft: rgba(56, 193, 114, 0.16);
|
|
--amber: #f2b84b;
|
|
--amber-dark: #ffcf6b;
|
|
--amber-soft: rgba(242, 184, 75, 0.16);
|
|
--red: #ff6b6b;
|
|
--red-soft: rgba(255, 107, 107, 0.15);
|
|
--blue: #58a6ff;
|
|
--blue-dark: #8cc2ff;
|
|
--blue-soft: rgba(88, 166, 255, 0.16);
|
|
--shadow: 0 18px 50px rgba(0, 0, 0, 0.38);
|
|
--radius: 10px;
|
|
--rail-w: 240px;
|
|
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
}
|
|
|
|
* { box-sizing: border-box; }
|
|
|
|
body {
|
|
margin: 0;
|
|
min-height: 100dvh;
|
|
background: var(--bg);
|
|
color: var(--ink);
|
|
-webkit-text-size-adjust: 100%;
|
|
}
|
|
|
|
button, input, select { font: inherit; }
|
|
button { cursor: pointer; }
|
|
|
|
svg {
|
|
width: 18px;
|
|
height: 18px;
|
|
fill: none;
|
|
stroke: currentColor;
|
|
stroke-width: 1.9;
|
|
stroke-linecap: round;
|
|
stroke-linejoin: round;
|
|
}
|
|
|
|
.icon-sprite { position: absolute; width: 0; height: 0; overflow: hidden; }
|
|
|
|
/* ---------------- shell + rail ---------------- */
|
|
|
|
.app-shell {
|
|
display: grid;
|
|
grid-template-columns: var(--rail-w) minmax(0, 1fr);
|
|
min-height: 100dvh;
|
|
}
|
|
|
|
.rail {
|
|
position: sticky;
|
|
top: 0;
|
|
align-self: start;
|
|
height: 100dvh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 18px;
|
|
padding: 20px 14px;
|
|
background: #0f1216;
|
|
border-right: 1px solid #222932;
|
|
}
|
|
|
|
.brand { display: flex; align-items: center; gap: 12px; min-height: 44px; }
|
|
|
|
.brand-mark {
|
|
display: grid;
|
|
place-items: center;
|
|
width: 42px; height: 42px;
|
|
border: 1px solid rgba(255, 255, 255, 0.16);
|
|
background: #247857;
|
|
border-radius: 8px;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.brand strong { display: block; font-size: 16px; }
|
|
.brand span { display: block; margin-top: 2px; color: var(--muted); font-size: 12px; }
|
|
|
|
.nav-list { display: grid; gap: 6px; }
|
|
|
|
.nav-link {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
width: 100%;
|
|
min-height: 44px;
|
|
padding: 0 12px;
|
|
border: 0;
|
|
background: transparent;
|
|
color: #b6c0ca;
|
|
border-radius: 8px;
|
|
font-weight: 700;
|
|
text-align: left;
|
|
}
|
|
|
|
.nav-link:hover { background: rgba(255, 255, 255, 0.06); color: #fff; }
|
|
.nav-link.is-active { background: rgba(56, 193, 114, 0.14); color: #fff; }
|
|
|
|
.rail-foot {
|
|
margin-top: auto;
|
|
display: grid;
|
|
gap: 4px;
|
|
padding: 10px 12px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
background: var(--surface);
|
|
color: var(--muted);
|
|
font-size: 12px;
|
|
}
|
|
.rail-foot span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
|
/* ---------------- views + topbar ---------------- */
|
|
|
|
.main { min-width: 0; padding: 26px 28px 40px; }
|
|
|
|
.view { display: none; }
|
|
.view.is-active { display: block; animation: fade 180ms ease; }
|
|
@keyframes fade { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
|
|
|
|
.topbar {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.eyebrow {
|
|
margin: 0 0 6px;
|
|
color: var(--green-dark);
|
|
font-size: 11px;
|
|
font-weight: 800;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
h1, h2 { margin: 0; }
|
|
h1 { font-size: 28px; line-height: 1.12; }
|
|
h2 { font-size: 18px; line-height: 1.2; }
|
|
|
|
.topbar-actions { display: flex; align-items: center; gap: 10px; flex: none; }
|
|
|
|
.icon-button, .primary-button, .row-button {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
min-height: 40px;
|
|
border: 1px solid var(--border);
|
|
background: var(--surface);
|
|
color: var(--ink);
|
|
border-radius: 8px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.icon-button { width: 40px; padding: 0; flex: none; }
|
|
.primary-button { padding: 0 16px; border-color: var(--green-dark); background: var(--green); color: #fff; }
|
|
.primary-button:hover { background: var(--green-dark); }
|
|
|
|
/* ---------------- status strip ---------------- */
|
|
|
|
.status-strip {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
gap: 12px;
|
|
margin-bottom: 18px;
|
|
}
|
|
|
|
/* ---------------- global download banner ---------------- */
|
|
/* Sticky banner above the status strip, visible only while at least one
|
|
download is queued/active. The pulse dot and gradient bar at the bottom
|
|
echo the per-game card progress styling so the two read as one system. */
|
|
.global-dl {
|
|
position: relative;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
margin-bottom: 18px;
|
|
padding: 16px 18px 0;
|
|
border: 1px solid var(--border);
|
|
background:
|
|
radial-gradient(120% 80% at 0% 0%, var(--green-soft), transparent 60%),
|
|
linear-gradient(180deg, rgba(56, 193, 114, 0.06), transparent 60%),
|
|
var(--surface);
|
|
border-radius: var(--radius);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.global-dl[hidden] { display: none; }
|
|
|
|
.global-dl-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.global-dl-pulse {
|
|
flex: none;
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
background: var(--green);
|
|
box-shadow: 0 0 0 0 var(--green);
|
|
animation: globalDlPulse 1.6s ease-out infinite;
|
|
}
|
|
|
|
@keyframes globalDlPulse {
|
|
0% { box-shadow: 0 0 0 0 rgba(56, 193, 114, 0.55); }
|
|
70% { box-shadow: 0 0 0 14px rgba(56, 193, 114, 0); }
|
|
100% { box-shadow: 0 0 0 0 rgba(56, 193, 114, 0); }
|
|
}
|
|
|
|
.global-dl-text { display: flex; flex-direction: column; gap: 4px; min-width: 0; flex: 1; }
|
|
|
|
.global-dl-line1 {
|
|
display: flex; align-items: baseline; gap: 10px;
|
|
min-width: 0; flex-wrap: wrap;
|
|
}
|
|
.global-dl-eyebrow {
|
|
text-transform: uppercase; letter-spacing: 0.08em;
|
|
font-size: 11px; font-weight: 600; color: var(--green-dark);
|
|
}
|
|
.global-dl-name {
|
|
font-size: 16px; font-weight: 600; color: #fff;
|
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
min-width: 0; flex: 1;
|
|
}
|
|
.global-dl-position {
|
|
font-size: 12px; color: var(--muted);
|
|
padding: 2px 8px; border-radius: 999px;
|
|
background: rgba(255, 255, 255, 0.04); border: 1px solid var(--border);
|
|
}
|
|
.global-dl-position[hidden] { display: none; }
|
|
|
|
.global-dl-line2 {
|
|
display: flex; align-items: center; gap: 8px;
|
|
font-size: 12px; color: var(--muted);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
.global-dl-line2 b { color: #d6dde4; font-weight: 600; }
|
|
.global-dl-sep { opacity: 0.5; }
|
|
|
|
.global-dl-track {
|
|
position: relative;
|
|
height: 3px;
|
|
margin: 0 -18px; /* extend bar edge-to-edge */
|
|
background: rgba(56, 193, 114, 0.08);
|
|
overflow: hidden;
|
|
}
|
|
.global-dl-bar {
|
|
display: block;
|
|
height: 100%;
|
|
width: 0%;
|
|
background: linear-gradient(90deg, var(--green) 0%, var(--green-dark) 100%);
|
|
box-shadow: 0 0 12px rgba(56, 193, 114, 0.4);
|
|
transition: width 400ms ease-out;
|
|
}
|
|
.global-dl-bar.is-indeterminate {
|
|
width: 32% !important;
|
|
animation: globalDlIndet 1.6s ease-in-out infinite;
|
|
}
|
|
@keyframes globalDlIndet {
|
|
0% { transform: translateX(-100%); }
|
|
100% { transform: translateX(320%); }
|
|
}
|
|
|
|
.metric {
|
|
min-width: 0;
|
|
padding: 14px 16px;
|
|
border: 1px solid var(--border);
|
|
background: var(--surface);
|
|
border-radius: var(--radius);
|
|
}
|
|
|
|
.metric span, .metric em {
|
|
display: block;
|
|
color: var(--muted);
|
|
font-size: 12px;
|
|
font-style: normal;
|
|
}
|
|
|
|
.metric strong {
|
|
display: block;
|
|
margin: 6px 0 3px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
font-size: 20px;
|
|
}
|
|
|
|
/* ---------------- toolbar ---------------- */
|
|
|
|
.toolbar {
|
|
display: grid;
|
|
grid-template-columns: minmax(220px, 1fr) auto;
|
|
gap: 12px;
|
|
align-items: center;
|
|
margin-bottom: 14px;
|
|
}
|
|
|
|
.search-box {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
min-height: 44px;
|
|
padding: 0 12px;
|
|
border: 1px solid var(--border);
|
|
background: var(--surface);
|
|
border-radius: 8px;
|
|
}
|
|
.search-box input { width: 100%; border: 0; outline: 0; background: transparent; color: var(--ink); font-size: 16px; }
|
|
.search-box:focus-within { border-color: var(--blue); box-shadow: 0 0 0 2px var(--blue-soft); }
|
|
|
|
.segmented {
|
|
display: flex;
|
|
gap: 2px;
|
|
padding: 4px;
|
|
border: 1px solid var(--border);
|
|
background: var(--surface-2);
|
|
border-radius: 8px;
|
|
}
|
|
.segmented button {
|
|
min-height: 34px;
|
|
padding: 0 12px;
|
|
border: 0;
|
|
background: transparent;
|
|
color: var(--muted);
|
|
border-radius: 6px;
|
|
font-weight: 700;
|
|
white-space: nowrap;
|
|
}
|
|
.segmented button.is-selected { background: var(--surface); color: var(--ink); }
|
|
|
|
/* ---------------- game cards ---------------- */
|
|
|
|
.game-grid { display: grid; gap: 12px; }
|
|
|
|
.game-card {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 14px;
|
|
padding: 16px;
|
|
border: 1px solid var(--border);
|
|
background: var(--surface);
|
|
border-radius: var(--radius);
|
|
}
|
|
.game-card.is-downloading { border-color: rgba(56, 193, 114, 0.5); box-shadow: inset 3px 0 0 var(--green); }
|
|
.game-card.is-installing { border-color: rgba(242, 184, 75, 0.5); box-shadow: inset 3px 0 0 var(--amber); }
|
|
|
|
.card-row {
|
|
display: grid;
|
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
gap: 14px;
|
|
align-items: center;
|
|
}
|
|
|
|
.lead { display: flex; flex-direction: column; align-items: center; gap: 10px; flex: none; }
|
|
.lead .toggle { flex-direction: column; gap: 4px; }
|
|
|
|
.cover {
|
|
display: grid;
|
|
place-items: center;
|
|
width: 48px; height: 48px;
|
|
color: #fff;
|
|
background: linear-gradient(135deg, #2d6d9d, #247857);
|
|
border-radius: 10px;
|
|
font-size: 14px;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.card-body { min-width: 0; }
|
|
.card-body h3 { margin: 0 0 6px; overflow-wrap: anywhere; font-size: 16px; }
|
|
|
|
.subline { display: flex; flex-wrap: wrap; gap: 4px 10px; color: var(--muted); font-size: 12px; }
|
|
.pills { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
|
|
|
|
.versions {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 4px 16px;
|
|
margin-top: 10px;
|
|
font-size: 12px;
|
|
color: var(--muted);
|
|
}
|
|
.versions b { color: var(--ink); font-weight: 700; }
|
|
|
|
.card-actions {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
flex: none;
|
|
width: 136px;
|
|
}
|
|
|
|
.pill {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
min-height: 24px;
|
|
padding: 0 9px;
|
|
border: 1px solid var(--border);
|
|
background: var(--surface-2);
|
|
color: var(--muted);
|
|
border-radius: 999px;
|
|
font-size: 12px;
|
|
font-weight: 800;
|
|
}
|
|
.pill.ok { border-color: rgba(56, 193, 114, 0.34); background: var(--green-soft); color: var(--green-dark); }
|
|
.pill.warn { border-color: rgba(242, 184, 75, 0.38); background: var(--amber-soft); color: var(--amber); }
|
|
.pill.external { border-color: rgba(88, 166, 255, 0.34); background: var(--blue-soft); color: var(--blue); }
|
|
.pill.shadow { border-color: rgba(181, 146, 255, 0.34); background: rgba(181, 146, 255, 0.14); color: #c7b1ff; }
|
|
.pill.blocked { border-color: rgba(255, 107, 107, 0.34); background: var(--red-soft); color: var(--red); }
|
|
.pill.live { border-color: rgba(56, 193, 114, 0.5); background: var(--green-soft); color: var(--green-dark); }
|
|
.pill.live::before {
|
|
content: ""; width: 7px; height: 7px; margin-right: 6px; border-radius: 50%;
|
|
background: var(--green); animation: pulse 1.2s ease-in-out infinite;
|
|
}
|
|
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.35; } }
|
|
|
|
/* ---------------- one morphing action button ---------------- */
|
|
|
|
.row-button {
|
|
width: 100%; /* fills the fixed-width actions column -> never reflows */
|
|
padding: 0 12px;
|
|
}
|
|
/* go = green (Update / Resume / Install), pause = amber, cancel/stop = red */
|
|
.row-button.is-update { border-color: var(--green); color: #04150c; background: var(--green); }
|
|
.row-button.is-update:hover { background: var(--green-dark); }
|
|
.row-button.is-pause { border-color: var(--amber); color: #1c1402; background: var(--amber); }
|
|
.row-button.is-pause:hover { background: var(--amber-dark); }
|
|
.row-button.is-cancel { border-color: var(--red); color: #1a0606; background: var(--red); }
|
|
.row-button.is-cancel:hover { background: #ff8a8a; }
|
|
.row-button.is-ghost { background: transparent; color: var(--muted); }
|
|
.row-button.is-ghost:hover { color: var(--ink); border-color: var(--muted); }
|
|
.row-button:disabled { opacity: 0.6; cursor: default; }
|
|
|
|
/* ---------------- in-tile download progress ---------------- */
|
|
|
|
.card-progress { display: grid; gap: 8px; }
|
|
.progress {
|
|
height: 10px;
|
|
overflow: hidden;
|
|
background: #242b33;
|
|
border-radius: 999px;
|
|
}
|
|
.progress > i {
|
|
display: block;
|
|
width: 0%;
|
|
height: 100%;
|
|
background: linear-gradient(90deg, var(--green), var(--blue));
|
|
transition: width 320ms ease;
|
|
}
|
|
.progress-meta {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
justify-content: space-between;
|
|
gap: 4px 14px;
|
|
color: var(--muted);
|
|
font-size: 12px;
|
|
}
|
|
.progress-meta b { color: var(--ink); font-weight: 700; font-variant-numeric: tabular-nums; }
|
|
|
|
/* ---------------- settings ---------------- */
|
|
|
|
.panel { padding: 18px; border: 1px solid var(--border); background: var(--surface); border-radius: var(--radius); }
|
|
|
|
.settings-grid { display: grid; gap: 12px; }
|
|
.field { display: grid; gap: 6px; }
|
|
.field span, .allowlist > span, .source-policy > span { color: var(--muted); font-size: 12px; font-weight: 700; }
|
|
.field input, .field select {
|
|
min-height: 44px;
|
|
padding: 0 12px;
|
|
border: 1px solid var(--border);
|
|
background: var(--surface-2);
|
|
color: var(--ink);
|
|
border-radius: 8px;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.switch-row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 14px;
|
|
padding: 14px;
|
|
border: 1px solid var(--border);
|
|
background: var(--surface-2);
|
|
border-radius: 8px;
|
|
}
|
|
|
|
/* number stepper — big, controller-friendly targets with a clear focus ring
|
|
(the UI is operated by the PS5 controller via the home tile). */
|
|
.stepper { display: inline-flex; align-items: center; gap: 12px; flex: none; }
|
|
.stepper-btn {
|
|
width: 54px; height: 54px;
|
|
display: inline-flex; align-items: center; justify-content: center;
|
|
font-size: 30px; line-height: 1; font-weight: 600;
|
|
color: var(--ink);
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 14px;
|
|
cursor: pointer;
|
|
-webkit-tap-highlight-color: transparent;
|
|
transition: background .12s ease, border-color .12s ease, transform .07s ease;
|
|
}
|
|
.stepper-btn:hover { background: var(--surface); border-color: var(--muted); }
|
|
.stepper-btn:active { transform: scale(0.93); background: var(--green-soft); border-color: var(--green); }
|
|
/* :focus (not only :focus-visible) so the controller's focus is always obvious */
|
|
.stepper-btn:focus { outline: none; border-color: var(--green); box-shadow: 0 0 0 3px var(--green-soft); }
|
|
.stepper-btn:disabled { opacity: 0.32; cursor: default; transform: none; }
|
|
.stepper-value {
|
|
min-width: 52px;
|
|
text-align: center;
|
|
font-size: 26px; font-weight: 700;
|
|
font-variant-numeric: tabular-nums;
|
|
color: var(--ink);
|
|
}
|
|
.switch-row > span:first-child strong { display: block; font-size: 14px; }
|
|
.switch-row > span:first-child em { display: block; margin-top: 3px; color: var(--muted); font-size: 12px; font-style: normal; }
|
|
|
|
/* slider toggle (used for per-game enable + settings) */
|
|
.toggle { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; user-select: none; flex: none; }
|
|
.toggle input { position: absolute; opacity: 0; width: 0; height: 0; }
|
|
.toggle .track {
|
|
flex: none;
|
|
width: 44px; height: 26px;
|
|
border-radius: 999px;
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
position: relative;
|
|
transition: background 0.15s, border-color 0.15s;
|
|
}
|
|
.toggle .track::after {
|
|
content: "";
|
|
position: absolute;
|
|
top: 2px; left: 2px;
|
|
width: 20px; height: 20px;
|
|
border-radius: 50%;
|
|
background: var(--muted);
|
|
transition: transform 0.15s, background 0.15s;
|
|
}
|
|
.toggle input:checked + .track { background: var(--green-soft); border-color: var(--green); }
|
|
.toggle input:checked + .track::after { transform: translateX(18px); background: var(--green); }
|
|
.toggle input:focus-visible + .track { outline: 2px solid var(--blue); outline-offset: 2px; }
|
|
.toggle .label { font-size: 12px; color: var(--muted); min-width: 22px; }
|
|
|
|
.allowlist, .source-policy { display: grid; gap: 8px; margin-top: 16px; }
|
|
.allowlist div { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
.source-policy > div { display: grid; gap: 8px; }
|
|
|
|
.policy-row {
|
|
display: grid;
|
|
grid-template-columns: 116px minmax(100px, 0.8fr) minmax(0, 1.4fr);
|
|
gap: 10px;
|
|
align-items: center;
|
|
padding: 10px;
|
|
background: var(--surface-2);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
}
|
|
.policy-row strong { font-size: 13px; }
|
|
.policy-row em { overflow: hidden; color: var(--muted); font-size: 12px; font-style: normal; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
|
.host-chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
min-height: 26px;
|
|
padding: 0 9px;
|
|
background: var(--blue-soft);
|
|
color: var(--blue);
|
|
border-radius: 999px;
|
|
font-size: 12px;
|
|
font-weight: 800;
|
|
}
|
|
|
|
/* ---------------- logs ---------------- */
|
|
|
|
pre {
|
|
min-height: 200px;
|
|
margin: 0;
|
|
padding: 14px;
|
|
overflow: auto;
|
|
background: #0f1216;
|
|
border: 1px solid var(--border);
|
|
color: #dfe7ef;
|
|
border-radius: var(--radius);
|
|
font-size: 12px;
|
|
line-height: 1.55;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.empty-state {
|
|
padding: 22px;
|
|
border: 1px dashed var(--border);
|
|
background: var(--surface);
|
|
color: var(--muted);
|
|
border-radius: var(--radius);
|
|
text-align: center;
|
|
}
|
|
|
|
/* ---------------- toast ---------------- */
|
|
|
|
.toast {
|
|
position: fixed;
|
|
left: 50%;
|
|
bottom: max(18px, env(safe-area-inset-bottom));
|
|
transform: translate(-50%, 8px);
|
|
z-index: 20;
|
|
max-width: min(440px, calc(100vw - 32px));
|
|
padding: 12px 16px;
|
|
color: var(--ink);
|
|
background: #171c22;
|
|
border: 1px solid var(--border);
|
|
border-radius: 10px;
|
|
box-shadow: var(--shadow);
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
transition: opacity 160ms ease, transform 160ms ease;
|
|
}
|
|
.toast.is-visible { opacity: 1; transform: translate(-50%, 0); }
|
|
|
|
/* ---------------- responsive ---------------- */
|
|
|
|
@media (max-width: 1024px) {
|
|
.app-shell { grid-template-columns: 1fr; }
|
|
.rail {
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 10;
|
|
height: auto;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 10px 14px;
|
|
border-right: 0;
|
|
border-bottom: 1px solid #222932;
|
|
}
|
|
.rail .brand { margin: 0; flex: none; }
|
|
.nav-list { display: flex; gap: 6px; }
|
|
.rail-foot { display: none; }
|
|
.status-strip { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
}
|
|
|
|
@media (max-width: 720px) {
|
|
.main { padding: 18px 16px 40px; }
|
|
.brand span { display: none; }
|
|
.nav-link span { display: none; } /* icon-only nav on small screens */
|
|
.nav-link { min-width: 44px; justify-content: center; padding: 0; }
|
|
h1 { font-size: 22px; }
|
|
.toolbar { grid-template-columns: 1fr; }
|
|
.segmented { overflow-x: auto; }
|
|
.segmented button { flex: 0 0 auto; }
|
|
.card-row { grid-template-columns: auto minmax(0, 1fr); }
|
|
.card-actions { grid-column: 1 / -1; width: 100%; flex-direction: row; }
|
|
.card-actions .row-button { flex: 1; }
|
|
}
|
|
|
|
@media (max-width: 460px) {
|
|
.status-strip { grid-template-columns: 1fr; }
|
|
.switch-row { align-items: flex-start; }
|
|
}
|