$sloprun.dev

Passphrase Strength Meter

demo sandbox: allow-scripts · csp: default-src 'none'

sandboxed and isolated in your browser · never enter a real password in a demo

▶ 5 ran · ✓ 0 worked
i ran it — no login needed:
share: preview embed ↗
post it anywhere:
email the card:

0 reported it broke · the ▶ count is distinct visitors who ran it, counted once each, anonymously

Most strength meters count character classes and hand out a green bar for a capital letter; this one tries to guess your passphrase the way a cracking tool would, searching every way of chopping the text into known patterns — a few hundred common words, dates, counting runs, repeats, keyboard walks, with lookalike swaps like @→a priced at what they actually cost — and reporting the cheapest split it finds. It opens on Summer2019! at 26 bits, gone instantly, and the breakdown underneath says exactly why: one word, one year, one exclamation mark. Try Tr0ub4dor&3, which lands near 25 bits for precisely the reason the famous comic gives, then correct horse battery staple at 47 — then type a real sentence and watch it get caught as prose and priced at barely more than a bit per character, while the same number of unrelated words keeps its full value. The one to press is roll one for me — it draws eight words with real randomness and prints the true entropy beside the meter's own guess, which is usually the more flattering of the two; nothing is stored, nothing is sent, and the page makes no network requests at all.

Embed this demo — put a runnable demo in your blog post

Paste this where you write. It renders a live, runnable demo inline anywhere raw HTML / iframes are allowed — Ghost, WordPress, Notion, Discourse, your own site.

<iframe src="https://sloprun.dev/embed/password-strength" width="100%" height="640" loading="lazy" allow="" style="border:1px solid #E3E2DC;border-radius:10px" title="Passphrase Strength Meter — a runnable demo on sloprun.dev"></iframe>
preview ↗

On Medium and dev.to the plain link becomes a rich preview card that links back here — they don't run third-party iframes, so paste the URL there and the reader clicks through to run it. https://sloprun.dev/p/password-strength

Source

the code you see is the code that just ran raw ↗
Source — the code you see is the code that just ran 1087 lines 50.9 KB index.html
demos/password-strength/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Passphrase Strength Meter — sloprun.dev</title>
<style>
/* sloprun design tokens — inline this block into every demo (self-contained rule).
   Identity: instrument-panel. Machine facts in mono; human words in sans.
   Green is EARNED: only for "it ran / it worked" states, never decoration. */
:root {
  --bg: #F6F6F3;          /* neutral paper, not cream */
  --surface: #FFFFFF;
  --ink: #1A1D21;
  --muted: #5A6068;
  --line: #E3E2DC;
  --accent: #C05600;      /* phosphor amber, darkened for light bg */
  --accent-soft: #FFF3E6;
  --run: #1A7F37;         /* earned green */
  --run-soft: #E7F4EA;
  --danger: #C93C37;
  --radius: 10px;
  --font-sans: system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
    --line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
    --run: #3FB950; --run-soft: #16281B; --danger: #F47067;
  }
}
:root[data-theme="dark"] {
  --bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
  --line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
  --run: #3FB950; --run-soft: #16281B; --danger: #F47067;
}
body { background: var(--bg); color: var(--ink); font-family: var(--font-sans); }

/* ---- pattern palette (per match type). Green stays out of here — green is earned. ---- */
:root {
  --c-word:#2B5CA8; --c-word-bg:#E1EAF8;
  --c-date:#6D3FA6; --c-date-bg:#EDE3FA;
  --c-seq:#0E6C7C;  --c-seq-bg:#DCEEF2;
  --c-rep:#A63F6B;  --c-rep-bg:#FAE3EE;
  --c-key:#8A5A00;  --c-key-bg:#F7EDD8;
  --c-sep:#5A6068;  --c-sep-bg:#EFEEEA;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --c-word:#8FB6F5; --c-word-bg:#1C2E4C;
    --c-date:#C4A3F1; --c-date-bg:#2C2148;
    --c-seq:#6FD3E0;  --c-seq-bg:#14323A;
    --c-rep:#F0A0C0;  --c-rep-bg:#3A1C2A;
    --c-key:#E0B060;  --c-key-bg:#33291A;
    --c-sep:#9AA0A8;  --c-sep-bg:#23262D;
  }
}
:root[data-theme="dark"] {
  --c-word:#8FB6F5; --c-word-bg:#1C2E4C;
  --c-date:#C4A3F1; --c-date-bg:#2C2148;
  --c-seq:#6FD3E0;  --c-seq-bg:#14323A;
  --c-rep:#F0A0C0;  --c-rep-bg:#3A1C2A;
  --c-key:#E0B060;  --c-key-bg:#33291A;
  --c-sep:#9AA0A8;  --c-sep-bg:#23262D;
}

/* ---- shell ---- */
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body { font-size: 16px; line-height: 1.55; -webkit-text-size-adjust: 100%; overflow-x: hidden; }
.wrap { max-width: 1000px; margin: 0 auto; padding: 26px 18px 34px; }
h1 { font-size: 1.5rem; line-height: 1.2; margin: 0 0 7px; letter-spacing: -.015em; }
h2 { font-size: .95rem; margin: 0; }
.lede { margin: 0; color: var(--muted); font-size: .95rem; max-width: 68ch; }
.lede b { color: var(--ink); font-weight: 600; }
.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;
}
.mono { font-family: var(--font-mono); }
.k-lab {
  font-family: var(--font-mono); font-size: .66rem; letter-spacing: .12em;
  text-transform: uppercase; color: var(--muted);
}

/* ---- cards ---- */
.card {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 14px; margin-top: 16px;
}
.cardhead { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 12px; margin-bottom: 11px; }
.cardhead .note { font-size: .78rem; color: var(--muted); margin-left: auto; }

