Open demo

CSS code

HTML
<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);
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Glowing stepper deploy wizard</span>
<form class="wz card" data-bs-theme="dark" data-wizard aria-labelledby="dp-title"><div class="card-body">
  <h2 class="wz-title fw-bold mb-0" id="dp-title">Deploy to Stratus</h2>
  <p class="wz-sub text-body-secondary">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="visually-hidden" 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="visually-hidden" 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="visually-hidden" data-sr></span></li>
  </ol>

  <section class="wz-panel" data-step aria-labelledby="dp-h1">
    <h3 class="wz-h fw-bold mb-0" 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><legend class="visually-hidden">Repository</legend>
      <div class="list-group">
        <label class="list-group-item list-group-item-action wz-repo d-flex align-items-center gap-3"><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="flex-grow-1 d-grid"><b class="font-monospace">acme/storefront</b><span class="small text-body-secondary">Next.js · updated 2 h ago</span></span><input class="form-check-input" type="radio" name="repo" value="acme/storefront" checked required></label>
        <label class="list-group-item list-group-item-action wz-repo d-flex align-items-center gap-3"><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="flex-grow-1 d-grid"><b class="font-monospace">acme/docs</b><span class="small text-body-secondary">Astro · updated yesterday</span></span><input class="form-check-input" type="radio" name="repo" value="acme/docs"></label>
        <label class="list-group-item list-group-item-action wz-repo d-flex align-items-center gap-3"><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="flex-grow-1 d-grid"><b class="font-monospace">acme/api</b><span class="small text-body-secondary">Node · updated 4 days ago</span></span><input class="form-check-input" 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 fw-bold mb-0" id="dp-h2" tabindex="-1"><span class="wz-kick">Step 2 of 3</span>Configure the build</h3>
    <div class="row g-3">
      <div class="col-sm-6"><label class="form-label" for="dp-preset">Framework preset</label>
        <select class="form-select" 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="col-sm-6"><label class="form-label" for="dp-root">Root directory</label>
        <input class="form-control font-monospace" id="dp-root" name="root" required spellcheck="false" value="./"></div>
    </div>
    <div><label class="form-label" for="dp-build">Build command</label>
      <input class="form-control font-monospace" id="dp-build" name="build" spellcheck="false" aria-describedby="dp-build-hint" value="next build">
      <div class="form-text mt-1" id="dp-build-hint">Runs after install. Leave it empty to use the preset's default.</div></div>
    <fieldset><legend>Environment variables</legend>
      <div class="vstack gap-2">
        <div class="row g-2 wz-env-head" aria-hidden="true"><span class="col-5">Key</span><span class="col-7">Value</span></div>
        <div class="row g-2" data-row><div class="col-5"><input class="form-control font-monospace" name="env-key" aria-label="Key 1" spellcheck="false" autocomplete="off" value="DATABASE_URL"></div><div class="col-7"><input class="form-control font-monospace" name="env-value" aria-label="Value 1" spellcheck="false" autocomplete="off" value="postgres://[email protected]:5432/shop"></div></div>
        <div class="row g-2" data-row><div class="col-5"><input class="form-control font-monospace" name="env-key" aria-label="Key 2" spellcheck="false" autocomplete="off" value="NEXT_PUBLIC_REGION"></div><div class="col-7"><input class="form-control font-monospace" name="env-value" aria-label="Value 2" spellcheck="false" autocomplete="off" value="fra1"></div></div>
        <button type="button" class="btn btn-sm btn-outline-secondary align-self-start" 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 fw-bold mb-0" id="dp-h3" tabindex="-1"><span class="wz-kick">Step 3 of 3</span>Choose a region and review</h3>
    <fieldset><legend>Region</legend>
      <div class="wz-tiles">
        <label class="wz-tile"><input class="form-check-input" type="radio" name="region" value="Frankfurt (fra1)" checked required><b>Frankfurt</b><span>fra1 · Germany</span></label>
        <label class="wz-tile"><input class="form-check-input" type="radio" name="region" value="Virginia (iad1)"><b>Virginia</b><span>iad1 · US East</span></label>
        <label class="wz-tile"><input class="form-check-input" type="radio" name="region" value="Singapore (sin1)"><b>Singapore</b><span>sin1 · Asia Pacific</span></label>
      </div></fieldset>
    <dl class="list-group mb-0">
      <div class="list-group-item d-flex justify-content-between gap-3 small"><dt>Repository</dt><dd data-out="repo">acme/storefront</dd></div>
      <div class="list-group-item d-flex justify-content-between gap-3 small"><dt>Framework</dt><dd data-out="preset">Next.js</dd></div>
      <div class="list-group-item d-flex justify-content-between gap-3 small"><dt>Root directory</dt><dd data-out="root">./</dd></div>
      <div class="list-group-item d-flex justify-content-between gap-3 small"><dt>Build command</dt><dd data-out="build">next build</dd></div>
      <div class="list-group-item d-flex justify-content-between gap-3 small"><dt>Variables</dt><dd data-out="env-key">DATABASE_URL, NEXT_PUBLIC_REGION</dd></div>
      <div class="list-group-item d-flex justify-content-between gap-3 small"><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 fw-bold mb-0" 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 font-monospace"><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="btn btn-outline-secondary" data-restart>Deploy another project</button>
  </section>

  <div class="wz-nav">
    <button type="button" class="btn btn-outline-secondary" data-back hidden>Back</button>
    <button type="button" class="btn btn-primary" data-next hidden>Continue</button>
    <button type="submit" class="btn btn-primary">Deploy</button>
  </div>
</div></form></div>

