librelad 875a60f90f LibrePortal v0.1.0 — initial release
A free, open, self-hosted app platform (GNU AGPLv3): one-click app deploys,
Traefik reverse proxy with automatic SSL, rootless Docker support, gluetun
VPN routing, and a web dashboard to manage it all.

Free & open forever to self-host; optional paid hosted services fund it.
See PROMISE.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-21 20:37:54 +01:00

1055 lines
28 KiB
CSS
Executable File

/* ──────────────────────────────────────────────────────────────────────
Setup Wizard — multi-step slide-right
Reuses the shared .aurora-bg + .aurora-stars from aurora-background.css
so it shares the loading screen's visual identity. The wizard itself is
a translucent shell over that background, with a horizontal track of
step panels that slides as the user advances.
────────────────────────────────────────────────────────────────────── */
body.setup-wizard-open {
overflow: hidden;
}
.setup-wizard {
position: fixed;
inset: 0;
z-index: 9999;
color: var(--text-primary);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
display: flex;
align-items: center;
justify-content: center;
overflow-y: auto;
padding: 2rem 1.5rem;
opacity: 0;
animation: setupFadeIn 0.6s ease forwards;
box-sizing: border-box;
}
.setup-wizard.hiding {
animation: setupFadeOut 0.5s ease forwards;
}
.setup-wizard.setup-launched .setup-card {
transform: scale(0.96);
opacity: 0.5;
filter: blur(2px);
transition: all 0.5s ease;
}
@keyframes setupFadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes setupFadeOut { from { opacity: 1; } to { opacity: 0; } }
/* Vertical stack: logo header on top, card below — same shape as
`.login-content` in the login overlay so the two surfaces share a
visual identity. The aurora-header / aurora-logo / aurora-subtitle
classes themselves are inherited from aurora-background.css so the
logo treatment is byte-identical to login + loading. */
.setup-content {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
max-width: 620px;
margin: 1rem;
}
/* Header inherits .aurora-header / .aurora-logo sizing from
aurora-background.css so it matches the loading screen byte-for-byte —
single source of truth. The only wizard-specific tweak is a slightly
tighter bottom margin since a card sits below it. */
.setup-content .aurora-header {
margin-bottom: 1.5rem;
}
/* Card — translucent panel matching .login-card / loading screen style */
.setup-card {
width: 100%;
background: rgba(var(--text-rgb), 0.06);
border: 1px solid rgba(var(--text-rgb), 0.12);
border-radius: 14px;
padding: 24px 28px 20px;
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
animation: setupCardRise 0.4s cubic-bezier(0.16, 1, 0.3, 1) 0.05s both;
display: flex;
flex-direction: column;
gap: 20px;
}
@keyframes setupCardRise {
from { transform: translateY(16px) scale(0.97); opacity: 0; }
to { transform: translateY(0) scale(1); opacity: 1; }
}
/* Progress bar — same look as loading screen's progress section */
.setup-progress {
display: flex;
flex-direction: column;
gap: 8px;
}
.setup-progress-bar {
background: rgba(var(--text-rgb), 0.10);
border-radius: 8px;
padding: 2px;
border: 1px solid rgba(var(--text-rgb), 0.08);
height: 12px;
box-sizing: border-box;
overflow: hidden;
}
.setup-progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--accent), var(--accent-hover));
border-radius: 6px;
width: 0%;
transition: width 0.45s cubic-bezier(0.16, 1, 0.3, 1);
position: relative;
overflow: hidden;
}
.setup-progress-fill::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(90deg, transparent, rgba(var(--text-rgb),0.3), transparent);
animation: setupShimmer 2s infinite;
}
@keyframes setupShimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
.setup-progress-text {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: rgba(var(--text-rgb), 0.78);
font-family: 'SF Mono', Menlo, monospace;
letter-spacing: 0.5px;
}
.setup-progress-text > #sw-progress-step {
display: inline-flex;
align-items: center;
gap: 6px;
}
.setup-progress-sep {
color: rgba(var(--text-rgb), 0.20);
margin: 0 2px;
}
.setup-progress-icon {
display: inline-flex;
align-items: center;
color: var(--accent);
}
.setup-progress-name {
color: var(--text-primary);
font-weight: 600;
letter-spacing: 0.3px;
}
/* Step transitions — fade in / fade out. The previous slide-track approach
fought browser flex-basis math whenever step content height varied
(e.g. step 2 reveals the domain field when Public is toggled). Fade is
simpler: only the active step is in the layout, the card naturally
heights to its content, and the swap feels like the wizard "settles
into the next thought" rather than swinging horizontally. */
.setup-track-wrap {
position: relative;
width: 100%;
}
.setup-track {
display: block;
width: 100%;
}
.setup-step {
display: none;
flex-direction: column;
gap: 16px;
}
.setup-step.active {
display: flex;
animation: stepFadeIn 0.32s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes stepFadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.setup-form {
display: flex;
flex-direction: column;
gap: 18px;
}
/* Multi-domain editor — list of removable rows + an "Add domain" button. */
.setup-domain-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.setup-domain-row {
display: flex;
flex-direction: column;
gap: 8px;
}
/* Empty status pill collapses so blank-domain rows don't claim vertical space */
.setup-domain-row .setup-dns-status:empty {
display: none;
}
.setup-domain-row .setup-input-row {
align-items: stretch;
}
.setup-domain-remove {
flex: 0 0 auto;
width: 36px;
background: rgba(var(--text-rgb), 0.04);
border: 1px solid rgba(var(--text-rgb), 0.10);
border-radius: 8px;
color: rgba(var(--text-rgb), 0.65);
font-size: 18px;
font-weight: 500;
cursor: pointer;
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.setup-domain-remove:hover {
background: rgba(var(--status-danger-rgb), 0.18);
border-color: rgba(var(--status-danger-rgb), 0.45);
color: var(--status-danger);
}
.setup-domain-add {
align-self: flex-start;
display: inline-flex;
align-items: center;
gap: 8px;
background: rgba(var(--accent-rgb), 0.10);
border: 1px dashed rgba(var(--accent-rgb), 0.40);
border-radius: 10px;
padding: 8px 14px;
color: var(--accent);
font-size: 13px;
font-weight: 600;
cursor: pointer;
margin-top: 8px;
transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
}
.setup-domain-add:hover {
background: rgba(var(--accent-rgb), 0.20);
border-color: rgba(var(--accent-rgb), 0.65);
border-style: solid;
transform: translateY(-1px);
}
.setup-domain-add span {
font-size: 16px;
line-height: 1;
}
.setup-step-note {
font-size: 12px;
color: rgba(var(--text-rgb), 0.65);
margin: 12px 0 0;
font-style: italic;
}
.setup-section-hint {
font-size: 12px;
color: rgba(var(--text-rgb), 0.62);
margin: 0 0 10px;
}
/* Field styling — matches the login form's compact subtle look:
small lowercase labels, translucent inputs with cyan focus glow. */
.setup-field {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.setup-field label {
font-size: 0.75rem;
font-weight: 500;
color: var(--text-secondary);
letter-spacing: 0.02em;
text-transform: none;
margin: 0;
}
.setup-field input[type=text],
.setup-field input[type=email],
.setup-field select {
width: 100%;
background: rgba(var(--text-rgb), 0.06);
border: 1px solid rgba(var(--text-rgb), 0.12);
border-radius: 8px;
padding: 0.6rem 0.875rem;
color: var(--text-primary);
font-size: 0.95rem;
font-family: inherit;
box-sizing: border-box;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
}
.setup-field input[type=text]:focus,
.setup-field input[type=email]:focus,
.setup-field select:focus {
outline: none;
background: rgba(var(--text-rgb), 0.10);
border-color: rgba(var(--accent-rgb), 0.55);
box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.18);
}
.setup-field input::placeholder {
color: var(--text-secondary);
}
/* Live validation — green border + glow when valid, red when invalid.
The error message is held on data-error and surfaced via a tooltip
that floats ABOVE the input on focus/hover (like the ? badge does).
Uses a bright mint #86efac (134,239,172) so the border reads against
the dark wizard backdrop — the theme's #28a745 is too muddy here. */
.setup-field input.is-valid,
.setup-field select.is-valid {
border-color: #86efac;
box-shadow: 0 0 0 3px rgba(134, 239, 172, 0.22);
}
.setup-field input.is-invalid,
.setup-field select.is-invalid {
border-color: rgba(var(--status-danger-rgb), 0.85);
box-shadow: 0 0 0 3px rgba(var(--status-danger-rgb), 0.22);
}
/* Mirror the valid/invalid state onto the custom-select button when the
native <select> has been enhanced. The native element keeps the .is-*
class (and stays in the DOM hidden) so :has() still resolves. */
.setup-input-row:has(select.is-valid) .custom-select-button {
border-color: #86efac;
box-shadow: 0 0 0 3px rgba(134, 239, 172, 0.22);
}
.setup-input-row:has(select.is-invalid) .custom-select-button {
border-color: rgba(var(--status-danger-rgb), 0.85);
box-shadow: 0 0 0 3px rgba(var(--status-danger-rgb), 0.22);
}
/* Make room for the left field icon on the themed dropdown button. */
.setup-input-row > .custom-select .custom-select-button {
padding-left: 2.25rem;
}
/* The custom-select button has its own chevron + we paint the validity
state via its border, so the right-side ✓/✕ badge is redundant. */
.setup-input-row:has(select.custom-select-native)::after {
display: none;
}
/* Valid checkmark + invalid cross indicator on the right side of the input */
.setup-input-row::after {
content: '';
position: absolute;
right: 14px;
top: 50%;
transform: translateY(-50%);
width: 16px;
height: 16px;
background-repeat: no-repeat;
background-position: center;
background-size: contain;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease;
}
.setup-input-row:has(.is-valid)::after {
opacity: 1;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2348c774' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'><polyline points='20 6 9 17 4 12'/></svg>");
}
.setup-input-row:has(.is-invalid)::after {
opacity: 1;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ef4444' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'><line x1='18' y1='6' x2='6' y2='18'/><line x1='6' y1='6' x2='18' y2='18'/></svg>");
}
.setup-input-row:has(.is-valid) .setup-input-with-icon,
.setup-input-row:has(.is-invalid) .setup-input-with-icon {
padding-right: 2.5rem !important;
}
/* Shift the tick/cross left when sitting on a <select> — the browser's
native chevron occupies the right edge, so right:14px collides with it. */
.setup-input-row:has(select.is-valid)::after,
.setup-input-row:has(select.is-invalid)::after {
right: 32px;
}
.setup-input-row:has(select.is-valid) .setup-input-with-icon,
.setup-input-row:has(select.is-invalid) .setup-input-with-icon {
padding-right: 3.5rem !important;
}
/* Error message floating above the input — same visual language as the
? tooltip but anchored to the input row. We read the message from
data-error which the JS mirrors from the input onto the row (pseudo
elements can only read attrs from their own host element). */
.setup-input-row[data-error]::before {
content: attr(data-error);
position: absolute;
bottom: calc(100% + 8px);
left: 14px;
background: rgba(var(--bg-rgb), 0.45);
color: var(--status-danger);
font-size: 0.72rem;
font-weight: 500;
letter-spacing: 0;
padding: 7px 10px;
border-radius: 8px;
border: 1px solid rgba(var(--status-danger-rgb), 0.45);
white-space: nowrap;
pointer-events: none;
opacity: 0;
z-index: 11;
transition: opacity 0.18s ease, transform 0.18s ease;
transform: translateY(4px);
}
.setup-input-row[data-error]:hover::before,
.setup-input-row[data-error]:focus-within::before {
opacity: 1;
transform: translateY(0);
}
/* Input with leading icon — icon sits absolutely positioned over the
left padding zone of the input. Reroll button (when present) sits to
the right of the input via the row's flex layout. */
.setup-input-row {
position: relative;
display: flex;
gap: 8px;
align-items: stretch;
}
.setup-field-icon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
display: flex;
align-items: center;
justify-content: center;
color: var(--accent);
pointer-events: none;
z-index: 1;
transition: color 0.2s ease, filter 0.2s ease;
}
.setup-input-row .setup-input-with-icon:focus ~ .setup-field-icon,
.setup-input-row:focus-within .setup-field-icon {
color: var(--accent);
}
.setup-input-with-icon {
flex: 1;
padding-left: 2.25rem !important;
}
.setup-field-icon svg { display: block; }
.setup-field-icon-emoji {
font-size: 16px;
line-height: 1;
}
.setup-input-row:focus-within .setup-field-icon-emoji {
}
#sw-name.setup-input-with-icon {
font-family: 'SF Mono', Menlo, monospace;
letter-spacing: 0.3px;
color: var(--accent);
padding-right: 7.75rem !important;
}
.setup-input-row:has(#sw-name.is-valid) #sw-name,
.setup-input-row:has(#sw-name.is-invalid) #sw-name {
padding-right: 7.75rem !important;
}
.setup-input-row:has(#sw-name)::after {
display: none;
}
/* Tooltip "?" badge after the label. Hover or keyboard-focus reveals
a small floating tip with the description text. */
.setup-tooltip {
display: inline-flex;
align-items: center;
justify-content: center;
width: 14px;
height: 14px;
border-radius: 50%;
background: rgba(var(--accent-rgb), 0.15);
color: var(--accent);
font-size: 0.65rem;
font-weight: 700;
margin-left: 6px;
cursor: help;
position: relative;
user-select: none;
vertical-align: middle;
border: 1px solid rgba(var(--accent-rgb), 0.35);
transition: background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
}
.setup-tooltip:hover,
.setup-tooltip:focus {
outline: none;
background: rgba(var(--accent-rgb), 0.30);
color: var(--text-primary);
}
.setup-tooltip::after {
content: attr(data-tip);
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%) translateY(4px);
background: rgba(var(--bg-rgb), 0.45);
color: var(--text-primary);
font-size: 0.72rem;
font-weight: 400;
letter-spacing: 0;
text-transform: none;
padding: 8px 10px;
border-radius: 8px;
border: 1px solid rgba(var(--accent-rgb), 0.40);
width: max-content;
max-width: 240px;
white-space: normal;
text-align: left;
line-height: 1.35;
pointer-events: none;
opacity: 0;
transition: opacity 0.15s ease, transform 0.15s ease;
z-index: 10;
}
.setup-tooltip::before {
content: '';
position: absolute;
bottom: calc(100% + 2px);
left: 50%;
transform: translateX(-50%);
border: 5px solid transparent;
border-top-color: rgba(var(--accent-rgb), 0.55);
pointer-events: none;
opacity: 0;
transition: opacity 0.15s ease;
}
.setup-tooltip:hover::after,
.setup-tooltip:focus::after {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
.setup-tooltip:hover::before,
.setup-tooltip:focus::before {
opacity: 1;
}
.setup-name-pulse {
animation: setupNamePulse 0.6s ease;
}
@keyframes setupNamePulse {
0% { box-shadow: 0 0 0 0 rgba(var(--accent-rgb), 0.55); }
60% { box-shadow: 0 0 0 12px rgba(var(--accent-rgb), 0); }
100% { box-shadow: 0 0 0 0 rgba(var(--accent-rgb), 0); }
}
.setup-manifest {
position: absolute;
right: 6px;
top: 50%;
transform: translateY(-50%);
height: calc(100% - 12px);
z-index: 2;
background: rgba(var(--accent-rgb), 0.12);
color: var(--accent);
border: 1px solid rgba(var(--accent-rgb), 0.32);
border-radius: 8px;
padding: 0 12px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 8px;
transition: background 0.18s ease, border-color 0.18s ease, transform 0.15s ease, box-shadow 0.18s ease, color 0.18s ease;
white-space: nowrap;
}
.setup-manifest .setup-manifest-icon {
color: var(--accent);
transition: transform 0.6s cubic-bezier(0.16, 1, 0.3, 1), color 0.2s ease, filter 0.2s ease;
}
.setup-manifest:hover {
background: rgba(var(--accent-rgb), 0.22);
border-color: rgba(var(--accent-rgb), 0.55);
color: var(--text-primary);
transform: translateY(calc(-50% - 1px));
}
.setup-manifest:hover .setup-manifest-icon {
color: var(--accent);
}
/* Click animation: full-spin icon + cosmic burst halo around the button */
.setup-manifest.manifesting {
animation: manifestBurst 0.7s ease;
}
.setup-manifest.manifesting .setup-manifest-icon {
transform: rotate(360deg);
}
@keyframes manifestBurst {
0% { box-shadow: 0 0 0 0 rgba(var(--accent-rgb), 0.75), 0 0 0 0 rgba(var(--accent-rgb), 0.55); }
60% { box-shadow: 0 0 0 14px rgba(var(--accent-rgb), 0), 0 0 0 28px rgba(var(--accent-rgb), 0); }
100% { box-shadow: 0 0 0 0 rgba(var(--accent-rgb), 0), 0 0 0 0 rgba(var(--accent-rgb), 0); }
}
/* DNS check status */
.setup-dns-status {
font-size: 12px;
margin-top: 8px;
padding: 6px 10px;
border-radius: 6px;
font-family: 'SF Mono', Menlo, monospace;
min-height: 14px;
}
.setup-dns-status.checking {
background: rgba(var(--text-rgb), 0.05);
color: rgba(var(--text-rgb), 0.6);
}
.setup-dns-status.ok {
background: rgba(var(--status-success-rgb), 0.12);
color: var(--status-success);
border: 1px solid rgba(var(--status-success-rgb), 0.3);
}
.setup-dns-status.warn {
background: rgba(var(--status-warning-rgb), 0.10);
color: var(--status-warning);
border: 1px solid rgba(var(--status-warning-rgb), 0.3);
}
/* App selection sections */
.setup-section {
border-top: 1px solid rgba(var(--text-rgb), 0.06);
padding-top: 14px;
}
.setup-section:first-child { border-top: none; padding-top: 0; }
.setup-section-title {
font-size: 12px;
font-weight: 700;
letter-spacing: 1.5px;
text-transform: uppercase;
color: rgba(var(--accent-rgb), 1);
margin-bottom: 10px;
}
.setup-app {
display: flex;
align-items: center;
gap: 14px;
padding: 12px 14px;
background: rgba(var(--text-rgb), 0.03);
border: 1px solid rgba(var(--text-rgb), 0.08);
border-radius: 12px;
cursor: pointer;
margin-bottom: 8px;
transition: all 0.15s ease;
}
.setup-app:hover {
background: rgba(var(--text-rgb), 0.06);
border-color: rgba(var(--accent-rgb), 0.25);
}
.setup-app input[type=checkbox] {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
flex-shrink: 0;
cursor: pointer;
border-radius: 6px;
background: rgba(var(--text-rgb), 0.04);
border: 1.5px solid rgba(var(--text-rgb), 0.18);
box-shadow: inset 0 0 0 1px rgba(var(--text-rgb), 0.02);
position: relative;
transition: background 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease, transform 0.12s ease;
}
.setup-app:hover input[type=checkbox] {
border-color: rgba(var(--accent-rgb), 0.55);
box-shadow: 0 0 0 4px rgba(var(--accent-rgb), 0.08);
}
.setup-app input[type=checkbox]:focus-visible {
outline: none;
border-color: rgba(var(--accent-rgb), 0.85);
box-shadow: 0 0 0 4px rgba(var(--accent-rgb), 0.20);
}
.setup-app input[type=checkbox]:checked {
background: linear-gradient(135deg, var(--accent), var(--accent));
border-color: rgba(var(--accent-rgb), 0.9);
box-shadow: 0 0 0 1px rgba(var(--accent-rgb), 0.35);
}
.setup-app input[type=checkbox]:checked::after {
content: '';
position: absolute;
inset: 0;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3.2' stroke-linecap='round' stroke-linejoin='round'><polyline points='20 6 9 17 4 12'/></svg>");
background-repeat: no-repeat;
background-position: center;
background-size: 14px 14px;
animation: setupCheckPop 0.22s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes setupCheckPop {
0% { transform: scale(0.4); opacity: 0; }
100% { transform: scale(1); opacity: 1; }
}
.setup-app:has(input:checked) {
background: rgba(var(--accent-rgb), 0.10);
border-color: rgba(var(--accent-rgb), 0.45);
}
.setup-app-icon-wrap {
width: 36px;
height: 36px;
border-radius: 9px;
background: rgba(var(--text-rgb), 0.06);
border: 1px solid rgba(var(--text-rgb), 0.08);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
overflow: hidden;
}
.setup-app-icon-wrap .setup-app-icon {
width: 24px;
height: 24px;
object-fit: contain;
}
.setup-app:has(input:checked) .setup-app-icon-wrap {
background: rgba(var(--accent-rgb), 0.20);
border-color: rgba(var(--accent-rgb), 0.45);
}
.setup-app-info { flex: 1; min-width: 0; }
.setup-app-name { font-size: 14px; font-weight: 600; color: var(--text-primary); }
.setup-app-desc { font-size: 12px; color: rgba(var(--text-rgb), 0.82); margin-top: 2px; }
/* Parent tile + sub-option as one merged card. Parent loses its bottom
radius; sub-option is a flush drawer below with only the bottom corners
rounded. Shared horizontal bounds so the two pieces read as one. */
.setup-app-group { margin-bottom: 8px; }
.setup-app-group .setup-app {
margin-bottom: 0;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
border-bottom-color: rgba(var(--text-rgb), 0.04);
}
.setup-app-suboption {
display: flex;
align-items: center;
gap: 10px;
/* Left padding lines the sub-checkbox up under the parent's checkbox
(parent: 14px padding + ~3px to centre the smaller 14px box). */
padding: 7px 14px 8px 17px;
margin: 0;
background: rgba(var(--text-rgb), 0.035);
border: 1px solid rgba(var(--text-rgb), 0.08);
border-top: none;
border-radius: 0 0 12px 12px;
font-size: 12px;
color: rgba(var(--text-rgb), 0.82);
cursor: pointer;
transition: all 0.15s ease;
}
/* When the parent is selected, the drawer picks up the same blue tint so
the merged card reads as one selected unit. */
.setup-app-group:has(.setup-app input[type=checkbox]:checked) .setup-app {
border-bottom-color: rgba(var(--accent-rgb), 0.30);
}
.setup-app-group:has(.setup-app input[type=checkbox]:checked) .setup-app-suboption {
background: rgba(var(--accent-rgb), 0.08);
border-color: rgba(var(--accent-rgb), 0.40);
border-top: none;
}
.setup-app-suboption:hover {
background: rgba(var(--accent-rgb), 0.12);
}
.setup-app-suboption.disabled {
opacity: 0.35;
pointer-events: none;
}
.setup-app-suboption input[type=checkbox] {
-webkit-appearance: none;
appearance: none;
width: 14px;
height: 14px;
flex-shrink: 0;
cursor: pointer;
border-radius: 4px;
background: rgba(var(--text-rgb), 0.04);
border: 1.4px solid rgba(var(--text-rgb), 0.20);
position: relative;
transition: background 0.15s ease, border-color 0.15s ease;
}
.setup-app-suboption:hover input[type=checkbox] {
border-color: rgba(var(--accent-rgb), 0.55);
}
.setup-app-suboption input[type=checkbox]:checked {
background: linear-gradient(135deg, var(--accent), var(--accent));
border-color: rgba(var(--accent-rgb), 0.9);
}
.setup-app-suboption input[type=checkbox]:checked::after {
content: '';
position: absolute;
inset: 0;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3.2' stroke-linecap='round' stroke-linejoin='round'><polyline points='20 6 9 17 4 12'/></svg>");
background-repeat: no-repeat;
background-position: center;
background-size: 10px 10px;
}
.setup-app-suboption-label { font-weight: 500; }
/* Navigation */
.setup-nav {
display: flex;
gap: 10px;
margin-top: 6px;
}
.setup-btn-back,
.setup-btn-next {
background: rgba(var(--text-rgb), 0.06);
color: var(--text-primary);
border: 1px solid rgba(var(--text-rgb), 0.12);
border-radius: 12px;
padding: 14px 18px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s ease;
}
.setup-btn-back { flex: 0 0 auto; }
.setup-btn-next { flex: 1; }
.setup-btn-back:hover:not(:disabled),
.setup-btn-next:hover:not(:disabled) {
background: rgba(var(--accent-rgb), 0.14);
border-color: rgba(var(--accent-rgb), 0.40);
transform: translateY(-1px);
}
.setup-btn-back:disabled {
opacity: 0.35;
cursor: not-allowed;
}
.setup-launch {
flex: 1;
background: linear-gradient(135deg, var(--accent), var(--accent-hover));
border: none;
border-radius: 12px;
padding: 14px 18px;
color: var(--text-primary);
font-size: 15px;
font-weight: 700;
letter-spacing: 0.5px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
transition: all 0.2s ease;
}
.setup-launch:hover:not(:disabled) {
transform: translateY(-2px);
filter: brightness(1.05);
}
.setup-launch:active:not(:disabled) { transform: translateY(0); }
.setup-launch:disabled { opacity: 0.6; cursor: not-allowed; }
.setup-launch-arrow { transition: transform 0.2s ease; }
.setup-launch:hover:not(:disabled) .setup-launch-arrow { transform: translateX(4px); }
/* Error */
.setup-error {
background: rgba(var(--status-danger-rgb), 0.10);
border: 1px solid rgba(var(--status-danger-rgb), 0.3);
color: var(--status-danger);
padding: 10px 14px;
border-radius: 10px;
font-size: 13px;
}
/* Top-nav disabled state — applied while setup isn't complete. */
.topbar-nav.setup-needed .nav-item {
opacity: 0.35;
pointer-events: none;
filter: grayscale(60%);
}
/* Setup-in-progress banner — pinned to top of viewport while the wizard's
tasks are still running (any page). Auto-removed when finalize completes. */
.setup-progress-banner {
position: fixed;
top: 14px;
left: 50%;
transform: translateX(-50%);
z-index: 9000;
background: rgba(var(--bg-rgb), 0.45);
border: 1px solid rgba(var(--accent-rgb), 0.40);
border-radius: 12px;
padding: 10px 16px;
color: var(--text-primary);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 13px;
min-width: 320px;
max-width: min(520px, 92vw);
animation: setupBannerIn 0.4s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.setup-progress-banner.leaving {
animation: setupBannerOut 0.35s ease both;
}
.setup-progress-banner.failed {
border-color: rgba(var(--status-danger-rgb), 0.5);
}
.setup-progress-banner-inner {
display: grid;
grid-template-columns: 18px 1fr;
grid-template-rows: auto auto;
column-gap: 12px;
align-items: center;
}
.setup-progress-banner-icon {
grid-row: 1 / span 2;
color: var(--accent);
display: flex;
align-items: center;
justify-content: center;
}
.setup-progress-banner.failed .setup-progress-banner-icon {
color: var(--status-danger);
}
.setup-progress-banner-text {
grid-column: 2;
grid-row: 1;
letter-spacing: 0.2px;
}
.setup-progress-banner-text strong {
font-weight: 600;
color: var(--text-primary);
}
.setup-progress-banner-count {
color: rgba(var(--text-rgb), 0.78);
font-family: 'SF Mono', Menlo, monospace;
font-size: 12px;
margin-left: 4px;
}
.setup-progress-banner-bar {
grid-column: 2;
grid-row: 2;
height: 4px;
background: rgba(var(--text-rgb), 0.08);
border-radius: 999px;
overflow: hidden;
margin-top: 6px;
}
.setup-progress-banner-fill {
height: 100%;
width: 0%;
background: linear-gradient(90deg, var(--accent), var(--accent-hover));
border-radius: 999px;
transition: width 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}
.setup-progress-banner.failed .setup-progress-banner-fill {
background: linear-gradient(90deg, var(--status-danger), var(--status-danger-hover));
}
@keyframes setupBannerIn {
from { opacity: 0; transform: translate(-50%, -16px); }
to { opacity: 1; transform: translate(-50%, 0); }
}
@keyframes setupBannerOut {
from { opacity: 1; transform: translate(-50%, 0); }
to { opacity: 0; transform: translate(-50%, -16px); }
}
@media (max-width: 600px) {
.setup-shell { padding: 22px 18px 18px; border-radius: 14px; }
.setup-logo h1 { font-size: 20px; }
.setup-input-row { flex-direction: column; }
.setup-input-row .setup-field-icon { top: 22px; transform: none; }
.setup-input-row .setup-input-with-icon { padding-left: 2.25rem !important; }
.setup-reroll { padding: 10px; }
.setup-nav { flex-direction: column; }
.setup-btn-back { order: 2; }
.setup-btn-next, .setup-launch { order: 1; }
}