design(round 1): accessibility + layout pass (design-only)

Addresses the baseline design review's failing objective gates and layout
defects; no backend or flow changes.

Accessibility (was the ceiling — 3 of 4 objective gates failed):
- Contrast to AA: lifted --text-faint (#6e6e6e→#8a8a8a) and --text-dim;
  darkened --accent-dim (#7c5cd6→#6a48e0) so white button labels reach
  4.5:1; new --accent-bright (#c9b3ff) for accent text on the tinted
  active background (tree/project active rows, admin badge, invite/ghost
  buttons all lifted off sub-AA pairings). Same fix applied to the
  server-rendered auth pages, which now share the app's token values.
- Keyboard + focus: file tree, project, and org-name rows are now
  focusable (tabindex/role + Enter/Space) with a global :focus-visible
  ring; restored input focus rings on app and auth pages.
- Touch targets: every header + sidebar control is a 44x44 hit area on
  mobile; secondary file actions (History/Upload/Download) collapse under
  a "⋯ More" menu so the row still fits with zero horizontal overflow.

Layout defects:
- #meta no longer wraps to 5 lines / shoves the action buttons — single
  truncating line; #crumb truncates too.
- Long tree filenames ellipsize (label span flex:1 min-width:0) instead
  of hard-clipping.

Consistency:
- Fixed the .markdown specificity leak: admin/history/onboarding views no
  longer inherit markdown type rules (content class toggles per view), so
  the admin type scale renders as declared.
- Recolored the off-system gold Admin badge to the accent family; added a
  3-step radius token scale; swapped the mojibake-ish ▣/⛛ markers.
- Added a global [hidden] guard so explicit display rules can't override
  the hidden attribute.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
This commit is contained in:
Snow Lee
2026-07-08 23:36:41 -07:00
co-authored by Claude Fable 5
parent b29454d073
commit 442232bab2
5 changed files with 198 additions and 48 deletions
+57
View File
@@ -0,0 +1,57 @@
---
name: beardrive-designer
description: Product designer who reviews the BearDrive web app's visual design, UX, and layout. Drives the real UI with Playwright (headless Chromium via Bash + node) across desktop and mobile viewports, inspects computed styles, and reports prioritized design findings with concrete CSS/markup fixes and per-category scores. Use for a design/UX critique of the running web app.
tools: Bash, Read, Write, Glob, Grep
model: opus
---
You are a senior product designer doing a rigorous design review of the
BearDrive web application (a dark-themed, dependency-free file viewer + team
admin console, styled after Obsidian). You care about craft: visual
hierarchy, consistency, spacing rhythm, typography, color, motion, and — above
all — whether the interface communicates clearly and feels considered. You are
opinionated but specific: every critique names the element and proposes a fix.
## What you're reviewing
The frontend is hand-written vanilla JS/CSS in
`internal/webapp/static/` (`index.html`, `app.js`, `style.css`) plus
server-rendered auth pages (`internal/webapp/authlocal.go`, the `authPage`
helper) and the public share page shell (`internal/webapp/shares.go`,
`sharedMarkdownShell`). Repo root: /Users/snow/workspace/runbear/sfs.
## How to review
Drive the REAL UI with Playwright headless. A ready-to-use install is at the
path given in your task prompt (require("playwright") from that directory);
Chromium is already downloaded. Write small node scripts, run them with Bash,
and **capture screenshots of every distinct screen and state** into your
working directory (given in the prompt) with descriptive names.
- Review at **desktop (1280×800)** and **mobile (390×844)** at minimum; spot-check a narrow tablet width for reflow.
- Cover every surface: sign-in / sign-up / verify / awaiting-approval pages, the empty/onboarding state, the file tree + markdown reading view, file history, the ⌘K command palette, the org-admin panel, the hub "Signup & access" settings, the share dialog/modal, and a public `/s/` share page.
- Exercise **states**, not just happy paths: hover, keyboard focus rings, active/selected, disabled, empty lists, error toasts, long file names / deep trees (overflow), and a very long document.
- Use the browser to read **computed styles** (font sizes, line-height, colors, contrast ratios, spacing, touch-target sizes) — back up claims with numbers, don't just eyeball. Read `style.css` for ground truth on the design system (CSS variables, scale).
## What to evaluate
1. **Visual design** — typographic scale & rhythm, color usage and restraint, contrast (call out WCAG AA failures with ratios), spacing consistency (is there a coherent spacing scale?), border/radius/shadow consistency, iconography coherence (the UI mixes emoji and glyphs — assess), dark-theme execution.
2. **UX** — clarity of hierarchy and affordances, discoverability, feedback on actions, error/empty states, cognitive load, progressive disclosure, consistency of interaction patterns (e.g. native prompt()/confirm() vs in-app modals), copy/microcopy quality.
3. **Layout** — alignment and grid discipline, whitespace and density, responsive behavior and breakpoints, overflow handling (horizontal scroll is a defect), viewport fit, the sidebar/topbar/content composition.
4. **Accessibility** — color contrast, visible focus states, keyboard operability, touch-target sizes (≥44px on mobile), semantic markup / ARIA, labels on icon-only controls.
## Reporting
Return (as your FINAL message — it goes to the orchestrator, not the user) a
structured report:
- **Verdict** — 2-3 sentences: overall design maturity and the single most
impactful thing to change.
- **Category scores (15)** — Visual, UX, Layout, Accessibility, each with a one-line justification.
- **Findings** — numbered, ordered by impact. Each: severity
(critical / high / medium / polish), the exact element/screen, what's wrong
and why it matters, and a concrete fix (name the CSS property, value,
selector, or markup change). Cite screenshot filenames.
- **What's working** — briefly credit the strong design decisions, so the
signal isn't only negative.
Rules: be concrete and back visual claims with measured values where you can.
Distinguish taste from defects — label subjective calls as such. Never edit
product code; you review only. If a Playwright script fails, debug it up to
twice, then fall back to reading the CSS/markup directly and note the fallback.
+17 -13
View File
@@ -543,21 +543,25 @@ func authPage(w http.ResponseWriter, title, body string) {
fmt.Fprintf(w, `<!doctype html><html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"><title>%s — BearDrive</title>
<style>
body{font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:#1e1e24;color:#ddd;
/* Shares the app's token values so sign-in and the app read as one product. */
body{font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:#1e1e1e;color:#dadada;
display:flex;justify-content:center;padding-top:12vh;margin:0}
.card{background:#26262e;border:1px solid #3a3a44;border-radius:10px;padding:28px 32px;width:340px}
.card{background:#262626;border:1px solid #363636;border-radius:12px;padding:28px 32px;width:340px}
h1{font-size:17px;margin:0 0 16px}
label{display:block;font-size:12.5px;color:#aaa;margin:12px 0 4px}
input{width:100%%;box-sizing:border-box;padding:8px 10px;border-radius:6px;border:1px solid #3a3a44;
background:#1e1e24;color:#ddd;font:inherit}
button{margin-top:18px;width:100%%;padding:9px;border:none;border-radius:6px;background:#7c5cff;
color:#fff;font:inherit;cursor:pointer}
button:hover{background:#8f73ff}
.err{color:#ff7b72;font-size:13px;margin:10px 0 0}
.msg{color:#7ee787;font-size:13px;margin:10px 0 0}
.alt{margin-top:16px;font-size:12.5px;color:#888}
.alt a{color:#8f73ff;text-decoration:none}
code{background:#1e1e24;padding:2px 6px;border-radius:4px}
label{display:block;font-size:12.5px;color:#a6a6a6;margin:12px 0 4px}
input{width:100%%;box-sizing:border-box;padding:9px 11px;border-radius:6px;border:1px solid #363636;
background:#1e1e1e;color:#dadada;font:inherit}
input:focus-visible{outline:2px solid #a882ff;outline-offset:1px;border-color:#a882ff}
button{margin-top:18px;width:100%%;padding:10px;border:none;border-radius:6px;background:#6a48e0;
color:#fff;font:inherit;font-weight:600;cursor:pointer}
button:hover{background:#7c5cd6}
button:focus-visible{outline:2px solid #c9b3ff;outline-offset:2px}
.err{color:#ff9b91;font-size:13px;margin:10px 0 0}
.msg{color:#8ce59a;font-size:13px;margin:10px 0 0}
.alt{margin-top:16px;font-size:12.5px;color:#8a8a8a}
.alt a{color:#c9b3ff;text-decoration:none}
.alt a:hover{text-decoration:underline}
code{background:#1e1e1e;padding:2px 6px;border-radius:4px}
</style></head><body><div class="card"><h1>%s</h1>%s</div></body></html>`,
html.EscapeString(title), html.EscapeString(title), body)
}
+52 -4
View File
@@ -113,8 +113,11 @@ async function loadProjects() {
const row = document.createElement("div");
row.className = "row" + (currentProject && currentProject.id === p.id ? " active" : "");
row.textContent = p.name;
row.title = p.id;
row.title = p.name;
row.tabIndex = 0;
row.setAttribute("role", "button");
row.onclick = () => selectProject(p, null);
row.onkeydown = (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); row.click(); } };
ul.appendChild(li).appendChild(row);
}
nav.appendChild(ul);
@@ -129,6 +132,7 @@ function selectProject(p, path) {
$("crumb").textContent = "";
$("meta").textContent = "";
$("download").hidden = true;
$("content").className = "view";
$("content").innerHTML = `<div class="empty">Select a file to read it.<br><span class="empty-hint">On a phone, tap ☰ to browse.</span></div>`;
loadProjects(); // refresh active highlight
updateOrgBar();
@@ -165,6 +169,7 @@ async function acceptInviteFromHash() {
and — since any member can — offer to start a new project. */
function showEmptyState() {
$("orgbar").hidden = true;
$("content").className = "view";
const auth = serverConfig.auth && serverConfig.auth.enabled;
$("content").innerHTML = `
<div class="onboard">
@@ -243,6 +248,7 @@ function updateOrgBar() {
nm.textContent = org.name;
nm.title = "Manage organization";
nm.onclick = () => showOrgAdmin(org);
nm.onkeydown = (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); nm.click(); } };
const btn = $("invite-btn");
btn.hidden = org.role !== "owner";
btn.textContent = "Manage";
@@ -257,9 +263,10 @@ async function showOrgAdmin(org) {
closeSidebarOnMobile();
$("crumb").textContent = org.name;
$("meta").textContent = "";
$("share-btn").hidden = $("history-btn").hidden = $("download").hidden = true;
$("share-btn").hidden = $("history-btn").hidden = $("download").hidden = $("more-btn").hidden = true;
const owner = org.role === "owner";
const box = $("content");
box.className = "view";
box.innerHTML = `<div class="admin"><h1 id="org-title"></h1></div>`;
box.querySelector("#org-title").textContent = org.name + (owner ? "" : " · member");
const panel = box.querySelector(".admin");
@@ -455,8 +462,9 @@ async function showHubSettings() {
try { pol = await getJSON("api/admin/policy"); } catch (e) { toast(e.message, true); return; }
currentPath = null; markActive(); closeSidebarOnMobile();
$("crumb").textContent = "Signup & access";
$("share-btn").hidden = $("history-btn").hidden = $("download").hidden = true;
$("share-btn").hidden = $("history-btn").hidden = $("download").hidden = $("more-btn").hidden = true;
const box = $("content");
box.className = "view";
box.innerHTML = `<div class="admin"><h1>Signup &amp; access</h1>
<p class="admin-sub">Who can create an account on this hub, and how new accounts are vetted.</p></div>`;
const panel = box.querySelector(".admin");
@@ -523,8 +531,9 @@ async function showPending() {
try { pending = (await getJSON("api/admin/pending")).pending || []; } catch { }
currentPath = null; markActive();
$("crumb").textContent = "Pending signups";
$("share-btn").hidden = $("history-btn").hidden = $("download").hidden = true;
$("share-btn").hidden = $("history-btn").hidden = $("download").hidden = $("more-btn").hidden = true;
const box = $("content");
box.className = "view";
box.innerHTML = `<div class="admin"><h1>Pending signups</h1></div>`;
const panel = box.querySelector(".admin");
const list = el(panel, "div", "admin-list");
@@ -590,8 +599,14 @@ function renderNode(n) {
chev.className = "chev";
chev.textContent = "▾"; // ▾
const label = document.createElement("span");
label.className = "label";
label.textContent = n.name;
row.append(chev, label);
// Keyboard-operable: the row behaves as a button.
row.tabIndex = 0;
row.setAttribute("role", "button");
row.title = n.name;
row.onkeydown = (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); row.click(); } };
li.appendChild(row);
if (n.dir) {
if (serverConfig.mode === "hub") {
@@ -633,7 +648,9 @@ async function openFile(p) {
const dl = $("download");
dl.href = apiBase + "download?path=" + encodeURIComponent(p);
dl.hidden = false;
$("more-btn").hidden = false;
const content = $("content");
content.className = "markdown"; // document view: markdown type rules apply
content.innerHTML = "";
$("meta").textContent = "";
try {
@@ -803,6 +820,8 @@ async function showHistory(q) {
$("crumb").textContent = "History — " + title;
$("meta").textContent = "";
$("download").hidden = true;
content.className = "view";
$("more-btn").hidden = true;
content.innerHTML = "";
const wrap = document.createElement("div");
wrap.className = "history";
@@ -1137,6 +1156,35 @@ $("palette-overlay").addEventListener("click", (e) => {
/* Visible search affordance in the top bar → opens the palette. */
$("search-btn").addEventListener("click", paletteOpen);
/* Mobile "⋯ More": the secondary file actions (History, Upload, Download)
collapse behind one 44px button so every target stays tappable without
the header overflowing. The menu proxies to the real buttons, so their
behavior and visibility rules are the single source of truth. */
function buildMoreMenu() {
const menu = $("more-menu");
menu.innerHTML = "";
const items = [
["History", $("history-btn")],
["Upload", $("upload-btn")],
["Download", $("download")],
].filter(([, el]) => el && !el.hidden);
for (const [label, el] of items) {
const b = document.createElement("button");
b.className = "more-item";
b.textContent = label;
b.onclick = () => { $("more-menu").hidden = true; el.click(); };
menu.appendChild(b);
}
return items.length;
}
$("more-btn").addEventListener("click", (e) => {
e.stopPropagation();
const menu = $("more-menu");
if (menu.hidden) { buildMoreMenu(); menu.hidden = false; }
else menu.hidden = true;
});
document.addEventListener("click", () => { $("more-menu").hidden = true; });
/* Mobile: the sidebar is off-canvas; a hamburger toggles it. */
function toggleSidebar() { document.body.classList.toggle("sb-open"); }
function closeSidebarOnMobile() { document.body.classList.remove("sb-open"); }
+3 -1
View File
@@ -21,7 +21,7 @@
<nav id="projects" aria-label="Projects" hidden></nav>
<nav id="tree" aria-label="Files"></nav>
<footer id="orgbar" hidden>
<span id="org-name" title="Manage organization"></span>
<span id="org-name" title="Manage organization" role="button" tabindex="0"></span>
<button id="invite-btn" hidden title="Manage this organization">Manage</button>
</footer>
</aside>
@@ -36,6 +36,8 @@
<button id="upload-btn" class="btn" hidden>Upload</button>
<input id="upload-input" type="file" hidden>
<a id="download" class="btn" hidden download>Download</a>
<button id="more-btn" class="btn icon-only" hidden title="More actions" aria-label="More actions"></button>
<div id="more-menu" hidden role="menu"></div>
</header>
<article id="content" class="markdown">
<div class="empty">Select a file to read it.</div>
+69 -30
View File
@@ -6,15 +6,23 @@
--bg-active: #3f3550;
--border: #363636;
--text: #dadada;
--text-dim: #9a9a9a;
--text-faint: #6e6e6e;
--text-dim: #a6a6a6; /* AA on --bg and --bg-side */
--text-faint: #8a8a8a; /* raised from #6e6e6e → ~4.7:1 on --bg */
--accent: #a882ff;
--accent-dim: #7c5cd6;
--accent-bright: #c9b3ff; /* accent text on the tinted --bg-active (AA) */
--accent-dim: #6a48e0; /* darker so white labels reach 4.5:1 */
--code-bg: #2a2a2a;
/* radius scale: controls / cards / overlays */
--r-ctl: 6px;
--r-card: 8px;
--r-over: 12px;
}
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }
* { box-sizing: border-box; }
[hidden] { display: none !important; } /* explicit display rules must not override the hidden attribute */
html, body { height: 100%; margin: 0; }
#more-btn { display: none; } /* mobile-only; shown in the responsive block */
body {
display: flex;
background: var(--bg);
@@ -66,9 +74,10 @@ body {
overflow: hidden;
text-overflow: ellipsis;
}
#projects .row::before { content: "▣ "; color: var(--text-faint); }
#projects .row::before { content: "◇"; margin-right: 7px; color: var(--text-faint); }
#projects .row:hover { background: var(--bg-hover); color: var(--text); }
#projects .row.active { background: var(--bg-active); color: var(--accent); }
#projects .row.active { background: var(--bg-active); color: var(--accent-bright); box-shadow: inset 2px 0 0 var(--accent); }
#projects .row.active::before { color: var(--accent); }
#tree { flex: 1; overflow-y: auto; padding: 8px 6px 24px; font-size: 13.5px; }
#orgbar {
display: flex;
@@ -82,14 +91,14 @@ body {
}
#orgbar[hidden] { display: none; }
#org-name { cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#org-name::before { content: "⛛ "; color: var(--text-faint); }
#org-name::before { content: "◈"; margin-right: 7px; color: var(--text-faint); }
#org-name:hover { color: var(--text); }
#invite-btn {
border: none; cursor: pointer; font: inherit; font-size: 11.5px;
padding: 2px 10px; border-radius: 5px;
background: var(--bg-active); color: var(--text-dim);
padding: 3px 10px; border-radius: var(--r-ctl);
background: var(--bg-active); color: var(--text);
}
#invite-btn:hover { color: var(--text); background: var(--bg-hover); }
#invite-btn:hover { color: #fff; background: var(--bg-hover); }
#tree ul { list-style: none; margin: 0; padding-left: 14px; }
#tree > ul { padding-left: 4px; }
#tree li > .row {
@@ -105,7 +114,8 @@ body {
text-overflow: ellipsis;
}
#tree li > .row:hover { background: var(--bg-hover); color: var(--text); }
#tree li > .row.active { background: var(--bg-active); color: var(--accent); }
#tree li > .row.active { background: var(--bg-active); color: var(--accent-bright); box-shadow: inset 2px 0 0 var(--accent); }
#tree li > .row .label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
#tree .chev { width: 12px; flex: none; font-size: 10px; color: var(--text-faint); transition: transform .12s; }
#tree li.collapsed > ul { display: none; }
#tree li.collapsed > .row .chev { transform: rotate(-90deg); }
@@ -114,6 +124,7 @@ body {
/* ---- main pane ---- */
#main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
#topbar {
position: relative;
display: flex;
align-items: center;
gap: 12px;
@@ -121,21 +132,24 @@ body {
border-bottom: 1px solid var(--border);
min-height: 46px;
}
#crumb { font-size: 13px; color: var(--text-dim); }
#meta { flex: 1; font-size: 12px; color: var(--text-faint); text-align: right; }
#crumb { font-size: 13px; color: var(--text); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Single truncating line never let author/time metadata wrap and shove
the action buttons around. */
#meta { flex: 1; min-width: 0; font-size: 12px; color: var(--text-faint); text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.btn {
flex: none;
padding: 4px 12px;
border-radius: 6px;
padding: 5px 12px;
border-radius: var(--r-ctl);
background: var(--accent-dim);
color: #fff;
font-size: 12.5px;
text-decoration: none;
border: none; cursor: pointer; font-family: inherit;
}
.btn:hover { background: var(--accent); }
#content { flex: 1; overflow-y: auto; padding: 28px 48px 80px; }
#history-btn, #share-btn { border: none; cursor: pointer; font: inherit; font-size: 12.5px; background: var(--bg-active); color: var(--text-dim); }
#history-btn:hover, #share-btn:hover { color: var(--text); background: var(--bg-hover); }
#history-btn, #share-btn { border: none; cursor: pointer; font: inherit; font-size: 12.5px; background: var(--bg-active); color: var(--text); }
#history-btn:hover, #share-btn:hover { color: #fff; background: var(--bg-hover); }
.dir-history { margin-left: auto; padding: 0 4px; visibility: hidden; color: var(--text-faint); font-size: 11px; }
#tree li > .row:hover .dir-history { visibility: visible; }
.dir-history:hover { color: var(--text); }
@@ -223,8 +237,8 @@ body {
/* ---- vault header actions ---- */
.vault-actions { display: flex; align-items: center; gap: 8px; }
.adminbar { border: none; cursor: pointer; font: inherit; font-size: 11.5px; padding: 2px 8px; border-radius: 5px; background: #4a3a12; color: #f5c451; }
.adminbar:hover { background: #5c4816; }
.adminbar { border: none; cursor: pointer; font: inherit; font-size: 11.5px; padding: 3px 9px; border-radius: var(--r-ctl); background: var(--bg-active); color: var(--accent-bright); }
.adminbar:hover { background: var(--bg-hover); color: #fff; }
.icon-btn2 { border: none; background: transparent; color: var(--text-faint); font-size: 15px; cursor: pointer; padding: 0 2px; line-height: 1; }
.icon-btn2:hover { color: var(--text); }
@@ -233,8 +247,8 @@ body {
.icon-btn:hover { color: var(--text); background: var(--bg-hover); }
#search-btn { display: inline-flex; align-items: center; gap: 6px; }
#search-btn kbd { font: 11px var(--mono, ui-monospace, Menlo, monospace); background: var(--bg); border: 1px solid var(--border); border-radius: 4px; padding: 0 5px; color: var(--text-faint); }
.btn.ghost { background: var(--bg-active); color: var(--text-dim); }
.btn.ghost:hover { background: var(--bg-hover); color: var(--text); }
.btn.ghost { background: var(--bg-active); color: var(--text); }
.btn.ghost:hover { background: var(--bg-hover); color: #fff; }
/* ---- onboarding empty state ---- */
.onboard { max-width: 560px; margin: 8vh auto 0; }
@@ -296,25 +310,50 @@ body {
#sidebar { position: fixed; z-index: 60; top: 0; left: 0; height: 100%; transform: translateX(-100%); transition: transform .2s ease; box-shadow: 0 0 40px rgba(0,0,0,.5); }
body.sb-open #sidebar { transform: translateX(0); }
body.sb-open #sb-backdrop { display: block; position: fixed; inset: 0; background: rgba(0,0,0,.5); z-index: 50; }
.icon-btn { display: inline-block; }
.icon-btn { display: inline-flex; }
#content { padding: 20px 18px 60px; }
#topbar { padding: 8px 14px; gap: 8px; }
#topbar { padding: 6px 10px; gap: 4px; }
#crumb { font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#search-btn kbd { display: none; }
/* On a phone the header shrinks every control to an icon so all the
per-file actions stay reachable (the R2 mistake was hiding them). */
#topbar .btn { font-size: 0; gap: 0; padding: 5px 8px; }
#search-btn::before { content: "🔍"; font-size: 15px; }
#share-btn::before { content: "↗"; font-size: 15px; }
#history-btn::before { content: "🕘"; font-size: 14px; }
#upload-btn::before { content: "⬆"; font-size: 14px; }
#download::before { content: "⬇"; font-size: 14px; }
/* Every header control is a 44×44 hit area (WCAG target size). Only the
primary actions (menu, search, share) stay in the bar; History, Upload
and Download collapse under "⋯ More" so the row never overflows. */
#topbar .btn, #topbar .icon-btn {
min-width: 44px; min-height: 44px; padding: 0;
display: inline-flex; align-items: center; justify-content: center;
font-size: 0; gap: 0;
}
#menu-btn { font-size: 20px; }
#search-btn::before { content: "🔍"; font-size: 16px; }
#share-btn::before { content: "↗"; font-size: 17px; }
#more-btn:not([hidden]) { display: inline-flex; font-size: 20px; }
#history-btn, #upload-btn, #download { display: none !important; }
#meta { display: none; }
/* Sidebar header controls also meet the 44px target on touch. */
.icon-btn2, #signout, .adminbar { min-width: 44px; min-height: 44px; display: inline-flex; align-items: center; justify-content: center; }
#vault { padding: 8px 12px; }
.markdown, .admin, .onboard, .history { max-width: 100%; }
.markdown table, pre.plain { display: block; overflow-x: auto; max-width: 100%; }
.ob-row { flex-direction: column; }
}
/* ---- "More" overflow menu (mobile) ---- */
#more-btn.icon-only { font-size: 15px; }
#more-menu {
position: absolute; right: 10px; top: 100%; z-index: 80;
background: var(--bg-side); border: 1px solid var(--border);
border-radius: var(--r-card); box-shadow: 0 12px 32px rgba(0,0,0,.5);
padding: 6px; min-width: 160px;
}
#more-menu[hidden] { display: none; }
.more-item {
display: block; width: 100%; text-align: left;
min-height: 44px; padding: 0 14px;
background: transparent; border: none; cursor: pointer;
color: var(--text); font: inherit; font-size: 14px; border-radius: var(--r-ctl);
}
.more-item:hover { background: var(--bg-hover); }
/* ---- markdown ---- */
.markdown { max-width: 820px; }
.markdown h1, .markdown h2, .markdown h3, .markdown h4 {