<div><span class="lbl">Progress ring header</span>
<form class="wz card" data-bs-theme="dark" data-wizard aria-labelledby="tf-title"><div class="card-body">
  <div class="d-flex align-items-center gap-3">
    <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 fw-bold mb-0" id="tf-title">Turn on two-factor authentication</h2>
      <p class="wz-sub text-body-secondary">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="visually-hidden" data-sr></span></li>
    <li>Scan<span class="visually-hidden" data-sr></span></li>
    <li>Verify<span class="visually-hidden" data-sr></span></li>
    <li>Backup<span class="visually-hidden" data-sr></span></li>
  </ol>

  <section class="wz-panel" data-step aria-labelledby="tf-h1">
    <h3 class="wz-h fw-bold mb-0" 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="list-group">
      <li class="list-group-item d-flex flex-wrap justify-content-between column-gap-3 row-gap-1 py-3"><b>1Password</b><span class="small text-body-secondary">iOS, Android, Mac, Windows</span></li>
      <li class="list-group-item d-flex flex-wrap justify-content-between column-gap-3 row-gap-1 py-3"><b>Authy</b><span class="small text-body-secondary">iOS, Android</span></li>
      <li class="list-group-item d-flex flex-wrap justify-content-between column-gap-3 row-gap-1 py-3"><b>Google Authenticator</b><span class="small text-body-secondary">iOS, Android</span></li>
    </ul>
  </section>

  <section class="wz-panel" data-step aria-labelledby="tf-h2">
    <h3 class="wz-h fw-bold mb-0" id="tf-h2" tabindex="-1"><span class="wz-kick">Step 2 of 4</span>Scan the code</h3>
    <div class="wz-qr d-flex flex-wrap align-items-center gap-4">
      <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 d-grid gap-2">
        <p class="m-0 text-body-secondary">Open your app, tap <b class="text-body">Add account</b> and point the camera at the code.</p>
        <p class="m-0 text-body-secondary">Can't scan it? Enter this setup key instead:</p>
        <div class="d-flex flex-wrap align-items-center gap-2"><code class="wz-code" id="tf-key">JBSW Y3DP EHPK 3PXP</code>
          <button type="button" class="btn btn-sm btn-outline-secondary" data-copy="tf-key">Copy</button><span class="visually-hidden" role="status" data-status></span></div>
      </div>
    </div>
  </section>

  <section class="wz-panel" data-step aria-labelledby="tf-h3">
    <h3 class="wz-h fw-bold mb-0" id="tf-h3" tabindex="-1"><span class="wz-kick">Step 3 of 4</span>Enter the 6-digit code</h3>
    <div><label class="form-label" for="tf-code">Code from your app</label>
      <input class="form-control font-monospace 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">
      <div class="form-text mt-1" id="tf-code-hint">Six digits, no spaces. The code changes every 30 seconds.</div></div>
  </section>

  <section class="wz-panel" data-step aria-labelledby="tf-h4">
    <h3 class="wz-h fw-bold mb-0" 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="btn btn-sm btn-outline-secondary 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>
    <div class="form-check wz-checkline"><input class="form-check-input" type="checkbox" id="tf-saved" name="saved" value="yes" required><label class="form-check-label" for="tf-saved">I saved my backup codes</label></div>
  </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 fw-bold mb-0" 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="btn btn-outline-secondary" data-restart>Start over</button>
  </section>

  <div class="wz-nav">
    <button type="button" class="btn btn-outline-secondary" data-back hidden>Back</button>
    <button type="button" class="btn btn-primary" data-next hidden>Continue</button>
    <button type="submit" class="btn btn-primary">Turn on 2FA</button>
  </div>