/* ---- buttons ---- */
.btn {
  font-family: var(--font-mono); font-size: .72rem; letter-spacing: .02em;
  background: var(--bg); color: var(--ink); border: 1px solid var(--line);
  border-radius: 999px; padding: 6px 11px; min-height: 32px; cursor: pointer;
  white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis;
}
.btn:hover { border-color: var(--accent); color: var(--accent); }
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.btn[aria-pressed="true"] { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
.btn.solid { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
.row { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }

/* ---- input ---- */
.field { position: relative; display: flex; align-items: stretch; gap: 8px; }
.field .btn { flex: 0 0 auto; }
input.pw {
  flex: 1 1 auto; min-width: 0; font-family: var(--font-mono); font-size: 1.02rem;
  padding: 12px 13px; border-radius: 9px; border: 1px solid var(--line);
  background: var(--bg); color: var(--ink); letter-spacing: .02em;
}
input.pw::placeholder { color: var(--muted); opacity: 1; }
input.pw:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
/* the soft ring is nearly invisible on white, so keyboard focus gets a real outline */
input.pw:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.privacy {
  margin: 9px 0 0; font-family: var(--font-mono); font-size: .68rem; color: var(--muted);
  display: flex; gap: 7px; align-items: flex-start; line-height: 1.5;
}
.privacy svg { flex: 0 0 auto; margin-top: 2px; }
.presets { margin-top: 11px; }

/* ---- readout grid ---- */
.grid { display: grid; grid-template-columns: minmax(0,1fr) minmax(0,1.08fr); gap: 16px; }
@media (max-width: 760px) { .grid { grid-template-columns: minmax(0,1fr); } }

.bitsline { display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap; }
.bits {
  font-family: var(--font-mono); font-size: clamp(2.3rem, 10vw, 3.2rem); line-height: .95;
  letter-spacing: -.03em; font-variant-numeric: tabular-nums; color: var(--ink);
}
.bits.b0, .bits.b1 { color: var(--danger); }
.bits.b2 { color: var(--accent); }
.bits.b3, .bits.b4 { color: var(--run); }
.bitsunit { font-family: var(--font-mono); font-size: .78rem; color: var(--muted); }
.band {
  font-family: var(--font-mono); font-size: .68rem; letter-spacing: .1em; text-transform: uppercase;
  border: 1px solid var(--line); border-radius: 999px; padding: 4px 10px; display: inline-block;
}
/* The pill keeps a coloured border and sits on --surface: accent-on-accent-soft is
   only 4.2:1 in the light theme, which this 11px uppercase label would fail. */
.band.b0, .band.b1 { color: var(--danger); border-color: var(--danger); }
.band.b2 { color: var(--accent); border-color: var(--accent); }
.band.b3, .band.b4 { color: var(--run); border-color: var(--run); }

.meter { margin-top: 13px; }
.track {
  position: relative; height: 15px; border-radius: 999px; background: var(--bg);
  border: 1px solid var(--line); overflow: hidden;
}
.fill { height: 100%; width: 0; border-radius: 999px; background: var(--muted); transition: width .32s ease, background-color .32s ease; }
.fill.b0, .fill.b1 { background: var(--danger); }
.fill.b2 { background: var(--accent); }
.fill.b3, .fill.b4 { background: var(--run); }
.tick { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--line); }
.scale { position: relative; height: 1.2em; margin-top: 5px; font-family: var(--font-mono); font-size: .64rem; color: var(--muted); }
.scale span { position: absolute; top: 0; transform: translateX(-50%); white-space: nowrap; }
.scale span:first-child { transform: none; }
.scale span:last-child { transform: translateX(-100%); }
.guesses { margin-top: 10px; font-family: var(--font-mono); font-size: .7rem; color: var(--muted); word-break: break-word; }
.guesses b { color: var(--ink); font-weight: 600; }
.ruler { margin: 12px 0 0; font-size: .82rem; color: var(--muted); max-width: 46ch; }
.ruler b { font-family: var(--font-mono); color: var(--ink); font-weight: 600; }

/* ---- rates ---- */
table.rates { width: 100%; border-collapse: collapse; }
table.rates td { padding: 8px 0; border-top: 1px solid var(--line); vertical-align: baseline; }
table.rates tr:first-child td { border-top: 0; }
table.rates td.who { font-size: .84rem; padding-right: 10px; }
table.rates td.who small { display: block; font-family: var(--font-mono); font-size: .66rem; color: var(--muted); letter-spacing: .03em; }
table.rates td.t { font-family: var(--font-mono); font-size: .82rem; text-align: right; white-space: nowrap; color: var(--ink); }
table.rates td.t.bad { color: var(--danger); }
table.rates td.t.good { color: var(--run); }

/* ---- chips / breakdown ---- */
.chips { display: flex; flex-wrap: wrap; gap: 6px; align-items: flex-start; }
.chip {
  border: 1px solid var(--line); border-radius: 8px; padding: 5px 7px 4px;
  background: var(--bg); min-width: 0; max-width: 100%;
}
.chip .tok {
  font-family: var(--font-mono); font-size: .84rem; display: block;
  overflow-wrap: anywhere; word-break: break-word; color: var(--ink);
}
.chip .lab {
  font-family: var(--font-mono); font-size: .62rem; letter-spacing: .08em; text-transform: uppercase;
  display: block; margin-top: 3px; color: var(--muted);
}
.chip.t-word  { border-color: var(--c-word); background: var(--c-word-bg); }
.chip.t-word .tok, .chip.t-word .lab { color: var(--c-word); }
.chip.t-date  { border-color: var(--c-date); background: var(--c-date-bg); }
.chip.t-date .tok, .chip.t-date .lab { color: var(--c-date); }
.chip.t-seq   { border-color: var(--c-seq); background: var(--c-seq-bg); }
.chip.t-seq .tok, .chip.t-seq .lab { color: var(--c-seq); }
.chip.t-rep   { border-color: var(--c-rep); background: var(--c-rep-bg); }
.chip.t-rep .tok, .chip.t-rep .lab { color: var(--c-rep); }
.chip.t-key   { border-color: var(--c-key); background: var(--c-key-bg); }
.chip.t-key .tok, .chip.t-key .lab { color: var(--c-key); }
.chip.t-sep   { border-color: var(--line); background: var(--c-sep-bg); padding-left: 6px; padding-right: 6px; }
.chip.t-sep .tok, .chip.t-sep .lab { color: var(--c-sep); }
.chip.t-free  { border-style: dashed; }
.sp { color: var(--muted); }

.legend { display: flex; flex-wrap: wrap; gap: 4px 12px; margin-top: 11px; }
.legend span { font-family: var(--font-mono); font-size: .64rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); display: inline-flex; align-items: center; gap: 5px; }
.legend i { width: 9px; height: 9px; border-radius: 3px; display: inline-block; border: 1px solid currentColor; }

/* ---- findings ---- */
ul.tips { list-style: none; margin: 0; padding: 0; }
ul.tips li { display: flex; gap: 9px; padding: 9px 0; border-top: 1px solid var(--line); font-size: .88rem; }
ul.tips li:first-child { border-top: 0; }
ul.tips .dot { flex: 0 0 auto; width: 8px; height: 8px; border-radius: 2px; margin-top: 6px; background: var(--muted); }
ul.tips .d-word { background: var(--c-word); }
ul.tips .d-date { background: var(--c-date); }
ul.tips .d-seq  { background: var(--c-seq); }
ul.tips .d-rep  { background: var(--c-rep); }
ul.tips .d-key  { background: var(--c-key); }
ul.tips .d-shape{ background: var(--danger); }
ul.tips .d-good { background: var(--run); }
ul.tips code { font-family: var(--font-mono); font-size: .84em; background: var(--bg); border: 1px solid var(--line); border-radius: 5px; padding: 0 4px; overflow-wrap: anywhere; }
ul.tips li.good { color: var(--run); }
ul.tips li.good code { border-color: var(--run); }

.rolled {
  margin-top: 11px; border: 1px solid var(--run); background: var(--run-soft);
  border-radius: 9px; padding: 10px 12px; font-size: .84rem;
}
.rolled .k-lab { color: var(--run); }
.rolled b { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

details.method { margin-top: 16px; }
details.method summary {
  cursor: pointer; font-family: var(--font-mono); font-size: .7rem; letter-spacing: .06em;
  text-transform: uppercase; color: var(--muted); padding: 4px 0;
}
details.method summary:hover { color: var(--accent); }
details.method summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }
details.method .body { font-size: .86rem; color: var(--muted); max-width: 74ch; }
details.method .body p { margin: 9px 0; }
details.method .body b { color: var(--ink); }
details.method .body code { font-family: var(--font-mono); font-size: .85em; color: var(--ink); }

