Files
Knutwurst 23e4dda1e2 Redesign game cards and the Other versions dialog
Cards show installed, update and newest version side by side, and
problems as notes with their action. Other versions became a button.
The dialog groups versions into available, needs newer firmware,
installed and older (folded), with larger type throughout. Text is at
least 13px, buttons 44px tall, and every control has a focus ring.
Phones get two status tiles per row and full-width card content.
2026-10-17 15:19:00 +02:00

1092 lines
34 KiB
CSS

:root {
color-scheme: dark;
--bg: #0a0c0f;
--surface: #13171c;
--surface-2: #1a1f26;
--surface-3: #212731;
--ink: #eef2f6;
--muted: #a4afba;
--border: #262d36;
--border-strong: #36404b;
--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: 12px;
--focus: 0 0 0 3px rgba(88, 166, 255, 0.55);
--fs-xs: 13px;
--fs-sm: 14px;
--fs-md: 16px;
--ease: cubic-bezier(0.2, 0.8, 0.2, 1);
--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);
font-size: var(--fs-md);
line-height: 1.5;
-webkit-text-size-adjust: 100%;
}
/* One focus ring for everything the controller, keyboard or a phone can
reach. */
:where(button, a, summary, select, input, textarea, [tabindex]):focus-visible {
outline: none;
box-shadow: var(--focus);
}
.visually-hidden {
position: absolute; width: 1px; height: 1px; margin: -1px;
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
}
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: var(--fs-xs); }
.brand .brand-by { display: inline; margin-top: 0; }
.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: var(--fs-xs);
}
.rail-foot span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rail-foot .rail-donate { justify-self: start; margin-top: 4px; }
.donate-row {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-top: 16px;
padding: 12px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface-2);
color: var(--muted);
}
.donate-row .row-button { width: auto; padding: 0 16px; }
.diag-body { display: grid; gap: 14px; }
.diag-list { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; }
.diag-row { display: grid; grid-template-columns: 22px minmax(0, 1fr); gap: 8px; align-items: start; }
.diag-row > b { font-size: 16px; }
.diag-row.is-ok > b { color: var(--green); }
.diag-row.is-bad > b { color: var(--red); }
.diag-row em { color: var(--muted); font-style: normal; font-size: var(--fs-sm); }
.diag-report {
width: 100%;
box-sizing: border-box;
padding: 10px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface-2);
color: var(--ink);
font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace;
resize: vertical;
}
.diag-copy { justify-self: start; width: auto; padding: 0 18px; }
.donate-open { display: inline-flex; align-items: center; justify-content: center; text-decoration: none; justify-self: start; padding: 0 18px; }
/* ---------------- 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: 12px;
font-weight: 800;
letter-spacing: 0.06em;
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: 44px;
border: 1px solid var(--border);
background: var(--surface);
color: var(--ink);
border-radius: 10px;
font-weight: 700;
font-size: var(--fs-sm);
transition: background 150ms ease, border-color 150ms ease, color 150ms ease, transform 90ms ease;
-webkit-tap-highlight-color: transparent;
}
.icon-button:hover { border-color: var(--border-strong); background: var(--surface-2); }
.icon-button:active, .primary-button:active, .row-button:not(:disabled):active { transform: scale(0.97); }
.icon-button { width: 44px; 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: 12px; 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: var(--fs-xs); 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: var(--fs-xs); 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: var(--fs-xs);
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);
transition: border-color 150ms ease;
}
.game-card:hover { border-color: var(--border-strong); }
.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: 16px;
align-items: start;
}
.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: 72px; height: 72px;
color: #fff;
background: linear-gradient(135deg, #2d6d9d, #247857);
border-radius: 12px;
font-size: 18px;
font-weight: 800;
overflow: hidden;
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.06), 0 6px 18px rgba(0, 0, 0, 0.35);
}
.cover img { width: 100%; height: 100%; object-fit: cover; display: block; }
.card-body { min-width: 0; display: grid; gap: 10px; align-content: start; }
.card-body h3 { margin: 0; overflow-wrap: anywhere; font-size: 18px; line-height: 1.25; }
.tid {
color: var(--muted);
font-size: var(--fs-xs);
font-variant-numeric: tabular-nums;
letter-spacing: 0.02em;
margin-right: 2px;
}
.pills { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
/* Installed -> update -> newest, read left to right. */
.vtrack {
display: flex;
flex-wrap: wrap;
align-items: stretch;
gap: 8px;
}
.vstep {
display: grid;
align-content: start;
gap: 2px;
min-width: 0;
padding: 8px 12px;
border: 1px solid var(--border);
border-radius: 10px;
background: var(--surface-2);
}
.vstep > span {
color: var(--muted);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.vstep > b { font-size: var(--fs-md); font-variant-numeric: tabular-nums; }
.vstep > em { color: var(--muted); font-size: var(--fs-xs); font-style: normal; }
.vstep.is-target { border-color: rgba(56, 193, 114, 0.45); background: var(--green-soft); }
.vstep.is-target > span { color: var(--green-dark); }
.vstep.is-dim { background: transparent; border-style: dashed; }
.vstep.is-dim > b { color: var(--muted); }
.vtrack-arrow { display: grid; place-items: center; color: var(--muted); }
.vtrack-arrow svg { width: 20px; height: 20px; }
/* A note that belongs to a card or dialog: icon, text, optional action. */
.callout {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: 6px 10px;
padding: 10px 12px;
border: 1px solid var(--border);
border-radius: 10px;
background: var(--surface-2);
color: var(--ink);
font-size: var(--fs-sm);
line-height: 1.45;
}
.callout > svg { flex: none; margin-top: 1px; color: var(--green-dark); }
.callout > span { flex: 1 1 220px; min-width: 0; }
.callout .link-button { flex: none; font-weight: 700; }
.callout.is-warn { border-color: rgba(242, 184, 75, 0.38); background: var(--amber-soft); }
.callout.is-warn > svg { color: var(--amber); }
.callout.is-quiet { color: var(--muted); }
.spinner {
flex: none;
width: 16px; height: 16px; margin-top: 2px;
border: 2px solid var(--border-strong);
border-top-color: var(--green);
border-radius: 50%;
animation: spin 0.9s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.card-actions {
display: flex;
flex-direction: column;
gap: 8px;
flex: none;
width: 168px;
}
.pill {
display: inline-flex;
align-items: center;
min-height: 26px;
padding: 0 10px;
border: 1px solid var(--border);
background: var(--surface-2);
color: var(--muted);
border-radius: 999px;
font-size: var(--fs-xs);
font-weight: 700;
}
.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;
font-size: 15px;
}
/* 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(--ink); border-color: var(--border-strong); }
.row-button.is-ghost:hover { background: var(--surface-2); border-color: var(--muted); }
.row-button.is-ghost svg { 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: progressSweep 1.6s linear infinite; /* duration = INDET_MS */
transition: none;
}
@keyframes progressSweep {
from { transform: translateX(-100%); }
to { transform: translateX(320%); }
}
@media (prefers-reduced-motion: reduce) {
.progress.is-indeterminate > i { animation: none; width: 100% !important; opacity: 0.5; }
}
.progress-meta {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 4px 14px;
color: var(--muted);
font-size: var(--fs-xs);
}
.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: var(--fs-xs); 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: var(--fs-xs); 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: var(--fs-xs); 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: var(--fs-sm); overflow-wrap: anywhere; }
.storage-row em { color: var(--muted); font-style: normal; font-size: var(--fs-xs); }
.storage-row b { font-variant-numeric: tabular-nums; }
.storage-empty { color: var(--muted); font-size: var(--fs-sm); }
.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: var(--fs-sm); }
.policy-row em { overflow: hidden; color: var(--muted); font-size: var(--fs-xs); 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: var(--fs-xs);
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: var(--fs-xs);
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.74);
-webkit-backdrop-filter: blur(6px);
backdrop-filter: blur(6px);
animation: fadeIn 160ms ease-out;
}
@keyframes fadeIn { from { opacity: 0; } }
@keyframes sheetIn { from { opacity: 0; transform: translateY(10px) scale(0.985); } }
.guide {
display: grid;
gap: 16px;
padding: 22px;
border: 1px solid var(--border);
border-radius: 16px;
background: var(--surface);
box-shadow: var(--shadow);
animation: sheetIn 220ms var(--ease);
}
@media (prefers-reduced-motion: reduce) {
.guide, .guide-backdrop { animation: none; }
}
/* Dialog with a long list: the header stays, the list scrolls. */
.guide-backdrop:has(.is-sheet) { grid-template-columns: minmax(0, 620px); }
.guide.is-sheet {
grid-template-rows: auto minmax(0, 1fr);
max-height: calc(100dvh - 48px);
padding: 0;
gap: 0;
overflow: hidden;
}
.is-sheet .guide-head { padding: 20px 22px 16px; border-bottom: 1px solid var(--border); }
.sheet-body { display: grid; gap: 18px; align-content: start; padding: 18px 22px 22px; overflow-y: auto; overscroll-behavior: contain; }
.sheet-title { display: flex; align-items: center; gap: 14px; min-width: 0; }
.sheet-title > div { min-width: 0; }
.cover.cover-sm { width: 56px; height: 56px; flex: none; border-radius: 10px; font-size: 16px; }
.vgroup { display: grid; gap: 8px; }
.vgroup h3 {
display: flex; align-items: center; gap: 8px;
margin: 0;
color: var(--muted);
font-size: 12px;
font-weight: 800;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.vgroup h3 span {
min-width: 22px; padding: 1px 7px; border-radius: 999px;
background: var(--surface-3); color: var(--ink);
font-size: 12px; letter-spacing: 0; text-align: center;
}
.vlist { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; }
.vrow {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 10px 14px;
padding: 12px 14px;
border: 1px solid var(--border);
border-radius: 12px;
background: var(--surface-2);
}
.vrow-main { display: grid; gap: 2px; min-width: 0; }
.vrow-main strong { font-size: 18px; font-variant-numeric: tabular-nums; letter-spacing: 0.01em; }
.vrow-main span { color: var(--muted); font-size: var(--fs-sm); font-variant-numeric: tabular-nums; }
.vrow .row-button { width: auto; padding: 0 18px; }
.vrow.is-ok { border-color: rgba(56, 193, 114, 0.35); }
.vrow.is-ok:hover { background: var(--surface-3); }
.vrow.is-needs_fw .vrow-main strong { color: var(--muted); }
.vrow.is-installed { background: transparent; border-style: dashed; }
.vrow.is-older { padding: 8px 14px; background: transparent; }
.vrow.is-older .vrow-main strong { font-size: var(--fs-md); color: var(--muted); }
.vbadge {
display: inline-flex; align-items: center; gap: 6px;
min-height: 30px; padding: 0 12px;
border: 1px solid var(--border); border-radius: 999px;
font-size: var(--fs-xs); font-weight: 700; white-space: nowrap;
}
.vbadge svg { width: 15px; height: 15px; }
.vbadge.is-warn { border-color: rgba(242, 184, 75, 0.38); background: var(--amber-soft); color: var(--amber); }
.vbadge.is-ok { border-color: rgba(56, 193, 114, 0.34); background: var(--green-soft); color: var(--green-dark); }
.volder summary {
display: inline-flex; align-items: center; gap: 6px;
min-height: 40px; padding: 0 10px 0 4px;
border-radius: 8px;
color: var(--muted); font-size: var(--fs-sm); font-weight: 700;
cursor: pointer; list-style: none;
}
.volder summary::-webkit-details-marker { display: none; }
.volder summary:hover { color: var(--ink); }
.volder summary svg { transition: transform 150ms ease; }
.volder[open] summary svg { transform: rotate(90deg); }
.sheet-source { margin: 0; color: var(--muted); font-size: var(--fs-sm); }
.sheet-source a { display: inline-flex; align-items: center; gap: 4px; color: var(--blue-dark); text-decoration: none; }
.sheet-source a:hover { text-decoration: underline; }
.sheet-source svg { width: 14px; height: 14px; }
.vskeleton { display: grid; gap: 8px; }
.vskeleton i {
display: block; height: 62px; border-radius: 12px;
background: linear-gradient(90deg, var(--surface-2) 0%, var(--surface-3) 50%, var(--surface-2) 100%);
background-size: 200% 100%;
animation: shimmer 1.2s ease-in-out infinite;
}
@keyframes shimmer { from { background-position: 100% 0; } to { background-position: -100% 0; } }
@media (prefers-reduced-motion: reduce) { .vskeleton i, .spinner { animation: none; } }
/* 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: 44px; padding: 0; }
.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: var(--fs-sm); }
/* 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: var(--fs-sm);
}
.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: var(--fs-sm); }
@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: 20px; }
}
@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; }
/* The card body's parts join the card's grid, so the versions and notes
use the full width under the cover instead of a narrow column. */
.card-row { grid-template-columns: auto minmax(0, 1fr); column-gap: 14px; row-gap: 10px; }
.card-body { display: contents; }
.lead { grid-row: span 2; }
.card-body h3 { align-self: end; }
.card-body .pills { align-self: start; }
.vtrack, .card-body .callout, .card-actions { grid-column: 1 / -1; }
.cover { width: 64px; height: 64px; }
.card-actions { width: 100%; flex-direction: row; flex-wrap: wrap; }
.card-actions .row-button { flex: 1 1 140px; }
.vstep { flex: 1 1 110px; }
.global-dl-name { order: 3; flex-basis: 100%; }
.guide.is-sheet { max-height: none; }
.sheet-body { padding: 16px; }
.is-sheet .guide-head { padding: 16px; }
}
@media (max-width: 460px) {
.status-strip { gap: 8px; }
.metric { padding: 12px; }
.metric strong { font-size: 17px; }
.topbar .eyebrow { display: none; }
.switch-row { align-items: flex-start; }
.vrow { flex-wrap: nowrap; padding: 12px; }
.vrow .row-button { padding: 0 14px; }
.vrow .row-button svg { display: none; }
}