diff --git a/CHANGELOG/pages/proto-home-v5.md b/CHANGELOG/pages/proto-home-v5.md
index 7e49337e..8456bbc1 100644
--- a/CHANGELOG/pages/proto-home-v5.md
+++ b/CHANGELOG/pages/proto-home-v5.md
@@ -2,6 +2,39 @@
Append-only lane for the ScratchNode live-event prototype and production static surface.
+## 2026-06-03 — Mobile visual reset: type scale + accent/mono discipline, cut first-viewport chrome
+The mobile room read as a prototype because two systems were missing, not any single bad
+component. **Root cause:** (1) `:root` had color/radius/motion tokens but **no type scale** —
+every component hardcoded its size, so nothing ranked; (2) `--accent`/`--mono` had no discipline
+(accent sprayed on logo/code/CTA/links; mono on human prose, not just machine IDs); (3) the first
+viewport re-explained the room 4x and leaked host/debug labels. Plus a flex-gap bug split the
+"ScratchNode" wordmark into "Scratch Node".
+
+Presentational + copy only — send/render path, dedup, and `data-sn-live` untouched:
+- **Type scale** in `:root` (`--fs-display/title/base/sub/label/mono`); one `--fs-display` per
+ screen; hero -> 22px; empty-state title is the one 18px display element. **Mono reserved for
+ machine IDs only** (room code + `/ask`) — de-mono'd the strip, "LIVE ROOM" divider, menu headers.
+- **Wordmark** wrapped in `.h-logo-word` so the `flex; gap` stops splitting it -> renders "ScratchNode".
+- **Room code** is a quiet muted mono chip (was a heavy accent button); menu is a borderless icon;
+ **accent reserved for the one primary action** (send).
+- **Event strip:** removed "0 FAQ"; gated event-mode + L0 capture (host/debug controls) to
+ `data-role="host"`; de-mono'd to `--ui`.
+- **De-duped:** dropped the hero joined-count (lives once, in the strip); "Disposable event brain"
+ -> "Live event log - public wiki when it ends". Welcome banner quieted + hidden on mobile.
+- **Composer:** placeholder -> "Message or /ask..." (fixes the clipped placeholder); helpline
+ 2 lines -> 1; privacy shows "Public" / "Private" text instead of an ambiguous open-lock glyph.
+- **Empty state:** removed the giant "Ask the first question" accent CTA (the composer IS the CTA);
+ composer-first copy teaches message / `/ask` / private note.
+- **Keyboard-open fix:** `visualViewport` `--keyboard-offset` pins the fixed composer above the
+ keyboard; footer + welcome collapse while typing (`data-input-focused`) -> no footer behind keyboard.
+- **Menu:** "Continue in NodeBench" gated to named users; "Keyboard shortcuts" hidden on mobile.
+
+Verified: 51/51 chromium e2e (`scratchnode-live-route-honesty` incl. `home-v5-output-contract` +
+`scratchnode-public-wiki`); static launch scan PASS; before/after + keyboard + menu screenshots at 390px.
+Cherry-picked clean onto `origin/main` (only the hero region rebased: kept main's `
` tag + my copy/scale).
+
+**Commit**: `this commit`. **Author**: Homen Shum + Claude.
+
## 2026-06-03 — Host public-write hardening: FAQ-promote + wiki-publish require a verified host (scratchnode/002)
`snPromoteFaq` and `snPublishWiki` were the last two host-only PUBLIC-write actions still
reading the host key via the weak `_snReadHostOwnerKey()` (which falls back through
diff --git a/public/proto/home-v5.html b/public/proto/home-v5.html
index c93fc025..4834c153 100644
--- a/public/proto/home-v5.html
+++ b/public/proto/home-v5.html
@@ -54,6 +54,18 @@
--purple: #a78bfa;
--ui: "Manrope", system-ui, -apple-system, sans-serif;
--mono: "JetBrains Mono", "SF Mono", Menlo, monospace;
+ /* ─── Type scale (one ladder, so the eye can rank) ───
+ Discipline: ONE --fs-display per screen. --mono is reserved for machine
+ identifiers ONLY (room code + the /ask token), never human-readable prose.
+ --accent (solid terracotta) marks the SINGLE primary action on a screen;
+ everything else uses --accent-ghost or a neutral border. */
+ --fs-display: 22px; /* the one large element: empty-state title / hero */
+ --fs-title: 15px; /* event title, menu row, answer heads */
+ --fs-base: 14px; /* chat + body */
+ --fs-sub: 12px; /* metadata, helper, status */
+ --fs-label: 11px; /* tracked caps section headers */
+ --fs-mono: 11px; /* room code + /ask token only */
+ --accent-ghost: rgba(217,119,87,.10);
--r: 12px;
--r-sm: 8px;
--motion-fast: 120ms;
@@ -114,22 +126,25 @@
.h-logo { display: flex; align-items: center; gap: 6px; font-weight: 700; font-size: 14px; }
.h-logo-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 10px rgba(217,119,87,.7); animation: logoBreathe 3.4s ease-in-out infinite; }
@keyframes logoBreathe { 0%,100% { transform: scale(1); box-shadow: 0 0 10px rgba(217,119,87,.62); } 50% { transform: scale(1.28); box-shadow: 0 0 18px rgba(217,119,87,.92); } }
-.h-logo span { color: var(--accent); }
+.h-logo-word { display: inline-flex; } /* keeps "ScratchNode" as one word — the .h-logo gap must not split it */
+.h-logo-word > span { color: var(--accent); }
.h-live { display: inline-flex; align-items: center; gap: 6px; padding: 3px 9px; border-radius: 100px; background: rgba(94,168,103,.1); border: 1px solid rgba(94,168,103,.25); font-family: var(--mono); font-size: 10px; font-weight: 700; color: var(--green); letter-spacing: .1em; }
.h-live::before { content: ''; width: 5px; height: 5px; border-radius: 50%; background: var(--green); box-shadow: 0 0 6px rgba(94,168,103,.7); animation: livePulse 2s infinite; }
@keyframes livePulse { 0%,100% { opacity: 1; transform: scale(1); } 50% { opacity: .42; transform: scale(.75); } }
@media (prefers-reduced-motion: reduce) { body::before, .h-logo-dot, .h-live::before { animation: none; } }
.h-spacer { flex: 1; }
+/* Room code = quiet machine-id chip (mono is allowed here — it IS an identifier).
+ NOT accent: accent is reserved for the one primary action on screen. */
.h-code {
- min-height: 36px; padding: 6px 14px; border-radius: var(--r-sm);
- background: transparent; border: 1px solid var(--line);
- color: var(--accent); font-family: var(--mono); font-size: 12px; font-weight: 700;
- letter-spacing: .18em; cursor: pointer; transition: border-color .12s;
+ min-height: 30px; padding: 4px 11px; border-radius: 100px;
+ background: rgba(255,255,255,.04); border: 1px solid var(--line);
+ color: var(--ink-muted); font-family: var(--mono); font-size: var(--fs-mono); font-weight: 600;
+ letter-spacing: .14em; cursor: pointer; transition: border-color .12s, color .12s;
}
-.h-code:hover { border-color: var(--accent); }
-.h-menu { width: 44px; height: 44px; display: inline-flex; align-items: center; justify-content: center; border-radius: var(--r-sm); border: 1px solid var(--line); background: transparent; color: var(--ink-muted); }
-.h-menu:hover { color: var(--ink); border-color: var(--ink-faint); }
-@media (max-width: 540px) { .h-menu { width: 44px; height: 44px; } .h-code { min-height: 44px; } }
+.h-code:hover { border-color: var(--ink-faint); color: var(--ink); }
+.h-menu { width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center; border-radius: var(--r-sm); border: 0; background: transparent; color: var(--ink-faint); }
+.h-menu:hover { color: var(--ink); background: rgba(255,255,255,.05); }
+@media (max-width: 540px) { .h-menu { width: 44px; height: 44px; } .h-code { min-height: 36px; } }
/* ─── Event identity strip (persistent after scroll) ─── */
.event-strip {
@@ -139,26 +154,35 @@
background: linear-gradient(180deg, rgba(21,20,19,.86), rgba(21,20,19,.5));
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
border-bottom: 1px solid var(--line);
- font-family: var(--mono); font-size: 10px; color: var(--ink-muted);
- letter-spacing: .04em;
+ font-family: var(--ui); font-size: var(--fs-sub); color: var(--ink-muted);
+ letter-spacing: 0;
overflow-x: auto; white-space: nowrap;
scrollbar-width: none;
}
.event-strip::-webkit-scrollbar { display: none; }
-.event-strip b { color: var(--ink); font-weight: 700; font-family: var(--ui); font-size: 11px; letter-spacing: 0; }
+.event-strip b { color: var(--ink); font-weight: 700; font-family: var(--ui); font-size: var(--fs-sub); letter-spacing: 0; }
.event-strip .dot { color: var(--ink-faint); }
+/* Host/debug controls (event mode, capture level, FAQ count) are NOT for public
+ attendees — they leaked into the public strip. Gate them to the host role.
+ Elements stay in the DOM (JS reads ev-faq-count / ev-cap-label / ev-mode-label). */
+.event-strip .ev-mode,
+.event-strip .ev-cap,
+.event-strip .ev-host-only { display: none; }
+body[data-role="host"] .event-strip .ev-mode,
+body[data-role="host"] .event-strip .ev-cap { display: inline-flex; }
+body[data-role="host"] .event-strip .ev-host-only { display: inline; }
.event-strip .ev-link {
margin-left: auto;
- color: var(--accent); border: 1px solid rgba(217,119,87,.3);
- padding: 2px 8px; border-radius: 100px;
+ color: var(--ink-muted); border: 1px solid var(--line);
+ padding: 3px 10px; border-radius: 100px;
text-decoration: none; cursor: pointer;
flex-shrink: 0;
}
-.event-strip .ev-link:hover { background: rgba(217,119,87,.08); }
+.event-strip .ev-link:hover { color: var(--ink); border-color: var(--ink-faint); }
@media (max-width: 540px) {
- .event-strip { padding: 5px 14px; font-size: 9px; gap: 8px; }
- .event-strip b { font-size: 10px; }
- .event-strip .ev-link { font-size: 9px; }
+ .event-strip { padding: 6px 14px; font-size: var(--fs-sub); gap: 8px; }
+ .event-strip b { font-size: var(--fs-sub); }
+ .event-strip .ev-link { font-size: var(--fs-label); }
}
/* ─── Main (single column) ─── */
@@ -174,9 +198,9 @@
@media (prefers-reduced-motion: reduce) { main.m { animation: none; } }
/* ─── Hero (small, scannable) ─── */
-.hero { margin-bottom: 20px; }
-.hero h1 { font-size: 26px; font-weight: 800; letter-spacing: -.02em; margin: 0 0 4px; }
-.hero-meta { font-size: 13px; color: var(--ink-muted); }
+.hero { margin-bottom: 18px; }
+.hero :is(h1, h2) { font-size: var(--fs-display); font-weight: 800; letter-spacing: -.02em; margin: 0 0 4px; }
+.hero-meta { font-size: var(--fs-sub); color: var(--ink-muted); }
.hero-meta b { color: var(--ink); font-weight: 600; }
/* ─── Composer (gravitational center) ─── */
@@ -242,6 +266,7 @@
border: 1px solid var(--line);
}
.c-helpline .pill.ask { color: var(--accent); border-color: rgba(217,119,87,.3); background: rgba(217,119,87,.06); }
+.c-helpline .privacy-state { margin-left: auto; font-size: var(--fs-label); font-weight: 600; color: var(--ink-muted); }
body[data-mode="private"] .c-helpline .privacy-state { color: var(--purple); }
/* Role-gated host-only actions on agent cards */
@@ -272,7 +297,7 @@
/* ─── Feed ─── */
.feed { display: flex; flex-direction: column; gap: 4px; }
-.feed-divider { display: flex; align-items: center; gap: 10px; margin: 8px 0 4px; font-family: var(--mono); font-size: 10px; color: var(--ink-faint); letter-spacing: .12em; text-transform: uppercase; }
+.feed-divider { display: flex; align-items: center; gap: 10px; margin: 8px 0 4px; font-family: var(--ui); font-size: var(--fs-label); font-weight: 600; color: var(--ink-faint); letter-spacing: .12em; text-transform: uppercase; }
.feed-divider::after { content: ''; flex: 1; height: 1px; background: var(--line); }
/* Chat row (minimal, dense) */
@@ -621,7 +646,7 @@
.menu-sheet button { min-height: 44px; }
.menu-sheet[data-open="true"] { transform: translateY(0); }
.menu-sheet-handle { width: 32px; height: 3px; border-radius: 2px; background: var(--line); margin: 0 auto 14px; }
-.menu-sheet h4 { margin: 0 0 8px; font-size: 11px; font-weight: 700; color: var(--ink-faint); text-transform: uppercase; letter-spacing: .12em; font-family: var(--mono); }
+.menu-sheet h4 { margin: 0 0 8px; font-size: var(--fs-label); font-weight: 700; color: var(--ink-faint); text-transform: uppercase; letter-spacing: .12em; font-family: var(--ui); }
.menu-sheet button {
display: block; width: 100%; text-align: left;
padding: 10px 12px; margin: 2px 0;
@@ -644,18 +669,23 @@
.menu-sheet h4[data-show-for]:not([data-show-for="all"]) { display: none; }
body[data-role="host"] .menu-sheet h4[data-show-for="host"] { display: block; }
body[data-named="true"] .menu-sheet h4[data-show-for="named"] { display: block; }
+/* Keyboard shortcuts is a desktop affordance — hide it from the mobile sheet. */
+@media (max-width: 540px) { .menu-sheet .menu-desktop-only { display: none; } }
.menu-scrim { position: fixed; inset: 0; z-index: 105; background: rgba(0,0,0,.5); display: none; }
.menu-scrim[data-open="true"] { display: block; }
/* ─── First-visit welcome banner ─── */
.welcome {
- display: none; align-items: center; gap: 10px; padding: 10px 14px;
+ display: none; align-items: center; gap: 10px; padding: 9px 12px;
margin: 0 0 12px;
- background: linear-gradient(135deg, rgba(217,119,87,.1), rgba(217,119,87,.02));
- border: 1px solid rgba(217,119,87,.25); border-radius: var(--r-sm);
- font-size: 12px; color: var(--ink-muted);
+ background: rgba(255,255,255,.03);
+ border: 1px solid var(--line); border-radius: var(--r-sm);
+ font-size: var(--fs-sub); color: var(--ink-muted);
}
body[data-new-user="true"] .welcome { display: flex; }
+/* Mobile: the empty-state copy + composer already carry the how-to. The banner
+ is redundant chrome on a small first viewport — hide it. */
+@media (max-width: 540px) { body[data-new-user="true"] .welcome { display: none; } }
.welcome-emoji { font-size: 16px; }
.welcome-text { flex: 1; line-height: 1.4; }
.welcome-text strong { color: var(--ink); }
@@ -683,7 +713,7 @@
.id-avatar { width: 24px; height: 24px; border-radius: 50%; background: linear-gradient(135deg, var(--accent), #b85f44); color: #fff; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 700; flex-shrink: 0; }
.id-text { flex: 1; }
.id-text b { color: var(--ink); font-weight: 600; }
-.id-set { background: transparent; border: 0; color: var(--accent); font-size: 11px; cursor: pointer; padding: 4px 6px; border-radius: 4px; font-family: var(--mono); }
+.id-set { background: transparent; border: 0; color: var(--accent); font-size: var(--fs-sub); cursor: pointer; padding: 4px 6px; border-radius: 4px; font-family: var(--ui); }
.id-set:hover { background: rgba(217,119,87,.08); }
/* ─── Empty state (when feed has zero rows) ─── */
@@ -695,8 +725,9 @@
body[data-feed-empty="true"] #feed > .row,
body[data-feed-empty="true"] #feed > .ans { display: none; }
.empty-icon { font-size: 28px; opacity: .6; margin-bottom: 4px; }
-.empty-title { font-size: 14px; color: var(--ink); font-weight: 600; }
-.empty-body { font-size: 12px; color: var(--ink-muted); max-width: 280px; line-height: 1.5; } /* AA contrast — instructional empty-state copy must be readable */
+.empty-title { font-size: 18px; color: var(--ink); font-weight: 700; letter-spacing: -.01em; } /* the one display element when the feed is empty */
+.empty-body { font-size: var(--fs-sub); color: var(--ink-muted); max-width: 280px; line-height: 1.5; } /* AA contrast — instructional empty-state copy must be readable */
+.empty-body b { color: var(--accent); font-family: var(--mono); font-weight: 600; } /* /ask token */
/* ─── Attention pulses for first-visit ─── */
body[data-new-user="true"] #lock,
@@ -736,13 +767,12 @@
/* ─── "What is this" inline link ─── */
.about-link {
- display: inline-flex; align-items: center; gap: 4px;
- margin-left: 6px; padding: 2px 7px; border-radius: 100px;
- background: rgba(255,255,255,.04); border: 1px solid var(--line);
- color: var(--ink-faint); font-size: 10px; font-family: var(--mono); letter-spacing: .04em;
- text-transform: uppercase; cursor: pointer; transition: all .12s;
+ display: inline; margin-left: 7px; padding: 0;
+ background: none; border: 0;
+ color: var(--ink-faint); font-size: var(--fs-sub); font-family: var(--ui); letter-spacing: 0;
+ text-transform: none; cursor: pointer; transition: color .12s;
}
-.about-link:hover { color: var(--ink); border-color: var(--ink-faint); }
+.about-link:hover { color: var(--ink); text-decoration: underline; }
/* ─── UNIVERSAL FEATURE SHEET (slides up — name, about, share, notes, wiki, people, signin, host, shortcuts) ─── */
.sheet-scrim { position: fixed; inset: 0; z-index: 115; background: rgba(0,0,0,.5); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); display: none; }
@@ -988,9 +1018,7 @@
.about-card h4 { margin: 0 0 4px; font-size: 13px; color: var(--accent); font-weight: 700; }
.about-card p { margin: 0; font-size: 12px; color: var(--ink-muted); line-height: 1.55; }
-/* Empty state CTA */
-.empty-cta { margin-top: 14px; padding: 10px 18px; min-height: 44px; background: var(--accent); color: #fff; border: 0; border-radius: var(--r-sm); font-family: var(--ui); font-size: 13px; font-weight: 600; cursor: pointer; }
-.empty-cta:hover { filter: brightness(1.08); }
+/* Empty-state CTA removed — the composer IS the call to action (no duplicate button). */
/* Footer (tiny) */
.f { padding: 24px 20px calc(32px + var(--safe-bot)); text-align: center; font-size: 11px; color: var(--ink-faint); }
@@ -1005,13 +1033,21 @@
/* Mobile: pin the composer to the bottom (Slack/Discord/iMessage convention — thumb reach,
newest message sits right above where you type). Desktop keeps the sticky top command bar. */
.c {
- position: fixed; top: auto; bottom: 0; left: 0; right: 0; z-index: 45;
+ position: fixed; top: auto; left: 0; right: 0; z-index: 45;
+ /* Pin above the on-screen keyboard — --keyboard-offset is set from
+ visualViewport when the input is focused (see keyboard-aware script). */
+ bottom: var(--keyboard-offset, 0px);
margin: 0;
padding: 8px calc(14px + var(--safe-right)) calc(8px + var(--safe-bot)) calc(14px + var(--safe-left));
border-top: 1px solid var(--line);
background: var(--bg);
box-shadow: 0 -8px 24px -12px rgba(0,0,0,.55);
+ transition: bottom .18s var(--ease-out);
}
+ @media (prefers-reduced-motion: reduce) { .c { transition: none; } }
+ /* While typing: collapse non-essential chrome so input + feed stay visible above the keyboard. */
+ body[data-input-focused="true"] .f,
+ body[data-input-focused="true"] .welcome { display: none; }
.row { grid-template-columns: 32px 1fr; column-gap: 8px; padding: 5px 4px 6px; }
.row-avatar { width: 32px; height: 32px; font-size: 12px; }
.row-time { font-size: 9px; }
@@ -1022,7 +1058,7 @@
.c-mode { padding: 0; width: 26px; min-width: 26px; height: 26px; justify-content: center; gap: 0; }
.c-mode #c-mode-label { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
.c-mode .dot { width: 7px; height: 7px; }
- .feed-divider { font-size: 9px; }
+ .feed-divider { font-size: var(--fs-label); }
}
/* Landscape (short height) — hide hero, compact composer */
@@ -3269,7 +3305,7 @@ Your wiki is live.
Skip to chat