footer { margin-top: 24px; font-family: var(--font-mono); font-size: .7rem; color: var(--muted); }

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}
</style>
</head>
<body>
<div class="wrap">
  <h1>Passphrase Strength Meter</h1>
  <p class="lede">Type a passphrase and watch it get taken apart the way a cracking tool would take it apart: <b>words, dates, runs and repeats</b> get named, and what is left over is the only part anyone has to actually guess.</p>

  <section class="card" aria-labelledby="h-in">
    <div class="cardhead">
      <h2 id="h-in">Your passphrase</h2>
      <span class="note" id="lenNote"></span>
    </div>
    <div class="field">
      <label class="sr-only" for="pw">Passphrase to measure</label>
      <input class="pw" id="pw" type="text" maxlength="128" spellcheck="false" autocomplete="off"
             autocapitalize="off" autocorrect="off" placeholder="type something…">
      <button class="btn" id="toggle" type="button" aria-pressed="false" title="Hide the characters on screen">HIDE</button>
    </div>
    <p class="privacy">
      <svg width="12" height="14" viewBox="0 0 12 14" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.4">
        <rect x="1.2" y="5.8" width="9.6" height="7.2" rx="1.4"/><path d="M3.4 5.8V4a2.6 2.6 0 0 1 5.2 0v1.8"/>
      </svg>
      <span>Everything happens in this tab. Nothing is stored, nothing is remembered, and this page makes no network requests of any kind — you can watch the network panel while you type.</span>
    </p>
    <div class="presets">
      <div class="k-lab" style="margin-bottom:6px">Try one</div>
      <div class="row" id="presets"></div>
    </div>
  </section>

  <section class="card" aria-labelledby="h-out">
    <div class="cardhead">
      <h2 id="h-out">How hard is it to guess?</h2>
      <span class="note">estimate, not a promise</span>
    </div>
    <div class="grid">
      <div>
        <div class="bitsline">
          <span class="bits mono" id="bits">0</span>
          <span class="bitsunit">bits of<br>guesswork</span>
          <span class="band" id="band" style="margin-left:auto">EMPTY</span>
        </div>
        <div class="meter">
          <div class="track" id="track" role="progressbar" aria-labelledby="h-out" aria-valuemin="0" aria-valuemax="90" aria-valuenow="0" aria-valuetext="empty">
            <div class="fill" id="fill"></div>
            <div class="tick" style="left:33.33%"></div>
            <div class="tick" style="left:50%"></div>
            <div class="tick" style="left:66.67%"></div>
            <div class="tick" style="left:83.33%"></div>
          </div>
          <div class="scale" aria-hidden="true">
            <span style="left:0">0</span><span style="left:33.33%">30</span><span style="left:50%">45</span><span style="left:66.67%">60</span><span style="left:83.33%">75</span><span style="left:100%">90+</span>
          </div>
        </div>
        <p class="guesses mono" id="guesses"></p>
        <p class="ruler">Every extra bit doubles the work. <b>30 bits</b> is a coffee break,
        <b>45</b> is an afternoon, <b>60</b> is a real wall, and past <b>75</b> nobody is getting
        in by guessing — they will phone you and ask instead.</p>
        <div id="rolled"></div>
      </div>
      <div>
        <div class="k-lab" style="margin-bottom:4px">Time to guess it, on average</div>
        <table class="rates"><tbody id="rates"></tbody></table>
      </div>
    </div>
    <p class="sr-only" id="live" role="status" aria-live="polite"></p>
  </section>

  <section class="card" aria-labelledby="h-parts">
    <div class="cardhead">
      <h2 id="h-parts">What a cracker sees</h2>
      <span class="note">cheapest split found, left to right</span>
    </div>
    <div class="chips" id="chips"></div>
    <div class="legend" aria-hidden="true">
      <span style="color:var(--c-word)"><i></i>word</span>
      <span style="color:var(--c-date)"><i></i>date</span>
      <span style="color:var(--c-seq)"><i></i>run</span>
      <span style="color:var(--c-rep)"><i></i>repeat</span>
      <span style="color:var(--c-key)"><i></i>keyboard</span>
      <span style="color:var(--muted)"><i style="border-style:dashed"></i>nothing to say</span>
    </div>
  </section>

  <section class="card" aria-labelledby="h-tips">
    <div class="cardhead"><h2 id="h-tips">What to do about it</h2></div>
    <ul class="tips" id="tips"></ul>
  </section>

  <details class="method">
    <summary>How this number is worked out</summary>
    <div class="body">
      <p>The meter tries every way of chopping your passphrase into known patterns — a <b>word list</b> of about 450 entries baked into this page, <b>dates and years</b>, <b>counting runs</b> like <code>4567</code> or <code>wxyz</code>, <b>repeats</b> like <code>lolololol</code>, and <b>keyboard walks</b> like <code>asdfg</code> — and then picks the split that would cost an attacker the <em>least</em>. Anything the meter can't name is priced as raw brute force over the characters you actually used.</p>
      <p>Substitutions are priced honestly: <code>@</code> for <code>a</code> and <code>0</code> for <code>o</code> are tried by every real tool, so they buy a bit or two, not a category. Splitting a passphrase into several parts does cost the attacker something (they have to guess the shape too), so the meter adds a penalty for each extra piece. And if the text is stitched together with function words — <code>the</code>, <code>is</code>, <code>my</code>, <code>for</code> — it is treated as running English and capped at about <b>1.2 bits per character</b>, because a sentence you composed is far weaker than the same letters drawn at random. Four <em>unrelated</em> words are not a sentence and keep their full price.</p>
      <p><b>Bits</b> is just the base-2 logarithm of that guess count. Each extra bit doubles the work. Times assume the attacker gets the answer halfway through, on average.</p>
      <p><b>Where it lies to you:</b> the word list here is tiny — real attack dictionaries carry hundreds of millions of leaked passwords, names and phrases. If your passphrase leans on a word this page has never heard of, the meter will price it as random and flatter you. Treat the number as a ceiling, never a floor.</p>
    </div>
  </details>

  <footer>demo · sloprun.dev</footer>