</div></form></div>
CSS
/* 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);
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-3 block text-[.72rem] tracking-[.12em] text-[#9aa4b8] uppercase">Glowing stepper deploy wizard</span>
<form class="rounded-[18px] border border-wz-line bg-wz-card bg-[radial-gradient(640px_200px_at_50%_-80px,rgba(34,211,238,.1),transparent_70%)] bg-no-repeat px-8 py-[30px] text-wz-ink shadow-[0_24px_60px_-24px_#000] scheme-dark max-[600px]:px-[18px] max-[600px]:py-6" data-wizard aria-labelledby="dp-title">
  <h2 class="text-[1.3rem] font-bold tracking-[-.015em]" id="dp-title">Deploy to Stratus</h2>
  <p class="mt-1 text-[.92rem] text-wz-muted">Stratus redeploys on every push to main. You can change all of this later in Project settings.</p>
  <ol class="mt-[26px] grid grid-cols-3" data-stepper aria-label="Progress">
    <li class="group relative flex flex-col items-center gap-2 text-center text-[.8rem] text-wz-muted data-[state=current]:font-semibold data-[state=current]:text-wz-ink max-[600px]:text-[.72rem] not-first:before:absolute not-first:before:top-4 not-first:before:right-[calc(50%+26px)] not-first:before:left-[calc(-50%+26px)] not-first:before:h-0.5 not-first:before:rounded-sm not-first:before:bg-wz-track not-first:after:absolute not-first:after:top-4 not-first:after:right-[calc(50%+26px)] not-first:after:left-[calc(-50%+26px)] not-first:after:h-0.5 not-first:after:origin-left not-first:after:scale-x-0 not-first:after:rounded-sm not-first:after:bg-wz-accent not-first:after:shadow-[0_0_8px_1px_var(--color-wz-glow)] not-first:after:transition-transform not-first:after:duration-[450ms] not-first:data-[state=current]:after:scale-x-100 not-first:data-[state=done]:after:scale-x-100 not-first:motion-reduce:after:transition-none"><span class="grid size-[34px] place-items-center rounded-full border-2 border-wz-field bg-wz-card text-[.88rem] font-bold text-[#c3c9d6] transition-colors duration-250 group-data-[state=current]:border-wz-accent group-data-[state=current]:bg-[#0c2530] group-data-[state=current]:text-wz-accent group-data-[state=current]:shadow-[0_0_14px_var(--color-wz-glow)] group-data-[state=done]:border-wz-done group-data-[state=done]:bg-wz-done group-data-[state=done]:text-wz-card motion-safe:group-data-[state=current]:animate-wz-pulse motion-reduce:transition-none"><b class="group-data-[state=done]:hidden">1</b><svg class="hidden size-[17px] fill-none stroke-current stroke-3 group-data-[state=done]:block" stroke-linecap="round" stroke-linejoin="round" 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 class="group relative flex flex-col items-center gap-2 text-center text-[.8rem] text-wz-muted data-[state=current]:font-semibold data-[state=current]:text-wz-ink max-[600px]:text-[.72rem] not-first:before:absolute not-first:before:top-4 not-first:before:right-[calc(50%+26px)] not-first:before:left-[calc(-50%+26px)] not-first:before:h-0.5 not-first:before:rounded-sm not-first:before:bg-wz-track not-first:after:absolute not-first:after:top-4 not-first:after:right-[calc(50%+26px)] not-first:after:left-[calc(-50%+26px)] not-first:after:h-0.5 not-first:after:origin-left not-first:after:scale-x-0 not-first:after:rounded-sm not-first:after:bg-wz-accent not-first:after:shadow-[0_0_8px_1px_var(--color-wz-glow)] not-first:after:transition-transform not-first:after:duration-[450ms] not-first:data-[state=current]:after:scale-x-100 not-first:data-[state=done]:after:scale-x-100 not-first:motion-reduce:after:transition-none"><span class="grid size-[34px] place-items-center rounded-full border-2 border-wz-field bg-wz-card text-[.88rem] font-bold text-[#c3c9d6] transition-colors duration-250 group-data-[state=current]:border-wz-accent group-data-[state=current]:bg-[#0c2530] group-data-[state=current]:text-wz-accent group-data-[state=current]:shadow-[0_0_14px_var(--color-wz-glow)] group-data-[state=done]:border-wz-done group-data-[state=done]:bg-wz-done group-data-[state=done]:text-wz-card motion-safe:group-data-[state=current]:animate-wz-pulse motion-reduce:transition-none"><b class="group-data-[state=done]:hidden">2</b><svg class="hidden size-[17px] fill-none stroke-current stroke-3 group-data-[state=done]:block" stroke-linecap="round" stroke-linejoin="round" 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 class="group relative flex flex-col items-center gap-2 text-center text-[.8rem] text-wz-muted data-[state=current]:font-semibold data-[state=current]:text-wz-ink max-[600px]:text-[.72rem] not-first:before:absolute not-first:before:top-4 not-first:before:right-[calc(50%+26px)] not-first:before:left-[calc(-50%+26px)] not-first:before:h-0.5 not-first:before:rounded-sm not-first:before:bg-wz-track not-first:after:absolute not-first:after:top-4 not-first:after:right-[calc(50%+26px)] not-first:after:left-[calc(-50%+26px)] not-first:after:h-0.5 not-first:after:origin-left not-first:after:scale-x-0 not-first:after:rounded-sm not-first:after:bg-wz-accent not-first:after:shadow-[0_0_8px_1px_var(--color-wz-glow)] not-first:after:transition-transform not-first:after:duration-[450ms] not-first:data-[state=current]:after:scale-x-100 not-first:data-[state=done]:after:scale-x-100 not-first:motion-reduce:after:transition-none"><span class="grid size-[34px] place-items-center rounded-full border-2 border-wz-field bg-wz-card text-[.88rem] font-bold text-[#c3c9d6] transition-colors duration-250 group-data-[state=current]:border-wz-accent group-data-[state=current]:bg-[#0c2530] group-data-[state=current]:text-wz-accent group-data-[state=current]:shadow-[0_0_14px_var(--color-wz-glow)] group-data-[state=done]:border-wz-done group-data-[state=done]:bg-wz-done group-data-[state=done]:text-wz-card motion-safe:group-data-[state=current]:animate-wz-pulse motion-reduce:transition-none"><b class="group-data-[state=done]:hidden">3</b><svg class="hidden size-[17px] fill-none stroke-current stroke-3 group-data-[state=done]:block" stroke-linecap="round" stroke-linejoin="round" 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="mt-7 grid animate-wz-in gap-[18px] motion-reduce:animate-none" data-step aria-labelledby="dp-h1">
    <h3 class="text-[1.15rem] font-bold tracking-[-.01em] focus:outline-none" id="dp-h1" tabindex="-1"><span class="mb-0.5 block text-[.72rem] font-bold tracking-[.1em] text-wz-accent uppercase">Step 1 of 3</span>Pick a repository</h3>
    <p class="-mt-2.5 text-[.92rem] text-wz-muted">Showing repositories from the acme organisation on GitHub.</p>
    <fieldset class="grid min-w-0 gap-1.5"><legend class="sr-only">Repository</legend>
      <div class="grid divide-y divide-wz-line overflow-hidden rounded-xl border border-wz-line">
        <label class="flex cursor-pointer items-center gap-3.5 px-4 py-[13px] transition-colors hover:bg-[#141a2a] has-checked:bg-[rgba(34,211,238,.07)] has-checked:shadow-[inset_3px_0_0_var(--color-wz-accent)] motion-reduce:transition-none"><span class="grid size-9 flex-none place-items-center rounded-[9px] border border-wz-line bg-[#161d2e] text-wz-muted"><svg class="size-[18px] fill-none stroke-current stroke-[1.8]" stroke-linecap="round" stroke-linejoin="round" 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="grid min-w-0 flex-1"><b class="font-mono text-[.9rem] font-semibold wrap-anywhere">acme/storefront</b><span class="text-[.8rem] text-wz-muted">Next.js · updated 2 h ago</span></span><input class="size-[18px] flex-none cursor-pointer appearance-none rounded-full border-2 border-wz-field bg-wz-input checked:border-wz-accent checked:bg-[radial-gradient(circle,var(--color-wz-accent)_0_4px,transparent_4.5px)] focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-wz-accent" type="radio" name="repo" value="acme/storefront" checked required></label>
        <label class="flex cursor-pointer items-center gap-3.5 px-4 py-[13px] transition-colors hover:bg-[#141a2a] has-checked:bg-[rgba(34,211,238,.07)] has-checked:shadow-[inset_3px_0_0_var(--color-wz-accent)] motion-reduce:transition-none"><span class="grid size-9 flex-none place-items-center rounded-[9px] border border-wz-line bg-[#161d2e] text-wz-muted"><svg class="size-[18px] fill-none stroke-current stroke-[1.8]" stroke-linecap="round" stroke-linejoin="round" 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="grid min-w-0 flex-1"><b class="font-mono text-[.9rem] font-semibold wrap-anywhere">acme/docs</b><span class="text-[.8rem] text-wz-muted">Astro · updated yesterday</span></span><input class="size-[18px] flex-none cursor-pointer appearance-none rounded-full border-2 border-wz-field bg-wz-input checked:border-wz-accent checked:bg-[radial-gradient(circle,var(--color-wz-accent)_0_4px,transparent_4.5px)] focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-wz-accent" type="radio" name="repo" value="acme/docs"></label>
        <label class="flex cursor-pointer items-center gap-3.5 px-4 py-[13px] transition-colors hover:bg-[#141a2a] has-checked:bg-[rgba(34,211,238,.07)] has-checked:shadow-[inset_3px_0_0_var(--color-wz-accent)] motion-reduce:transition-none"><span class="grid size-9 flex-none place-items-center rounded-[9px] border border-wz-line bg-[#161d2e] text-wz-muted"><svg class="size-[18px] fill-none stroke-current stroke-[1.8]" stroke-linecap="round" stroke-linejoin="round" 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="grid min-w-0 flex-1"><b class="font-mono text-[.9rem] font-semibold wrap-anywhere">acme/api</b><span class="text-[.8rem] text-wz-muted">Node · updated 4 days ago</span></span><input class="size-[18px] flex-none cursor-pointer appearance-none rounded-full border-2 border-wz-field bg-wz-input checked:border-wz-accent checked:bg-[radial-gradient(circle,var(--color-wz-accent)_0_4px,transparent_4.5px)] focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-wz-accent" type="radio" name="repo" value="acme/api"></label>
      </div></fieldset>
  </section>

  <section class="mt-7 grid animate-wz-in gap-[18px] motion-reduce:animate-none" data-step aria-labelledby="dp-h2">
    <h3 class="text-[1.15rem] font-bold tracking-[-.01em] focus:outline-none" id="dp-h2" tabindex="-1"><span class="mb-0.5 block text-[.72rem] font-bold tracking-[.1em] text-wz-accent uppercase">Step 2 of 3</span>Configure the build</h3>
    <div class="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-4">
      <div class="grid min-w-0 gap-1.5"><label class="text-[.88rem] font-semibold" for="dp-preset">Framework preset</label>
        <select class="min-h-11 w-full min-w-0 rounded-[10px] border-[1.5px] border-wz-field bg-wz-input px-3 py-2.5 text-[.95rem] text-wz-ink focus-visible:border-wz-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-wz-accent" 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="grid min-w-0 gap-1.5"><label class="text-[.88rem] font-semibold" for="dp-root">Root directory</label>
        <input class="min-h-11 w-full min-w-0 rounded-[10px] border-[1.5px] border-wz-field bg-wz-input px-3 py-2.5 font-mono text-[.88rem] text-wz-ink focus-visible:border-wz-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-wz-accent" id="dp-root" name="root" required spellcheck="false" value="./"></div>
    </div>
    <div class="grid min-w-0 gap-1.5"><label class="text-[.88rem] font-semibold" for="dp-build">Build command</label>
      <input class="min-h-11 w-full min-w-0 rounded-[10px] border-[1.5px] border-wz-field bg-wz-input px-3 py-2.5 font-mono text-[.88rem] text-wz-ink focus-visible:border-wz-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-wz-accent" id="dp-build" name="build" spellcheck="false" aria-describedby="dp-build-hint" value="next build">
      <span class="text-[.8rem] text-wz-muted" id="dp-build-hint">Runs after install. Leave it empty to use the preset's default.</span></div>
    <fieldset class="grid min-w-0 gap-1.5"><legend class="mb-1.5 text-[.88rem] font-semibold">Environment variables</legend>
      <div class="grid gap-2">
        <div class="grid grid-cols-[minmax(0,2fr)_minmax(0,3fr)] gap-2 text-[.75rem] font-semibold tracking-[.08em] text-wz-muted uppercase" aria-hidden="true"><span>Key</span><span>Value</span></div>
        <div class="grid grid-cols-[minmax(0,2fr)_minmax(0,3fr)] gap-2" data-row><input class="min-h-11 w-full min-w-0 rounded-[10px] border-[1.5px] border-wz-field bg-wz-input px-3 py-2.5 font-mono text-[.88rem] text-wz-ink focus-visible:border-wz-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-wz-accent" name="env-key" aria-label="Key 1" spellcheck="false" autocomplete="off" value="DATABASE_URL"><input class="min-h-11 w-full min-w-0 rounded-[10px] border-[1.5px] border-wz-field bg-wz-input px-3 py-2.5 font-mono text-[.88rem] text-wz-ink focus-visible:border-wz-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-wz-accent" name="env-value" aria-label="Value 1" spellcheck="false" autocomplete="off" value="postgres://[email protected]:5432/shop"></div>
        <div class="grid grid-cols-[minmax(0,2fr)_minmax(0,3fr)] gap-2" data-row><input class="min-h-11 w-full min-w-0 rounded-[10px] border-[1.5px] border-wz-field bg-wz-input px-3 py-2.5 font-mono text-[.88rem] text-wz-ink focus-visible:border-wz-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-wz-accent" name="env-key" aria-label="Key 2" spellcheck="false" autocomplete="off" value="NEXT_PUBLIC_REGION"><input class="min-h-11 w-full min-w-0 rounded-[10px] border-[1.5px] border-wz-field bg-wz-input px-3 py-2.5 font-mono text-[.88rem] text-wz-ink focus-visible:border-wz-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-wz-accent" name="env-value" aria-label="Value 2" spellcheck="false" autocomplete="off" value="fra1"></div>
        <button type="button" class="inline-flex min-h-[38px] cursor-pointer items-center justify-center gap-2 justify-self-start rounded-[10px] border-[1.5px] border-wz-field bg-transparent px-3.5 text-[.86rem] font-semibold text-wz-ink transition-colors hover:border-[#7d879d] hover:bg-[#171e30] focus-visible:shadow-[0_0_0_3px_var(--color-wz-card),0_0_0_5px_var(--color-wz-accent)] focus-visible:outline-none motion-reduce:transition-none" data-add-row><svg class="size-4 fill-none stroke-current stroke-[2.2]" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>Add variable</button>
      </div></fieldset>
  </section>

  <section class="mt-7 grid animate-wz-in gap-[18px] motion-reduce:animate-none" data-step aria-labelledby="dp-h3">
    <h3 class="text-[1.15rem] font-bold tracking-[-.01em] focus:outline-none" id="dp-h3" tabindex="-1"><span class="mb-0.5 block text-[.72rem] font-bold tracking-[.1em] text-wz-accent uppercase">Step 3 of 3</span>Choose a region and review</h3>
    <fieldset class="grid min-w-0 gap-1.5"><legend class="mb-1.5 text-[.88rem] font-semibold">Region</legend>
      <div class="grid grid-cols-[repeat(auto-fit,minmax(150px,1fr))] gap-4">
        <label class="relative grid cursor-pointer gap-0.5 rounded-xl border-[1.5px] border-[#2f3850] bg-[#0c111d] py-3.5 pr-3.5 pl-11 transition-[border-color,box-shadow] duration-200 has-checked:border-wz-accent has-checked:shadow-[0_0_20px_-6px_var(--color-wz-glow)] motion-reduce:transition-none"><input class="absolute top-4 left-3.5 size-[18px] flex-none cursor-pointer appearance-none rounded-full border-2 border-wz-field bg-wz-input checked:border-wz-accent checked:bg-[radial-gradient(circle,var(--color-wz-accent)_0_4px,transparent_4.5px)] focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-wz-accent" type="radio" name="region" value="Frankfurt (fra1)" checked required><b class="text-[.95rem]">Frankfurt</b><span class="font-mono text-[.8rem] text-wz-muted">fra1 · Germany</span></label>
        <label class="relative grid cursor-pointer gap-0.5 rounded-xl border-[1.5px] border-[#2f3850] bg-[#0c111d] py-3.5 pr-3.5 pl-11 transition-[border-color,box-shadow] duration-200 has-checked:border-wz-accent has-checked:shadow-[0_0_20px_-6px_var(--color-wz-glow)] motion-reduce:transition-none"><input class="absolute top-4 left-3.5 size-[18px] flex-none cursor-pointer appearance-none rounded-full border-2 border-wz-field bg-wz-input checked:border-wz-accent checked:bg-[radial-gradient(circle,var(--color-wz-accent)_0_4px,transparent_4.5px)] focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-wz-accent" type="radio" name="region" value="Virginia (iad1)"><b class="text-[.95rem]">Virginia</b><span class="font-mono text-[.8rem] text-wz-muted">iad1 · US East</span></label>
        <label class="relative grid cursor-pointer gap-0.5 rounded-xl border-[1.5px] border-[#2f3850] bg-[#0c111d] py-3.5 pr-3.5 pl-11 transition-[border-color,box-shadow] duration-200 has-checked:border-wz-accent has-checked:shadow-[0_0_20px_-6px_var(--color-wz-glow)] motion-reduce:transition-none"><input class="absolute top-4 left-3.5 size-[18px] flex-none cursor-pointer appearance-none rounded-full border-2 border-wz-field bg-wz-input checked:border-wz-accent checked:bg-[radial-gradient(circle,var(--color-wz-accent)_0_4px,transparent_4.5px)] focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-wz-accent" type="radio" name="region" value="Singapore (sin1)"><b class="text-[.95rem]">Singapore</b><span class="font-mono text-[.8rem] text-wz-muted">sin1 · Asia Pacific</span></label>
      </div></fieldset>
    <dl class="grid divide-y divide-wz-line overflow-hidden rounded-xl border border-wz-line bg-[#0c111d]">
      <div class="flex justify-between gap-4 px-3.5 py-2.5 text-[.9rem]"><dt class="text-wz-muted">Repository</dt><dd class="text-right font-mono text-[.84rem] wrap-anywhere" data-out="repo">acme/storefront</dd></div>
      <div class="flex justify-between gap-4 px-3.5 py-2.5 text-[.9rem]"><dt class="text-wz-muted">Framework</dt><dd class="text-right font-mono text-[.84rem] wrap-anywhere" data-out="preset">Next.js</dd></div>
      <div class="flex justify-between gap-4 px-3.5 py-2.5 text-[.9rem]"><dt class="text-wz-muted">Root directory</dt><dd class="text-right font-mono text-[.84rem] wrap-anywhere" data-out="root">./</dd></div>
      <div class="flex justify-between gap-4 px-3.5 py-2.5 text-[.9rem]"><dt class="text-wz-muted">Build command</dt><dd class="text-right font-mono text-[.84rem] wrap-anywhere" data-out="build">next build</dd></div>
      <div class="flex justify-between gap-4 px-3.5 py-2.5 text-[.9rem]"><dt class="text-wz-muted">Variables</dt><dd class="text-right font-mono text-[.84rem] wrap-anywhere" data-out="env-key">DATABASE_URL, NEXT_PUBLIC_REGION</dd></div>
      <div class="flex justify-between gap-4 px-3.5 py-2.5 text-[.9rem]"><dt class="text-wz-muted">Region</dt><dd class="text-right font-mono text-[.84rem] wrap-anywhere" data-out="region">Frankfurt (fra1)</dd></div>
    </dl>
  </section>

  <section class="mt-7 grid animate-wz-in justify-items-center gap-[18px] pt-3 pb-1 text-center motion-reduce:animate-none" data-done hidden aria-labelledby="dp-h4">
    <span class="grid size-14 place-items-center rounded-full border-2 border-wz-done bg-[rgba(163,230,53,.1)] text-wz-done shadow-[0_0_24px_-4px_rgba(163,230,53,.45)]"><svg class="size-[26px] fill-none stroke-current stroke-[2.6]" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg></span>
    <h3 class="text-[1.15rem] font-bold tracking-[-.01em] focus:outline-none" id="dp-h4" tabindex="-1">Deployed to production</h3>
    <p class="text-[.92rem] text-wz-muted">Your project is live. New pushes to main deploy on their own.</p>
<pre class="w-full rounded-xl border border-wz-line bg-[#070a12] px-4 py-3.5 text-left font-mono text-[.82rem] leading-[1.7] whitespace-pre-wrap text-wz-muted wrap-anywhere"><b class="font-normal text-wz-done">✓</b> Cloned acme/storefront · main @ 3f9c2e1
<b class="font-normal text-wz-done">✓</b> Installed 412 packages in 9 s
<b class="font-normal text-wz-done">✓</b> Build completed in 41 s · <i class="text-wz-accent not-italic">https://storefront-7f3a.stratus.app</i></pre>
    <button type="button" class="inline-flex min-h-11 cursor-pointer items-center justify-center gap-2 rounded-[10px] border-[1.5px] border-wz-field bg-transparent px-[22px] text-[.95rem] font-semibold text-wz-ink transition-colors hover:border-[#7d879d] hover:bg-[#171e30] focus-visible:shadow-[0_0_0_3px_var(--color-wz-card),0_0_0_5px_var(--color-wz-accent)] focus-visible:outline-none motion-reduce:transition-none" data-restart>Deploy another project</button>
  </section>

  <div class="mt-7 flex gap-2.5 border-t border-wz-line pt-5">
    <button type="button" class="inline-flex min-h-11 cursor-pointer items-center justify-center gap-2 rounded-[10px] border-[1.5px] border-wz-field bg-transparent px-[22px] text-[.95rem] font-semibold text-wz-ink transition-colors hover:border-[#7d879d] hover:bg-[#171e30] focus-visible:shadow-[0_0_0_3px_var(--color-wz-card),0_0_0_5px_var(--color-wz-accent)] focus-visible:outline-none motion-reduce:transition-none" data-back hidden>Back</button>
    <button type="button" class="ms-auto inline-flex min-h-11 cursor-pointer items-center justify-center gap-2 rounded-[10px] border-[1.5px] border-wz-accent bg-wz-accent px-[22px] text-[.95rem] font-semibold text-wz-on shadow-[0_0_22px_-6px_var(--color-wz-glow)] transition-colors hover:border-wz-hi hover:bg-wz-hi focus-visible:shadow-[0_0_0_3px_var(--color-wz-card),0_0_0_5px_var(--color-wz-accent)] focus-visible:outline-none motion-reduce:transition-none" data-next hidden>Continue</button>
    <button type="submit" class="ms-auto inline-flex min-h-11 cursor-pointer items-center justify-center gap-2 rounded-[10px] border-[1.5px] border-wz-accent bg-wz-accent px-[22px] text-[.95rem] font-semibold text-wz-on shadow-[0_0_22px_-6px_var(--color-wz-glow)] transition-colors hover:border-wz-hi hover:bg-wz-hi focus-visible:shadow-[0_0_0_3px_var(--color-wz-card),0_0_0_5px_var(--color-wz-accent)] focus-visible:outline-none motion-reduce:transition-none">Deploy</button>
  </div>
</form></div>

<div><span class="mb-3 block text-[.72rem] tracking-[.12em] text-[#9aa4b8] uppercase">Progress ring header</span>
<form class="rounded-[18px] border border-wz-line bg-wz-card bg-[radial-gradient(640px_200px_at_50%_-80px,rgba(34,211,238,.1),transparent_70%)] bg-no-repeat px-8 py-[30px] text-wz-ink shadow-[0_24px_60px_-24px_#000] scheme-dark max-[600px]:px-[18px] max-[600px]:py-6" data-wizard aria-labelledby="tf-title">
  <div class="flex items-center gap-[18px] max-[600px]:gap-3.5">
    <div class="group relative grid size-[72px] flex-none place-items-center text-[.8rem] font-bold whitespace-nowrap max-[600px]:size-16" data-ring style="--wz-p:25" aria-hidden="true"><svg class="absolute inset-0 size-full -rotate-90 overflow-visible" viewBox="0 0 40 40"><circle class="fill-none stroke-wz-track stroke-[3.5]" cx="20" cy="20" r="17"/><circle class="fill-none stroke-wz-accent stroke-[3.5] drop-shadow-[0_0_3px_var(--color-wz-glow)] transition-[stroke-dashoffset,stroke] duration-500 [stroke-dasharray:100] [stroke-dashoffset:calc(100px_-_var(--wz-p,25)*1px)] group-data-[full]:stroke-wz-done group-data-[full]:drop-shadow-[0_0_3px_rgba(163,230,53,.5)] motion-reduce:transition-none" stroke-linecap="round" cx="20" cy="20" r="17" pathLength="100"/></svg><span class="relative" data-count>1 of 4</span></div>
    <div><h2 class="text-[1.3rem] font-bold tracking-[-.015em]" id="tf-title">Turn on two-factor authentication</h2>
      <p class="mt-1 text-[.92rem] text-wz-muted">Ledgerly will ask for a code from your phone each time you sign in.</p></div>
  </div>
  <ol class="mt-[22px] grid grid-cols-4 gap-2" data-stepper aria-label="Progress">
    <li class="grid gap-2 text-[.78rem] text-wz-muted before:h-1 before:rounded-full before:bg-wz-track before:transition-[background-color,box-shadow] before:duration-300 data-[state=current]:font-semibold data-[state=current]:text-wz-ink data-[state=current]:before:bg-wz-accent data-[state=current]:before:shadow-[0_0_10px_var(--color-wz-glow)] data-[state=done]:before:bg-wz-done motion-reduce:before:transition-none">App<span class="sr-only" data-sr></span></li>
    <li class="grid gap-2 text-[.78rem] text-wz-muted before:h-1 before:rounded-full before:bg-wz-track before:transition-[background-color,box-shadow] before:duration-300 data-[state=current]:font-semibold data-[state=current]:text-wz-ink data-[state=current]:before:bg-wz-accent data-[state=current]:before:shadow-[0_0_10px_var(--color-wz-glow)] data-[state=done]:before:bg-wz-done motion-reduce:before:transition-none">Scan<span class="sr-only" data-sr></span></li>
    <li class="grid gap-2 text-[.78rem] text-wz-muted before:h-1 before:rounded-full before:bg-wz-track before:transition-[background-color,box-shadow] before:duration-300 data-[state=current]:font-semibold data-[state=current]:text-wz-ink data-[state=current]:before:bg-wz-accent data-[state=current]:before:shadow-[0_0_10px_var(--color-wz-glow)] data-[state=done]:before:bg-wz-done motion-reduce:before:transition-none">Verify<span class="sr-only" data-sr></span></li>
    <li class="grid gap-2 text-[.78rem] text-wz-muted before:h-1 before:rounded-full before:bg-wz-track before:transition-[background-color,box-shadow] before:duration-300 data-[state=current]:font-semibold data-[state=current]:text-wz-ink data-[state=current]:before:bg-wz-accent data-[state=current]:before:shadow-[0_0_10px_var(--color-wz-glow)] data-[state=done]:before:bg-wz-done motion-reduce:before:transition-none">Backup<span class="sr-only" data-sr></span></li>
  </ol>

  <section class="mt-7 grid animate-wz-in gap-[18px] motion-reduce:animate-none" data-step aria-labelledby="tf-h1">
    <h3 class="text-[1.15rem] font-bold tracking-[-.01em] focus:outline-none" id="tf-h1" tabindex="-1"><span class="mb-0.5 block text-[.72rem] font-bold tracking-[.1em] text-wz-accent uppercase">Step 1 of 4</span>Get an authenticator app</h3>
    <p class="-mt-2.5 text-[.92rem] text-wz-muted">Install one of these on your phone if you don't have one yet. Any app that supports TOTP works.</p>
    <ul class="grid divide-y divide-wz-line overflow-hidden rounded-xl border border-wz-line">
      <li class="flex flex-wrap justify-between gap-x-4 gap-y-1 px-4 py-3 text-[.92rem]"><b>1Password</b><span class="text-[.84rem] text-wz-muted">iOS, Android, Mac, Windows</span></li>
      <li class="flex flex-wrap justify-between gap-x-4 gap-y-1 px-4 py-3 text-[.92rem]"><b>Authy</b><span class="text-[.84rem] text-wz-muted">iOS, Android</span></li>
      <li class="flex flex-wrap justify-between gap-x-4 gap-y-1 px-4 py-3 text-[.92rem]"><b>Google Authenticator</b><span class="text-[.84rem] text-wz-muted">iOS, Android</span></li>
    </ul>
  </section>

  <section class="mt-7 grid animate-wz-in gap-[18px] motion-reduce:animate-none" data-step aria-labelledby="tf-h2">
    <h3 class="text-[1.15rem] font-bold tracking-[-.01em] focus:outline-none" id="tf-h2" tabindex="-1"><span class="mb-0.5 block text-[.72rem] font-bold tracking-[.1em] text-wz-accent uppercase">Step 2 of 4</span>Scan the code</h3>
    <div class="flex flex-wrap items-center gap-6">
      <svg class="size-[132px] flex-none rounded-[10px] shadow-[0_0_0_1px_var(--color-wz-line),0_0_30px_-10px_var(--color-wz-glow)]" 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="grid flex-[1_1_220px] gap-2.5 text-[.92rem]">
        <p class="text-wz-muted">Open your app, tap <b class="text-wz-ink">Add account</b> and point the camera at the code.</p>
        <p class="text-wz-muted">Can't scan it? Enter this setup key instead:</p>
        <div class="flex flex-wrap items-center gap-2.5"><code class="rounded-lg border border-wz-line bg-wz-input px-3 py-2 font-mono text-[.98rem] tracking-[.06em] text-wz-ink" id="tf-key">JBSW Y3DP EHPK 3PXP</code>
          <button type="button" class="inline-flex min-h-[38px] cursor-pointer items-center justify-center gap-2 justify-self-start rounded-[10px] border-[1.5px] border-wz-field bg-transparent px-3.5 text-[.86rem] font-semibold text-wz-ink transition-colors hover:border-[#7d879d] hover:bg-[#171e30] focus-visible:shadow-[0_0_0_3px_var(--color-wz-card),0_0_0_5px_var(--color-wz-accent)] focus-visible:outline-none motion-reduce:transition-none" data-copy="tf-key">Copy</button><span class="sr-only" role="status" data-status></span></div>
      </div>
    </div>
  </section>

  <section class="mt-7 grid animate-wz-in gap-[18px] motion-reduce:animate-none" data-step aria-labelledby="tf-h3">
    <h3 class="text-[1.15rem] font-bold tracking-[-.01em] focus:outline-none" id="tf-h3" tabindex="-1"><span class="mb-0.5 block text-[.72rem] font-bold tracking-[.1em] text-wz-accent uppercase">Step 3 of 4</span>Enter the 6-digit code</h3>
    <div class="grid min-w-0 gap-1.5"><label class="text-[.88rem] font-semibold" for="tf-code">Code from your app</label>
      <input class="min-h-11 w-full max-w-[240px] min-w-0 rounded-[10px] border-[1.5px] border-wz-field bg-wz-input px-3 py-2.5 text-center font-mono text-[1.35rem] tracking-[.35em] text-wz-ink focus-visible:border-wz-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-wz-accent" 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="text-[.8rem] text-wz-muted" id="tf-code-hint">Six digits, no spaces. The code changes every 30 seconds.</span></div>
  </section>

  <section class="mt-7 grid animate-wz-in gap-[18px] motion-reduce:animate-none" data-step aria-labelledby="tf-h4">
    <h3 class="text-[1.15rem] font-bold tracking-[-.01em] focus:outline-none" id="tf-h4" tabindex="-1"><span class="mb-0.5 block text-[.72rem] font-bold tracking-[.1em] text-wz-accent uppercase">Step 4 of 4</span>Save your backup codes</h3>
    <p class="-mt-2.5 text-[.92rem] text-wz-muted">If you lose your phone, each of these signs you in once. Keep them in your password manager.</p>
    <ul class="grid grid-cols-[repeat(auto-fill,minmax(120px,1fr))] gap-x-4 gap-y-2.5 rounded-xl border border-wz-line bg-wz-input p-3.5 text-center font-mono text-[.92rem]" 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="inline-flex min-h-[38px] cursor-pointer items-center justify-center gap-2 justify-self-start rounded-[10px] border-[1.5px] border-wz-field bg-transparent px-3.5 text-[.86rem] font-semibold text-wz-ink transition-colors hover:border-[#7d879d] hover:bg-[#171e30] focus-visible:shadow-[0_0_0_3px_var(--color-wz-card),0_0_0_5px_var(--color-wz-accent)] focus-visible:outline-none motion-reduce:transition-none" data-download="ledgerly-backup-codes.txt"><svg class="size-4 fill-none stroke-current stroke-[2.2]" stroke-linecap="round" stroke-linejoin="round" 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="flex cursor-pointer items-center gap-2.5 text-[.92rem] font-semibold"><span class="relative grid flex-none"><input class="peer size-5 cursor-pointer appearance-none rounded-md border-2 border-wz-field bg-wz-input checked:border-wz-accent checked:bg-wz-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-wz-accent" type="checkbox" name="saved" value="yes" required><svg class="pointer-events-none absolute inset-0 m-auto size-3.5 fill-none stroke-wz-on stroke-[3.5] opacity-0 peer-checked:opacity-100" stroke-linecap="round" stroke-linejoin="round" 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="mt-7 grid animate-wz-in justify-items-center gap-[18px] pt-3 pb-1 text-center motion-reduce:animate-none" data-done hidden aria-labelledby="tf-h5">
    <span class="grid size-14 place-items-center rounded-full border-2 border-wz-done bg-[rgba(163,230,53,.1)] text-wz-done shadow-[0_0_24px_-4px_rgba(163,230,53,.45)]"><svg class="size-[26px] fill-none stroke-current stroke-[2.6]" stroke-linecap="round" stroke-linejoin="round" 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="text-[1.15rem] font-bold tracking-[-.01em] focus:outline-none" id="tf-h5" tabindex="-1">Two-factor authentication is on</h3>
    <p class="text-[.92rem] text-wz-muted">Next time you sign in to Ledgerly, we'll ask for a code from your app.</p>
    <button type="button" class="inline-flex min-h-11 cursor-pointer items-center justify-center gap-2 rounded-[10px] border-[1.5px] border-wz-field bg-transparent px-[22px] text-[.95rem] font-semibold text-wz-ink transition-colors hover:border-[#7d879d] hover:bg-[#171e30] focus-visible:shadow-[0_0_0_3px_var(--color-wz-card),0_0_0_5px_var(--color-wz-accent)] focus-visible:outline-none motion-reduce:transition-none" data-restart>Start over</button>
  </section>

  <div class="mt-7 flex gap-2.5 border-t border-wz-line pt-5">
    <button type="button" class="inline-flex min-h-11 cursor-pointer items-center justify-center gap-2 rounded-[10px] border-[1.5px] border-wz-field bg-transparent px-[22px] text-[.95rem] font-semibold text-wz-ink transition-colors hover:border-[#7d879d] hover:bg-[#171e30] focus-visible:shadow-[0_0_0_3px_var(--color-wz-card),0_0_0_5px_var(--color-wz-accent)] focus-visible:outline-none motion-reduce:transition-none" data-back hidden>Back</button>
    <button type="button" class="ms-auto inline-flex min-h-11 cursor-pointer items-center justify-center gap-2 rounded-[10px] border-[1.5px] border-wz-accent bg-wz-accent px-[22px] text-[.95rem] font-semibold text-wz-on shadow-[0_0_22px_-6px_var(--color-wz-glow)] transition-colors hover:border-wz-hi hover:bg-wz-hi focus-visible:shadow-[0_0_0_3px_var(--color-wz-card),0_0_0_5px_var(--color-wz-accent)] focus-visible:outline-none motion-reduce:transition-none" data-next hidden>Continue</button>
    <button type="submit" class="ms-auto inline-flex min-h-11 cursor-pointer items-center justify-center gap-2 rounded-[10px] border-[1.5px] border-wz-accent bg-wz-accent px-[22px] text-[.95rem] font-semibold text-wz-on shadow-[0_0_22px_-6px_var(--color-wz-glow)] transition-colors hover:border-wz-hi hover:bg-wz-hi focus-visible:shadow-[0_0_0_3px_var(--color-wz-card),0_0_0_5px_var(--color-wz-accent)] focus-visible:outline-none motion-reduce:transition-none">Turn on 2FA</button>
  </div>
</form></div>
CSS
@theme {
  --color-wz-card: #0f1422;
  --color-wz-line: #232b3d;
  --color-wz-ink: #e6e9f2;
  --color-wz-muted: #9aa4b8;
  --color-wz-field: #646d83;
  --color-wz-input: #0a0e19;
  --color-wz-track: #2c3447;
  --color-wz-accent: #22d3ee;
  --color-wz-hi: #67e8f9;
  --color-wz-glow: rgba(34, 211, 238, .55);
  --color-wz-done: #a3e635;
  --color-wz-on: #04141a;
  --animate-wz-in: wz-in .3s ease;
  --animate-wz-pulse: wz-pulse 2s ease-out infinite;
  @keyframes wz-in { from { opacity: 0; transform: translateY(6px); } }
  @keyframes wz-pulse {
    0% { box-shadow: 0 0 14px var(--color-wz-glow), 0 0 0 0 rgba(34, 211, 238, .45); }
    70%, 100% { box-shadow: 0 0 14px var(--color-wz-glow), 0 0 0 10px rgba(34, 211, 238, 0); }
  }
}
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);
});

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.