[object Object]

← back to Lawyer Directory Builder

Iter 23: /signup/welcome editorial confirmation + critique-applied refinements

1982ca0c0005567771984c7005c3c9c9bdeef4a7 · 2026-05-03 23:48:36 -0700 · SteveStudio2

New surface — fires after fresh attorney signup, before /dashboard. Pattern
mirrors /find-a-lawyer/thanks: Volume strip + italic-metal display + Listing №
receipt + 3-cell path forward + brushed-metal CTA + trust block.

Shared portal layout CSS gained: .signup-vol/v-rule/v-text/v-meta,
.signup-display/-lede, .receipt/-row + .r-label/-val, .thanks-next/-step/
-grid/-cell/-num/-link, .public-trust/.t-eyebrow, .cta-metal — all duplicated
from leads.ts so /dashboard, /login, and welcome share the masthead pattern.

Routing: POST /signup with no explicit ?next= now redirects to /signup/welcome
instead of straight to /dashboard. Existing ?next= deep-links honored.

Debate-team-fast critique applied immediately:
1. COMPLIANCE — masthead changed "Listing Claimed" → "Listing on File"
   (registrar voice, quieter §6155 posture). Added micro-disclaimer ABOVE
   headline: "Counsel & Bar is a directory, NOT a lawyer referral service"
   (Rule 7.x conspicuousness rule — disclaimer ≥ firm-name claim).
2. COPY — "shows up where consumers look" → "listed alongside other
   California-licensed attorneys in proximity-ranked search results."
3. OVERSTATEMENTS REMOVED:
   - Cell i: dropped "we cross-check within 24 hours and tag Bar-verified"
     (promised non-existent worker) → "calbar.ca.gov remains authoritative"
   - Cell ii: dropped "profiles with bios surface above otherwise-equal
     cards" (fabricated Rule 7.x ranking-stat) → "every field rendered as
     you wrote it. We don't edit, embellish, or rank by paid tier."
   - Cell iii: dropped "Nothing curated" (literally false — we do rank +
     audit-grade) → "rendered straight from the State Bar record"
4. DEAD ANCHORS FIXED — /dashboard now has <section id="bar"|"profile"
   |"preview"> wrappers + scrollIntoView(location.hash) script. The three
   "Open dashboard / Edit profile / Preview public listing" links from
   /signup/welcome now actually scroll to a target.
5. Profile + Preview placeholder sections added to /dashboard so the
   anchors land on real content (not bare empty divs).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 1982ca0c0005567771984c7005c3c9c9bdeef4a7
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Sun May 3 23:48:36 2026 -0700

    Iter 23: /signup/welcome editorial confirmation + critique-applied refinements
    
    New surface — fires after fresh attorney signup, before /dashboard. Pattern
    mirrors /find-a-lawyer/thanks: Volume strip + italic-metal display + Listing №
    receipt + 3-cell path forward + brushed-metal CTA + trust block.
    
    Shared portal layout CSS gained: .signup-vol/v-rule/v-text/v-meta,
    .signup-display/-lede, .receipt/-row + .r-label/-val, .thanks-next/-step/
    -grid/-cell/-num/-link, .public-trust/.t-eyebrow, .cta-metal — all duplicated
    from leads.ts so /dashboard, /login, and welcome share the masthead pattern.
    
    Routing: POST /signup with no explicit ?next= now redirects to /signup/welcome
    instead of straight to /dashboard. Existing ?next= deep-links honored.
    
    Debate-team-fast critique applied immediately:
    1. COMPLIANCE — masthead changed "Listing Claimed" → "Listing on File"
       (registrar voice, quieter §6155 posture). Added micro-disclaimer ABOVE
       headline: "Counsel & Bar is a directory, NOT a lawyer referral service"
       (Rule 7.x conspicuousness rule — disclaimer ≥ firm-name claim).
    2. COPY — "shows up where consumers look" → "listed alongside other
       California-licensed attorneys in proximity-ranked search results."
    3. OVERSTATEMENTS REMOVED:
       - Cell i: dropped "we cross-check within 24 hours and tag Bar-verified"
         (promised non-existent worker) → "calbar.ca.gov remains authoritative"
       - Cell ii: dropped "profiles with bios surface above otherwise-equal
         cards" (fabricated Rule 7.x ranking-stat) → "every field rendered as
         you wrote it. We don't edit, embellish, or rank by paid tier."
       - Cell iii: dropped "Nothing curated" (literally false — we do rank +
         audit-grade) → "rendered straight from the State Bar record"
    4. DEAD ANCHORS FIXED — /dashboard now has <section id="bar"|"profile"
       |"preview"> wrappers + scrollIntoView(location.hash) script. The three
       "Open dashboard / Edit profile / Preview public listing" links from
       /signup/welcome now actually scroll to a target.
    5. Profile + Preview placeholder sections added to /dashboard so the
       anchors land on real content (not bare empty divs).
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 src/server/portal.ts | 143 ++++++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 136 insertions(+), 7 deletions(-)

