<div><span class="lbl">Glowing stepper deploy wizard</span>
<form class="wz" data-wizard aria-labelledby="dp-title">
<h2 class="wz-title" id="dp-title">Deploy to Stratus</h2>
<p class="wz-sub">Stratus redeploys on every push to main. You can change all of this later in Project settings.</p>
<ol class="wz-glow" data-stepper aria-label="Progress">
<li><span class="wz-dot"><b>1</b><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg></span>Repository<span class="sr-only" data-sr></span></li>
<li><span class="wz-dot"><b>2</b><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg></span>Configure<span class="sr-only" data-sr></span></li>
<li><span class="wz-dot"><b>3</b><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg></span>Region & review<span class="sr-only" data-sr></span></li>
</ol>
<section class="wz-panel" data-step aria-labelledby="dp-h1">
<h3 class="wz-h" id="dp-h1" tabindex="-1"><span class="wz-kick">Step 1 of 3</span>Pick a repository</h3>
<p class="wz-lead">Showing repositories from the acme organisation on GitHub.</p>
<fieldset class="wz-field"><legend class="sr-only">Repository</legend>
<div class="wz-list">
<label class="wz-repo"><span class="wz-ico"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 18.5v-13A2.5 2.5 0 0 1 7.5 3H19v14H7.5A2.5 2.5 0 0 0 5 19.5 1.5 1.5 0 0 0 6.5 21H19"/></svg></span>
<span class="wz-repo-txt"><b>acme/storefront</b><span>Next.js · updated 2 h ago</span></span><input class="wz-radio" type="radio" name="repo" value="acme/storefront" checked required></label>
<label class="wz-repo"><span class="wz-ico"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 18.5v-13A2.5 2.5 0 0 1 7.5 3H19v14H7.5A2.5 2.5 0 0 0 5 19.5 1.5 1.5 0 0 0 6.5 21H19"/></svg></span>
<span class="wz-repo-txt"><b>acme/docs</b><span>Astro · updated yesterday</span></span><input class="wz-radio" type="radio" name="repo" value="acme/docs"></label>
<label class="wz-repo"><span class="wz-ico"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 18.5v-13A2.5 2.5 0 0 1 7.5 3H19v14H7.5A2.5 2.5 0 0 0 5 19.5 1.5 1.5 0 0 0 6.5 21H19"/></svg></span>
<span class="wz-repo-txt"><b>acme/api</b><span>Node · updated 4 days ago</span></span><input class="wz-radio" type="radio" name="repo" value="acme/api"></label>
</div></fieldset>
</section>
<section class="wz-panel" data-step aria-labelledby="dp-h2">
<h3 class="wz-h" id="dp-h2" tabindex="-1"><span class="wz-kick">Step 2 of 3</span>Configure the build</h3>
<div class="wz-row">
<div class="wz-field"><label for="dp-preset">Framework preset</label>
<select class="wz-input" id="dp-preset" name="preset"><option>Next.js</option><option>Astro</option><option>Remix</option><option>Node.js</option><option>Other</option></select></div>
<div class="wz-field"><label for="dp-root">Root directory</label>
<input class="wz-input wz-mono" id="dp-root" name="root" required spellcheck="false" value="./"></div>
</div>
<div class="wz-field"><label for="dp-build">Build command</label>
<input class="wz-input wz-mono" id="dp-build" name="build" spellcheck="false" aria-describedby="dp-build-hint" value="next build">
<span class="wz-hint" id="dp-build-hint">Runs after install. Leave it empty to use the preset's default.</span></div>
<fieldset class="wz-field"><legend class="wz-legend">Environment variables</legend>
<div class="wz-env">
<div class="wz-env-head" aria-hidden="true"><span>Key</span><span>Value</span></div>
<div class="wz-env-row" data-row><input class="wz-input wz-mono" name="env-key" aria-label="Key 1" spellcheck="false" autocomplete="off" value="DATABASE_URL"><input class="wz-input wz-mono" name="env-value" aria-label="Value 1" spellcheck="false" autocomplete="off" value="postgres://[email protected]:5432/shop"></div>
<div class="wz-env-row" data-row><input class="wz-input wz-mono" name="env-key" aria-label="Key 2" spellcheck="false" autocomplete="off" value="NEXT_PUBLIC_REGION"><input class="wz-input wz-mono" name="env-value" aria-label="Value 2" spellcheck="false" autocomplete="off" value="fra1"></div>
<button type="button" class="wz-btn ghost sm wz-add" data-add-row><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>Add variable</button>
</div></fieldset>
</section>
<section class="wz-panel" data-step aria-labelledby="dp-h3">
<h3 class="wz-h" id="dp-h3" tabindex="-1"><span class="wz-kick">Step 3 of 3</span>Choose a region and review</h3>
<fieldset class="wz-field"><legend class="wz-legend">Region</legend>
<div class="wz-tiles">
<label class="wz-tile"><input class="wz-radio" type="radio" name="region" value="Frankfurt (fra1)" checked required><b>Frankfurt</b><span>fra1 · Germany</span></label>
<label class="wz-tile"><input class="wz-radio" type="radio" name="region" value="Virginia (iad1)"><b>Virginia</b><span>iad1 · US East</span></label>
<label class="wz-tile"><input class="wz-radio" type="radio" name="region" value="Singapore (sin1)"><b>Singapore</b><span>sin1 · Asia Pacific</span></label>
</div></fieldset>
<dl class="wz-review">
<div><dt>Repository</dt><dd data-out="repo">acme/storefront</dd></div>
<div><dt>Framework</dt><dd data-out="preset">Next.js</dd></div>
<div><dt>Root directory</dt><dd data-out="root">./</dd></div>
<div><dt>Build command</dt><dd data-out="build">next build</dd></div>
<div><dt>Variables</dt><dd data-out="env-key">DATABASE_URL, NEXT_PUBLIC_REGION</dd></div>
<div><dt>Region</dt><dd data-out="region">Frankfurt (fra1)</dd></div>
</dl>
</section>
<section class="wz-panel wz-done" data-done hidden aria-labelledby="dp-h4">
<span class="wz-badge"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg></span>
<h3 class="wz-h" id="dp-h4" tabindex="-1">Deployed to production</h3>
<p class="wz-lead">Your project is live. New pushes to main deploy on their own.</p>
<pre class="wz-log"><b>✓</b> Cloned acme/storefront · main @ 3f9c2e1
<b>✓</b> Installed 412 packages in 9 s
<b>✓</b> Build completed in 41 s · <i>https://storefront-7f3a.stratus.app</i></pre>
<button type="button" class="wz-btn ghost" data-restart>Deploy another project</button>
</section>
<div class="wz-nav">
<button type="button" class="wz-btn ghost" data-back hidden>Back</button>
<button type="button" class="wz-btn" data-next hidden>Continue</button>
<button type="submit" class="wz-btn">Deploy</button>
</div>
</form></div>
<div><span class="lbl">Progress ring header</span>
<form class="wz" data-wizard aria-labelledby="tf-title">
<div class="wz-head">
<div class="wz-ring" data-ring style="--wz-p:25" aria-hidden="true"><svg viewBox="0 0 40 40"><circle cx="20" cy="20" r="17"/><circle class="wz-arc" cx="20" cy="20" r="17" pathLength="100"/></svg><span data-count>1 of 4</span></div>
<div><h2 class="wz-title" id="tf-title">Turn on two-factor authentication</h2>
<p class="wz-sub">Ledgerly will ask for a code from your phone each time you sign in.</p></div>
</div>
<ol class="wz-bars" data-stepper aria-label="Progress">
<li>App<span class="sr-only" data-sr></span></li>
<li>Scan<span class="sr-only" data-sr></span></li>
<li>Verify<span class="sr-only" data-sr></span></li>
<li>Backup<span class="sr-only" data-sr></span></li>
</ol>
<section class="wz-panel" data-step aria-labelledby="tf-h1">
<h3 class="wz-h" id="tf-h1" tabindex="-1"><span class="wz-kick">Step 1 of 4</span>Get an authenticator app</h3>
<p class="wz-lead">Install one of these on your phone if you don't have one yet. Any app that supports TOTP works.</p>
<ul class="wz-apps">
<li><b>1Password</b><span>iOS, Android, Mac, Windows</span></li>
<li><b>Authy</b><span>iOS, Android</span></li>
<li><b>Google Authenticator</b><span>iOS, Android</span></li>
</ul>
</section>
<section class="wz-panel" data-step aria-labelledby="tf-h2">
<h3 class="wz-h" id="tf-h2" tabindex="-1"><span class="wz-kick">Step 2 of 4</span>Scan the code</h3>
<div class="wz-qr">
<svg viewBox="-2 -2 25 25" aria-hidden="true"><rect x="-2" y="-2" width="25" height="25" fill="#fff"/><path fill="#0f1422" d="M0 0h7v1H0zM8 0h2v1H8zM11 0h1v1H11zM14 0h7v1H14zM0 1h1v1H0zM6 1h1v1H6zM8 1h2v1H8zM11 1h2v1H11zM14 1h1v1H14zM20 1h1v1H20zM0 2h1v1H0zM2 2h3v1H2zM6 2h1v1H6zM8 2h3v1H8zM12 2h1v1H12zM14 2h1v1H14zM16 2h3v1H16zM20 2h1v1H20zM0 3h1v1H0zM2 3h3v1H2zM6 3h1v1H6zM8 3h1v1H8zM12 3h1v1H12zM14 3h1v1H14zM16 3h3v1H16zM20 3h1v1H20zM0 4h1v1H0zM2 4h3v1H2zM6 4h1v1H6zM9 4h1v1H9zM11 4h2v1H11zM14 4h1v1H14zM16 4h3v1H16zM20 4h1v1H20zM0 5h1v1H0zM6 5h1v1H6zM8 5h2v1H8zM11 5h1v1H11zM14 5h1v1H14zM20 5h1v1H20zM0 6h7v1H0zM8 6h1v1H8zM10 6h1v1H10zM12 6h1v1H12zM14 6h7v1H14zM9 7h1v1H9zM11 7h2v1H11zM0 8h1v1H0zM2 8h2v1H2zM5 8h3v1H5zM10 8h1v1H10zM15 8h1v1H15zM17 8h2v1H17zM20 8h1v1H20zM1 9h1v1H1zM7 9h1v1H7zM11 9h1v1H11zM16 9h1v1H16zM0 10h2v1H0zM3 10h6v1H3zM10 10h3v1H10zM14 10h2v1H14zM20 10h1v1H20zM0 11h2v1H0zM4 11h2v1H4zM7 11h2v1H7zM11 11h4v1H11zM0 12h1v1H0zM5 12h4v1H5zM10 12h11v1H10zM10 13h5v1H10zM17 13h1v1H17zM19 13h2v1H19zM0 14h7v1H0zM8 14h2v1H8zM11 14h2v1H11zM15 14h1v1H15zM17 14h1v1H17zM0 15h1v1H0zM6 15h1v1H6zM9 15h3v1H9zM15 15h2v1H15zM0 16h1v1H0zM2 16h3v1H2zM6 16h1v1H6zM10 16h1v1H10zM12 16h5v1H12zM19 16h1v1H19zM0 17h1v1H0zM2 17h3v1H2zM6 17h1v1H6zM10 17h5v1H10zM0 18h1v1H0zM2 18h3v1H2zM6 18h1v1H6zM8 18h1v1H8zM14 18h1v1H14zM16 18h1v1H16zM19 18h2v1H19zM0 19h1v1H0zM6 19h1v1H6zM10 19h3v1H10zM15 19h1v1H15zM19 19h1v1H19zM0 20h7v1H0zM8 20h2v1H8zM14 20h1v1H14zM17 20h4v1H17z"/></svg>
<div class="wz-qr-txt">
<p>Open your app, tap <b>Add account</b> and point the camera at the code.</p>
<p>Can't scan it? Enter this setup key instead:</p>
<div class="wz-key"><code class="wz-code" id="tf-key">JBSW Y3DP EHPK 3PXP</code>
<button type="button" class="wz-btn ghost sm" data-copy="tf-key">Copy</button><span class="sr-only" role="status" data-status></span></div>
</div>
</div>
</section>
<section class="wz-panel" data-step aria-labelledby="tf-h3">
<h3 class="wz-h" id="tf-h3" tabindex="-1"><span class="wz-kick">Step 3 of 4</span>Enter the 6-digit code</h3>
<div class="wz-field"><label for="tf-code">Code from your app</label>
<input class="wz-input wz-otp" id="tf-code" name="otp" inputmode="numeric" autocomplete="one-time-code" pattern="\d{6}" maxlength="6" required aria-describedby="tf-code-hint" value="482913">
<span class="wz-hint" id="tf-code-hint">Six digits, no spaces. The code changes every 30 seconds.</span></div>
</section>
<section class="wz-panel" data-step aria-labelledby="tf-h4">
<h3 class="wz-h" id="tf-h4" tabindex="-1"><span class="wz-kick">Step 4 of 4</span>Save your backup codes</h3>
<p class="wz-lead">If you lose your phone, each of these signs you in once. Keep them in your password manager.</p>
<ul class="wz-codes" data-codes>
<li>7kq2-mx9d</li><li>p4tz-81wf</li><li>c9hn-3vbe</li><li>x2ra-6yqs</li>
<li>f8wd-t4kc</li><li>m3uj-9zpe</li><li>r6bv-2nhx</li><li>h5ly-7qta</li>
</ul>
<button type="button" class="wz-btn ghost sm wz-add" data-download="ledgerly-backup-codes.txt"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4v11m-5-5 5 5 5-5M5 20h14"/></svg>Download .txt</button>
<label class="wz-check"><span class="wz-box"><input type="checkbox" name="saved" value="yes" required><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg></span>I saved my backup codes</label>
</section>
<section class="wz-panel wz-done" data-done hidden aria-labelledby="tf-h5">
<span class="wz-badge"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3 5 6v5c0 4.5 3 8.3 7 10 4-1.7 7-5.5 7-10V6l-7-3Z"/><path d="m9 12 2.2 2.2L15.5 10"/></svg></span>
<h3 class="wz-h" id="tf-h5" tabindex="-1">Two-factor authentication is on</h3>
<p class="wz-lead">Next time you sign in to Ledgerly, we'll ask for a code from your app.</p>
<button type="button" class="wz-btn ghost" data-restart>Start over</button>
</section>
<div class="wz-nav">
<button type="button" class="wz-btn ghost" data-back hidden>Back</button>
<button type="button" class="wz-btn" data-next hidden>Continue</button>
<button type="submit" class="wz-btn">Turn on 2FA</button>
</div>
</form></div>
CSS
/* Dark wizards: an <ol> stepper, one <section data-step> per step and a shared Back / Continue bar on a dark card.
The script hides every step but the current one, sets data-state and aria-current="step" on the stepper,
fills the progress ring from --wz-p and moves focus to the new step's heading.
Without JavaScript every step shows and the form submits normally. */
.wz{--wz-card:#0f1422;--wz-line:#232b3d;--wz-ink:#e6e9f2;--wz-muted:#9aa4b8;--wz-field:#646d83;--wz-input:#0a0e19;--wz-track:#2c3447;
--wz-accent:#22d3ee;--wz-accent-hi:#67e8f9;--wz-glow:rgba(34,211,238,.55);--wz-done:#a3e635;--wz-on:#04141a;
--wz-mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
color-scheme:dark;background:var(--wz-card) radial-gradient(640px 200px at 50% -80px,rgba(34,211,238,.1),transparent 70%) no-repeat;
border:1px solid var(--wz-line);border-radius:18px;padding:30px 32px;color:var(--wz-ink);box-shadow:0 24px 60px -24px #000}
.wz [hidden]{display:none!important}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
.wz-title{font-size:1.3rem;font-weight:700;letter-spacing:-.015em;margin:0}
.wz-sub{margin:4px 0 0;color:var(--wz-muted);font-size:.92rem}
.wz-mono{font-family:var(--wz-mono)}
/* glowing stepper: dots joined by a track; the cyan fill scales in and glows as each step is reached */
.wz-glow{list-style:none;margin:26px 0 0;padding:0;display:grid;grid-template-columns:repeat(3,1fr)}
.wz-glow li{position:relative;display:flex;flex-direction:column;align-items:center;gap:8px;text-align:center;font-size:.8rem;color:var(--wz-muted)}
.wz-glow li + li::before,.wz-glow li + li::after{content:"";position:absolute;top:16px;left:calc(-50% + 26px);right:calc(50% + 26px);height:2px;border-radius:2px;background:var(--wz-track)}
.wz-glow li + li::after{background:var(--wz-accent);box-shadow:0 0 8px 1px var(--wz-glow);transform:scaleX(0);transform-origin:left;transition:transform .45s ease}
.wz-glow [data-state="current"]::after,.wz-glow [data-state="done"]::after{transform:scaleX(1)}
.wz-dot{width:34px;height:34px;border-radius:50%;display:grid;place-items:center;border:2px solid var(--wz-field);background:var(--wz-card);font-weight:700;font-size:.88rem;color:#c3c9d6;transition:background .25s,border-color .25s,color .25s}
.wz-dot svg{display:none;width:17px;height:17px;fill:none;stroke:currentColor;stroke-width:3;stroke-linecap:round;stroke-linejoin:round}
.wz-glow [data-state="current"]{color:var(--wz-ink);font-weight:600}
.wz-glow [data-state="current"] .wz-dot{border-color:var(--wz-accent);background:#0c2530;color:var(--wz-accent);box-shadow:0 0 14px var(--wz-glow);animation:wz-pulse 2s ease-out infinite}
.wz-glow [data-state="done"] .wz-dot{border-color:var(--wz-done);background:var(--wz-done);color:var(--wz-card)}
.wz-glow [data-state="done"] .wz-dot b{display:none}
.wz-glow [data-state="done"] .wz-dot svg{display:block}
@keyframes wz-pulse{0%{box-shadow:0 0 14px var(--wz-glow),0 0 0 0 rgba(34,211,238,.45)}70%,100%{box-shadow:0 0 14px var(--wz-glow),0 0 0 10px rgba(34,211,238,0)}}
/* ring header: an SVG circle with pathLength="100" whose stroke-dashoffset follows --wz-p (0-100) */
.wz-head{display:flex;align-items:center;gap:18px}
.wz-ring{position:relative;flex:none;width:72px;height:72px;display:grid;place-items:center;font-size:.8rem;font-weight:700;white-space:nowrap}
.wz-ring svg{position:absolute;inset:0;width:100%;height:100%;overflow:visible;transform:rotate(-90deg)}
.wz-ring circle{fill:none;stroke:var(--wz-track);stroke-width:3.5}
.wz-ring .wz-arc{stroke:var(--wz-accent);stroke-linecap:round;stroke-dasharray:100;stroke-dashoffset:calc(100px - var(--wz-p, 25) * 1px);filter:drop-shadow(0 0 3px var(--wz-glow));transition:stroke-dashoffset .5s ease,stroke .3s}
.wz-ring[data-full] .wz-arc{stroke:var(--wz-done);filter:drop-shadow(0 0 3px rgba(163,230,53,.5))}
.wz-bars{list-style:none;margin:22px 0 0;padding:0;display:grid;grid-template-columns:repeat(4,1fr);gap:8px}
.wz-bars li{display:grid;gap:8px;font-size:.78rem;color:var(--wz-muted)}
.wz-bars li::before{content:"";height:4px;border-radius:4px;background:var(--wz-track);transition:background .3s,box-shadow .3s}
.wz-bars [data-state="current"]{color:var(--wz-ink);font-weight:600}
.wz-bars [data-state="current"]::before{background:var(--wz-accent);box-shadow:0 0 10px var(--wz-glow)}
.wz-bars [data-state="done"]::before{background:var(--wz-done)}
/* step panels */
.wz-panel{margin-top:28px;display:grid;gap:18px;animation:wz-in .3s ease}
@keyframes wz-in{from{opacity:0;transform:translateY(6px)}}
.wz-h{margin:0;font-size:1.15rem;font-weight:700;letter-spacing:-.01em}
.wz-h:focus{outline:none}
.wz-kick{display:block;font-size:.72rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--wz-accent);margin-bottom:2px}
.wz-lead{margin:-10px 0 0;color:var(--wz-muted);font-size:.92rem}
.wz-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:16px}
.wz-field{display:grid;gap:6px;min-width:0}
.wz-field > label,.wz-legend{font-size:.88rem;font-weight:600}
.wz-hint{font-size:.8rem;color:var(--wz-muted)}
.wz-input{width:100%;min-width:0;font:inherit;font-size:.95rem;color:var(--wz-ink);background:var(--wz-input);border:1.5px solid var(--wz-field);border-radius:10px;padding:10px 12px;min-height:44px}
.wz-input.wz-mono{font-family:var(--wz-mono);font-size:.88rem}
.wz-input:focus-visible{outline:2px solid var(--wz-accent);outline-offset:2px;border-color:var(--wz-accent)}
fieldset.wz-field{border:0;margin:0;padding:0}
.wz-legend{padding:0;margin-bottom:6px}
.wz-radio{appearance:none;flex:none;margin:0;width:18px;height:18px;border:2px solid var(--wz-field);border-radius:50%;background:var(--wz-input);cursor:pointer}
.wz-radio:checked{border-color:var(--wz-accent);background:radial-gradient(circle,var(--wz-accent) 0 4px,transparent 4.5px)}
.wz-radio:focus-visible{outline:2px solid var(--wz-accent);outline-offset:3px}
/* repository list: the whole row is the radio's label; the checked row gets a cyan edge */
.wz-list{display:grid;border:1px solid var(--wz-line);border-radius:12px;overflow:hidden}
.wz-repo{display:flex;align-items:center;gap:14px;padding:13px 16px;cursor:pointer;transition:background .2s}
.wz-repo + .wz-repo{border-top:1px solid var(--wz-line)}
.wz-repo:hover{background:#141a2a}
.wz-repo:has(:checked){background:rgba(34,211,238,.07);box-shadow:inset 3px 0 0 var(--wz-accent)}
.wz-ico{flex:none;width:36px;height:36px;border-radius:9px;display:grid;place-items:center;background:#161d2e;border:1px solid var(--wz-line);color:var(--wz-muted)}
.wz-ico svg{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
.wz-repo-txt{flex:1;min-width:0;display:grid}
.wz-repo-txt b{font-family:var(--wz-mono);font-size:.9rem;font-weight:600;overflow-wrap:anywhere}
.wz-repo-txt span{font-size:.8rem;color:var(--wz-muted)}
/* environment variables: key / value rows the script can clone */
.wz-env{display:grid;gap:8px}
.wz-env-row,.wz-env-head{display:grid;grid-template-columns:minmax(0,2fr) minmax(0,3fr);gap:8px}
.wz-env-head{font-size:.75rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--wz-muted)}
.wz-add{justify-self:start}
/* region tiles */
.wz-tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:16px}
.wz-tile{position:relative;display:grid;gap:2px;padding:14px 14px 14px 44px;border:1.5px solid #2f3850;border-radius:12px;background:#0c111d;cursor:pointer;transition:border-color .2s,box-shadow .2s}
.wz-tile .wz-radio{position:absolute;left:14px;top:16px}
.wz-tile b{font-size:.95rem}
.wz-tile span{font-size:.8rem;color:var(--wz-muted);font-family:var(--wz-mono)}
.wz-tile:has(:checked){border-color:var(--wz-accent);box-shadow:0 0 20px -6px var(--wz-glow)}
/* review list */
.wz-review{margin:0;display:grid;border:1px solid var(--wz-line);border-radius:12px;overflow:hidden;background:#0c111d}
.wz-review div{display:flex;justify-content:space-between;gap:16px;padding:10px 14px;font-size:.9rem}
.wz-review div + div{border-top:1px solid var(--wz-line)}
.wz-review dt{color:var(--wz-muted)}
.wz-review dd{margin:0;font-family:var(--wz-mono);font-size:.84rem;text-align:right;overflow-wrap:anywhere}
/* 2FA pieces: app list, QR code with a manual key, one-time code, backup codes */
.wz-apps{list-style:none;margin:0;padding:0;display:grid;border:1px solid var(--wz-line);border-radius:12px;overflow:hidden}
.wz-apps li{display:flex;justify-content:space-between;flex-wrap:wrap;gap:4px 16px;padding:12px 16px;font-size:.92rem}
.wz-apps li + li{border-top:1px solid var(--wz-line)}
.wz-apps span{color:var(--wz-muted);font-size:.84rem}
.wz-qr{display:flex;flex-wrap:wrap;align-items:center;gap:24px}
.wz-qr svg{flex:none;width:132px;height:132px;border-radius:10px;box-shadow:0 0 0 1px var(--wz-line),0 0 30px -10px var(--wz-glow)}
.wz-qr-txt{flex:1 1 220px;display:grid;gap:10px;font-size:.92rem}
.wz-qr-txt p{margin:0;color:var(--wz-muted)}
.wz-qr-txt b{color:var(--wz-ink)}
.wz-key{display:flex;flex-wrap:wrap;align-items:center;gap:10px}
.wz-code{font-family:var(--wz-mono);font-size:.98rem;letter-spacing:.06em;padding:8px 12px;border:1px solid var(--wz-line);border-radius:8px;background:var(--wz-input);color:var(--wz-ink)}
.wz-otp{max-width:240px;font-family:var(--wz-mono);font-size:1.35rem;letter-spacing:.35em;text-align:center}
.wz-codes{list-style:none;margin:0;padding:14px;display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:10px 16px;border:1px solid var(--wz-line);border-radius:12px;background:var(--wz-input);font-family:var(--wz-mono);font-size:.92rem;text-align:center}
.wz-check{display:flex;align-items:center;gap:10px;font-size:.92rem;font-weight:600;cursor:pointer}
.wz-box{position:relative;flex:none;display:grid}
.wz-box input{appearance:none;margin:0;width:20px;height:20px;border:2px solid var(--wz-field);border-radius:6px;background:var(--wz-input);cursor:pointer}
.wz-box input:checked{border-color:var(--wz-accent);background:var(--wz-accent)}
.wz-box input:focus-visible{outline:2px solid var(--wz-accent);outline-offset:2px}
.wz-box svg{position:absolute;inset:0;margin:auto;width:14px;height:14px;fill:none;stroke:var(--wz-on);stroke-width:3.5;stroke-linecap:round;stroke-linejoin:round;opacity:0;pointer-events:none}
.wz-box input:checked + svg{opacity:1}
/* Back / Continue bar and buttons; focus rings keep a gap in the card colour */
.wz-nav{display:flex;gap:10px;margin-top:28px;padding-top:20px;border-top:1px solid var(--wz-line)}
.wz-btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:44px;padding:0 22px;border-radius:10px;border:1.5px solid var(--wz-accent);background:var(--wz-accent);color:var(--wz-on);font:inherit;font-size:.95rem;font-weight:600;cursor:pointer;box-shadow:0 0 22px -6px var(--wz-glow);transition:background .2s,border-color .2s,box-shadow .2s}
.wz-btn:hover{background:var(--wz-accent-hi);border-color:var(--wz-accent-hi)}
.wz-btn.ghost{background:transparent;color:var(--wz-ink);border-color:var(--wz-field);box-shadow:none}
.wz-btn.ghost:hover{background:#171e30;border-color:#7d879d}
.wz-btn.sm{min-height:38px;padding:0 14px;font-size:.86rem}
.wz-btn svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round}
.wz-btn:focus-visible{outline:none;box-shadow:0 0 0 3px var(--wz-card),0 0 0 5px var(--wz-accent)}
.wz-nav .wz-btn:not(.ghost){margin-left:auto}
/* done panels */
.wz-done{text-align:center;justify-items:center;padding:12px 0 4px}
.wz-done .wz-lead{margin:0}
.wz-badge{width:56px;height:56px;border-radius:50%;display:grid;place-items:center;border:2px solid var(--wz-done);background:rgba(163,230,53,.1);color:var(--wz-done);box-shadow:0 0 24px -4px rgba(163,230,53,.45)}
.wz-badge svg{width:26px;height:26px;fill:none;stroke:currentColor;stroke-width:2.6;stroke-linecap:round;stroke-linejoin:round}
.wz-log{width:100%;margin:0;padding:14px 16px;text-align:left;font-family:var(--wz-mono);font-size:.82rem;line-height:1.7;color:var(--wz-muted);background:#070a12;border:1px solid var(--wz-line);border-radius:12px;white-space:pre-wrap;overflow-wrap:anywhere}
.wz-log b{font-weight:400;color:var(--wz-done)}
.wz-log i{font-style:normal;color:var(--wz-accent)}
@media (prefers-reduced-motion:reduce){.wz *,.wz *::before,.wz *::after{transition:none!important;animation:none!important}}
@media (max-width:600px){.wz{padding:24px 18px}.wz-glow li{font-size:.72rem}.wz-head{gap:14px}.wz-ring{width:64px;height:64px}}
JavaScript
// Form wizard: shows one step at a time, marks the stepper with data-state and aria-current="step",
// checks the step's fields before moving on and moves focus to the new step's heading.
// Extras in this set: a progress ring ([data-ring]), live review slots, "Add variable" rows, Copy and Download buttons.
document.querySelectorAll('[data-wizard]').forEach(function (wz) {
var panels = wz.querySelectorAll('[data-step]'), steps = wz.querySelectorAll('[data-stepper] > li');
var back = wz.querySelector('[data-back]'), next = wz.querySelector('[data-next]'), ring = wz.querySelector('[data-ring]');
var submit = wz.querySelector('[type="submit"]'), done = wz.querySelector('[data-done]'), at = 0;
function mark(i) {
steps.forEach(function (s, n) {
s.dataset.state = n < i ? 'done' : n === i ? 'current' : 'next';
if (n === i) s.setAttribute('aria-current', 'step'); else s.removeAttribute('aria-current');
s.querySelector('[data-sr]').textContent = n < i ? ', completed' : '';
});
if (ring) { // the ring's stroke-dashoffset reads --wz-p (0-100)
var k = Math.min(i + 1, steps.length);
ring.style.setProperty('--wz-p', k / steps.length * 100);
ring.querySelector('[data-count]').textContent = k + ' of ' + steps.length;
ring.toggleAttribute('data-full', i >= steps.length);
}
}
function out() {
wz.querySelectorAll('[data-out]').forEach(function (o) {
var f = wz.elements[o.dataset.out], v = f && (f.length && !f.options ? [].filter.call(f, function (x) { return x.checked || !/radio|checkbox/.test(x.type) && x.value; }).map(function (x) { return x.value; }).join(', ') : f.value);
o.textContent = v || 'None';
});
}
function go(i, focus) {
at = i; mark(i); out();
panels.forEach(function (p, n) { p.hidden = n !== i; });
back.hidden = i === 0; next.hidden = i === panels.length - 1; submit.hidden = !next.hidden;
if (focus) panels[i].querySelector('[tabindex="-1"]').focus();
}
function valid() {
var bad = [].find.call(panels[at].querySelectorAll('input, select, textarea'), function (f) { return !f.checkValidity(); });
if (bad) bad.reportValidity();
return !bad;
}
wz.noValidate = true;
wz.addEventListener('change', out);
back.addEventListener('click', function () { go(at - 1, true); });
next.addEventListener('click', function () { if (valid()) go(at + 1, true); });
wz.addEventListener('submit', function (e) {
e.preventDefault();
if (!valid()) return;
if (at < panels.length - 1) return go(at + 1, true);
// Last step: send the form here (fetch or wz.submit()). The demo shows the done panel instead.
panels[at].hidden = true; back.parentNode.hidden = true; mark(steps.length);
done.hidden = false; done.querySelector('[tabindex="-1"]').focus();
});
if (done) done.querySelector('[data-restart]').addEventListener('click', function () {
wz.reset(); done.hidden = true; back.parentNode.hidden = false; go(0, true);
});
// "Add variable": clone the last key/value row, empty it, renumber its labels and focus it
var add = wz.querySelector('[data-add-row]');
if (add) add.addEventListener('click', function () {
var rows = wz.querySelectorAll('[data-row]'), last = rows[rows.length - 1], row = last.cloneNode(true);
row.querySelectorAll('input').forEach(function (f) { f.value = ''; f.setAttribute('aria-label', f.getAttribute('aria-label').replace(/\d+$/, rows.length + 1)); });
last.after(row); row.querySelector('input').focus();
});
// Copy the setup key (spaces removed) and download the backup codes as a .txt file
wz.querySelectorAll('[data-copy]').forEach(function (b) {
b.addEventListener('click', function () {
var key = document.getElementById(b.dataset.copy).textContent.replace(/\s/g, ''), note = wz.querySelector('[data-status]');
if (navigator.clipboard) navigator.clipboard.writeText(key).then(function () { b.textContent = 'Copied'; if (note) note.textContent = 'Setup key copied'; }, function () {});
});
});
var dl = wz.querySelector('[data-download]');
if (dl) dl.addEventListener('click', function () {
var a = document.createElement('a'), codes = [].map.call(wz.querySelectorAll('[data-codes] li'), function (li) { return li.textContent; });
a.href = URL.createObjectURL(new Blob([codes.join('\n') + '\n'], { type: 'text/plain' })); a.download = dl.dataset.download; a.click();
});
go(0);
});
/* Dark wizards on Bootstrap 5: a .card in data-bs-theme="dark" with .form-control, .form-select, .form-check-input,
.list-group and .btn, restyled through Bootstrap's CSS variables. The steppers and the ring are the custom part.
The script hides every step but the current one, sets data-state and aria-current="step" on the stepper,
fills the progress ring from --wz-p and moves focus to the new step's heading.
Without JavaScript every step shows and the form submits normally. */
.wz{--wz-card:#0f1422;--wz-line:#232b3d;--wz-ink:#e6e9f2;--wz-muted:#9aa4b8;--wz-field:#646d83;--wz-input:#0a0e19;--wz-track:#2c3447;
--wz-accent:#22d3ee;--wz-accent-hi:#67e8f9;--wz-glow:rgba(34,211,238,.55);--wz-done:#a3e635;--wz-on:#04141a;
--bs-body-bg:var(--wz-input);--bs-body-color:var(--wz-ink);--bs-emphasis-color:var(--wz-ink);--bs-secondary-color:var(--wz-muted);
--bs-border-color:var(--wz-field);--bs-border-radius:10px;--bs-card-bg:var(--wz-card);--bs-card-border-color:var(--wz-line);--bs-card-color:var(--wz-ink);
--bs-font-monospace:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
background:var(--wz-card) radial-gradient(640px 200px at 50% -80px,rgba(34,211,238,.1),transparent 70%) no-repeat;
border-radius:18px;box-shadow:0 24px 60px -24px #000}
.wz .card-body{padding:30px 32px}
.wz .form-control,.wz .form-select{border-width:1.5px;min-height:44px;padding:10px 12px;font-size:.95rem}
.wz .form-control.font-monospace{font-size:.88rem}
.wz .form-select{padding-right:36px}
.wz .form-control:focus,.wz .form-select:focus{border-color:var(--wz-accent);box-shadow:none;outline:2px solid var(--wz-accent);outline-offset:2px}
.wz .form-label,.wz legend{font-size:.88rem;font-weight:600;margin-bottom:6px}
.wz .form-text{font-size:.8rem;color:var(--wz-muted)}
.wz .form-check-input{width:18px;height:18px;margin:0;flex:none;border-width:2px;border-color:var(--wz-field);background-color:var(--wz-input);cursor:pointer}
.wz .form-check-input:checked{border-color:var(--wz-accent)}
.wz .form-check-input:checked[type=radio]{background-color:transparent;--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-7 -7 14 14'%3e%3ccircle r='4' fill='%2322d3ee'/%3e%3c/svg%3e")}
.wz .form-check-input[type=checkbox]{width:20px;height:20px;border-radius:6px}
.wz .form-check-input:checked[type=checkbox]{background-color:var(--wz-accent);--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3e%3cpath fill='none' stroke='%2304141a' stroke-linecap='round' stroke-linejoin='round' stroke-width='3.5' d='m5 12.5 4.5 4.5L19 7.5'/%3e%3c/svg%3e");background-size:14px}
.wz .form-check-input:focus{box-shadow:none;border-color:var(--wz-accent)}
.wz .form-check-input:focus-visible{outline:2px solid var(--wz-accent);outline-offset:3px}
.wz .btn{--bs-btn-padding-x:22px;--bs-btn-font-weight:600;--bs-btn-font-size:.95rem;--bs-btn-border-width:1.5px;--bs-btn-border-radius:10px;min-height:44px;display:inline-flex;align-items:center;justify-content:center;gap:8px}
.wz .btn-sm{--bs-btn-padding-x:14px;--bs-btn-font-size:.86rem;min-height:38px}
.wz .btn svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round}
.wz .btn-primary{--bs-btn-bg:var(--wz-accent);--bs-btn-color:var(--wz-on);--bs-btn-border-color:var(--wz-accent);--bs-btn-hover-bg:var(--wz-accent-hi);--bs-btn-hover-color:var(--wz-on);--bs-btn-hover-border-color:var(--wz-accent-hi);--bs-btn-active-bg:var(--wz-accent-hi);--bs-btn-active-color:var(--wz-on);--bs-btn-active-border-color:var(--wz-accent-hi);box-shadow:0 0 22px -6px var(--wz-glow)}
.wz .btn-outline-secondary{--bs-btn-color:var(--wz-ink);--bs-btn-border-color:var(--wz-field);--bs-btn-hover-bg:#171e30;--bs-btn-hover-color:var(--wz-ink);--bs-btn-hover-border-color:#7d879d;--bs-btn-active-bg:#171e30;--bs-btn-active-color:var(--wz-ink);--bs-btn-active-border-color:#7d879d}
.wz .btn:focus-visible{outline:0;box-shadow:0 0 0 3px var(--wz-card),0 0 0 5px var(--wz-accent)}
.wz .list-group{--bs-list-group-bg:#0c111d;--bs-list-group-color:var(--wz-ink);--bs-list-group-border-color:var(--wz-line);--bs-list-group-border-radius:12px;--bs-list-group-action-color:var(--wz-ink);--bs-list-group-action-hover-bg:#141a2a;--bs-list-group-action-hover-color:var(--wz-ink);--bs-list-group-action-active-bg:#141a2a;--bs-list-group-action-active-color:var(--wz-ink)}
.wz .list-group dt{font-weight:400;color:var(--wz-muted)}
.wz .list-group dd{margin:0;font-family:var(--bs-font-monospace);font-size:.84rem;text-align:right;overflow-wrap:anywhere}
.wz-title{font-size:1.3rem;letter-spacing:-.015em;line-height:1.5}
.wz-sub{margin:4px 0 0;font-size:.92rem}
/* glowing stepper: dots joined by a track; the cyan fill scales in and glows as each step is reached */
.wz-glow{list-style:none;margin:26px 0 0;padding:0;display:grid;grid-template-columns:repeat(3,1fr)}
.wz-glow li{position:relative;display:flex;flex-direction:column;align-items:center;gap:8px;text-align:center;font-size:.8rem;color:var(--wz-muted)}
.wz-glow li + li::before,.wz-glow li + li::after{content:"";position:absolute;top:16px;left:calc(-50% + 26px);right:calc(50% + 26px);height:2px;border-radius:2px;background:var(--wz-track)}
.wz-glow li + li::after{background:var(--wz-accent);box-shadow:0 0 8px 1px var(--wz-glow);transform:scaleX(0);transform-origin:left;transition:transform .45s ease}
.wz-glow [data-state="current"]::after,.wz-glow [data-state="done"]::after{transform:scaleX(1)}
.wz-dot{width:34px;height:34px;border-radius:50%;display:grid;place-items:center;border:2px solid var(--wz-field);background:var(--wz-card);font-weight:700;font-size:.88rem;color:#c3c9d6;transition:background .25s,border-color .25s,color .25s}
.wz-dot svg{display:none;width:17px;height:17px;fill:none;stroke:currentColor;stroke-width:3;stroke-linecap:round;stroke-linejoin:round}
.wz-glow [data-state="current"]{color:var(--wz-ink);font-weight:600}
.wz-glow [data-state="current"] .wz-dot{border-color:var(--wz-accent);background:#0c2530;color:var(--wz-accent);box-shadow:0 0 14px var(--wz-glow);animation:wz-pulse 2s ease-out infinite}
.wz-glow [data-state="done"] .wz-dot{border-color:var(--wz-done);background:var(--wz-done);color:var(--wz-card)}
.wz-glow [data-state="done"] .wz-dot b{display:none}
.wz-glow [data-state="done"] .wz-dot svg{display:block}
@keyframes wz-pulse{0%{box-shadow:0 0 14px var(--wz-glow),0 0 0 0 rgba(34,211,238,.45)}70%,100%{box-shadow:0 0 14px var(--wz-glow),0 0 0 10px rgba(34,211,238,0)}}
/* ring header: an SVG circle with pathLength="100" whose stroke-dashoffset follows --wz-p (0-100) */
.wz-ring{position:relative;flex:none;width:72px;height:72px;display:grid;place-items:center;font-size:.8rem;font-weight:700;white-space:nowrap}
.wz-ring svg{position:absolute;inset:0;width:100%;height:100%;overflow:visible;transform:rotate(-90deg)}
.wz-ring circle{fill:none;stroke:var(--wz-track);stroke-width:3.5}
.wz-ring .wz-arc{stroke:var(--wz-accent);stroke-linecap:round;stroke-dasharray:100;stroke-dashoffset:calc(100px - var(--wz-p, 25) * 1px);filter:drop-shadow(0 0 3px var(--wz-glow));transition:stroke-dashoffset .5s ease,stroke .3s}
.wz-ring[data-full] .wz-arc{stroke:var(--wz-done);filter:drop-shadow(0 0 3px rgba(163,230,53,.5))}
.wz-bars{list-style:none;margin:22px 0 0;padding:0;display:grid;grid-template-columns:repeat(4,1fr);gap:8px}
.wz-bars li{display:grid;gap:8px;font-size:.78rem;color:var(--wz-muted)}
.wz-bars li::before{content:"";height:4px;border-radius:4px;background:var(--wz-track);transition:background .3s,box-shadow .3s}
.wz-bars [data-state="current"]{color:var(--wz-ink);font-weight:600}
.wz-bars [data-state="current"]::before{background:var(--wz-accent);box-shadow:0 0 10px var(--wz-glow)}
.wz-bars [data-state="done"]::before{background:var(--wz-done)}
/* step panels */
.wz-panel{margin-top:28px;display:grid;gap:18px;animation:wz-in .3s ease}
@keyframes wz-in{from{opacity:0;transform:translateY(6px)}}
.wz-h{font-size:1.15rem;letter-spacing:-.01em;line-height:1.5}
.wz-h:focus{outline:none}
.wz-kick{display:block;font-size:.72rem;letter-spacing:.1em;text-transform:uppercase;color:var(--wz-accent);margin-bottom:2px}
.wz-lead{margin:-10px 0 0;color:var(--wz-muted);font-size:.92rem}
/* repository rows, region tiles, env rows, backup codes */
.wz-repo{cursor:pointer;padding:13px 16px}
.wz-repo:has(:checked){background:rgba(34,211,238,.07);box-shadow:inset 3px 0 0 var(--wz-accent)}
.wz-ico{flex:none;width:36px;height:36px;border-radius:9px;display:grid;place-items:center;background:#161d2e;border:1px solid var(--wz-line);color:var(--wz-muted)}
.wz-ico svg{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
.wz-repo b{font-size:.9rem;font-weight:600;overflow-wrap:anywhere}
.wz-repo .small{font-size:.8rem}
.wz-env-head{font-size:.75rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--wz-muted)}
.wz-tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:16px}
.wz-tile{position:relative;display:grid;gap:2px;padding:14px 14px 14px 44px;border:1.5px solid #2f3850;border-radius:12px;background:#0c111d;cursor:pointer;transition:border-color .2s,box-shadow .2s}
.wz-tile .form-check-input{position:absolute;left:14px;top:16px}
.wz-tile b{font-size:.95rem}
.wz-tile span{font-size:.8rem;color:var(--wz-muted);font-family:var(--bs-font-monospace)}
.wz-tile:has(:checked){border-color:var(--wz-accent);box-shadow:0 0 20px -6px var(--wz-glow)}
.wz-qr-txt{flex:1 1 220px;font-size:.92rem}
.wz-add{justify-self:start}
.wz-qr svg{flex:none;width:132px;height:132px;border-radius:10px;box-shadow:0 0 0 1px var(--wz-line),0 0 30px -10px var(--wz-glow)}
.wz-code{font-size:.98rem;letter-spacing:.06em;padding:8px 12px;border:1px solid var(--wz-line);border-radius:8px;background:var(--wz-input);color:var(--wz-ink)}
.wz-otp{max-width:240px;font-size:1.35rem!important;letter-spacing:.35em;text-align:center}
.wz-codes{list-style:none;margin:0;padding:14px;display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:10px 16px;border:1px solid var(--wz-line);border-radius:12px;background:var(--wz-input);font-family:var(--bs-font-monospace);font-size:.92rem;text-align:center}
.wz-checkline{display:flex;align-items:center;gap:10px;padding:0;margin:0;font-size:.92rem;font-weight:600}
.wz-checkline .form-check-label{cursor:pointer}
/* Back / Continue bar and done panels */
.wz-nav{display:flex;gap:10px;margin-top:28px;padding-top:20px;border-top:1px solid var(--wz-line)}
.wz-nav .btn-primary{margin-left:auto}
.wz-done{text-align:center;justify-items:center;padding:12px 0 4px}
.wz-done .wz-lead{margin:0}
.wz-badge{width:56px;height:56px;border-radius:50%;display:grid;place-items:center;border:2px solid var(--wz-done);background:rgba(163,230,53,.1);color:var(--wz-done);box-shadow:0 0 24px -4px rgba(163,230,53,.45)}
.wz-badge svg{width:26px;height:26px;fill:none;stroke:currentColor;stroke-width:2.6;stroke-linecap:round;stroke-linejoin:round}
.wz-log{width:100%;margin:0;padding:14px 16px;text-align:left;font-size:.82rem;line-height:1.7;color:var(--wz-muted);background:#070a12;border:1px solid var(--wz-line);border-radius:12px;white-space:pre-wrap;overflow-wrap:anywhere}
.wz-log b{font-weight:400;color:var(--wz-done)}
.wz-log i{font-style:normal;color:var(--wz-accent)}
@media (prefers-reduced-motion:reduce){.wz *,.wz *::before,.wz *::after{transition:none!important;animation:none!important}}
@media (max-width:600px){.wz .card-body{padding:24px 18px}.wz-glow li{font-size:.72rem}.wz-ring{width:64px;height:64px}}
JavaScript
// Form wizard: shows one step at a time, marks the stepper with data-state and aria-current="step",
// checks the step's fields before moving on and moves focus to the new step's heading.
// Extras in this set: a progress ring ([data-ring]), live review slots, "Add variable" rows, Copy and Download buttons.
document.querySelectorAll('[data-wizard]').forEach(function (wz) {
var panels = wz.querySelectorAll('[data-step]'), steps = wz.querySelectorAll('[data-stepper] > li');
var back = wz.querySelector('[data-back]'), next = wz.querySelector('[data-next]'), ring = wz.querySelector('[data-ring]');
var submit = wz.querySelector('[type="submit"]'), done = wz.querySelector('[data-done]'), at = 0;
function mark(i) {
steps.forEach(function (s, n) {
s.dataset.state = n < i ? 'done' : n === i ? 'current' : 'next';
if (n === i) s.setAttribute('aria-current', 'step'); else s.removeAttribute('aria-current');
s.querySelector('[data-sr]').textContent = n < i ? ', completed' : '';
});
if (ring) { // the ring's stroke-dashoffset reads --wz-p (0-100)
var k = Math.min(i + 1, steps.length);
ring.style.setProperty('--wz-p', k / steps.length * 100);
ring.querySelector('[data-count]').textContent = k + ' of ' + steps.length;
ring.toggleAttribute('data-full', i >= steps.length);
}
}
function out() {
wz.querySelectorAll('[data-out]').forEach(function (o) {
var f = wz.elements[o.dataset.out], v = f && (f.length && !f.options ? [].filter.call(f, function (x) { return x.checked || !/radio|checkbox/.test(x.type) && x.value; }).map(function (x) { return x.value; }).join(', ') : f.value);
o.textContent = v || 'None';
});
}
function go(i, focus) {
at = i; mark(i); out();
panels.forEach(function (p, n) { p.hidden = n !== i; });
back.hidden = i === 0; next.hidden = i === panels.length - 1; submit.hidden = !next.hidden;
if (focus) panels[i].querySelector('[tabindex="-1"]').focus();
}
function valid() {
var bad = [].find.call(panels[at].querySelectorAll('input, select, textarea'), function (f) { return !f.checkValidity(); });
if (bad) bad.reportValidity();
return !bad;
}
wz.noValidate = true;
wz.addEventListener('change', out);
back.addEventListener('click', function () { go(at - 1, true); });
next.addEventListener('click', function () { if (valid()) go(at + 1, true); });
wz.addEventListener('submit', function (e) {
e.preventDefault();
if (!valid()) return;
if (at < panels.length - 1) return go(at + 1, true);
// Last step: send the form here (fetch or wz.submit()). The demo shows the done panel instead.
panels[at].hidden = true; back.parentNode.hidden = true; mark(steps.length);
done.hidden = false; done.querySelector('[tabindex="-1"]').focus();
});
if (done) done.querySelector('[data-restart]').addEventListener('click', function () {
wz.reset(); done.hidden = true; back.parentNode.hidden = false; go(0, true);
});
// "Add variable": clone the last key/value row, empty it, renumber its labels and focus it
var add = wz.querySelector('[data-add-row]');
if (add) add.addEventListener('click', function () {
var rows = wz.querySelectorAll('[data-row]'), last = rows[rows.length - 1], row = last.cloneNode(true);
row.querySelectorAll('input').forEach(function (f) { f.value = ''; f.setAttribute('aria-label', f.getAttribute('aria-label').replace(/\d+$/, rows.length + 1)); });
last.after(row); row.querySelector('input').focus();
});
// Copy the setup key (spaces removed) and download the backup codes as a .txt file
wz.querySelectorAll('[data-copy]').forEach(function (b) {
b.addEventListener('click', function () {
var key = document.getElementById(b.dataset.copy).textContent.replace(/\s/g, ''), note = wz.querySelector('[data-status]');
if (navigator.clipboard) navigator.clipboard.writeText(key).then(function () { b.textContent = 'Copied'; if (note) note.textContent = 'Setup key copied'; }, function () {});
});
});
var dl = wz.querySelector('[data-download]');
if (dl) dl.addEventListener('click', function () {
var a = document.createElement('a'), codes = [].map.call(wz.querySelectorAll('[data-codes] li'), function (li) { return li.textContent; });
a.href = URL.createObjectURL(new Blob([codes.join('\n') + '\n'], { type: 'text/plain' })); a.download = dl.dataset.download; a.click();
});
go(0);
});
// Form wizard: shows one step at a time, marks the stepper with data-state and aria-current="step",
// checks the step's fields before moving on and moves focus to the new step's heading.
// Extras in this set: a progress ring ([data-ring]), live review slots, "Add variable" rows, Copy and Download buttons.
document.querySelectorAll('[data-wizard]').forEach(function (wz) {
var panels = wz.querySelectorAll('[data-step]'), steps = wz.querySelectorAll('[data-stepper] > li');
var back = wz.querySelector('[data-back]'), next = wz.querySelector('[data-next]'), ring = wz.querySelector('[data-ring]');
var submit = wz.querySelector('[type="submit"]'), done = wz.querySelector('[data-done]'), at = 0;
function mark(i) {
steps.forEach(function (s, n) {
s.dataset.state = n < i ? 'done' : n === i ? 'current' : 'next';
if (n === i) s.setAttribute('aria-current', 'step'); else s.removeAttribute('aria-current');
s.querySelector('[data-sr]').textContent = n < i ? ', completed' : '';
});
if (ring) { // the ring's stroke-dashoffset reads --wz-p (0-100)
var k = Math.min(i + 1, steps.length);
ring.style.setProperty('--wz-p', k / steps.length * 100);
ring.querySelector('[data-count]').textContent = k + ' of ' + steps.length;
ring.toggleAttribute('data-full', i >= steps.length);
}
}
function out() {
wz.querySelectorAll('[data-out]').forEach(function (o) {
var f = wz.elements[o.dataset.out], v = f && (f.length && !f.options ? [].filter.call(f, function (x) { return x.checked || !/radio|checkbox/.test(x.type) && x.value; }).map(function (x) { return x.value; }).join(', ') : f.value);
o.textContent = v || 'None';
});
}
function go(i, focus) {
at = i; mark(i); out();
panels.forEach(function (p, n) { p.hidden = n !== i; });
back.hidden = i === 0; next.hidden = i === panels.length - 1; submit.hidden = !next.hidden;
if (focus) panels[i].querySelector('[tabindex="-1"]').focus();
}
function valid() {
var bad = [].find.call(panels[at].querySelectorAll('input, select, textarea'), function (f) { return !f.checkValidity(); });
if (bad) bad.reportValidity();
return !bad;
}
wz.noValidate = true;
wz.addEventListener('change', out);
back.addEventListener('click', function () { go(at - 1, true); });
next.addEventListener('click', function () { if (valid()) go(at + 1, true); });
wz.addEventListener('submit', function (e) {
e.preventDefault();
if (!valid()) return;
if (at < panels.length - 1) return go(at + 1, true);
// Last step: send the form here (fetch or wz.submit()). The demo shows the done panel instead.
panels[at].hidden = true; back.parentNode.hidden = true; mark(steps.length);
done.hidden = false; done.querySelector('[tabindex="-1"]').focus();
});
if (done) done.querySelector('[data-restart]').addEventListener('click', function () {
wz.reset(); done.hidden = true; back.parentNode.hidden = false; go(0, true);
});
// "Add variable": clone the last key/value row, empty it, renumber its labels and focus it
var add = wz.querySelector('[data-add-row]');
if (add) add.addEventListener('click', function () {
var rows = wz.querySelectorAll('[data-row]'), last = rows[rows.length - 1], row = last.cloneNode(true);
row.querySelectorAll('input').forEach(function (f) { f.value = ''; f.setAttribute('aria-label', f.getAttribute('aria-label').replace(/\d+$/, rows.length + 1)); });
last.after(row); row.querySelector('input').focus();
});
// Copy the setup key (spaces removed) and download the backup codes as a .txt file
wz.querySelectorAll('[data-copy]').forEach(function (b) {
b.addEventListener('click', function () {
var key = document.getElementById(b.dataset.copy).textContent.replace(/\s/g, ''), note = wz.querySelector('[data-status]');
if (navigator.clipboard) navigator.clipboard.writeText(key).then(function () { b.textContent = 'Copied'; if (note) note.textContent = 'Setup key copied'; }, function () {});
});
});
var dl = wz.querySelector('[data-download]');
if (dl) dl.addEventListener('click', function () {
var a = document.createElement('a'), codes = [].map.call(wz.querySelectorAll('[data-codes] li'), function (li) { return li.textContent; });
a.href = URL.createObjectURL(new Blob([codes.join('\n') + '\n'], { type: 'text/plain' })); a.download = dl.dataset.download; a.click();
});
go(0);
});
About this set
Two multi-step forms for dark interfaces, with a cyan glow for the current step and lime for finished ones. The Glowing stepper deploy wizard walks through deploying a repository to a fictional host: pick a repository from a radio list, configure the framework preset, root directory, build command and environment variables (an Add variable button clones a key and value row), then choose a region from radio cards and check a live summary before pressing Deploy. Its horizontal stepper fills each connector with a glowing cyan line and gives the current dot a soft pulse. The Progress ring header turns on two-factor authentication in four steps: authenticator apps, a QR code with a copyable setup key, a six-digit code field and a grid of backup codes with a Download .txt button and a required confirmation checkbox. An SVG ring in the header fills from a custom property the script sets and shows 2 of 4 in the middle. Use the first for longer setup flows and the second for short security flows. One small vanilla script, identical in all three versions, shows one step at a time, checks each step with native validation and moves focus to the new heading; without JavaScript every step shows and the form submits normally.
What’s included
Glowing horizontal stepper with a pulsing current dot and lime done states
SVG progress ring driven by a --wz-p custom property
Add variable rows, Copy setup key and Download .txt backup codes in a few lines of vanilla JS
Live review summary that updates as fields change
Native checkValidity and reportValidity per step; Enter acts as Continue
Works without JavaScript: all steps show and the form submits
Accessibility
The stepper is an ordered list with aria-current="step" on the current item and hidden ", completed" text on finished ones, and focus moves to each new step's heading, which starts with "Step 2 of 4". Every field has a label or aria-label, field borders and radio outlines reach 3:1 against the card, and muted text reaches 4.5:1. Focus rings are cyan with a gap in the card colour so they stay visible next to bright controls, and the pulse, ring and panel animations switch off under prefers-reduced-motion.
Customise it
Change the --wz-* custom properties on .wz (card, line, field, accent, glow, done) in the plain CSS and Bootstrap versions; Bootstrap also maps them onto --bs-body-bg, --bs-border-color and the .btn and .list-group variables. In Tailwind, edit the --color-wz-* values in @theme.