diff --git a/docs/blog/passwordgenerator-v3.md b/docs/blog/passwordgenerator-v3.md
new file mode 100644
index 0000000..d8e2d14
--- /dev/null
+++ b/docs/blog/passwordgenerator-v3.md
@@ -0,0 +1,200 @@
+# PasswordGenerator v3 is here, and it's a proper rewrite
+
+If you've used my PasswordGenerator package over the years, thank you. It's been
+downloaded millions of times on NuGet, which still amazes me. It started as a
+small helper to create random passwords in .NET, and the community kept it going
+with bug reports, pull requests and ideas.
+
+Version 3 is the biggest update it has ever had. I've rewritten the core, fixed
+some long-standing correctness issues, and added a load of features I've wanted
+for a while. Here's the tour.
+
+## Why a rewrite?
+
+The honest answer is that the old code had a few problems hiding under the
+surface. The randomness wasn't as fair as it should have been, there was an
+off-by-one in the length handling, and the shuffle relied on a GUID, which was
+never really fit for the job.
+
+None of these made the package unusable, but for something whose entire purpose
+is generating secure passwords, "good enough" isn't good enough. So I took the
+chance to put it right.
+
+## The security and correctness fixes
+
+This is the part I care about most.
+
+1. **Properly secure, unbiased randomness.** v3 uses a cryptographically secure
+ random source and samples integers with `RandomNumberGenerator.GetInt32`,
+ which removes the modulo bias that crept into the old approach. Every
+ character now has a fair chance of being picked.
+2. **Fixed the off-by-one in length handling**, so you get exactly the length you
+ asked for.
+3. **Swapped the GUID-based shuffle for a Fisher-Yates shuffle**, which is the
+ right tool for shuffling a set of characters evenly.
+4. **Tidied up the RNG lifecycle.** The random source is now owned and disposed
+ properly, and the old static RNG and dead code are gone.
+5. **Empty special-character sets are now validated** instead of quietly giving
+ you a weaker password.
+
+```csharp
+// Same friendly API, much stronger guarantees underneath.
+var password = new Password(20).Next();
+```
+
+Here's how a password is built in v3. If you ask for, say, at least two digits,
+those get placed first, the rest of the length is filled from your chosen pool,
+and then the whole thing gets a proper crypto shuffle. So your requirements are
+guaranteed by construction, with no "generate and hope" retry loop.
+
+```mermaid
+flowchart LR
+ R["RequireAtLeast:
1 lower, 1 upper, 2 digits, 1 special"] --> Place["place those characters first"]
+ Place --> Rest["fill the rest from the full pool"]
+ Rest --> Shuf["crypto Fisher-Yates shuffle"]
+ Shuf --> Out["valid by construction"]
+ classDef good fill:#e6ffe6,stroke:#009900;
+ class Out good;
+```
+
+## New features worth knowing about
+
+There's quite a lot here, so I'll pick out the highlights.
+
+### Async and batch APIs
+
+You can now await your passwords and generate them in batches.
+
+```csharp
+string one = await pwd.NextAsync(cancellationToken);
+IReadOnlyList ten = pwd.Generate(10);
+IReadOnlyList ten2 = await pwd.GenerateAsync(10, cancellationToken);
+```
+
+### Dependency injection
+
+If you're building an ASP.NET Core app, you can register the generator once and
+inject `IPasswordGenerator` wherever you need it. You can configure it in code or
+bind it from your `appSettings.json`.
+
+```csharp
+services.AddPasswordGenerator(o =>
+{
+ o.Length = 20;
+ o.IncludeSpecial = true;
+ o.ExcludeAmbiguous = true;
+});
+```
+
+### Ready-made presets
+
+Sometimes you just want a sensible default for a common job. The new presets give
+you exactly that.
+
+```csharp
+string strong = Password.ForOwasp().Next(); // full printable-ASCII pool, length 16
+string nist = Password.ForNist().Next(); // NIST-aligned, length 12
+string otp = Password.ForOtp(6).Next(); // 6-digit one-time code
+string apiKey = Password.ForApiKey(32).Next(); // URL-safe token
+string envName = Password.ForEnvironmentName(12).Next(); // readable id, no look-alikes
+```
+
+### Much better passphrases
+
+This is my favourite addition. v3 generates passphrases from the EFF Large
+Wordlist, which is 7,776 words and gives you roughly 12.9 bits of entropy per
+word. A six-word phrase is now around 77 bits, which is genuinely strong and
+still easy to remember.
+
+```csharp
+string phrase = Password.ForPassphrase(4).Next(); // e.g. "maple-river-quartz-bloom-42"
+string byTarget = Password.ForPassphraseWithEntropy(80).Next(); // word count derived to clear 80 bits
+string memorable = Password.ForMemorable().Next(); // capitalised, ~80+ bits
+```
+
+If you've ever fought with a site that demands a number and a symbol, this one's
+for you. Pass `includeSymbol: true` and a random symbol gets attached to one of
+the words, so your phrase passes the composition rules while staying readable.
+
+```csharp
+string phrase = Password.ForPassphrase(4, includeSymbol: true).Next();
+// e.g. "maple-river#-quartz-bloom-42"
+```
+
+The wordlist is by the Electronic Frontier Foundation and used under a Creative
+Commons licence. Full credit to them, and you'll find the details in the
+THIRD-PARTY-NOTICES file in the repo.
+
+### Quality controls and entropy estimation
+
+You can now strip out look-alike characters, require a minimum number from a
+character class, use your own custom pool, and even ask the package how strong a
+password is in bits.
+
+```csharp
+var readable = new Password(20).ExcludeAmbiguous().Next(); // no I l 1 O 0 o
+var pwd = new Password(16).RequireAtLeast(CharacterClass.Numeric, 2).Next();
+var custom = new Password().WithCharacters("ABCDEF0123456789").LengthRequired(24).Next();
+double bits = new Password(20).EstimateEntropyBits();
+```
+
+## The one breaking change to watch for
+
+When the settings can't produce a valid password, `Next()` now throws an
+`ArgumentException` rather than returning an error message dressed up as your
+password. That old behaviour caught a few people out, so this is a deliberate
+fix. If you'd rather not deal with exceptions, there's a non-throwing path.
+
+```csharp
+if (new Password(16).TryNext(out var result))
+ Console.WriteLine(result);
+```
+
+The difference is easier to see than to describe. In v2, a configuration that
+couldn't produce a password eventually handed you the string "Try again" as if it
+were a real password. In v3, you either get a real password or a clear signal that
+something's wrong.
+
+```mermaid
+stateDiagram-v2
+ state "v2 (before)" as Old {
+ [*] --> RetryLoop
+ RetryLoop --> OKo: valid
+ RetryLoop --> StrFail: gives up, returns 'Try again' string
+ }
+ state "v3 (now)" as New {
+ [*] --> Validate
+ Validate --> BuildOK: build guarantees a real password
+ Validate --> Throw: invalid config, throws or returns false
+ }
+```
+
+## What you need to run it
+
+v3 targets `net8.0` and `net10.0`, so you'll need .NET 8 or later. If you're
+still on .NET Framework or an older runtime, the 2.x line targets
+`netstandard2.0` and keeps working, so you're not stuck.
+
+The good news is that the familiar v2 API is unchanged. Your existing `Next()`,
+`NextGroup()`, constructors and `IncludeX()` calls all still work, aside from
+that error-handling change above.
+
+## Upgrading
+
+If you're coming from v2, have a quick read of the v2 to v3 migration guide in
+the repo before you upgrade. It walks through the small number of things to check
+and maps the presets to the OWASP and NIST guidance behind them.
+
+```
+Install-Package PasswordGenerator
+```
+
+## A thank you
+
+None of this happens without the community. The bug reports, the pull requests
+and the conversations over the years shaped where v3 ended up, so thank you to
+everyone who chipped in.
+
+Give v3 a go, and if you spot something or have an idea, open an issue on the
+[GitHub repo](https://github.com/prjseal/PasswordGenerator). I'd love to hear how
+you're using it.