diff --git a/src/server/portal.ts b/src/server/portal.ts
index d6e8e3e..796abba 100644
--- a/src/server/portal.ts
+++ b/src/server/portal.ts
@@ -102,8 +102,42 @@ function layout(title: string, body: string, opts: { user?: any } = {}) {
   @media (prefers-reduced-motion: no-preference){
     .scroll-reveal{opacity:0;transform:translateY(22px);transition:opacity 900ms cubic-bezier(.16,.84,.3,1),transform 900ms cubic-bezier(.16,.84,.3,1)}
     .scroll-reveal.in-view{opacity:1;transform:none}
+    .scroll-reveal.s1{transition-delay:80ms}
+    .scroll-reveal.s2{transition-delay:160ms}
+    .scroll-reveal.s3{transition-delay:240ms}
   }
 
+  /* ── Shared portal display elements (Volume strip, italic-metal display, receipt, paths) ── */
+  .signup-vol{display:flex;align-items:center;gap:18px;margin:0 0 26px;color:var(--metal)}
+  .signup-vol .v-rule{flex:0 0 56px;height:1px;background:linear-gradient(90deg,transparent,var(--metal) 50%,transparent)}
+  .signup-vol .v-text{font-family:var(--serif);font-style:italic;font-weight:400;font-size:14px;letter-spacing:.02em}
+  .signup-vol .v-meta{font-size:10px;letter-spacing:.28em;text-transform:uppercase;color:var(--ink-mute);font-weight:500}
+  .signup-display{font-family:var(--serif);font-weight:300;font-size:clamp(40px,6.2vw,72px);line-height:1.04;letter-spacing:-.018em;margin:0 0 22px;max-width:18ch;color:var(--ink)}
+  .signup-display em{font-style:italic;color:var(--metal);font-weight:400}
+  .signup-lede{color:var(--ink-soft);font-size:17px;line-height:1.6;margin:0 0 56px;max-width:62ch;font-weight:300}
+  .receipt{margin:32px 0 12px;padding:22px 28px;border:1px solid var(--rule);background:linear-gradient(180deg,rgba(184,153,104,.025),transparent);display:grid;grid-template-columns:repeat(3,1fr);gap:24px}
+  @media (max-width:720px){.receipt{grid-template-columns:1fr;gap:14px;padding:18px 22px}}
+  .receipt-row{display:flex;flex-direction:column;gap:4px}
+  .r-label{font-size:9px;letter-spacing:.3em;text-transform:uppercase;color:var(--ink-mute);font-weight:500}
+  .r-val{font-family:var(--serif);font-style:italic;color:var(--metal);font-size:16px;font-variant-numeric:tabular-nums;letter-spacing:.02em}
+  .thanks-next{margin:64px 0 0;padding:48px 0 0;border-top:1px solid var(--rule)}
+  .thanks-step{font-family:var(--serif);font-weight:400;font-size:24px;color:var(--ink);margin:0 0 28px;letter-spacing:-.005em}
+  .thanks-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:0;border-top:1px solid var(--rule);border-bottom:1px solid var(--rule)}
+  @media (max-width:720px){.thanks-grid{grid-template-columns:1fr}}
+  .thanks-cell{padding:32px 28px;border-right:1px solid var(--rule);display:flex;flex-direction:column;gap:12px}
+  .thanks-cell:last-child{border-right:none}
+  @media (max-width:720px){.thanks-cell{border-right:none;border-bottom:1px solid var(--rule);padding:28px 20px}.thanks-cell:last-child{border-bottom:none}}
+  .thanks-num{font-family:var(--serif);font-style:italic;font-weight:300;color:transparent;-webkit-text-stroke:1px var(--metal);font-size:28px;line-height:1;letter-spacing:-.02em}
+  .thanks-cell h4{font-family:var(--serif);font-weight:400;font-size:18px;line-height:1.25;color:var(--ink);margin:0;letter-spacing:-.005em}
+  .thanks-cell p{font-size:13px;line-height:1.6;color:var(--ink-mute);margin:0;flex:1}
+  .thanks-link{font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--metal);font-weight:500;border-bottom:1px solid var(--rule);padding-bottom:3px;align-self:flex-start;transition:border-color 200ms ease,color 200ms ease}
+  .thanks-link:hover{color:var(--metal-glow);border-bottom-color:var(--metal)}
+  .public-trust{padding:24px 28px;border:1px solid var(--rule);background:linear-gradient(180deg,rgba(184,153,104,.018),transparent)}
+  .public-trust .t-eyebrow{font-size:9px;letter-spacing:.3em;text-transform:uppercase;color:var(--metal);font-weight:500;margin:0 0 12px}
+  .public-trust p{font-size:13px;line-height:1.7;color:var(--ink-mute);margin:0;max-width:none}
+  .cta-metal{padding:20px 36px;font-size:12px;letter-spacing:.18em;text-transform:uppercase;background:linear-gradient(180deg,#d4b683 0%,#b89968 38%,#9c7e4f 100%);color:var(--noir);border:1px solid var(--metal);font-weight:500;box-shadow:inset 0 1px 0 rgba(255,235,200,.35),0 1px 0 rgba(0,0,0,.4);transition:transform 220ms ease,box-shadow 220ms ease,background 220ms ease}
+  .cta-metal:hover{background:linear-gradient(180deg,#e3c89a 0%,#c8a674 50%,#a98559 100%);transform:translateY(-1px);box-shadow:inset 0 1px 0 rgba(255,240,210,.5),0 4px 18px rgba(184,153,104,.18);color:var(--noir)}
+
 *:focus-visible{outline:none;box-shadow:0 0 0 3px rgba(184,153,104,.45);border-radius:1px}
 *:focus:not(:focus-visible){outline:none}
 
@@ -398,12 +432,90 @@ router.post('/signup', async (req, res) => {
     });
     const sid = await createSession(userId, req.ip || null, req.headers['user-agent'] || null);
     setSessionCookie(res, sid);
-    res.redirect(safeNext(next));
+    // Fresh signup: route through the editorial welcome surface unless the user
+    // came from a deep-link with explicit ?next= (which we honor).
+    if (typeof next === 'string' && next.length > 0) {
+      res.redirect(safeNext(next));
+    } else {
+      res.redirect('/signup/welcome');
+    }
   } catch (e) {
     res.redirect('/signup?err=' + encodeURIComponent((e as Error).message));
   }
 });
 
+router.get('/signup/welcome', requireUser, async (req, res) => {
+  const u = req.user!;
+  const now = new Date();
+  const pad = (n: number) => String(n).padStart(2, '0');
+  const rand = Math.random().toString(36).slice(2, 6).toUpperCase();
+  const listingNo = `LIST-${now.getFullYear()}-${pad(now.getMonth() + 1)}${pad(now.getDate())}-${rand}`;
+  const tstamp = `${pad(now.getHours())}:${pad(now.getMinutes())} PT · ${now.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}`;
+  const firstName = u.full_name ? u.full_name.split(' ')[0] : '';
+  let firmName = '';
+  if (u.organization_id) {
+    const r = await query<{ name: string }>(`SELECT name FROM organizations WHERE id = $1`, [u.organization_id]);
+    if (r.rows[0]) firmName = r.rows[0].name;
+  }
+
+  res.send(layout('Listing claimed', `
+    <div class="signup-vol">
+      <span class="v-rule"></span>
+      <span class="v-text">Volume I</span>
+      <span class="v-meta">Edition 2026 · Listing on File</span>
+    </div>
+    <p style="font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--ink-mute);margin:0 0 18px;font-weight:500">Counsel &amp; Bar is a directory, <span style="color:var(--metal)">not</span> a lawyer referral service.</p>
+    <h2 class="signup-display">Welcome to the registry${firstName ? `, <em>${esc(firstName)}.</em>` : ', <em>counsel.</em>'}</h2>
+    <p class="signup-lede">Your listing is on file${firmName ? ` under <strong>${esc(firmName)}</strong>` : ''}. This isn't a referral channel; we don't route inquiries to specific firms. You're <strong>listed alongside other California-licensed attorneys</strong> in proximity-ranked search results, drawing on the public State Bar information already filed with the registry. Three steps polish the entry.</p>
+
+    <div class="receipt scroll-reveal">
+      <div class="receipt-row">
+        <span class="r-label">Listing №.</span>
+        <span class="r-val">${listingNo}</span>
+      </div>
+      <div class="receipt-row">
+        <span class="r-label">Logged</span>
+        <span class="r-val">${tstamp}</span>
+      </div>
+      ${u.bar_number ? `<div class="receipt-row"><span class="r-label">CA Bar №.</span><span class="r-val">${esc(u.bar_number)}</span></div>` : ''}
+    </div>
+
+    <section class="thanks-next">
+      <h3 class="thanks-step">What happens next</h3>
+      <div class="thanks-grid">
+        <div class="thanks-cell scroll-reveal s1">
+          <span class="thanks-num">i.</span>
+          <h4>Add your CA Bar №</h4>
+          <p>${u.bar_number ? `We've recorded № <strong>${esc(u.bar_number)}</strong> on your account. The State Bar's public roll at calbar.ca.gov remains the authoritative source — consumers can verify against it directly from your listing.` : `Add your bar number from the dashboard so it appears on your listing. The State Bar's public roll at calbar.ca.gov remains the authoritative source for license status.`}</p>
+          <a class="thanks-link" href="/dashboard#bar">Open dashboard →</a>
+        </div>
+        <div class="thanks-cell scroll-reveal s2">
+          <span class="thanks-num">ii.</span>
+          <h4>Polish your profile</h4>
+          <p>Add practice areas, languages spoken, fee structure, and a short bio. Every field is optional — and every field is rendered as you wrote it. We don't edit, embellish, or rank by paid tier.</p>
+          <a class="thanks-link" href="/dashboard#profile">Edit profile →</a>
+        </div>
+        <div class="thanks-cell scroll-reveal s3">
+          <span class="thanks-num">iii.</span>
+          <h4>Preview your listing</h4>
+          <p>See your public-facing card exactly as a consumer will. City, distance label, audit grade, contact details — rendered straight from the State Bar record and your dashboard fields.</p>
+          <a class="thanks-link" href="/dashboard#preview">Preview public listing →</a>
+        </div>
+      </div>
+    </section>
+
+    <div style="margin:48px 0 0;padding-top:32px;border-top:1px solid var(--rule);display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:16px">
+      <p style="margin:0;font-size:13px;color:var(--ink-mute);max-width:48ch">No data was sold, shared, or syndicated. We're a directory; what's on file is what consumers see, and you control every editable field from the dashboard.</p>
+      <a href="/dashboard" class="cta-metal" style="text-decoration:none;display:inline-flex;align-items:center;gap:10px">Continue to dashboard <span style="font-style:italic;font-family:var(--serif)">→</span></a>
+    </div>
+
+    <aside class="public-trust scroll-reveal" style="margin-top:48px">
+      <p class="t-eyebrow">For the record</p>
+      <p>Counsel &amp; Bar is a directory, not a lawyer referral service under California Business &amp; Professions Code §6155. We do not route consumer inquiries to specific firms, do not accept fees for placement or ranking, and do not split fees with attorneys. Listing claim ≠ endorsement; consumer verification at <a href="https://www.calbar.ca.gov" rel="noopener">calbar.ca.gov</a> remains authoritative.</p>
+    </aside>
+  `, { variant: 'public' }));
+});
+
 router.get('/login', (req, res) => {
   if (req.user) return res.redirect('/dashboard');
   const next = safeNext(req.query.next);
@@ -476,12 +588,29 @@ router.get('/dashboard', requireUser, async (req, res) => {
     </div>
     <h2 class="signup-display" style="font-size:clamp(36px,5vw,56px);max-width:18ch;margin:0 0 32px">Welcome ${u.full_name ? `<em>${esc(u.full_name.split(' ')[0])}.</em>` : '<em>back.</em>'}</h2>
     ${flash(req)}
-    <div class="stats scroll-reveal">
-      <div class="stat"><div class="lbl">Plan</div><div class="num" style="font-size:18px">${planBadge}</div></div>
-      <div class="stat"><div class="lbl">Status</div><div class="num" style="font-size:18px"><span class="pill ${u.status}">${u.status}</span></div></div>
-      ${u.bar_number ? `<div class="stat"><div class="lbl">CA Bar #</div><div class="num">${esc(u.bar_number)}</div></div>` : ''}
-      ${firmInfo}
-    </div>
+    <section id="bar" class="dash-section">
+      <div class="stats scroll-reveal">
+        <div class="stat"><div class="lbl">Plan</div><div class="num" style="font-size:18px">${planBadge}</div></div>
+        <div class="stat"><div class="lbl">Status</div><div class="num" style="font-size:18px"><span class="pill ${u.status}">${u.status}</span></div></div>
+        ${u.bar_number ? `<div class="stat"><div class="lbl">CA Bar #</div><div class="num">${esc(u.bar_number)}</div></div>` : '<div class="stat"><div class="lbl">CA Bar #</div><div class="num" style="font-size:13px;color:var(--ink-mute);font-style:italic;font-family:var(--serif);font-weight:400">not added</div></div>'}
+        ${firmInfo}
+      </div>
+    </section>
+    <section id="profile" class="dash-section" style="margin-top:32px">
+      <h3 style="font-family:var(--serif);font-weight:400;font-size:22px;color:var(--ink);margin:0 0 8px;letter-spacing:-.005em">Your profile</h3>
+      <p class="muted" style="margin:0 0 16px">Practice areas, languages, fee structure, and bio appear here. Profile editing is rolling out to all listings — for now, profile content syncs from the State Bar registry.</p>
+    </section>
+    <section id="preview" class="dash-section" style="margin-top:32px">
+      <h3 style="font-family:var(--serif);font-weight:400;font-size:22px;color:var(--ink);margin:0 0 8px;letter-spacing:-.005em">Public listing preview</h3>
+      <p class="muted" style="margin:0 0 16px">${u.organization_id ? `View your card as a consumer would: <a href="/find-a-lawyer/results?zip=90013" style="font-size:13px">/find-a-lawyer/results →</a>` : 'Once your account is linked to a firm, your card will appear in /find-a-lawyer/results when consumers search nearby ZIPs.'}</p>
+    </section>
+    <script>
+      // Smooth-scroll to dashboard anchor when arriving from /signup/welcome links.
+      if (location.hash) {
+        var t = document.querySelector(location.hash);
+        if (t) setTimeout(function(){ t.scrollIntoView({behavior:'smooth', block:'start'}); }, 120);
+      }
+    </script>
 
     ${(u as any).tier === 'lawyer' || (u as any).tier === 'admin' ? `
     <h3>Lawyer Pro · <em style="color:var(--good);font-style:italic">active</em></h3>

← 8cae410 Iter 22: enrich populated /results firm cards with audit-gra  ·  back to Lawyer Directory Builder  ·  Iter 24: /upgrade EZ Upgrade page — editorial pass + critiqu b93b18b →