fix(tier2-g): warmer Gate copy, generic example, plain-language advanced controls

Round-three pass on the Gate card based on the latest UX feedback.

- Tagline: "Keeps your guest list yours" -> "Only your guests get in"
- Walk-through example now uses a neutral placeholder name (Sam)
  instead of "Aunty Patience". GuestGuard's audience is wedding
  couples, corporate planners, and party hosts as much as family
  organisers, and Sam is at home in any of those contexts.
- The four-step "How does the Gate work?" expander was lightly
  rewritten for warmth and to remove em-dashes; the mechanism it
  describes is unchanged.
- Advanced strictness controls now open with a real explanation
  rather than a one-liner about "band thresholds". The intro maps
  the three sliders to the three reactions (watch / flag / refuse),
  explains what the 0-100 numbers mean, and reassures hosts that
  the presets above are still fine for almost everyone. Each
  slider also gets a one-line caption tying its level dot (green
  / amber / red) to the user-visible effect.
- The trusted-networks "What this is and isn't" panel had its quotes
  unwound into natural sentences; the empty-state copy was rewritten
  in the second person; "No decisions to review yet" reworded.
- Pass through every user-visible string and replaced em-dashes
  with periods, commas, or natural rephrasing. Em-dashes only remain
  in code comments now (developer-facing, not on screen).

