diff --git a/README.md b/README.md
index 7219ea1..50cc243 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ The previous Tor/OnionShare CLI version is preserved on the `legacy-tor` branch.
## Features
- **Author and publish.** Fill in the flyer fields and publish them as a Nostr `30078` parameterized replaceable event. Republishing with the same flyer name replaces the previous version.
-- **Anonymous by default.** The client generates an anonymous browser-side key to sign events. A NIP-07 browser extension is detected and used if present, but is never required.
+- **Anonymous by default, your identity if you want it.** The editor signs flyers under one of three identities: an **anonymous** per-device key (default, no setup), a **NIP-07 browser extension** (your secret never touches the page), or an **imported `nsec`**. An imported key signs in-memory for the session; tick "Remember on this device" to keep it, encrypted with a PIN (PBKDF2 → AES-GCM) — the plaintext secret is never stored. A short PIN is a convenience lock, not protection against a targeted attacker (see the [spec](docs/nostr-static-client-spec.md#publishing-identity)); the extension path is stronger.
- **Publish confirmation.** After signing and publishing, a modal shows the new flyer's URL to copy, so authors know they're done and where to find it (localized, like everything else).
- **Open from a link or ID.** Load a flyer from an `naddr`, `nevent1`, `note1`, raw 64-char event ID, or `nostr:` URL. The lookup field tolerates copy/paste whitespace and newlines, so an event ID copied off a printed tear-off (where it wraps across two lines) still resolves.
- **Short, scannable poster URLs.** Each poster URL is a standard NIP-19 `naddr` carrying a single relay hint, placed in the URL `#fragment` (no `?addr=` query). Shorter URLs make the tear-off QR code easier to scan. Older relay-list `naddr`s and `?addr=` URLs still resolve.
@@ -19,7 +19,7 @@ The previous Tor/OnionShare CLI version is preserved on the `legacy-tor` branch.
- **US Letter and A4.** Print size defaults to the right paper for the visitor's country (from the browser locale's region — `en-US` → Letter, `en-GB` → A4, no geolocation), with a manual override. Paper size is a local print preference and is never written into the event. See the [spec](docs/nostr-static-client-spec.md#print-paper-size).
- **Fits-the-page editing.** Fields that would overflow the printable sheet are rejected as you type, with an inline error under the affected field. See the [field length cheat sheet](docs/flyer-field-limits.md) for how much text fits each field per language and paper size.
- **QR codes.** The tear-off QR points at the poster URL (re-open and reprint); the main flyer QR points at the creator's content URL.
-- **Bulletin board.** A small screen-only link below the flyer preview (outside the printed sheet, never printed) opens a spreadsheet-style page of the most recent flyers found on the relays — title, link, posting npub, language, and date — sortable by any column.
+- **Bulletin board (members-only, trust-filtered).** A small screen-only link below the flyer preview (outside the printed sheet, never printed) opens a spreadsheet-style page of the most recent flyers found on the relays — title, link, posting npub, language, and date — sortable by any column. Viewing the board requires connecting a Nostr identity — via a NIP-07 browser extension, by pasting a private key (`nsec`, decoded in-page and never stored), or by creating a new key (revealed so you can save it). The board then shows only flyers from authors in your **web of trust** (your NIP-02 follow graph), with a "Show all" opt-out. A brand-new key with no follows is seeded from a curated trust anchor so the board is never an unmoderated firehose. Connecting is not required to author or print flyers, which stay fully anonymous. See the [roadmap](docs/roadmap.md) for the phased trust model.
- **Self-contained.** No build step and no CDNs — Nostr and QR libraries are vendored under `site/vendor/`.
## What Is In `main`
diff --git a/docs/nostr-static-client-spec.md b/docs/nostr-static-client-spec.md
index bfddb6c..10f07b6 100644
--- a/docs/nostr-static-client-spec.md
+++ b/docs/nostr-static-client-spec.md
@@ -124,6 +124,43 @@ The client and schema helper must:
- keep attachments unsupported in V1
- avoid remote frontend dependencies
+## Publishing identity
+
+The editor signs flyer events under one of three identities, chosen in the
+identity controls (`activeIdentity` / `signActiveEvent` in `nostr-client.js`):
+
+- **Anonymous (default).** The per-device key in `localStorage`
+ (`voxvera_nostr_anon_secret_hex`); signs locally with `finalizeEvent`.
+ Unchanged behavior — no connection required. "Generate anonymous npub" rolls a
+ fresh device key.
+- **Browser extension (NIP-07).** Signs via `window.nostr.signEvent`; the secret
+ never enters the page. The pubkey is fetched with `getPublicKey()`.
+- **Private key (nsec).** The user pastes an `nsec`; it is decoded to a secret
+ key used to sign with `finalizeEvent`. By default the secret lives **only in
+ memory** for the session. If the user ticks "Remember on this device", the
+ secret is encrypted with a PIN and stored as `voxvera_identity_nsec_enc`; the
+ plaintext secret is never written to storage.
+
+The chosen mode is remembered in `voxvera_identity_mode` (and the npub in
+`voxvera_identity_npub` for display). On reload: NIP-07 re-fetches the pubkey;
+a remembered nsec shows a **locked** state until the PIN is entered (`unlock`);
+a session-only nsec falls back to anonymous. `naddr`/poster-URL/`author npub`
+outputs all reflect the active identity.
+
+**PIN encryption details.** PBKDF2 (`SHA-256`, 600k iterations, random 16-byte
+salt) derives an AES-GCM-256 key (random 12-byte IV); the blob stores
+`{v, iters, salt, iv, ct}` base64. A minimum 4-digit numeric PIN is enforced,
+but longer PINs are allowed and encouraged.
+
+> **Security caveat (documented deliberately).** A short numeric PIN has a tiny
+> keyspace (a 4-digit PIN is ~10,000 combinations), so if the encrypted blob is
+> exfiltrated (targeted XSS, or devtools access on a shared/stolen device) it is
+> brute-forceable offline regardless of PBKDF2 cost. PIN-at-rest protects against
+> casual snooping and untargeted localStorage scraping, **not** a determined or
+> targeted attacker. The NIP-07 extension path (secret never in the page) is the
+> stronger option; a NIP-46 remote signer is a future addition. See
+> [`roadmap.md`](roadmap.md).
+
## Print paper size
The flyer supports two print sizes: **US Letter** (8.5×11in) and **A4**
@@ -167,6 +204,34 @@ author+`d` identifier, keeping the latest.
(`.board-link`, label `bulletin_board`), outside the printable sheet. It must
never print (hidden in the `@media print` block alongside the other app
chrome).
+- **The board is gated behind a connected Nostr identity.** On load it shows a
+ login gate (`#board-gate`) instead of the table, with three ways to connect:
+ a **NIP-07** extension (`window.nostr.getPublicKey()`), **pasting an `nsec`**
+ (decoded in-page to derive the pubkey, then discarded — the secret is never
+ stored or transmitted), or **creating a new key** (reuses this device's anon
+ key in `voxvera_nostr_anon_secret_hex` if present, else generates one, and
+ reveals the `nsec` with a save-it warning before continuing). On success it
+ reveals the table and surfaces the connected npub (`#board-identity`) with a
+ Disconnect control. The board only ever needs the *pubkey* (it reads, never
+ signs), so the connected pubkey is remembered in `localStorage`
+ (`voxvera_connected_pubkey`, shared with the main client) and a return visit
+ reconnects from it directly — no prompt or re-entry, any method. This is
+ **not access control** — the events are public on the relays regardless — it
+ establishes the *viewer's* identity for the web-of-trust filter below.
+ Authoring and printing flyers stays fully anonymous and never requires
+ connecting.
+- **Web-of-trust filter (NIP-02).** Once connected, the board fetches the
+ viewer's contact list (kind 3) and defaults to showing only flyers whose
+ author is in the viewer's follow graph (degree 1) plus the viewer themself. A
+ **"Show all"** checkbox opts out to the full relay firehose. A viewer with no
+ follow list of their own (a fresh key) is **seeded** from a pinned curator
+ account's follows (`FALLBACK_CURATOR_PUBKEY` in `board.js`), so a brand-new
+ viewer still gets a curated board. If no trust data can be fetched at all, the
+ board falls back to showing everything so it is never mysteriously empty. The
+ `#board-filter-note` explains which mode is active (following / seeded / all),
+ and re-localizes with the language selector. See [`roadmap.md`](roadmap.md)
+ for the phased plan (Phase 1 = the gate; Phase 2 = this filter; Phase 3 =
+ degree-2 trust, blocklist/report, NIP-05 badges).
- `board.js` is independent of `nostr-client.js` (which exports nothing), so its
UI strings live in `BOARD_UI` (all supported languages); it reuses the
vendored nostr-tools and `locales.js` (for language names + RTL direction).
@@ -180,8 +245,9 @@ author+`d` identifier, keeping the latest.
- The npub column is forward-looking — anonymous keys today, but a stable
identity per author maps cleanly onto this view later.
-The board has no moderation and anonymous keys provide no authenticity yet;
-these and other product/threat-model gaps are tracked in
+The login gate (Phase 1) and the web-of-trust filter (Phase 2) are in place.
+Remaining gaps — degree-2 trust, a blocklist/report flow, NIP-05 badges, and
+making the bootstrap curator configurable — are tracked in
[`roadmap.md`](roadmap.md).
## Localization
diff --git a/docs/roadmap.md b/docs/roadmap.md
index 94eac9e..f453dc8 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -7,14 +7,17 @@ roughly in priority order: the trust model (#1–#2) is the highest-leverage are
because almost every other risk flows from it being undefined.
Status legend: **Open** (not started) · **Partial** (some mitigation exists) ·
-**Planned** (committed direction).
+**Planned** (committed direction) · **In progress** (partially shipped).
---
-## 1. Bulletin board has no moderation or abuse story — **Planned**
+## 1. Bulletin board has no moderation or abuse story — **In progress**
-> Direction agreed — see *Planned direction: identity + web-of-trust on the
-> board* below (jointly addresses #1 and #2).
+> Direction agreed and **Phases 1–2 shipped**: the board requires a connected
+> Nostr identity to view, and it now filters to flyers from the viewer's NIP-02
+> web of trust (with a bootstrap seed for new keys). See *Planned direction:
+> identity + web-of-trust on the board* below (jointly addresses #1 and #2).
+> Remaining: degree-2 trust, blocklist/report, NIP-05 badges (Phase 3).
**Problem.** `board.html` lists every Nostr event tagged `t=voxvera` / `t=flyer`
@@ -44,7 +47,16 @@ the single biggest liability for promoting the board publicly.
Depends on **#2** (without identity, allowlists/blocklists are weak).
-## 2. Anonymous keys give no authenticity or accountability — **Planned**
+## 2. Anonymous keys give no authenticity or accountability — **In progress**
+
+> Phases 1–2 shipped: the board viewer connects a real Nostr identity (NIP-07)
+> and the board is filtered by that viewer's web of trust (with a curator seed
+> for new keys), so spam from unknown authors is hidden by default. The **editor
+> now lets authors publish under their own identity too** — NIP-07, an imported
+> `nsec` (optionally PIN-encrypted at rest), or stay anonymous (default) — so a
+> flyer can carry a stable, identifiable author when the creator wants one.
+> Authenticity *display* on the board (NIP-05 badges, an identified/anonymous
+> distinction) remains ahead.
**Problem.** The client generates a fresh anonymous key per browser, so the
board's "Posted by" npub is effectively random and there is no way to verify a
@@ -94,23 +106,62 @@ board can show them flyers from people they trust.
existing anonymous key to a kept identity.
**Phased plan.**
-- **Phase 1 — Identity.** A "Connect" flow (`window.nostr.getPublicKey()` with
- an nsec-import fallback) establishes the viewer's npub; publish under that key
- when connected; surface the connected npub. Anonymous create/print stays
- intact.
-- **Phase 2 — Web-of-trust filter.** Fetch the connected user's NIP-02 contact
- list and default the board to flyers from authors in their follow graph, with
- a "show all" opt-out. This is the spam gate.
-- **Phase 3 — Optional.** Local blocklist / "hide this", report-to-list, NIP-05
- verified badges.
+- **Phase 1 — Identity. ✅ Shipped.** The board (`board.html` / `board.js`) shows
+ a login gate instead of the table until the viewer connects. Three methods: a
+ NIP-07 extension (`window.nostr.getPublicKey()`), pasting an `nsec`, or creating
+ a new key (see decision 2 below). On success it reveals the table, surfaces the
+ connected npub with a Disconnect control, and remembers the pubkey in
+ `localStorage` (`voxvera_connected_pubkey`) so a return visit reconnects from
+ the pubkey alone — no prompt or re-entry, regardless of method. Anonymous
+ create/print is untouched — connecting is required only to *view the board*.
+- **Phase 2 — Web-of-trust filter. ✅ Shipped.** On connect the board fetches
+ the viewer's NIP-02 contact list (kind 3) and defaults to flyers from authors
+ in their follow graph (degree 1) plus themselves, with a **"Show all"**
+ opt-out. **Bootstrap seed:** a viewer with no follow list of their own (a
+ fresh key) is seeded from a pinned curator account's follows
+ (`FALLBACK_CURATOR_PUBKEY` in `board.js` =
+ `npub15jnttpymeytm80hatjqcvhhqhzrhx6gxp8pq0wn93rhnu8s9h9dsha32lx`), so a brand-
+ new viewer still gets a curated board instead of the unmoderated firehose. If
+ no trust data can be fetched at all, the board falls back to showing
+ everything (never mysteriously empty). Covered by the cross-engine e2e suite
+ (followed-only view + show-all opt-out; curator-seeded fallback).
+- **Phase 3 — Optional / next.** Degree-2 trust (follows-of-follows), a local
+ blocklist / "hide this", report-to-list, and NIP-05 verified badges. Also:
+ the bootstrap curator is currently a single hardcoded pubkey — consider making
+ it configurable or a small curated set.
-**Open decisions (need a call before building).**
-1. **Board viewing:** login-required (Nostr-native, smaller insider audience) vs
- open-with-WoT-applied-when-logged-in (broader physical-flyer reach; logged-out
- sees a curated/limited set). Leaning open-with-WoT, but depends on whether the
- board is a discovery surface or a members' space.
-2. **Login methods at launch:** NIP-07 only, or NIP-07 + nsec import (so mobile
- isn't excluded).
+**Decisions made (2026-06-10).**
+1. **Board viewing: login-required.** The board is blank (a Connect prompt)
+ until a Nostr identity is connected — framed as a members' space rather than a
+ public discovery surface.
+2. **Login methods: NIP-07 + nsec import + key generation. ✅ Shipped.** The gate
+ offers three ways in: a NIP-07 browser extension, pasting a private key
+ (`nsec`), or creating a new key. The nsec is decoded **in-page** to derive the
+ pubkey and then discarded — only the pubkey is persisted, since the board only
+ reads (never signs). "Create a new key" reuses this device's existing
+ anonymous key if present (so it doesn't orphan an editor identity) or
+ generates one, and reveals the `nsec` with a "save this — we can't recover it"
+ warning before continuing. This **resolves the earlier mobile-lockout
+ trade-off**: a phone visitor with no extension can now generate or paste a key
+ to view the board. Covered by the cross-engine e2e suite (nsec import +
+ pubkey-only persistence + reload restore; invalid nsec; key generation +
+ reveal + connect).
+
+**Follow-ups this opens.**
+- **In-page nsec handling.** Pasting a secret key into a web page is inherently
+ riskier than a NIP-07 extension that keeps the key isolated; the input is a
+ password field, the value is cleared after use, and nothing is transmitted, but
+ a NIP-46 remote signer would be a safer mobile path to add later.
+- **Editor identity (shipped).** The editor now signs under anon / NIP-07 /
+ imported nsec. A remembered nsec is PIN-encrypted at rest (PBKDF2 600k →
+ AES-GCM); plaintext is never stored. **Known weakness:** a short numeric PIN is
+ brute-forceable offline if the encrypted blob leaks — it guards casual snooping,
+ not a targeted attacker. Next: NIP-46 remote signer; an attempt-limit / lockout
+ on repeated wrong PINs; and optionally encrypting the anonymous device key the
+ same way (today it is stored in plaintext, as before).
+- **Editor vs board identity are independent.** The editor's publishing identity
+ and the board's viewing identity are stored separately and don't yet sync; a
+ future pass could share one connected identity across both pages.
## 3. Client distribution is centralized (a single chokepoint) — **Open**
diff --git a/e2e/README.md b/e2e/README.md
index 8f3a429..84b594b 100644
--- a/e2e/README.md
+++ b/e2e/README.md
@@ -32,6 +32,23 @@ engine-sensitive surfaces:
- anonymous key generation works (the **WebCrypto** path)
- the legacy `/nostr/` redirect preserves the query string and `#hash`
- an event-lookup URL shows the localized loading state, never the default flyer
+- the bulletin board is gated behind a Nostr connection: the gate shows when no
+ identity is connected, connecting (stubbed `window.nostr`) reveals the table
+ and the connected npub, disconnecting returns to the gate, and a remembered
+ connection auto-restores on revisit
+- the board's alternative connect methods: pasting an `nsec` derives the pubkey,
+ reveals the board, leaves no secret in the DOM or storage, and restores on
+ reload from the pubkey alone; an invalid `nsec` errors without connecting; and
+ "Create a new key" reveals the npub/nsec, stores the device key, then connects
+- the editor's publishing identity: defaults to anonymous; connecting NIP-07
+ (stubbed) signs/publishes under the extension key; importing an `nsec` signs
+ for the session without persisting the secret; an invalid `nsec` errors; a
+ remembered `nsec` is PIN-encrypted at rest (the stored blob never contains the
+ plaintext secret), locks on reload, rejects a wrong PIN, and unlocks with the
+ right one; "Forget" clears the stored identity back to anonymous
+- the board's web-of-trust filter (stubbed relays): a connected viewer sees only
+ flyers from authors in their NIP-02 follow graph, "Show all" opts out to the
+ full set, and a viewer with no follow list is seeded from the curator's follows
## Running
diff --git a/e2e/flyer.spec.js b/e2e/flyer.spec.js
index 8cc2a07..6bf8036 100644
--- a/e2e/flyer.spec.js
+++ b/e2e/flyer.spec.js
@@ -428,6 +428,124 @@ test.describe("VoxVera static client", () => {
await expect(modal).toBeHidden();
});
+ test("editor defaults to anonymous signing", async ({ page }) => {
+ await page.goto("/#editor");
+ await expect(page.locator("#signer-state")).toHaveText("Publishing anonymously");
+ });
+
+ test("editor can connect a NIP-07 identity and publish under it", async ({ page }) => {
+ const PK = "a".repeat(64);
+ await page.addInitScript((pk) => {
+ window.nostr = {
+ getPublicKey: async () => pk,
+ // Return a plausibly-signed event so the publish path completes.
+ signEvent: async (e) => ({ ...e, pubkey: pk, id: "f".repeat(64), sig: "0".repeat(128) })
+ };
+ class FakeWS {
+ constructor() { this.readyState = 1; setTimeout(() => this.onopen && this.onopen(), 1); }
+ send(data) {
+ try {
+ const m = JSON.parse(data);
+ if (m[0] === "EVENT" && m[1] && m[1].id) {
+ const id = m[1].id;
+ setTimeout(() => this.onmessage && this.onmessage({ data: JSON.stringify(["OK", id, true, ""]) }), 1);
+ }
+ } catch (_) {}
+ }
+ close() {}
+ }
+ window.WebSocket = FakeWS;
+ }, PK);
+ await page.goto("/#editor");
+ const expectedNpub = await page.evaluate((pk) => window.NostrTools.nip19.npubEncode(pk), PK);
+ await page.locator("#connect-nip07").click();
+ await expect(page.locator("#signer-state")).toHaveText("Publishing as your extension identity");
+ await expect(page.locator("#author-npub-output")).toHaveText(expectedNpub);
+ // Publishing routes through window.nostr.signEvent and still shows the modal.
+ await page.locator("#publish-event").click();
+ await expect(page.locator("#publish-modal")).toBeVisible();
+ });
+
+ test("editor imports an nsec for the session without storing the secret", async ({ page }) => {
+ await page.goto("/#editor");
+ const { nsec, npub } = await page.evaluate(() => {
+ const t = window.NostrTools;
+ const sk = t.generateSecretKey();
+ return { nsec: t.nip19.nsecEncode(sk), npub: t.nip19.npubEncode(t.getPublicKey(sk)) };
+ });
+ await page.locator("#use-nsec-toggle").click();
+ await page.locator("#nsec-input").fill(nsec);
+ await page.locator("#nsec-submit").click();
+ await expect(page.locator("#signer-state")).toHaveText("Publishing as your imported key");
+ await expect(page.locator("#author-npub-output")).toHaveText(npub);
+ // No "remember" → nothing encrypted is stored, and the mode falls back to
+ // anonymous on reload (so no dead lock).
+ expect(await page.evaluate(() => localStorage.getItem("voxvera_identity_nsec_enc"))).toBeNull();
+ expect(await page.evaluate(() => localStorage.getItem("voxvera_identity_mode"))).toBe("anon");
+ });
+
+ test("an invalid nsec in the editor shows an error", async ({ page }) => {
+ await page.goto("/#editor");
+ await page.locator("#use-nsec-toggle").click();
+ await page.locator("#nsec-input").fill("nsec1clearlynotvalid");
+ await page.locator("#nsec-submit").click();
+ await expect(page.locator("#identity-error")).toContainText(/valid nsec/i);
+ await expect(page.locator("#signer-state")).toHaveText("Publishing anonymously");
+ });
+
+ test("a remembered nsec is PIN-encrypted at rest and unlocks on reload", async ({ page }) => {
+ await page.goto("/#editor");
+ const { nsec, npub, hex } = await page.evaluate(() => {
+ const t = window.NostrTools;
+ const sk = t.generateSecretKey();
+ const toHex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
+ return { nsec: t.nip19.nsecEncode(sk), npub: t.nip19.npubEncode(t.getPublicKey(sk)), hex: toHex(sk) };
+ });
+ await page.locator("#use-nsec-toggle").click();
+ await page.locator("#nsec-input").fill(nsec);
+ await page.locator("#nsec-remember").check();
+ await page.locator("#nsec-pin").fill("13579");
+ await page.locator("#nsec-submit").click();
+ await expect(page.locator("#signer-state")).toHaveText("Publishing as your imported key");
+ // What's stored is an AES-GCM blob, never the plaintext secret.
+ const stored = await page.evaluate(() => localStorage.getItem("voxvera_identity_nsec_enc"));
+ expect(stored).toBeTruthy();
+ expect(stored).not.toContain(hex);
+ expect(stored).not.toContain(nsec);
+ expect(JSON.parse(stored)).toHaveProperty("ct");
+
+ // Reload → the identity is locked and requires the PIN. (reload() forces a
+ // real document load; goto("/#editor") here would be a same-document hash
+ // change that keeps JS memory, so the in-memory key would never clear.)
+ await page.reload();
+ await expect(page.locator("#identity-locked-row")).toBeVisible();
+ await expect(page.locator("#identity-state")).toHaveText("Locked");
+ // Wrong PIN is rejected.
+ await page.locator("#unlock-pin").fill("00000");
+ await page.locator("#unlock-key").click();
+ await expect(page.locator("#identity-error")).toContainText(/Wrong PIN/i);
+ // Correct PIN unlocks and restores the imported identity.
+ await page.locator("#unlock-pin").fill("13579");
+ await page.locator("#unlock-key").click();
+ await expect(page.locator("#signer-state")).toHaveText("Publishing as your imported key");
+ await expect(page.locator("#author-npub-output")).toHaveText(npub);
+ });
+
+ test("forgetting a stored identity clears it and returns to anonymous", async ({ page }) => {
+ await page.goto("/#editor");
+ const nsec = await page.evaluate(() => window.NostrTools.nip19.nsecEncode(window.NostrTools.generateSecretKey()));
+ await page.locator("#use-nsec-toggle").click();
+ await page.locator("#nsec-input").fill(nsec);
+ await page.locator("#nsec-remember").check();
+ await page.locator("#nsec-pin").fill("2468");
+ await page.locator("#nsec-submit").click();
+ await page.reload();
+ await expect(page.locator("#identity-locked-row")).toBeVisible();
+ await page.locator("#forget-key").click();
+ await expect(page.locator("#signer-state")).toHaveText("Publishing anonymously");
+ expect(await page.evaluate(() => localStorage.getItem("voxvera_identity_nsec_enc"))).toBeNull();
+ });
+
test("bulletin board language selector localizes the page", async ({ page }) => {
await page.goto("/board.html");
await expect(page.locator("#board-title")).toHaveText("Bulletin Board");
diff --git a/site/index.html b/site/index.html
index 970e367..23b2780 100644
--- a/site/index.html
+++ b/site/index.html
@@ -45,8 +45,31 @@
Checking identity
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Saved identity is locked. Enter your PIN to unlock it.
+
+
+
+