librelad 7b786aae45 ux(setup): align dev strip content with the cards above
Bump the dev strip's horizontal padding 14px -> 18px to match
.setup-level-card's content inset, so the strip icon/text sit on the
same left edge as the card titles above it instead of ~4px inboard.
Padding sides (not an icon margin) keeps the whole row aligned and
leaves the top/bottom entrance animation untouched.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 14:27:48 +01:00

1203 lines
33 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; }
}
/* ============================================================
Step 1 — Experience choice (Beginner vs Advanced).
Two big tap targets so the choice feels deliberate; the
chosen card lights up with the project accent.
============================================================ */
.setup-level-field { gap: 12px; display: flex; flex-direction: column; }
.setup-level-note { margin: 0; }
.setup-level-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 14px;
margin-top: 6px;
}
.setup-level-card {
position: relative;
padding: 18px 18px 16px;
background: rgba(var(--text-rgb), 0.04);
border: 1px solid rgba(var(--text-rgb), 0.14);
border-radius: 14px;
cursor: pointer;
transition: border-color .15s ease, background .15s ease, transform .15s ease, box-shadow .15s ease;
display: block;
}
.setup-level-card input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.setup-level-card:hover {
border-color: rgba(var(--accent-rgb), 0.4);
transform: translateY(-1px);
}
.setup-level-card:has(input:checked) {
background: linear-gradient(135deg, rgba(var(--accent-rgb), 0.18) 0%, rgba(var(--accent-rgb), 0.04) 100%);
border-color: rgba(var(--accent-rgb), 0.7);
box-shadow: 0 6px 22px rgba(var(--accent-rgb), 0.18);
}
.setup-level-card:has(input:focus-visible) {
outline: 2px solid rgba(var(--accent-rgb), 0.6);
outline-offset: 2px;
}
.setup-level-card-body {
display: flex;
flex-direction: column;
gap: 6px;
}
.setup-level-card-icon {
font-size: 1.6rem;
line-height: 1;
}
.setup-level-card-title {
font-size: 1.05rem;
font-weight: 700;
color: var(--text-primary);
}
.setup-level-card-desc {
font-size: 0.85rem;
color: rgba(var(--text-rgb), 0.65);
line-height: 1.45;
}
/* Dev-mode easter egg strip — revealed by 10 taps on the Advanced card
(see setup-wizard.js). [hidden] keeps it out of layout (no phantom gap
from the parent flex column) until JS clears the attribute, at which
point the entrance keyframes play. */
.setup-dev-strip {
position: relative;
display: flex;
align-items: center;
gap: 12px;
/* 18px sides match .setup-level-card's content inset, so the strip's
icon/text line up with the cards above. (Top/bottom stay 12px; the
entrance keyframes animate only those, not the sides.) */
padding: 12px 18px;
border-radius: 12px;
background: linear-gradient(135deg, rgba(var(--accent-rgb), 0.16) 0%, rgba(var(--accent-rgb), 0.05) 100%);
border: 1px solid rgba(var(--accent-rgb), 0.5);
box-shadow: 0 6px 22px rgba(var(--accent-rgb), 0.16);
overflow: hidden;
/* Grow-in settles the box; the glow pulse gives the "unlocked!" beat. */
animation:
swDevStripIn .45s cubic-bezier(.2, .8, .25, 1) both,
swDevStripGlow 1.1s ease-out .12s both;
}
/* Author display:flex above outranks the UA [hidden] rule, so re-hide. */
.setup-dev-strip[hidden] { display: none; }
/* One-shot shine sweep across the strip on reveal. */
.setup-dev-strip::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(115deg, transparent 35%, rgba(255, 255, 255, 0.18) 50%, transparent 65%);
transform: translateX(-120%);
pointer-events: none;
}
.setup-dev-strip:not([hidden])::after { animation: swDevShine .9s ease .28s both; }
.setup-dev-strip-icon {
display: inline-flex;
color: rgb(var(--accent-rgb));
flex: 0 0 auto;
}
.setup-dev-strip-icon svg { width: 22px; height: 22px; display: block; }
.setup-dev-strip:not([hidden]) .setup-dev-strip-icon {
animation: swDevIconPop .55s cubic-bezier(.2, 1.4, .4, 1) .12s both;
}
.setup-dev-strip-text { display: flex; flex-direction: column; gap: 2px; }
.setup-dev-strip-text strong { font-size: 0.95rem; font-weight: 700; color: var(--text-primary); }
.setup-dev-strip-text span { font-size: 0.8rem; color: rgba(var(--text-rgb), 0.65); }
.setup-dev-strip:not([hidden]) .setup-dev-strip-text {
animation: swDevTextIn .4s ease .18s both;
}
@keyframes swDevStripIn {
from { opacity: 0; transform: translateY(-8px) scale(.98); max-height: 0; padding-top: 0; padding-bottom: 0; }
60% { opacity: 1; }
to { opacity: 1; transform: translateY(0) scale(1); max-height: 160px; padding-top: 12px; padding-bottom: 12px; }
}
@keyframes swDevStripGlow {
0% { box-shadow: 0 0 0 0 rgba(var(--accent-rgb), 0); }
35% { box-shadow: 0 0 0 4px rgba(var(--accent-rgb), 0.22), 0 8px 26px rgba(var(--accent-rgb), 0.30); }
100% { box-shadow: 0 6px 22px rgba(var(--accent-rgb), 0.16); }
}
@keyframes swDevShine {
from { transform: translateX(-120%); }
to { transform: translateX(120%); }
}
@keyframes swDevIconPop {
0% { transform: scale(0) rotate(-25deg); opacity: 0; }
60% { transform: scale(1.15) rotate(8deg); opacity: 1; }
100% { transform: scale(1) rotate(0); opacity: 1; }
}
@keyframes swDevTextIn {
from { opacity: 0; transform: translateX(-6px); }
to { opacity: 1; transform: translateX(0); }
}
@media (prefers-reduced-motion: reduce) {
.setup-dev-strip,
.setup-dev-strip::after,
.setup-dev-strip .setup-dev-strip-icon,
.setup-dev-strip .setup-dev-strip-text { animation-duration: .01ms; }
}