No behaviour changes; no backend changes; no API changes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Kwaku Danso
2026-05-19 23:00:11 +01:00
parent dee3d738ac
commit b34715f152
+86 -47
View File
@@ -53,7 +53,7 @@ const PRESETS: Preset[] = [
{ {
id: 'relaxed', id: 'relaxed',
label: 'Relaxed', label: 'Relaxed',
description: "Casual party fine if friends share their link with each other.", description: "Casual party. It's fine if a few friends share their link with each other.",
medium: 50, high: 80, block: 95, medium: 50, high: 80, block: 95,
}, },
{ {
@@ -65,7 +65,7 @@ const PRESETS: Preset[] = [
{ {
id: 'strict', id: 'strict',
label: 'Strict', label: 'Strict',
description: "Wedding or private event — uninvited plus-ones are a problem and the guest list matters.", description: "Wedding or private event. Uninvited plus-ones are a problem and the guest list really matters.",
medium: 25, high: 50, block: 70, medium: 25, high: 50, block: 70,
}, },
{ {
@@ -270,30 +270,30 @@ function verdictLabel(v: string) {
<header class="mb-3 flex items-center justify-between"> <header class="mb-3 flex items-center justify-between">
<div> <div>
<h2 class="text-lg font-semibold">Gate</h2> <h2 class="text-lg font-semibold">Gate</h2>
<p class="text-xs text-zinc-500">Keeps your guest list yours.</p> <p class="text-xs text-zinc-500">Only your guests get in.</p>
</div> </div>
</header> </header>
<!-- Value-prop paragraph. This is the *point* of the feature in plain <!-- Value-prop paragraph. Plain language, no em-dashes, no example
words. The user named the actual pain ("preventing multiple sharing names. Hits the actual pain (forwarded / shared invitation
of events to uninvited guests") that phrase lives here verbatim. --> links) head-on. -->
<div class="mb-4 rounded-lg border border-brand-700/40 bg-brand-500/[0.04] p-4 text-sm leading-relaxed"> <div class="mb-4 rounded-lg border border-brand-700/40 bg-brand-500/[0.04] p-4 text-sm leading-relaxed">
<p class="text-zinc-200"> <p class="text-zinc-200">
Every guest gets their own personal invitation link only meant for them. Every guest gets their own personal invitation link. Only one person was meant to use it.
The <strong class="text-brand-300">Gate</strong> watches each link in the The <strong class="text-brand-300">Gate</strong> quietly watches each link, so when
background and stops <strong>forwarded or shared invitations</strong> someone forwards or shares an invitation, the people it ends up with can't actually use it.
from being used by people who weren't on your list.
</p> </p>
<p class="mt-2 text-xs text-zinc-400"> <p class="mt-2 text-xs text-zinc-400">
Real invited guests don't notice it. You don't need to set anything up Your invited guests won't notice a thing. There's nothing you need to set up for this
for it to work the Gate is on by default with sensible settings. to work; the Gate runs in the background with sensible defaults from the moment your
event is live.
</p> </p>
</div> </div>
<!-- "How does this work?" — collapsed by default so the page stays <!-- "How does this work?" — collapsed by default so the page stays
scannable. The expander walks through the actual mechanism in scannable. Plain-language walkthrough; uses a generic placeholder
plain language so curious / sceptical hosts can see what we're name (Sam) rather than family vocabulary so the example fits
doing without reading docs. --> wedding couples, party hosts, and corporate planners equally. -->
<details class="group mb-6 rounded-lg border border-zinc-800 bg-zinc-950"> <details class="group mb-6 rounded-lg border border-zinc-800 bg-zinc-950">
<summary class="flex cursor-pointer items-center justify-between p-3 text-sm font-medium text-zinc-200"> <summary class="flex cursor-pointer items-center justify-between p-3 text-sm font-medium text-zinc-200">
<span class="flex items-center gap-2"> <span class="flex items-center gap-2">
@@ -307,46 +307,49 @@ function verdictLabel(v: string) {
</svg> </svg>
</summary> </summary>
<div class="space-y-3 border-t border-zinc-900 p-4 text-sm text-zinc-300"> <div class="space-y-3 border-t border-zinc-900 p-4 text-sm text-zinc-300">
<p class="text-zinc-400">
Here's the short version, using "Sam" as a stand-in for any one of your invited guests.
</p>
<ol class="space-y-3"> <ol class="space-y-3">
<li class="flex gap-3"> <li class="flex gap-3">
<span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-brand-500/15 text-xs font-semibold text-brand-300">1</span> <span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-brand-500/15 text-xs font-semibold text-brand-300">1</span>
<p> <p>
When you send Aunty Patience her invitation, she gets her own personal link. When you send Sam an invitation, the link inside it is just for Sam. Nobody else
That link only belongs to her. has the same link.
</p> </p>
</li> </li>
<li class="flex gap-3"> <li class="flex gap-3">
<span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-brand-500/15 text-xs font-semibold text-brand-300">2</span> <span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-brand-500/15 text-xs font-semibold text-brand-300">2</span>
<p> <p>
The first time Aunty opens her link, the Gate quietly remembers a few details: The first time Sam opens it, the Gate quietly notes a few details about the visit:
her phone or browser, the general area she connected from, and the network shape. the phone or browser Sam is on, roughly where Sam is in the world, and the kind of
That becomes her "I'm me" signature. network Sam is connected to. Together those become Sam's "this is really me" picture.
</p> </p>
</li> </li>
<li class="flex gap-3"> <li class="flex gap-3">
<span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-brand-500/15 text-xs font-semibold text-brand-300">3</span> <span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-brand-500/15 text-xs font-semibold text-brand-300">3</span>
<p> <p>
Every time someone clicks Aunty's link after that — including Aunty herself — Every later click on Sam's link, including Sam coming back to update their reply,
the Gate compares them to her signature. Same phone, similar location? gets compared to that first visit. If it looks like the same person, they go straight
They sail through. No signature yet? They become Aunty's signature on the first visit. through with no friction at all.
</p> </p>
</li> </li>
<li class="flex gap-3"> <li class="flex gap-3">
<span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-brand-500/15 text-xs font-semibold text-brand-300">4</span> <span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-brand-500/15 text-xs font-semibold text-brand-300">4</span>
<p> <p>
If someone with a totally different phone on a totally different network tries to If a completely different device on a completely different network tries to use Sam's
use Aunty's link — like a friend Aunty forwarded it to the Gate notices. Depending link, say because Sam forwarded it to a friend, the Gate notices. Depending on how
on your strictness setting it'll either flag them for your review, or refuse them strict you've set it, the Gate will either flag that click for you to review, or stop
outright. the RSVP from going through at all.
</p> </p>
</li> </li>
</ol> </ol>
<p class="rounded-md border border-zinc-800 bg-zinc-900/60 p-3 text-xs text-zinc-400"> <p class="rounded-md border border-zinc-800 bg-zinc-900/60 p-3 text-xs text-zinc-400">
<strong class="text-zinc-300">Worth knowing:</strong> guests can switch from mobile data <strong class="text-zinc-300">Worth knowing:</strong> guests can switch between Wi-Fi
to Wi-Fi or change rooms without being flagged the Gate only cares about and mobile data, change rooms, or open their link a few days later without being
<em>meaningful</em> differences from the original visit, not normal day-to-day variation. flagged. The Gate only cares about <em>meaningful</em> differences from that first
If a real guest does ever get flagged (it happens), you can clear them with one click visit, not normal day-to-day variation. And if a real guest of yours ever does end up
in <em>Recent reviews</em> below. flagged, you can clear them with one click under <em>Recent reviews</em> below.
</p> </p>
</div> </div>
</details> </details>
@@ -393,7 +396,11 @@ function verdictLabel(v: string) {
You're running with custom strictness settings (set under Advanced). You're running with custom strictness settings (set under Advanced).
</p> </p>
<!-- Advanced sliders for power users who want to dial individual bands. --> <!-- Advanced three sliders that map directly to the engine's
band thresholds. Heavy intro copy is intentional: most
hosts who open this are curious rather than expert, and a
short plain-language explanation here keeps them from
feeling lost. -->
<details class="group mt-4 rounded-lg border border-zinc-800 bg-zinc-950"> <details class="group mt-4 rounded-lg border border-zinc-800 bg-zinc-950">
<summary class="flex cursor-pointer items-center justify-between p-3 text-sm text-zinc-300"> <summary class="flex cursor-pointer items-center justify-between p-3 text-sm text-zinc-300">
<span>Advanced strictness controls</span> <span>Advanced strictness controls</span>
@@ -402,40 +409,70 @@ function verdictLabel(v: string) {
</svg> </svg>
</summary> </summary>
<div class="space-y-4 border-t border-zinc-900 p-4"> <div class="space-y-4 border-t border-zinc-900 p-4">
<p class="text-xs text-zinc-500"> <div class="space-y-2 text-xs text-zinc-400">
These directly drive the band thresholds (0100). The presets above just write <p class="text-zinc-300">
sensible triples for you. For most hosts, the four presets above are all you need. These sliders are
here if you want to fine-tune exactly when each reaction kicks in.
</p> </p>
<p>
As a click on a guest's link looks more and more different from their first
visit, the Gate moves through three reactions: watch silently, flag for your
review, or refuse the RSVP. The numbers below (from 0 to 100) are how much
difference is enough to trigger each one. Lower numbers make the Gate more
sensitive; higher numbers make it more forgiving.
</p>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-3"> <div class="grid grid-cols-1 gap-4 md:grid-cols-3">
<label class="block text-sm"> <label class="block text-sm">
<span class="mb-1 flex items-center justify-between text-xs text-zinc-400"> <span class="mb-1 flex items-center justify-between text-xs text-zinc-400">
<span>Watch from</span> <span class="flex items-center gap-1.5">
<span class="inline-block h-2 w-2 rounded-full bg-brand-500"></span>
Watch from
</span>
<span class="font-mono tabular-nums text-zinc-300">{{ thresholds.medium }}</span> <span class="font-mono tabular-nums text-zinc-300">{{ thresholds.medium }}</span>
</span> </span>
<input v-model.number="thresholds.medium" type="range" min="0" max="100" <input v-model.number="thresholds.medium" type="range" min="0" max="100"
:disabled="!canEdit" class="w-full accent-brand-500" :disabled="!canEdit" class="w-full accent-brand-500"
@change="saveThresholds()" @change="saveThresholds()"
@input="clampOrder('medium')" /> @input="clampOrder('medium')" />
<p class="mt-1 text-[11px] leading-snug text-zinc-500">
When the Gate starts paying closer attention. Quiet for guests; visible only
to you in activity logs.
</p>
</label> </label>
<label class="block text-sm"> <label class="block text-sm">
<span class="mb-1 flex items-center justify-between text-xs text-zinc-400"> <span class="mb-1 flex items-center justify-between text-xs text-zinc-400">
<span>Flag from</span> <span class="flex items-center gap-1.5">
<span class="inline-block h-2 w-2 rounded-full bg-amber-500"></span>
Flag from
</span>
<span class="font-mono tabular-nums text-zinc-300">{{ thresholds.high }}</span> <span class="font-mono tabular-nums text-zinc-300">{{ thresholds.high }}</span>
</span> </span>
<input v-model.number="thresholds.high" type="range" min="0" max="100" <input v-model.number="thresholds.high" type="range" min="0" max="100"
:disabled="!canEdit" class="w-full accent-amber-500" :disabled="!canEdit" class="w-full accent-amber-500"
@change="saveThresholds()" @change="saveThresholds()"
@input="clampOrder('high')" /> @input="clampOrder('high')" />
<p class="mt-1 text-[11px] leading-snug text-zinc-500">
When the Gate marks the click for your review. The RSVP still goes through;
you decide whether to clear it or flag the guest.
</p>
</label> </label>
<label class="block text-sm"> <label class="block text-sm">
<span class="mb-1 flex items-center justify-between text-xs text-zinc-400"> <span class="mb-1 flex items-center justify-between text-xs text-zinc-400">
<span>Refuse from</span> <span class="flex items-center gap-1.5">
<span class="inline-block h-2 w-2 rounded-full bg-red-500"></span>
Refuse from
</span>
<span class="font-mono tabular-nums text-zinc-300">{{ thresholds.block }}</span> <span class="font-mono tabular-nums text-zinc-300">{{ thresholds.block }}</span>
</span> </span>
<input v-model.number="thresholds.block" type="range" min="0" max="100" <input v-model.number="thresholds.block" type="range" min="0" max="100"
:disabled="!canEdit" class="w-full accent-red-500" :disabled="!canEdit" class="w-full accent-red-500"
@change="saveThresholds()" @change="saveThresholds()"
@input="clampOrder('block')" /> @input="clampOrder('block')" />
<p class="mt-1 text-[11px] leading-snug text-zinc-500">
Where the Gate refuses the RSVP outright. The visitor sees a polite "this
invitation can't be used" and you get a notification.
</p>
</label> </label>
</div> </div>
</div> </div>
@@ -459,15 +496,17 @@ function verdictLabel(v: string) {
<strong>What this is and isn't:</strong> <strong>What this is and isn't:</strong>
</p> </p>
<p class="mb-2"> <p class="mb-2">
Adding a network here tells the Gate "always wave through clicks coming from Adding a network here tells the Gate to always wave through clicks coming from
this Wi-Fi — they're already with me." It's useful when you're hosting at home that Wi-Fi, because the people on it are already with you. It's useful when
or the office and your guests will connect through your network. you're hosting at home or in the office and your guests will be on your network
when they reply.
</p> </p>
<p> <p>
<strong class="text-zinc-300">It doesn't change how anyone else is treated.</strong> <strong class="text-zinc-300">It doesn't change how anyone else is treated.</strong>
Guests connecting from their own homes, mobile data, or anywhere else still get Guests connecting from their own homes, from mobile data, or from anywhere else
the regular check — they're not suspected of anything by default. The Gate works still get the regular check. They aren't suspected of anything just because they
perfectly well with this list empty. aren't on your network. The Gate works perfectly well with this list empty, and
most events stay that way.
</p> </p>
</div> </div>
@@ -547,7 +586,7 @@ function verdictLabel(v: string) {
</li> </li>
</ul> </ul>
<p v-else class="text-sm text-zinc-500"> <p v-else class="text-sm text-zinc-500">
No trusted networks and that's fine. The Gate is doing its job. You haven't added any trusted networks, and that's fine. The Gate is still doing its job.
</p> </p>
</div> </div>
@@ -577,7 +616,7 @@ function verdictLabel(v: string) {
and {{ feedback.length - 10 }} more. and {{ feedback.length - 10 }} more.
</li> </li>
</ul> </ul>
<p v-else class="text-sm text-zinc-500">No decisions to review yet — the gate hasn't flagged anyone.</p> <p v-else class="text-sm text-zinc-500">Nothing to review yet. The Gate hasn't flagged anyone.</p>
</div> </div>
</div> </div>