</div>
<script>
(function () {
  "use strict";

  /* ============================================================
     1. WORD LIST — small, everyday, roughly ordered by how often
        it turns up in leaked-password lists. Rank matters: the
        earlier a word sits, the cheaper it is to guess.
     ============================================================ */
  var RAW = (
    "password qwerty letmein welcome monkey dragon master shadow sunshine princess " +
    "football baseball basketball soccer hockey superman batman spiderman iloveyou " +
    "trustno1 whatever freedom hunter ninja computer starwars pokemon admin login " +
    "secret access flower hello charlie michael jennifer jessica ashley michelle " +
    "daniel matthew joshua andrew robert thomas william richard jordan taylor hannah " +
    "samantha amanda nicole brandon justin ryan kevin jason tyler austin dakota " +
    "harley maggie buster tigger chelsea cookie pepper ginger killer phoenix dolphin " +
    "mustang corvette ferrari yamaha guitar banana chocolate orange purple silver " +
    "golden diamond crystal troubador rainbow butterfly summer winter spring autumn " +
    "january february march april june july august september october november december " +
    "monday tuesday wednesday thursday friday saturday sunday " +
    "love life home house water music money happy lucky magic dream night morning " +
    "sunset sunrise midnight evening weekend holiday birthday " +
    "red blue green black white yellow pink brown grey violet " +
    "star moon sky cloud rain storm thunder lightning snow wind fire ocean river " +
    "lake mountain forest garden beach island desert valley meadow canyon " +
    "tree leaf rose daisy tulip clover cactus bamboo willow maple " +
    "apple cherry peach grape lemon melon mango berry peanut walnut " +
    "coffee sugar honey butter cheese bread pizza pasta burger noodle candy cake " +
    "soup salad pickle waffle pancake muffin biscuit " +
    "horse mouse rabbit bear wolf tiger lion eagle shark whale panda koala turtle " +
    "snake frog spider monkey donkey parrot penguin falcon otter badger beaver " +
    "book page story poem song dance movie game puzzle riddle " +
    "paint brush pencil paper canvas marble " +
    "phone laptop screen keyboard printer camera speaker battery " +
    "table chair window door floor kitchen bedroom garage attic basement " +
    "train plane boat truck wagon rocket bridge tunnel " +
    "city town village street market school office library museum station " +
    "friend family mother father sister brother uncle cousin " +
    "king queen prince wizard castle crown scroll jester pirate robot alien " +
    "planet galaxy comet meteor orbit " +
    "correct staple bucket ladder hammer anvil kettle teapot lantern candle " +
    "mirror pillow blanket carpet basket bottle barrel copper bronze " +
    "happy angry quiet brave clever funny silly crazy sweet bitter salty spicy " +
    "fresh clean dirty young tiny giant short narrow quick lazy busy " +
    "heavy empty bright shiny sharp smooth rough gentle " +
    "walk jump swim sleep laugh smile think learn teach write draw build break " +
    "climb ride drive cook wander whisper wonder " +
    "cat dog sun ice fox owl bee ant sea key box hat cup pen car map " +
    "gold rust salt milk mint sage moss fern reed dust ember frost " +
    "the and for you are was not but all can her out his has had one two six ten " +
    "its our who why how now warm cold cool dark soft hard loud brick sock rope " +
    "wire nail glue tape rock sand clay iron steel glass wood nest crumb"
  ).split(/\s+/);

  var DICT = Object.create(null), WORDS = [];
  for (var wi = 0; wi < RAW.length; wi++) {
    var w = RAW[wi];
    if (w && DICT[w] === undefined) { DICT[w] = WORDS.length + 1; WORDS.push(w); }
  }
  var MAXWORD = 0;
  for (var wj = 0; wj < WORDS.length; wj++) if (WORDS[wj].length > MAXWORD) MAXWORD = WORDS[wj].length;

  /* pool used by the "roll one for me" button — typeable lengths only */
  var POOL = WORDS.filter(function (x) { return x.length >= 4 && x.length <= 7; });

  /* ============================================================
     2. Small helpers. All guess counts live in log2 space so that
        nothing ever overflows to Infinity.
     ============================================================ */
  var L2 = Math.log2 || function (x) { return Math.log(x) / Math.LN2; };
  var LOG10_2 = Math.log(2) / Math.log(10);

  var LEET = {
    "4": "a", "@": "a", "3": "e", "0": "o", "$": "s", "5": "s", "7": "t",
    "+": "t", "8": "b", "9": "g", "(": "c", "{": "c", "6": "b"
  };
  var LEET2 = { "1": "il", "!": "il", "|": "il" };

  function lettersAt(ch) {
    var lo = ch.toLowerCase();
    if (lo >= "a" && lo <= "z") return { set: lo, sub: false };
    if (LEET[ch]) return { set: LEET[ch], sub: true };
    if (LEET2[ch]) return { set: LEET2[ch], sub: true };
    return null;
  }

  function isHighSurr(c) { var x = c.charCodeAt(0); return x >= 0xD800 && x <= 0xDBFF; }
  function isLowSurr(c) { var x = c.charCodeAt(0); return x >= 0xDC00 && x <= 0xDFFF; }
  /* true when cutting the string just before index i would split a surrogate pair */
  function splitsPair(s, i) {
    return i > 0 && i < s.length && isHighSurr(s[i - 1]) && isLowSurr(s[i]);
  }

  function nCk(n, k) {
    if (k > n) return 0;
    if (k === 0) return 1;
    var r = 1;
    for (var d = 1; d <= k; d++) { r *= n; r /= d; n -= 1; }
    return r;
  }

  function upperVariations(tok) {
    if (tok === tok.toLowerCase()) return 1;
    if (/^[A-Z][^A-Z]*$/.test(tok)) return 2;          /* Capitalised */
    if (/^[^a-z]*$/.test(tok)) return 2;               /* ALL CAPS */
    if (/^[^A-Z]*[A-Z]$/.test(tok)) return 2;          /* trailinG */
    var U = 0, L = 0, i;
    for (i = 0; i < tok.length; i++) {
      if (tok[i] >= "A" && tok[i] <= "Z") U++;
      else if (tok[i] >= "a" && tok[i] <= "z") L++;
    }
    var v = 0;
    for (i = 1; i <= Math.min(U, L); i++) v += nCk(U + L, i);
    return Math.max(v, 2);
  }

  var SYMBOLS = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
  function cardinality(s) {
    var lo = false, up = false, di = false, sp = false, sy = false, ot = false;
    for (var i = 0; i < s.length; i++) {
      var c = s[i];
      if (c >= "a" && c <= "z") lo = true;
      else if (c >= "A" && c <= "Z") up = true;
      else if (c >= "0" && c <= "9") di = true;
      else if (c === " ") sp = true;
      else if (SYMBOLS.indexOf(c) >= 0) sy = true;
      else ot = true;
    }
    var n = 0;
    if (lo) n += 26; if (up) n += 26; if (di) n += 10;
    if (sp) n += 1; if (sy) n += 33; if (ot) n += 100;
    return Math.max(n, 10);
  }

  /* per-match floor: nothing is ever free */
  function floorLg(tok, lg) {
    var min = tok.length === 1 ? L2(10) : L2(50);
    return Math.max(lg, min);
  }

  /* ============================================================
     3. MATCHERS — each returns {i, j, type, tok, lg, why}
     ============================================================ */

  function dictMatches(pw) {
    var out = [], n = pw.length, i, k;
    for (i = 0; i < n; i++) {
      var cands = [{ s: "", sub: 0 }];
      var lim = Math.min(MAXWORD, n - i);
      for (k = 0; k < lim; k++) {
        var opt = lettersAt(pw[i + k]);
        if (!opt) break;
        var next = [];
        for (var c = 0; c < cands.length && next.length < 24; c++) {
          for (var m = 0; m < opt.set.length; m++) {
            next.push({ s: cands[c].s + opt.set[m], sub: cands[c].sub + (opt.sub ? 1 : 0) });
          }
        }
        cands = next;
        if (k + 1 < 3) continue;
        for (var q = 0; q < cands.length; q++) {
          var rank = DICT[cands[q].s];
          if (rank === undefined) continue;
          var tok = pw.slice(i, i + k + 1);
          var subs = cands[q].sub;
          var lg = L2(rank) + L2(upperVariations(tok)) + (subs ? Math.min(subs, 8) : 0);
          out.push({
            i: i, j: i + k, type: "word", tok: tok, lg: floorLg(tok, lg),
            why: { word: cands[q].s, rank: rank, subs: subs,
                   caps: tok !== tok.toLowerCase(), rev: false }
          });
        }
      }
      /* backwards spelling — a standard rule in every cracking tool */
      for (k = 3; k <= Math.min(MAXWORD, n - i); k++) {
        var seg = pw.slice(i, i + k).toLowerCase();
        var rv = seg.split("").reverse().join("");
        if (rv === seg) continue;
        var r2 = DICT[rv];
        if (r2 === undefined) continue;
        var tok2 = pw.slice(i, i + k);
        out.push({
          i: i, j: i + k - 1, type: "word", tok: tok2,
          lg: floorLg(tok2, L2(r2) + L2(upperVariations(tok2)) + 1),
          why: { word: rv, rank: r2, subs: 0, caps: false, rev: true }
        });
      }
    }
    return out;
  }

  function seqMatches(pw) {
    var out = [], n = pw.length;
    function cls(c) {
      if (c >= "0" && c <= "9") return { sp: 10, first: (c === "0" || c === "1") };
      if (c >= "a" && c <= "z") return { sp: 26, first: (c === "a" || c === "z") };
      if (c >= "A" && c <= "Z") return { sp: 26, first: (c === "A" || c === "Z") };
      return null;
    }
    var i = 0;
    while (i < n - 1) {
      var a = cls(pw[i]);
      if (!a) { i++; continue; }
      var d = pw.charCodeAt(i + 1) - pw.charCodeAt(i);
      if (d !== 1 && d !== -1 || !cls(pw[i + 1]) || cls(pw[i + 1]).sp !== a.sp) { i++; continue; }
      var j = i + 1;
      while (j + 1 < n && cls(pw[j + 1]) && cls(pw[j + 1]).sp === a.sp &&
             pw.charCodeAt(j + 1) - pw.charCodeAt(j) === d) j++;
      if (j - i + 1 >= 3) {
        var tok = pw.slice(i, j + 1);
        var base = a.first ? 4 : a.sp;
        out.push({
          i: i, j: j, type: "seq", tok: tok,
          lg: floorLg(tok, L2(base) + L2(tok.length) + (d < 0 ? 1 : 0)),
          why: { down: d < 0, digits: a.sp === 10 }
        });
      }
      i = j;
    }
    return out;
  }

  var ROWS = ["`1234567890-=", "qwertyuiop[]\\", "asdfghjkl;'", "zxcvbnm,./"];
  function keyMatches(pw) {
    var out = [], n = pw.length, low = pw.toLowerCase();
    function pos(c) {
      for (var r = 0; r < ROWS.length; r++) { var k = ROWS[r].indexOf(c); if (k >= 0) return { r: r, k: k }; }
      return null;
    }
    var i = 0;
    while (i < n - 1) {
      var p = pos(low[i]), q = pos(low[i + 1]);
      if (!p || !q || p.r !== q.r || Math.abs(p.k - q.k) !== 1) { i++; continue; }
      var d = q.k - p.k, j = i + 1;
      while (j + 1 < n) {
        var a = pos(low[j]), b = pos(low[j + 1]);
        if (!a || !b || a.r !== b.r || b.k - a.k !== d) break;
        j++;
      }
      if (j - i + 1 >= 3) {
        var tok = pw.slice(i, j + 1);
        out.push({ i: i, j: j, type: "key", tok: tok, lg: floorLg(tok, L2(45) + L2(tok.length)), why: {} });
      }
      i = j;
    }
    return out;
  }

  function repMatches(pw, card) {
    var out = [], n = pw.length;
    for (var i = 0; i < n; i++) {
      for (var b = 1; b <= Math.min(10, Math.floor((n - i) / 2)); b++) {
        var base = pw.slice(i, i + b), rep = 1;
        while (pw.substr(i + rep * b, b) === base) rep++;
        if (rep < 2 || b * rep < 3) continue;
        var tok = pw.slice(i, i + b * rep);
        var lo = base.toLowerCase();
        var baseLg = DICT[lo] !== undefined ? L2(DICT[lo]) : base.length * L2(card);
        out.push({
          i: i, j: i + b * rep - 1, type: "rep", tok: tok,
          lg: floorLg(tok, baseLg + L2(rep)),
          why: { base: base, rep: rep }
        });
      }
    }
    return out;
  }

  function dateMatches(pw) {
    var out = [], m, re;
    re = /(19[0-9][0-9]|20[0-4][0-9])/g;
    while ((m = re.exec(pw)) !== null) {
      out.push({ i: m.index, j: m.index + 3, type: "date", tok: m[0], lg: floorLg(m[0], L2(150)), why: { kind: "year", year: m[0] } });
      re.lastIndex = m.index + 1;
    }
    re = /([0-9]{1,2})([\/\-\.])([0-9]{1,2})\2([0-9]{2,4})/g;
    while ((m = re.exec(pw)) !== null) {
      var a = +m[1], b = +m[3];
      if (a >= 1 && a <= 31 && b >= 1 && b <= 31 && (a <= 12 || b <= 12)) {
        var span = m[4].length === 4 ? 150 : 100;
        out.push({ i: m.index, j: m.index + m[0].length - 1, type: "date", tok: m[0],
                   lg: floorLg(m[0], L2(372 * span * 4)), why: { kind: "full" } });
      }
      re.lastIndex = m.index + 1;
    }
    re = /([0-9]{8})/g;
    while ((m = re.exec(pw)) !== null) {
      var s = m[0];
      var ok = (+s.slice(0, 4) >= 1900 && +s.slice(0, 4) <= 2049 && +s.slice(4, 6) >= 1 && +s.slice(4, 6) <= 12 && +s.slice(6) >= 1 && +s.slice(6) <= 31) ||
               (+s.slice(4) >= 1900 && +s.slice(4) <= 2049 && +s.slice(0, 2) >= 1 && +s.slice(0, 2) <= 31 && +s.slice(2, 4) >= 1 && +s.slice(2, 4) <= 31);
      if (ok) out.push({ i: m.index, j: m.index + 7, type: "date", tok: s, lg: floorLg(s, L2(372 * 150)), why: { kind: "full" } });
      re.lastIndex = m.index + 1;
    }
    return out;
  }

  /* Separators are all but free: an attacker picks one scheme (space, dash,
     underscore, dot) for the whole passphrase, so the cost is amortised, not per gap. */
  function sepMatches(pw) {
    var out = [];
    for (var i = 0; i < pw.length; i++) {
      if (" -_.".indexOf(pw[i]) >= 0) {
        out.push({ i: i, j: i, type: "sep", tok: pw[i], lg: 0.35, why: {} });
      }
    }
    return out;
  }

  /* ============================================================
     4. SCORER — find the cheapest split (dynamic programming over
        every position and every number of pieces), then price the
        shape itself: k pieces can be ordered k! ways and the
        attacker has to guess the shape too.
     ============================================================ */
  var LGFACT = [0];
  (function () { for (var i = 1; i <= 200; i++) LGFACT[i] = LGFACT[i - 1] + L2(i); })();
  var SHAPE_COST = 1;   /* bits per extra piece, on top of the k! term */
  var SHAPE_MAX = 14;   /* ...but layouts get enumerated too. Don't hand out infinite credit. */
  function shapeCost(k) { return Math.min(LGFACT[k] + (k - 1) * SHAPE_COST, SHAPE_MAX); }
  /* Shannon put running English at 0.6–1.3 bits per character; 1.2 is the generous end. */
  var ENGLISH_BITS_PER_CHAR = 1.2;

  /* Grammar glue. A string only counts as running English if it is stitched together
     with high-frequency function words. This is the whole test: without it, "correct
     horse battery staple" and every rolled passphrase look like sentences (letters and
     spaces, nothing else) and get capped at prose rates — which would be badly wrong,
     because four UNRELATED words carry their full independent price. */
  var GLUE = Object.create(null);
  ("the a an and or but if of to in on at for from by with as is are was were be been am " +
   "i me my mine you your yours he him his she her hers it its we us our ours they them their " +
   "this that these those there here what when where who whom why how which " +
   "not no yes do does did done have has had will would can could shall should may might must " +
   "so than then too very much more most just like about into over under after before again " +
   "all any some each every own same other out up down off").split(/\s+/)
    .forEach(function (w) { if (w) GLUE[w] = 1; });

  function sentenceLike(pw) {
    if (pw.length < 8) return false;
    var toks = pw.trim().split(/\s+/);
    if (toks.length < 3) return false;
    var letters = 0, total = 0, glue = 0, i, c, ch, t;
    for (i = 0; i < toks.length; i++) {
      t = toks[i].replace(/^[!?.,;:'"()\[\]-]+/, "").replace(/[!?.,;:'"()\[\]-]+$/, "");
      if (!t) return false;
      if (GLUE[t.toLowerCase()]) glue++;
      for (c = 0; c < t.length; c++) {
        ch = t[c]; total++;
        if ((ch >= "a" && ch <= "z") || (ch >= "A" && ch <= "Z")) letters++;
        else if (!LEET[ch] && !LEET2[ch] && ch !== "'" && ch !== "-") return false;
      }
    }
    if (!total || letters / total < 0.7) return false;
    return glue / toks.length >= 0.3;
  }

  function analyze(pw) {
    var n = pw.length, i, j, k;
    if (!n) return { bits: 0, seq: [], n: 0, card: 0 };

    var card = cardinality(pw), lgc = L2(card);
    var ms = dictMatches(pw)
      .concat(seqMatches(pw), keyMatches(pw), repMatches(pw, card), dateMatches(pw), sepMatches(pw));
    /* never let a chip cut an astral character in half (emoji are two code units) */
    ms = ms.filter(function (mm) { return !splitsPair(pw, mm.i) && !splitsPair(pw, mm.j + 1); });
    for (i = 0; i < n; i++) {
      if (splitsPair(pw, i)) continue;
      for (j = i; j < n; j++) {
        if (splitsPair(pw, j + 1)) continue;
        var tk = pw.slice(i, j + 1);
        ms.push({ i: i, j: j, type: "free", tok: tk, lg: floorLg(tk, (j - i + 1) * lgc), why: {} });
      }
    }

    var K = Math.min(n, 48);
    var byEnd = [], dp = [], bp = [];
    for (i = 0; i <= n; i++) {
      byEnd.push([]);
      var row = new Float64Array(K + 1);
      for (k = 0; k <= K; k++) row[k] = Infinity;
      dp.push(row); bp.push(new Array(K + 1));
    }
    dp[0][0] = 0;
    for (i = 0; i < ms.length; i++) byEnd[ms[i].j + 1].push(ms[i]);

    for (var e = 1; e <= n; e++) {
      var list = byEnd[e];
      for (var t = 0; t < list.length; t++) {
        var m = list[t], src = dp[m.i], dst = dp[e];
        for (k = 0; k < K; k++) {
          var v = src[k];
          if (v === Infinity) continue;
          var nv = v + m.lg;
          if (nv < dst[k + 1]) { dst[k + 1] = nv; bp[e][k + 1] = { from: m.i, fk: k, m: m }; }
        }
      }
    }

    var best = Infinity, bestK = 1;
    for (k = 1; k <= K; k++) {
      if (dp[n][k] === Infinity) continue;
      var tot = dp[n][k] + shapeCost(k);
      if (tot < best) { best = tot; bestK = k; }
    }

    var capped = false;
    if (sentenceLike(pw)) {
      var cap = ENGLISH_BITS_PER_CHAR * n;
      if (cap < best) { best = cap; capped = true; }
    }

    var seq = [], pos = n, kk = bestK, guard = 0;
    while (pos > 0 && guard++ < 300) {
      var node = bp[pos][kk];
      if (!node) break;
      seq.unshift(node.m);
      pos = node.from; kk = node.fk;
    }
    return { bits: best, seq: seq, n: n, card: card, pieces: bestK, capped: capped };
  }

  /* ============================================================
     5. Bands, times, formatting
     ============================================================ */
  var BANDS = [
    { at: 0,  name: "very weak" },
    { at: 30, name: "weak" },
    { at: 45, name: "fair" },
    { at: 60, name: "strong" },
    { at: 75, name: "very strong" }
  ];
  function bandOf(bits) { var b = 0; for (var i = 0; i < BANDS.length; i++) if (bits >= BANDS[i].at) b = i; return b; }

  var RATES = [
    { who: "Guessing at a login screen", note: "throttled · 10 tries/sec", lg10: 1 },
    { who: "Stolen database, slow hashing", note: "20 thousand tries/sec", lg10: 4.3 },
    { who: "Stolen database, fast hashing", note: "100 billion tries/sec", lg10: 11 },
    { who: "Rented rack of graphics cards", note: "10 trillion tries/sec", lg10: 13 }
  ];

  function fmtCount(x) {
    if (x >= 100) return String(Math.round(x));
    if (x >= 10) return x.toFixed(0);
    return x.toFixed(1).replace(/\.0$/, "");
  }
  function unit(v, word) { var t = fmtCount(v); return t + " " + word + (t === "1" ? "" : "s"); }
  function fmtTime(l10) {
    if (l10 < -0.5) return "instantly";
    if (l10 < 0.5) return "about a second";
    var s = Math.pow(10, Math.min(l10, 12));
    if (l10 < 12) {
      if (s < 60) return unit(s, "second");
      if (s < 3600) return unit(s / 60, "minute");
      if (s < 86400) return unit(s / 3600, "hour");
      if (s < 2629746) return unit(s / 86400, "day");
      if (s < 31556952) return unit(s / 2629746, "month");
    }
    var y = l10 - 7.4989;                       /* log10 of years */
    if (y < 3) return unit(Math.pow(10, y), "year");
    var big = [[3, "thousand"], [6, "million"], [9, "billion"], [12, "trillion"], [15, "quadrillion"]];
    for (var i = 0; i < big.length; i++) {
      if (y < big[i][0] + 3) return fmtCount(Math.pow(10, y - big[i][0])) + " " + big[i][1] + " years";
    }
    return "10^" + Math.round(y) + " years";
  }

  function esc(s) {
    return String(s).replace(/[&<>"']/g, function (c) {
      return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c];
    });
  }
  function showTok(tok) {
    if (hidden) return "•".repeat(Array.from(tok).length);
    return esc(tok).replace(/ /g, '<span class="sp">␣</span>');
  }

  /* ============================================================
     6. Plain-language findings
     ============================================================ */
  var TYPENAME = { word: "word", date: "date", seq: "run", rep: "repeat", key: "keyboard", sep: "separator", free: "nothing to say" };

  function findings(pw, res) {
    var t = [], seen = {}, i, m;
    var covered = {}, worst = null;

    for (i = 0; i < res.seq.length; i++) {
      m = res.seq[i];
      if (m.type !== "free" && m.type !== "sep") covered[m.type] = (covered[m.type] || 0) + m.tok.length;
      if (m.type === "word" && (!worst || m.why.rank < worst.why.rank)) worst = m;
    }

    function add(cls, html) { if (!seen[html]) { seen[html] = 1; t.push({ cls: cls, html: html }); } }

    if (worst) {
      add("d-word", "<span><code>" + showTok(worst.tok) + "</code> is word number <b>" + worst.why.rank +
        "</b> on this page's little list, and a real attack list holds millions more. They start at the top and work down." +
        (worst.tok.length * 2 >= pw.length ? " One word plus decoration is still one word." : "") + "</span>");
      if (worst.why.subs > 0) {
        add("d-word", "<span>Swapping letters for lookalikes doesn't buy what people think. Every cracking tool tries <code>@</code>→<code>a</code>, <code>0</code>→<code>o</code>, <code>3</code>→<code>e</code> automatically; it costs them a rule, not a search.</span>");
      }
      if (worst.why.caps) {
        add("d-word", "<span>A capital on the first letter is the most predictable capital there is — that's rule one in the rulebook.</span>");
      }
      if (worst.why.rev) {
        add("d-word", "<span>Spelling it backwards is also a standard rule. It doubles the work, once.</span>");
      }
    }
    if (covered.date) add("d-date", "<span>There are only a few thousand plausible birthdays and about a hundred plausible years, so a date adds roughly <b>7 to 17 bits</b> — less than three random characters.</span>");
    if (covered.seq) add("d-seq", "<span>Counting runs like <code>1234</code> or <code>wxyz</code> are generated before anything else, forwards and backwards.</span>");
    if (covered.rep) add("d-rep", "<span>A repeat is priced as the thing being repeated plus the number of times. <code>abcabcabc</code> costs barely more than <code>abc</code>.</span>");
    if (covered.key) add("d-key", "<span>Walks across the keyboard — <code>qwerty</code>, <code>asdfg</code>, <code>zxcvb</code> — are in every wordlist, because they feel random and aren't.</span>");

    if (pw.length && pw.length < 12) {
      add("d-shape", "<span>It is <b>" + pw.length + " characters</b>. Length is the cheapest strength you can buy: every extra character multiplies the attacker's work, while extra punctuation only adds to it.</span>");
    }
    if (/^[A-Z][^A-Z]*[0-9]*[!@#$%^&*?.]?$/.test(pw) && /[0-9!@#$%^&*?.]/.test(pw) && pw.length > 5) {
      add("d-shape", "<span>Capital in front, digits and a punctuation mark at the end — that is the shape almost everyone produces when told to “add a number and a symbol”, and cracking tools generate it first.</span>");
    } else if (/[0-9]{1,4}[!@#$%^&*?.]?$/.test(pw) && /[a-zA-Z]/.test(pw) && pw.length > 5) {
      add("d-shape", "<span>Digits bolted onto the end are the first thing tried after the bare word. They add a couple of bits, not a category.</span>");
    }

    if (res.capped) {
      add("d-shape", "<span>This reads like an ordinary English sentence, so the meter has held the estimate down to <b>" +
        res.bits.toFixed(1) + " bits</b>. Running English carries only about <b>1.2 bits per character</b> no matter how long it runs — once you have the first few words, the rest is half-guessable. A sentence you thought up is far weaker than the same number of words drawn at random.</span>");
    }

    var freeBits = 0, freeChars = 0;
    for (i = 0; i < res.seq.length; i++) if (res.seq[i].type === "free") { freeBits += res.seq[i].lg; freeChars += res.seq[i].tok.length; }
    var band = bandOf(res.bits);

    if (band >= 3) {
      t.unshift({ cls: "d-good", html: "<span>Nothing cheap left to find. " + (freeChars
        ? "The " + freeChars + " unnamed character" + (freeChars === 1 ? "" : "s") + " carry <b>" + freeBits.toFixed(1) + " bits</b> on their own — that part has to be brute-forced."
        : "The pieces are numerous and unrelated enough that guessing the arrangement is most of the work.") + "</span>" });
    } else {
      add("", "<span><b>What actually works:</b> four or more unrelated everyday words you can picture — a badger, a kettle, a bridge, some frost. Long and boring beats short and clever, every time. Hit <b>roll one for me</b> below the field to see the arithmetic.</span>");
    }
    return t;
  }

  /* ============================================================
     7. UI
     ============================================================ */
  var $ = function (id) { return document.getElementById(id); };
  var elPw = $("pw"), elToggle = $("toggle"), elBits = $("bits"), elBand = $("band"),
      elFill = $("fill"), elTrack = $("track"), elGuess = $("guesses"), elRates = $("rates"),
      elChips = $("chips"), elTips = $("tips"), elPresets = $("presets"), elLive = $("live"),
      elLen = $("lenNote"), elRolled = $("rolled");

  var hidden = false;
  var rolledInfo = null;
  var MAXLEN = parseInt(elPw.getAttribute("maxlength"), 10) || 128;

  var PRESETS = [
    { label: "Summer2019!", value: "Summer2019!" },
    { label: "Tr0ub4dor&3", value: "Tr0ub4dor&3" },
    { label: "correct horse battery staple", value: "correct horse battery staple" },
    { label: "qwerty123456", value: "qwerty123456" },
    { label: "P@ssw0rd!", value: "P@ssw0rd!" },
    { label: "xK7#mQp2vRt9", value: "xK7#mQp2vRt9" }
  ];

  function randInt(max) {
    try {
      if (window.crypto && window.crypto.getRandomValues) {
        var lim = Math.floor(4294967296 / max) * max, a = new Uint32Array(1);
        do { window.crypto.getRandomValues(a); } while (a[0] >= lim);
        return a[0] % max;
      }
    } catch (e) { /* no secure source here — fall through */ }
    return Math.floor(Math.random() * max);
  }

  function roll() {
    var perWord = L2(POOL.length);
    var need = Math.ceil(60 / perWord);
    var picked = [];
    for (var i = 0; i < need; i++) picked.push(POOL[randInt(POOL.length)]);
    rolledInfo = { words: need, perWord: perWord, real: need * perWord };
    elPw.value = picked.join(" ");
    render();
    elPw.focus();
  }

  function buildPresets() {
    var frag = document.createDocumentFragment();
    PRESETS.forEach(function (p) {
      var b = document.createElement("button");
      b.type = "button"; b.className = "btn"; b.textContent = p.label;
      b.addEventListener("click", function () { rolledInfo = null; elPw.value = p.value; render(); elPw.focus(); });
      frag.appendChild(b);
    });
    var r = document.createElement("button");
    r.type = "button"; r.className = "btn solid"; r.textContent = "↻ roll one for me";
    r.addEventListener("click", roll);
    frag.appendChild(r);
    elPresets.appendChild(frag);
  }

  var liveTimer = null;
  function announce(text) {
    if (liveTimer) clearTimeout(liveTimer);
    liveTimer = setTimeout(function () { elLive.textContent = text; }, 550);
  }

  function render() {
    var pw = elPw.value;
    var res = analyze(pw);
    var bits = res.bits, band = bandOf(bits), bn = BANDS[band].name;
    var i;

    elLen.innerHTML = pw.length
      ? '<span class="mono">' + pw.length + " chars · " + res.pieces + " piece" + (res.pieces === 1 ? "" : "s") +
        (pw.length >= MAXLEN ? " · at the " + MAXLEN + "-character limit" : "") + "</span>"
      : "";

    /* headline */
    elBits.textContent = pw.length ? bits.toFixed(1) : "0";
    elBits.className = pw.length ? "bits mono b" + band : "bits mono";
    elBand.textContent = pw.length ? bn : "empty";
    elBand.className = pw.length ? "band b" + band : "band";

    var pct = Math.max(0, Math.min(1, bits / 90)) * 100;
    elFill.style.width = (pw.length ? pct : 0) + "%";
    elFill.className = pw.length ? "fill b" + band : "fill";
    elTrack.setAttribute("aria-valuenow", pw.length ? Math.min(bits, 90).toFixed(1) : "0");
    elTrack.setAttribute("aria-valuetext", pw.length ? bits.toFixed(1) + " bits, " + bn : "nothing typed");

    /* guess count */
    if (pw.length) {
      var l10 = bits * LOG10_2;
      var expo = Math.floor(l10), mant = Math.pow(10, l10 - expo);
      var approx = expo < 6 ? Math.round(Math.pow(10, l10)).toLocaleString("en-US")
                            : mant.toFixed(1) + " × 10^" + expo;
      elGuess.innerHTML = "2^" + bits.toFixed(1) + " guesses &nbsp;≈&nbsp; <b>" + approx + "</b> " +
        (approx === "1" ? "try" : "tries") + " to be sure";
    } else {
      elGuess.textContent = "";
    }

    /* rolled-passphrase truth panel */
    if (rolledInfo && pw.length) {
      var gap = bits - rolledInfo.real, verdict;
      if (gap > 1.5) {
        verdict = "The meter guessed <b>" + bits.toFixed(1) + "</b> — too generous. It has to allow that you " +
                  "arranged those words yourself, and you didn't: a die did.";
      } else if (gap < -1.5) {
        verdict = "The meter guessed <b>" + bits.toFixed(1) + "</b> — lower, because the draw happened to land on " +
                  "words it prices cheaply. The arithmetic above is still the number that counts.";
      } else {
        verdict = "The meter guessed <b>" + bits.toFixed(1) + "</b>, close enough this time — but only the line above " +
                  "is a fact; the meter is always guessing.";
      }
      elRolled.innerHTML =
        '<div class="rolled"><div class="k-lab">rolled, so we know the real answer</div>' +
        "<b>" + rolledInfo.words + "</b> words drawn at random from this page's <b>" + POOL.length +
        "</b>-word pool is exactly <b>" + rolledInfo.real.toFixed(1) + " bits</b> — not an estimate, arithmetic. " +
        verdict + " Knowing where your randomness came from beats any score. A proper <b>7,776</b>-word list clears " +
        "60 bits in <b>" + Math.ceil(60 / L2(7776)) + "</b> words.</div>";
    } else {
      elRolled.innerHTML = "";
    }

    /* crack times */
    var rows = "";
    for (i = 0; i < RATES.length; i++) {
      var r = RATES[i];
      var lt = pw.length ? (bits - 1) * LOG10_2 - r.lg10 : -9;
      var cls = "";
      if (pw.length) {
        if (lt < 4.94) cls = " bad";                 /* under a day */
        else if (band >= 3 && lt > 9.5) cls = " good"; /* over a century, and earned */
      }
      rows += "<tr><td class=\"who\">" + r.who + "<small>" + r.note + "</small></td>" +
              "<td class=\"t" + cls + "\">" + (pw.length ? fmtTime(lt) : "—") + "</td></tr>";
    }
    elRates.innerHTML = rows;

    /* the split */
    if (!pw.length) {
      elChips.innerHTML = '<span class="k-lab">nothing typed yet</span>';
    } else {
      var c = "";
      for (i = 0; i < res.seq.length; i++) {
        var m = res.seq[i];
        var lab = m.type === "sep" ? "free" : TYPENAME[m.type] + " · " + m.lg.toFixed(1) + " bits";
        c += '<span class="chip t-' + m.type + '"><span class="tok">' + showTok(m.tok) +
             '</span><span class="lab">' + lab + "</span></span>";
      }
      elChips.innerHTML = c;
    }

    /* tips */
    if (!pw.length) {
      elTips.innerHTML = '<li><span class="dot"></span><span>Type something in the field above, or pick one of the examples, and the advice appears here.</span></li>';
    } else {
      var f = findings(pw, res), h = "";
      for (i = 0; i < f.length; i++) {
        h += '<li' + (f[i].cls === "d-good" ? ' class="good"' : "") + '><span class="dot ' + f[i].cls + '"></span>' + f[i].html + "</li>";
      }
      elTips.innerHTML = h;
    }

    announce(pw.length ? bits.toFixed(0) + " bits, " + bn : "empty");
  }

  /* ---- events ---- */
  elPw.addEventListener("input", function () { rolledInfo = null; render(); });
  elToggle.addEventListener("click", function () {
    hidden = !hidden;
    elPw.type = hidden ? "password" : "text";
    elToggle.textContent = hidden ? "SHOW" : "HIDE";
    elToggle.setAttribute("aria-pressed", hidden ? "true" : "false");
    elToggle.title = hidden ? "Show the characters on screen" : "Hide the characters on screen";
    render();
    elPw.focus();
  });

  /* platform theme */
  window.addEventListener("message", function (e) {
    var d = e.data;
    if (d && d.type === "sloprun:theme" && (d.theme === "light" || d.theme === "dark")) {
      document.documentElement.setAttribute("data-theme", d.theme);
    }
  });

  /* ---- boot: never an empty form ---- */
  buildPresets();
  elPw.value = "Summer2019!";
  render();
})();
</script>
</body>
</html>

source-visible by construction · nothing is published here without its code

Comments

0 total

No comments yet. If you ran it, say what happened.

Report this post

Goes straight to the moderation queue. Enough independent reports and the post is suspended automatically until a human looks.

what is wrong