The old 2x2 grid for 3+ members produced a cramped, misaligned look
(circles cut off inside their own border, no overlap, just four small
disks in a grid). The 2-member overlap aesthetic — equal-size tiles
that overlap diagonally — was the visual gold standard but only
existed for that one count.
Extend the same "huddle of overlapping faces" aesthetic to all
member counts:
• 3 members → equilateral triangle of 62%-size tiles (top, bl, br)
• 4 members → diamond of 58%-size tiles (top, right, bottom, left)
• 5-10 members → diamond with `+N` overflow occupying the bottom
slot (z-index boosted above neighbors so the digit is never clipped)
Tiles are positioned radially around the box center on a circle of
radius (S − T) / 2 so the farthest edges of each tile graze the
bounding rect — no clipping, no wasted whitespace. Z-index descends
clockwise from the top slot so each tile tucks slightly under its
clockwise neighbor, mirroring the 2-member z-stack.
The 0/1/2-member cases and the icon-override branch are unchanged.
Props interface unchanged — no call-site updates needed.
Tests updated to assert the new layout markers (`triangle`, `diamond`)
and to verify the geometry (top tile in triangle has smallest `top`;
overflow tile in diamond has largest `top`). 11/11 AvatarStack tests
pass; 360/360 web tests pass; typecheck clean.
design-system.md spec updated with the new layout table + geometry
section.
Multi-pass mobile chat polish landed across this session.
- MessageInput is now a floating glass-bubble (`position: absolute`) on both desktop and mobile — last messages scroll *behind* the translucent bubble. MobileChatScreen wraps MessageList + MessageInput in a `relative` parent so absolute positioning resolves. Removed the prior in-flow mobile branch that clipped message-list bottom against an invisible barrier.
- iOS PWA keyboard handling: new `useVisualViewportInset` hook subscribes to `visualViewport.resize/scroll` AND polls `vv.height` for ~600ms after focusin (iOS PWA standalone often fails to dispatch resize for keyboard transitions). MobileShell sizes its container to `vv.height` when keyboard is open — `bottom: 0` on the composer naturally lands flush with the keyboard top, regardless of how reliably resize events fire. Composer uses 6px gap above home indicator (keyboard closed) and 0px gap above keyboard (keyboard open). Added `interactive-widget=resizes-content` viewport meta as the cleaner native equivalent for Chrome/Android.
- MessageList bottom padding is dynamic via `--composer-clearance` CSS variable. MessageInput writes `composerHeight + bottomOffset + 12px` to its parent via ResizeObserver — re-fires on textarea autosize, reply banner, attachment tile growth, parent resize. Last message always has 12px breathing room above the bubble regardless of composer state.
- AttachmentRenderer generic file chip: `max-w-full sm:max-w-[400px]` on outer + `min-w-0` + `flex-shrink-0` on icon + `flex-1` on text + `flex-wrap` on badge row. Long filenames now ellipsize cleanly on narrow viewports instead of pushing the chip off-screen.
- New `useDragToClose` hook: shared bottom-sheet drag-down-to-dismiss gesture. Spread on handle/header only (body scrolling unaffected). 6px deadzone, 100px or 0.5px/ms velocity threshold, 200ms `cubic-bezier(0.22, 1, 0.36, 1)` close-out animation, rAF-staged transform for a stable from-value. `hasInteracted` latch prevents the open keyframe from re-firing mid-close (the bounce-up-then-vanish bug). Wired into InputPopover (emoji/GIF), MobileVoiceJoinSheet, MobileFolderSheet.
Specs: docs/systems/mobile-ui.md (Floating Composer + Drag-to-Close sections), docs/systems/message-list.md (--composer-clearance), docs/systems/design-system.md (glass-bubble row references).
iOS Safari auto-zooms (and shifts the viewport right) on focus when the
computed font-size is <16px. Single @media (max-width: 767px) block in
globals.css covers all four input tiers, every relevant bare <input>
type, <textarea>, <select>, and contenteditable surfaces. `!important`
is required because Tailwind utilities (text-[15px] on the chat composer,
text-sm on form fields) emit after @layer components and would otherwise
override the input-tier rules.
Replaces ad-hoc per-input `text-base md:text-sm` overrides with a single
global truth. New inputs/contenteditables get the fix for free.
Spec: docs/systems/design-system.md gains an "iOS Auto-Zoom Suppression"
section under Input Tiers.
requestFullscreen() on the voice container puts only its descendants in the
browser's top layer; overlays portaled to document.body were rendered outside
that layer and stayed invisible — most visibly the right-click context menu on
stream tiles and voice user panels.
Add usePortalContainer() hook returning document.fullscreenElement ?? document.body
and re-rendering on fullscreenchange. Migrate every overlay reachable during a
call: ContextMenuRenderer (desktop, submenu, mobile sheet), Tooltip,
ConfirmDialog, ConnectionInfoPopover, ScreenShareSettingsPopover, and
ScreenSharePicker (which previously rendered inline at App root).