mirror of
https://github.com/knutwurst/patchdl.git
synced 2026-10-24 23:01:06 +02:00
Library - Rescan when /user/app or Sony's app.db changes, after an install, and on the Refresh button (which also asks Sony for updates again). Later scans keep a handle to the system root and use relative paths instead of swapping the process root while other threads run; app.db is copied out first because SQLite resolves relative paths with getcwd(). The scan thread gets a 1 MB stack (the default made reads fail with EFAULT). - Take the newer of app.db and appmeta param.json as installed version: app.db lags behind until the game is started. - A package header decides "fake"; the installer's "fake": true marker only says the package didn't come from Sony's CDN, and PatchDL's own installs set it too. - PS4 titles are listed as not supported. Downloads - Fetch the manifest's .crc next to the package; the console asks for it. - Keep the sidecar (with the version) after a download completes, so a restart still knows the package is whole. - A package already on disk without a matching sidecar is hashed against the manifest piece by piece; only pieces that don't match are fetched. Install refuses an unverified package. Installs - One install per title: a second request (another tab, a double click) gets install_running; an installed version gets already_installed. Download, Cancel and Delete wait until the console is done reading. - Progress comes from the bytes PatchDL serves to the console (distinct ranges), since Sony's status query fails for these installs. Phases: waiting, reading with percent, finishing, done or stalled. Each title is tracked on its own. - After an install: check that the installed version matches and the new app.json names this package and its size. With "Delete PKG after install" on, delete the package only then. The setting existed but was never read; the old cleanup trusted app.db alone and deleted packages that were never applied. UI - Settings list every package on the console with size, version and whether it is installed, newer or for a game that's gone, with Delete. The storage tile links there. - The list reloads quietly when the server reports a change. - Bigger covers on wide screens, "Updates on/off" on the switch, no "Shared master" badge, no endless auto-install retries.
844 lines
25 KiB
CSS
844 lines
25 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); }
|
|
.chip-count { margin-left: 4px; color: var(--muted); font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
.chip-count:empty { display: none; }
|
|
|
|
/* ---------------- 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: 64px; height: 64px;
|
|
color: #fff;
|
|
background: linear-gradient(135deg, #2d6d9d, #247857);
|
|
border-radius: 12px;
|
|
font-size: 16px;
|
|
font-weight: 800;
|
|
overflow: hidden;
|
|
}
|
|
.cover img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
|
|
.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-note { flex-basis: 100%; line-height: 1.45; }
|
|
.card-note.is-warn { color: var(--amber); }
|
|
.prospero-line a { color: var(--blue); text-decoration: none; }
|
|
.prospero-line a:hover { text-decoration: underline; }
|
|
|
|
.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.prospero { border-color: rgba(64, 201, 198, 0.38); background: rgba(64, 201, 198, 0.14); color: #6fe0dc; }
|
|
.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.is-indeterminate > i {
|
|
animation: globalDlIndet 1.6s ease-in-out infinite;
|
|
transition: none;
|
|
}
|
|
.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; }
|
|
.lead .toggle .label { white-space: nowrap; }
|
|
|
|
.allowlist, .source-policy, .storage-list { display: grid; gap: 8px; margin-top: 16px; }
|
|
.storage-list > span em { color: var(--muted); font-style: normal; margin-left: 6px; }
|
|
.storage-list > div { display: grid; gap: 8px; }
|
|
.storage-row {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr) auto auto auto;
|
|
gap: 10px;
|
|
align-items: center;
|
|
padding: 10px;
|
|
background: var(--surface-2);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
}
|
|
.storage-row strong { font-size: 13px; overflow-wrap: anywhere; }
|
|
.storage-row em { color: var(--muted); font-style: normal; font-size: 12px; }
|
|
.storage-row b { font-variant-numeric: tabular-nums; }
|
|
.storage-empty { color: var(--muted); font-size: 13px; }
|
|
.link-button {
|
|
padding: 0;
|
|
border: 0;
|
|
background: none;
|
|
color: var(--blue);
|
|
font: inherit;
|
|
cursor: pointer;
|
|
text-decoration: underline;
|
|
text-underline-offset: 2px;
|
|
}
|
|
.link-button:hover { color: var(--ink); }
|
|
@media (max-width: 640px) {
|
|
.storage-row { grid-template-columns: minmax(0, 1fr) auto; }
|
|
}
|
|
.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); }
|
|
|
|
/* ---------------- older patch guide (Prospero) ---------------- */
|
|
|
|
.guide-backdrop {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 15; /* below the toast (20) */
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 560px); /* an auto track would grow to the content */
|
|
justify-content: center;
|
|
align-content: center;
|
|
padding: 24px 16px;
|
|
overflow-y: auto;
|
|
background: rgba(5, 7, 9, 0.72);
|
|
}
|
|
.guide {
|
|
display: grid;
|
|
gap: 16px;
|
|
padding: 22px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 14px;
|
|
background: var(--surface);
|
|
box-shadow: var(--shadow);
|
|
}
|
|
/* minmax(0, 1fr): without it an <input>'s intrinsic width pushes the tracks wider than the sheet */
|
|
.guide, .guide-steps, .guide-steps li { grid-template-columns: minmax(0, 1fr); }
|
|
.guide-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
|
|
.guide-head > div { min-width: 0; }
|
|
.guide-head h2 { margin: 0; font-size: 22px; }
|
|
.guide-game { margin: 4px 0 0; color: var(--muted); }
|
|
.guide-close { flex: none; width: 40px; padding: 0; font-size: 22px; line-height: 1; }
|
|
.guide-why { margin: 0; color: var(--muted); line-height: 1.55; }
|
|
.guide-why b { color: var(--ink); }
|
|
|
|
.guide-qr {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
padding: 14px;
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
background: var(--surface-2);
|
|
}
|
|
.guide-qr .qr { flex: none; width: 148px; height: 148px; border-radius: 6px; }
|
|
.guide-qr > div { display: grid; gap: 6px; min-width: 0; }
|
|
.guide-qr span { color: var(--muted); line-height: 1.5; }
|
|
.guide-qr code { overflow-wrap: anywhere; color: var(--blue-dark); font-size: 13px; }
|
|
|
|
/* Console view: the PS5 can't copy links, so it only hands over to a phone. */
|
|
.guide.is-handoff { width: min(560px, 100%); justify-self: center; text-align: center; }
|
|
.guide.is-handoff .guide-head { text-align: left; }
|
|
.guide-handoff { display: grid; justify-items: center; gap: 12px; }
|
|
.guide-handoff .qr { width: min(420px, 100%); height: auto; aspect-ratio: 1; border-radius: 8px; }
|
|
.guide-handoff p { margin: 0; color: var(--muted); line-height: 1.5; }
|
|
.guide-handoff code { overflow-wrap: anywhere; color: var(--blue-dark); font-size: 16px; }
|
|
|
|
.guide-steps { display: grid; gap: 12px; margin: 0; padding: 0; list-style: none; counter-reset: step; }
|
|
.guide-steps li {
|
|
display: grid;
|
|
gap: 10px;
|
|
padding: 14px 14px 14px 52px;
|
|
position: relative;
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
counter-increment: step;
|
|
}
|
|
.guide-steps li::before {
|
|
content: counter(step);
|
|
position: absolute;
|
|
left: 14px;
|
|
top: 14px;
|
|
width: 26px;
|
|
height: 26px;
|
|
display: grid;
|
|
place-items: center;
|
|
border-radius: 50%;
|
|
background: var(--green-soft);
|
|
color: var(--green-dark);
|
|
font-weight: 800;
|
|
font-size: 13px;
|
|
}
|
|
.guide-step-text { display: grid; gap: 4px; }
|
|
.guide-step-text span { color: var(--muted); line-height: 1.5; }
|
|
.guide-step-text b { color: var(--ink); }
|
|
.guide-open { width: auto; justify-self: start; padding: 0 18px; text-decoration: none; }
|
|
|
|
.guide-paste { display: flex; gap: 8px; }
|
|
.guide-input {
|
|
flex: 1;
|
|
min-width: 0;
|
|
width: 100%;
|
|
height: 44px;
|
|
padding: 0 12px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
background: var(--bg);
|
|
color: var(--ink);
|
|
font-size: 16px; /* below 16px iOS zooms the page on focus */
|
|
}
|
|
.guide-input::placeholder { color: var(--muted); }
|
|
.guide-input:focus { outline: none; border-color: var(--blue); }
|
|
.guide-start { width: auto; flex: none; min-height: 44px; padding: 0 18px; }
|
|
|
|
.guide-status:empty { display: none; }
|
|
.guide-status { display: grid; gap: 8px; }
|
|
.guide-msg { margin: 0; line-height: 1.5; }
|
|
.guide-msg.is-idle, .guide-msg.is-busy { color: var(--muted); }
|
|
.guide-msg.is-ok { color: var(--green-dark); }
|
|
.guide-msg.is-warn { color: var(--amber-dark); }
|
|
.guide-msg.is-error { color: var(--red); }
|
|
.guide-action { width: auto; justify-self: start; padding: 0 16px; }
|
|
.guide-progress { display: grid; gap: 6px; margin-top: 8px; color: var(--muted); font-size: 13px; }
|
|
|
|
@media (max-width: 720px) {
|
|
/* Already on a phone: the hand-off QR has nothing to add. */
|
|
.guide-qr { display: none; }
|
|
.guide-backdrop { align-content: start; padding: 12px; }
|
|
.guide { padding: 16px; }
|
|
}
|
|
|
|
/* ---------------- responsive ---------------- */
|
|
|
|
/* Wide screens (desktop, PS5 browser): bigger covers so a card doesn't look
|
|
empty. */
|
|
@media (min-width: 900px) {
|
|
.game-card { padding: 18px 20px; }
|
|
.cover { width: 104px; height: 104px; border-radius: 14px; font-size: 22px; }
|
|
.card-row { gap: 20px; }
|
|
.card-body h3 { font-size: 19px; margin-bottom: 8px; }
|
|
.subline { font-size: 13px; }
|
|
.versions { font-size: 13px; }
|
|
}
|
|
|
|
@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; }
|
|
}
|