Files
cinny/LOTUS_BUGS.md
T
jared 79f8fabb1b
CI / Build & Quality Checks (push) Successful in 10m46s
CI / Trigger Desktop Build (push) Successful in 6s
fix(ui): GIF picker surface tokens + background swatch chrome (N8, N70)
- N8: GifPicker non-TDS container used undefined var(--bg-surface) + raw
  rgba/12px/boxShadow. Switch to folds tokens (color.Surface.Container,
  config.radii.R400, color.Surface.ContainerLine, color.Other.Shadow).
  TDS branch keeps its --lt-* glow chrome.
- N70: ChatBgGrid/SeasonalBgGrid swatch buttons moved chrome (radius, border,
  hover, keyboard :focus-visible ring, selected via data-selected) into shared
  BgSwatch.css.ts using design tokens; only per-swatch size + live preview
  background stay inline (custom preview tiles, not MenuItem/Chip candidates).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 21:01:57 -04:00

172 KiB
Raw Blame History

Lotus Chat — Bug Report & Technical Audit

Date: June 2026

This document tracks identified bugs, edge cases, and architectural discrepancies found during the audit of the Lotus Chat codebase. Recommended fixes are provided for each item.


🚩 Critical & UI Bugs

12. PiP Mute Icon Misidentifies Whose Mic Is Muted

  • File: cinny/src/app/components/CallEmbedProvider.tsx
  • Status: FIXED ⚠️ UNTESTED — needs verification in a live call with at least one other participant who mutes/unmutes
  • Issue: The muted-mic badge in the Picture-in-Picture window used useRemoteAllMuted (fires when ANY remote participant is muted) and rendered in the bottom-left corner — the conventional position for "YOUR" mic status. Users read it as their own mic being muted.
  • Root Cause: PipMuteOverlay was triggering on remote-mute events while displaying in a position that implies local-user status.
  • Fix Applied:
    • Bottom-left badge now shows only when the LOCAL user's mic is muted (checked via !controlState.microphone from useCallControlState). Includes "You" label to make it unambiguous. Uses color.Critical.Main.
    • Top-right badge (new) shows "All muted" in color.Warning.Main when all remote participants are muted — positioned and labeled so it's clearly about other people, not the local user.
    • Both badges use aria-label / title for accessibility.

1. No Camera Focus During Screenshare

  • File: cinny/src/app/features/call/CallControls.tsx
  • Status: OPEN
  • Issue: Automatic screenshare spotlighting forces primary display override, preventing users from manually focusing on camera feeds.
  • Root Cause: Current spotlighting logic prioritizes active screenshare streams over manual participant selections, effectively ignoring or overriding user-initiated focus states.
  • Proposed Fix: Introduce a manual 'Focus' state that takes precedence over automatic screenshare spotlighting, implemented via a toggle/click UI on participant tiles. Update the video renderer to respect this manual override.

2. Chat Background Animation Flickering

  • File: cinny/src/app/features/lotus/chatBackground.ts
  • Status: FIXED ⚠️ UNTESTED — needs verification on a real device with an animated background active
  • Issue: Animated background properties cause visible flickering on message text and the composer area, particularly on browsers/GPUs susceptible to repaint-induced artifacts.
  • Root Cause: Animation triggers excessive repaints or layout recalculations on descendant elements, likely due to animating non-GPU accelerated properties on parent containers without proper rendering context isolation.
  • Fix Applied: getChatBg() now injects willChange: 'background-position' and contain: 'paint' for any animated variant. This promotes the element to its own compositor layer and isolates repaints from descendants. Background-position animation is already GPU-hinted on modern browsers; contain: paint prevents descendant elements from being invalidated during each frame.

3. Avatar Decorations in Element Call

  • File: cinny/src/app/components/avatar-decoration/AvatarDecoration.tsx
  • Status: OPEN
  • Issue: Avatar decorations are failing to render within the call/room interface member lists.
  • Root Cause: Likely a mismatch between the expected member object structure required by the AvatarDecoration component and the data actually provided by the call/room UI components. Matrix event data for decorations might not be propagating correctly to these UI member objects.
  • Proposed Fix: Analyze the data propagation chain from Matrix events to the member object in cinny/src/app/components/call and room, ensuring that decoration-related properties are correctly mapped and passed to the AvatarDecoration component.

4. DM and Group Message Calls

  • File: cinny/src/app/components/CallEmbedProvider.tsx
  • Status: PARTIALLY FIXED ⚠️ UNTESTED — Volume control added. Remaining: ringtone selection, suppression during active calls.
  • Issue: Incoming call ringtone is hardcoded, lacks volume control, and is suppressed if the user is already in an active call.
  • Root Cause: Ringing logic is tightly coupled to RTCNotification events in CallEmbedProvider.tsx, using a hardcoded audio file path. It lacks an abstraction for sound management or user-configurable settings for ringtones/volumes.
  • Fix Applied: Added ringtoneVolume setting (0100, default 70). IncomingCall reads this setting and applies audioElement.volume = ringtoneVolume / 100 before play(). Slider added to Settings → General → Calls section.
  • Remaining: (a) Ringtone selection (still hardcoded to call.ogg); (b) Suppression during active calls — not investigated.

5. Seasonal Themes and Chat Backgrounds Design

  • File: cinny/src/app/hooks/useTheme.ts, cinny/src/app/features/lotus/chatBackground.ts
  • Status: OPEN
  • Issue: Basic CSS or random moving lines are insufficient for high-fidelity wallpaper/theming. They lack professional design theory, coherence, and aesthetic depth.
  • Root Cause: Current implementation relies on basic CSS, lacks advanced design theory, and does not leverage modern, performant CSS wallpaper techniques.
  • Proposed Fix (Extreme Depth Redesign):
    • Research-Backed Implementation: Implement advanced design techniques (layered oklch gradients, backdrop-filter for refractive "liquid glass" effects, GPU-accelerated transform animations) to create living, breathing backgrounds.
    • Performance Optimization: Ensure all animations strictly use compositor-thread properties (transform, opacity) and apply contain: paint / will-change: transform to prevent layout thrashing/flickering.
    • Design Resources (Examples/Inspiration):
    • Goal: Treat each theme/background as a week-long development sprint to ensure professional polish, WCAG AA contrast compliance for overlaying UI, and seamless integration with the Lotus TDS.

6. Exclusive Background vs. Seasonal Choice

  • File: cinny/src/app/state/settings.ts
  • Status: FIXED ⚠️ UNTESTED — needs verification: (a) pick a background, confirm seasonal theme auto-clears; (b) pick a seasonal theme, confirm background auto-clears; (c) set both via old localStorage data and reload, confirm SeasonalEffect guard suppresses the overlay
  • Issue: Concurrent application of both Chat Backgrounds and Seasonal Themes causes visual clutter and high GPU usage.
  • Root Cause: These are currently handled as independent settings in the settingsAtom and applied simultaneously without mutual exclusion.
  • Fix Applied: Mutual exclusion enforced at two layers: (1) General.tsx — ChatBgGrid clears seasonalThemeOverride→'off' when any non-'none' background is picked; SeasonalBgGrid clears chatBackground→'none' when any real seasonal theme is selected. (2) SeasonalEffect.tsx — runtime guard returns null if chatBackground !== 'none', protecting against legacy persisted state.

7. Tiny Touch Targets in Composer Toolbar

  • File: cinny/src/app/features/room/RoomInput.tsx
  • Status: FIXED ⚠️ UNTESTED — needs verification on a real mobile device: open composer, confirm all toolbar buttons are tappable without mis-taps
  • Issue: Toolbar buttons have hit areas smaller than the WCAG-recommended 44x44px for touch, hindering mobile accessibility.
  • Fix Applied: Added touchTarget = { minWidth: '44px', minHeight: '44px' } computed from mobileOrTablet() and applied as style={touchTarget} to all 8 composer toolbar IconButton elements (attach, format, sticker, emoji, GIF, location, poll, schedule, send).

8. Horizontal Overflow in Room Settings

  • File: cinny/src/app/components/page/style.css.ts
  • Status: FIXED ⚠️ UNTESTED — needs verification: open Room Settings on a narrow mobile screen, confirm nav panel fills full width and no horizontal scrollbar appears
  • Issue: Wide tables and input elements in room settings cause horizontal overflow on mobile viewports.
  • Fix Applied: Added @media (max-width: 750px) { width: '100%' } to both '400' and '300' size variants of the PageNav vanilla-extract recipe in style.css.ts.

9. Modal Float-Style Responsiveness

  • File: Multiple modal files
  • Status: FIXED ⚠️ UNTESTED — needs verification by opening each modal on a real mobile device
  • Issue: Modals appear as floating boxes on mobile, creating navigation and readability challenges.
  • Fix Applied: Created useModalStyle(desktopMaxWidth) hook (src/app/hooks/useModalStyle.ts) that returns fullscreen styles on mobile (no border-radius, no max-width, height: 100%) and desktop box styles otherwise. Applied to all 22+ modal files: LeaveRoomPrompt, LeaveSpacePrompt, ReportRoomModal, ReportUserModal, DeviceVerification, InviteUserPrompt, LogoutDialog, DeviceVerificationSetup, DeviceVerificationReset, JoinAddressPrompt, JumpToTime, EditHistoryModal, ForwardMessageDialog, RemindMeDialog, CreateRoomModal, CreateSpaceModal, ScheduleMessageModal, PollCreator, AddExistingModal, RoomEncryption, RoomUpgrade, Modal500, ReadReceiptAvatars, RoomTopicViewer.
  • Note: UIAFlowOverlay already fullscreen via <Overlay> — no change needed. JoinRulesSwitcher/RoomNotificationSwitcher are dropdowns, not modals.

10. Composer Keyboard Obscurity

  • File: src/index.css
  • Status: FIXED ⚠️ UNTESTED — needs verification on iOS Safari specifically (the worst offender); on Android Chrome 100dvh has been standard since Chrome 108
  • Issue: The chat composer is often partially or fully obscured by the virtual keyboard on mobile.
  • Fix Applied: Added height: 100dvh (dynamic viewport height) to html alongside the existing height: 100% fallback. dvh updates when the software keyboard appears, ensuring the layout shrinks correctly and the composer stays visible.

11. Inline Jotai atom creation

  • File: cinny/src/app/hooks/useSpaceHierarchy.ts
  • Status: FALSE POSITIVE — CLOSED
  • Issue: Inline Jotai atom creation in a hook risks re-rendering components unnecessarily.
  • Resolution: useState(() => atom(...)) IS the correct Jotai pattern for local stable atom references. The factory function form of useState ensures the atom is created only once per component mount. No change warranted.

📦 Barrel File Audit

File Path Note Status
cinny/src/app/plugins/call/index.ts Extensive export * usage OPEN
cinny/src/app/plugins/text-area/index.ts Extensive export * usage OPEN
cinny/src/app/components/message/index.ts Extensive export * usage OPEN

🔍 Technical & Performance Refinements

Category Issue Description File Path Status
State Sync Fire-and-forget network call to set offline presence during pagehide event may not complete reliably, potentially causing UI drift in presence status. cinny/src/app/hooks/usePresenceUpdater.ts OPEN
State Sync Fire-and-forget network call setPresence().catch(...) suppresses errors, meaning the app may falsely assume presence update success. cinny/src/app/hooks/usePresenceUpdater.ts OPEN
Memory Leak Decrypted Media Memory Leak (Gallery & Lightbox) due to missing virtualization and blob revocation. cinny/src/app/features/room/MediaGallery.tsx PARTIALLY FIXED ⚠️ UNTESTED — Blob revocation was already correct; added enabled param to useDecryptedMediaUrl and useNearViewport(300px) to each GalleryTile to gate decryption until near-viewport, reducing burst on pagination. True virtualization (windowing) deferred — requires significant refactor.
Data Persistence Scheduled Messages are ephemeral (lost on refresh) due to fragile localStorage parsing. cinny/src/app/state/scheduledMessages.ts FIXED — now uses atomWithStorage + createJSONStorage (Jotai's built-in persistence with error-safe JSON parsing)
Memory Leak Potential memory leak due to uncleaned handleMouseMove listener in usePan. cinny/src/app/hooks/usePan.ts FALSE POSITIVE — usePan already uses attachedRef to track listeners and cleans them up in an unmount useEffect. No change needed.
Asset Optimization Large unoptimized media asset (213KB) found in public/res. public/res/Lotus.png OPEN
Data Persistence Non-atomic localStorage updates in session management can lead to inconsistent state. cinny/src/app/state/sessions.ts OPEN
Data Persistence Lack of cross-tab synchronization for localStorage updates in session management risks race conditions. cinny/src/app/state/sessions.ts OPEN
Network Resilience uploadContent lacks retry logic, failing immediately upon network error. cinny/src/app/utils/matrix.ts OPEN
Network Resilience rateLimitedActions uses basic retry logic without exponential backoff, which may exacerbate 429 issues. cinny/src/app/utils/matrix.ts FIXED — fallback delay now uses capped exponential backoff (min(1000 * 2^retryCount, 30_000)ms) when server doesn't send Retry-After; server header still takes precedence via getRetryAfterMs().
Matrix Event Robustness useMatrixEventRenderer handles unknown events gracefully by returning null, which may hide potentially important unrendered data. cinny/src/app/hooks/useMatrixEventRenderer.ts OPEN
Data Contract MatrixError instantiation with UploadResponse might be brittle. cinny/src/app/utils/matrix.ts OPEN
Type Safety addRoomIdToMDirect uses as any cast for AccountDataEvent.Direct, bypassing type contract validation. cinny/src/app/utils/matrix.ts OPEN
Robustness rateLimitedActions relies on MatrixError.httpStatus which might not exist on all error variants. cinny/src/app/utils/matrix.ts FALSE POSITIVE — MatrixError.httpStatus is defined as readonly httpStatus?: number in matrix-js-sdk/lib/http-api/errors.d.ts. It is optional (not on all instances) but the ?. optional chain already guards against undefined. No change needed.
Type Contract Custom types in cinny/src/types/matrix mirror SDK types instead of using them, risking drift and contract mismatches. cinny/src/types/matrix/ OPEN

🏗️ Architectural & Hygiene Audit

Category Issue Description File Path Status
Hygiene No stale development notes or TypeScript strictness issues found N/A OPEN

🏗️ TDS Compliance & Styling Issues

Issue Description File Path
Hardcoded inline style cursor: 'pointer' cinny/src/app/plugins/react-custom-html-parser.tsx
Hardcoded color #00D4FF, #FFB300 VERIFIED COMPLIANT cinny/src/app/components/event-readers/EventReaders.tsx
Hardcoded color #EE1D52, #9146ff, #ff4500, #cb3837, #f48024 ⚠️ BRAND EXCEPTION cinny/src/app/components/url-preview/UrlPreviewCard.tsx + UrlPreview.css.tsx — official third-party brand colors in SVG logos and site badge backgrounds; cannot convert to CSS variables without inventing new tokens (violates TDS rule 3)
Massive number of hardcoded backgroundColor values ⚠️ PATTERN CONTENT EXCEPTION cinny/src/app/features/lotus/chatBackground.ts — each background's base color is aesthetic content that defines the pattern identity; converting requires inventing 40+ CSS variables (violates TDS rule 3) or using CSS4 relative-color-syntax in inline styles (insufficient browser support); these are visual content, not UI chrome
Hardcoded colors #00FF88, #FF6B00 VERIFIED COMPLIANT cinny/src/app/features/call/CallControls.tsx
Hardcoded fallback hexes in toast colors FIXED cinny/src/app/features/toast/LotusToastContainer.tsx

🌐 Localization, Accessibility & Performance

Category Issue Description File Path Status
Localization Hardcoded UI string: "Chat Room" src/app/components/create-room/CreateRoomTypeSelector.tsx OPEN
Localization Hardcoded UI string: "Messages, photos, and videos." src/app/components/create-room/CreateRoomTypeSelector.tsx OPEN
Localization Hardcoded UI string: "Voice Room" src/app/components/create-room/CreateRoomTypeSelector.tsx OPEN
Localization Hardcoded UI string: "Live audio and video conversations." src/app/components/create-room/CreateRoomTypeSelector.tsx OPEN
Localization Hardcoded UI string: "Download" src/app/components/image-viewer/ImageViewer.tsx OPEN
Localization Hardcoded UI string: "Open Location" src/app/components/message/MsgTypeRenderers.tsx OPEN
Localization Hardcoded UI string: "Thread" src/app/components/message/Reply.tsx OPEN
Localization Hardcoded UI string: "View" src/app/components/message/content/ImageContent.tsx OPEN
Localization Hardcoded UI string: "Spoiler" src/app/components/message/content/ImageContent.tsx OPEN
Localization Hardcoded UI string: "Retry" src/app/components/message/content/ImageContent.tsx OPEN
Localization Hardcoded UI string: "Close" src/app/components/DeviceVerification.tsx OPEN
Localization Hardcoded UI string: "Accept" src/app/components/DeviceVerification.tsx OPEN
Localization Hardcoded UI string: "They Match" src/app/components/DeviceVerification.tsx OPEN
Localization Hardcoded UI string: "Okay" src/app/components/DeviceVerification.tsx OPEN
Localization Hardcoded UI string: "Join Server" src/app/components/url-preview/UrlPreviewCard.tsx OPEN
Localization Hardcoded UI string: "Invite" src/app/components/invite-user-prompt/InviteUserPrompt.tsx OPEN
Localization Hardcoded UI string: "Files" src/app/components/upload-board/UploadBoard.tsx OPEN
Localization Hardcoded UI string: "Send" src/app/components/upload-board/UploadBoard.tsx OPEN
Localization Hardcoded UI string: "Upload Failed" src/app/components/upload-board/UploadBoard.tsx OPEN
Localization Hardcoded UI string: "Password" src/app/components/uia-stages/PasswordStage.tsx OPEN
Bundle Size Large unoptimized media asset (213KB) public/res/Lotus.png OPEN
Matrix Logic Inefficient repeated mx.getRoom() calls in component render loops src/app/features/lobby/Lobby.tsx OPEN
Matrix Logic Inefficient repeated mx.getRoom() calls in component render loops src/app/components/emoji-board/EmojiBoard.tsx OPEN
Performance Numerous event handlers (e.g., handleUserClick, handleReplyClick) lack useCallback, leading to unnecessary re-renders of message components. cinny/src/app/features/room/RoomTimeline.tsx OPEN
Performance The submit function and file handling callbacks (e.g., handleSendUpload) are re-created on every render, causing re-renders of the editor and toolbar components. cinny/src/app/features/room/RoomInput.tsx OPEN
Accessibility button for edit history lacks aria-label cinny/src/app/components/message/content/FallbackContent.tsx FIXED ⚠️ UNTESTED — added aria-label="View edit history"
Accessibility button for reaction lacks aria-label cinny/src/app/components/message/Reaction.tsx FIXED ⚠️ UNTESTEDReaction component now computes aria-label="{shortcode} reaction, N people" internally using getShortcodeFor; custom (mxc://) emoji falls back to "custom emoji reaction".
Accessibility button for ThreadIndicator lacks aria-label cinny/src/app/components/message/Reply.tsx FIXED ⚠️ UNTESTED — added aria-label="View thread"
Accessibility button for ReplyLayout lacks aria-label cinny/src/app/components/message/Reply.tsx FIXED ⚠️ UNTESTED — added aria-label="Jump to original message"

🔧 Infrastructure, DevEx & Type Safety

Category Issue Description File Path Status
Dependencies lodash pinned to non-existent version 4.18.1 cinny/package.json OPEN
Dependencies Various pinned versions of @atlaskit, matrix-js-sdk cinny/package.json OPEN
Dependencies matrix-js-sdk pinned to Release Candidate (41.6.0-rc.0) cinny/package.json OPEN
Dependencies Unstable/experimental versions for build tools (vite 8.0.14, typescript 6.0.3, eslint 9.39.4) cinny/package.json OPEN
CI/CD package-manager-cache set to false cinny/.github/workflows/build-pull-request.yml OPEN
CI/CD Inefficient sequential execution in deployment cinny/.github/workflows/prod-deploy.yml OPEN
CI/CD Aggressive 1-minute timeout for Netlify deploy cinny/.github/workflows/prod-deploy.yml OPEN
DevEx Stale upstream bug tracker link/donations/CLA cinny/CONTRIBUTING.md OPEN
DevEx Alignment issue between README and CONTRIBUTING cinny/README.md OPEN
Testing No evident automated testing configuration/files cinny/src/ OPEN
Type Safety Extensive use of as any type assertions cinny/src/ OPEN
Security Hardcoded public CDN URL; consider moving to environment variable /root/code/cinny/scripts/syncDecorations.mjs OPEN
Architecture Modifying node_modules directly is brittle; use patch-package instead /root/code/cinny/scripts/patch-folds.mjs OPEN
Robustness Missing security headers (HSTS, CSP, etc.) and inefficient asset serving using rewrites instead of try_files /root/code/cinny/contrib/nginx/cinny.domain.tld.conf OPEN
Robustness Incomplete documentation/placeholder path in Caddyfile /root/code/cinny/contrib/caddy/caddyfile OPEN
Matrix SDK Inefficient listener management (setMaxListeners: 150) and incomplete SDK state transition handling. src/client/initMatrix.ts OPEN
PWA Robustness Service worker lacks caching strategy for application assets, resulting in no offline capability. cinny/src/sw.ts OPEN
PWA Integrity manifest: false in vite.config.js might prevent correct PWA installation if not handled externally. cinny/vite.config.js OPEN
PII Leakage Potential PII exposure via console.error (parameter e likely contains event data). cinny/src/app/plugins/call/CallEmbed.ts OPEN
PII Leakage Potential PII exposure via console.warn (parameter imgError/videoError/thumbError object). cinny/src/app/features/room/msgContent.ts OPEN
PII Leakage Potential PII exposure via console.error (parameter e likely contains event data). cinny/src/app/features/room/RoomInput.tsx OPEN

🏗️ Architectural & Resilience Audit

Category Issue Description File Path Status
Element Call Integration Lacks robust iframe failure monitoring beyond initial 'preparing' event; can result in a permanently hung 'Loading...' state with no user-visible error or recovery path. src/app/plugins/call/CallEmbed.ts OPEN
Component Resilience RoomTimeline has no ErrorBoundary wrapper — a single malformed event crashing the renderer takes down the entire timeline with no fallback UI. src/app/features/room/RoomTimeline.tsx OPEN
Component Resilience RoomInput has no ErrorBoundary wrapper — a crash in the composer leaves users unable to send messages. src/app/features/room/RoomInput.tsx OPEN
Fallback Logic No explicit empty/error fallback for Matrix SDK data calls in RoomTimeline; relies purely on SDK internal error propagation, meaning silent failures show a blank timeline. src/app/features/room/RoomTimeline.tsx OPEN
Dependency Potential for complex dependency chains due to deep nesting in src/app/features/ and src/app/hooks/. src/app/ OPEN
Hydration/Race Condition The SyncState listener registered by useSyncState may miss the initial 'PREPARED' event if the client initializes synchronously from IndexedDB before the effect runs, leading to an infinite loading state. cinny/src/app/pages/client/ClientRoot.tsx OPEN
Structure High number of small, highly coupled utility hooks (src/app/hooks/) may obscure dependency graphs. src/app/hooks/ OPEN
Dead Code Potential for unused CSS modules or UI components in src/app/features/. src/app/ OPEN
Security Sensitive session data (access tokens, device ID) stored in localStorage is vulnerable to XSS. src/app/state/sessions.ts OPEN
Privacy Sensitive user status messages and expiry timestamps are persisted in localStorage. src/app/features/settings/account/Profile.tsx OPEN
Privacy Unsent composer drafts stored in localStorage without encryption could leak info on shared devices. src/app/features/room/RoomInput.tsx OPEN
Persistence Scheduled messages relying on fragile localStorage parsing are prone to data loss on session expiry or error. src/app/state/scheduledMessages.ts OPEN
Bundle Bloat Inefficient lodash import; risks including entire library instead of necessary utilities. cinny/package.json OPEN
Bundle Bloat Large matrix-js-sdk (RC version) dependency; high potential for tree-shaking overhead. cinny/package.json OPEN
Build-Time Overhead lotusDenoise plugin performs heavy, sequential fs operations during closeBundle, significantly slowing build times. cinny/vite.config.js OPEN
Build-Time Overhead Complex manual viteStaticCopy configuration requiring multiple renames and path manipulations; risks redundant processing. cinny/vite.config.js OPEN
Architectural Debt Redundant style variant logic in SpacingVariant could be simplified. cinny/src/app/components/message/layout/layout.css.ts OPEN
Overhead Analysis Potential CSS bloat from DropTarget composition across multiple recipes (SidebarItem, SidebarFolder). cinny/src/app/components/sidebar/Sidebar.css.ts OPEN

🏗️ Git Workflow & History Audit

Category Issue Description File Path Status
Workflow Monolithic "Fix all bugs" commits (e.g., 10f6544e, aa48c9ef) make git bisect difficult. Git History OPEN
Workflow Inconsistent commit message prefixes (e.g., fix, feat, docs, assets). Git History OPEN
Workflow Use of fix or feat for large-scale changes affecting multiple disparate systems (e.g., 938ead79). Git History OPEN

🎨 Native UI/UX Consistency — Lotus vs. Cinny Baseline

Audit of every Lotus-custom UI feature against Cinny's native folds design-system conventions. "Native pattern" means the folds component library, vanilla-extract tokens (color.*, config.radii.*, config.space.*), and established Cinny component patterns. 52 findings, organized by severity.


🔴 Major — Broken Styling / Functional Regressions

N1. ProfileDecoration Save Button — Undefined --accent-cyan Variable (border invisible on all non-TDS themes)

  • File: src/app/features/settings/account/ProfileDecoration.tsx, lines 191213
  • Status: FIXED — replaced raw <button> with <Button size="400" variant="Success" fill="Solid" radii="300">, removed undefined --accent-cyan reference
  • Issue: The save button is a raw <button> with border: '1px solid var(--accent-cyan)' and color: 'var(--accent-cyan)'. The variable --accent-cyan (without the --lt- prefix) is never defined in any theme file — the correct prefixed form is --lt-accent-cyan. On all non-TDS themes the border is invisible and the text has no color.
  • Root Cause: Missing --lt- prefix. Additionally, the raw <button> should be a folds <Button> to match every other save button in the same Profile.tsx settings panel (e.g., ProfileDisplayName save at Profile.tsx:303).
  • Fix: Replace raw <button> with <Button size="400" variant="Success" fill="Solid" radii="300">. Remove the --accent-cyan reference.

N2. UserPrivateNotes Textarea — Undefined --border-interactive Variable (border invisible on all themes)

  • File: src/app/components/user-profile/UserRoomProfile.tsx, lines 246265
  • Status: FIXED — replaced undefined CSS vars with color.SurfaceVariant.ContainerLine, config.radii.R300, config.space.S200/S300
  • Issue: The notes textarea sets border: '1px solid var(--border-interactive)'. This variable is never defined anywhere in the codebase — the correct equivalents are --bg-surface-border (src/index.css) or color.SurfaceVariant.ContainerLine (folds token). The border is invisible on all themes.
  • Root Cause: Invented CSS variable name. Also uses raw pixel sizing (borderRadius: '6px', padding: '8px 10px', fontSize: '14px') instead of folds tokens.
  • Fix: Replace inline style with border: \1px solid ${color.SurfaceVariant.ContainerLine}`, borderRadius: config.radii.R300, padding: config.space.S200`.

N3. LotusToastContainer — Z-Index Places Toasts Below Night Light Overlay and All Modals

  • File: src/app/features/toast/LotusToastContainer.tsx, lines 184211; src/app/pages/App.tsx
  • Status: FIXED — raised toast zIndex from 9997 to 10001 (above Night Light at 9998 and modals at 9999)
  • Issue: The toast container uses hardcoded zIndex: 9997. The Night Light overlay is at z-index: 9998. The folds Overlay/Dialog components used for all modals resolve to z-index: 9999. Result: (a) toasts render under the Night Light tint and take on the warm orange filter; (b) any open modal covers toasts entirely, making notifications invisible.
  • Root Cause: The toast container does not use the folds OverlayContainerProvider portal that manages z-index correctly — it is a plain position: fixed div injected directly in App.tsx.
  • Fix: Either route the toast portal through OverlayContainerProvider (matching how all other floating UI works), or raise zIndex above all overlay layers (10001+). Also audit Night Light's z-index (9998) relative to toasts.

N4. PollContent Vote Buttons — Entirely Outside the Folds Design System

  • File: src/app/components/message/content/PollContent.tsx, lines 250358
  • Status: OPEN
  • Issue: Each poll answer is a native <button> with ~15 hardcoded inline style properties using undefined CSS variables (--accent-cyan, --accent-cyan-dim, --accent-cyan-border). Checkbox/radio indicators, percentage spans, and the poll label use raw pixel font sizes (0.68rem, 0.78rem, 0.88rem) and hardcoded borderRadius: '8px'. None of these variables exist in any theme — the entire component will render unstyled on non-TDS themes. All other interactive message content (audio, file, image) uses folds Chip or Button variants.
  • Root Cause: Custom implementation that bypasses folds primitives entirely.
  • Fix: Rewrite using folds Button or Chip for answers; replace --accent-cyan* with color.Secondary.* folds tokens; use Text size="T300" for labels.

🟠 Moderate — Interaction Pattern or Visual Deviations

# Area File Lines Issue Native Pattern
N5 Read Receipts ReadReceiptAvatars.tsx 62137 Trigger button is raw <button> with onMouseEnter/onMouseLeave JS style mutation for hover state — FIXED: hover/focus emphasis moved to co-located ReadReceiptAvatars.css.ts (:hover/:focus-visible), no JS .style mutation All interactive elements use useHover from react-aria and folds variant system for hover; direct .style mutation used nowhere else on buttons
N6 Read Receipts ReadReceiptAvatars.tsx & Message.tsx 3256 / 268283 Two code paths open EventReaders: avatar-pill path uses useModalStyle(360) for mobile fullscreen; context-menu path (MessageReadReceiptItem) does not — on mobile the context menu path opens a fixed-size non-fullscreen modal for the same content All modals that share a layout variant use useModalStyle consistently; MessageReadReceiptItem was not updated when useModalStyle was added
N7 Delivery Status Message.tsx 89148 DeliveryStatus renders Unicode glyphs (⟳ ✓ ✕) in a <span> with fontSize: '10px' instead of folds <Icon> components — FIXED: replaced with Icons.Check/Cross/Send via <Icon size="100"> Icons.Check, Icons.Cross, etc. are used for all other status glyphs; folds Text size tokens for all supplementary text
N8 GIF Picker GifPicker.tsx 83124 GIF picker container uses fully bespoke inline styles (borderRadius: '12px', boxShadow: '0 8px 32px rgba(0,0,0,0.4)', raw rgba border) — two separate style sets for TDS and non-TDS paths — FIXED: non-TDS path now uses folds tokens (color.Surface.Container, config.radii.R400, color.Surface.ContainerLine, color.Other.Shadow), dropping the undefined var(--bg-surface); the TDS branch keeps its --lt-* glow chrome (valid TDS styling) EmojiBoard has no caller-applied container styling; folds components handle their own surface internally via design tokens
N9 GIF Button RoomInput.tsx 10761087 GIF toolbar button renders <Text size="T200"> with hand-rolled fontWeight/fontSize/letterSpacing instead of <Icon>WON'T FIX (deliberate): folds has no GIF icon, and "GIF" is a widely-recognized text affordance (Slack/Discord/Element all use a text label). Converting to an arbitrary icon would be less clear, not more. All 8 other toolbar buttons (Smile, Sticker, Location, Poll, etc.) use <Icon src={...} /> exclusively
N10 Send Animation Message.tsx + Animations.css.ts 979998 / 6071 MsgAppearClass and MentionHighlightPulse both animate transform: scale on the same MessageBase DOM node — on self-sent mention messages both classes apply simultaneously and fight over the transform property — FIXED: mentionPulseKeyframes now animates only box-shadow (dropped the imperceptible scale(1.003)), so the appear-scale and the mention glow no longer contend for transform Pre-existing highlightAnime only animates backgroundColor; no prior transform animation on MessageBase
N11 AvatarDecoration AvatarDecoration.tsx 5 / 3841 Fixed 8px inset on all sides regardless of avatar size — at folds size "200" (~32px) the decoration bleeds 50% of the avatar diameter, clipping against overflow: hidden parent containers in member lists. Inset issue still OPEN. Related regression fixed in useAvatarDecoration.ts: the decoration fetch cached all failures (including transient 429/5xx) as "no decoration" permanently for the session, so a single rate-limited burst (member list / timeline mount many avatars at once) would make decorations vanish until a full reload. Now only a genuine 404 is cached; transient errors retry on the next mount. Folds Avatar and PresenceRingAvatar do not emit overflow outside their bounding box
N12 MediaGallery Drawer MediaGallery.tsx 651661 Drawer uses position: 'fixed' with hardcoded width: '320px' as inline styles on a <Box>FIXED: moved positioning/width into co-located MediaGallery.css.ts using toRem(320) + a max-width: 750px full-screen media query (mirrors MembersDrawer); border/header now use config.borderWidth/config.space tokens. Added Escape-to-close on the panel (previously only the lightbox handled Escape). Full chrome redesign (round 2) to match native conventions: panel + header switched from Surface to Background variant (matching MembersDrawer/Saved Messages); header now Text size="H4" + plain close IconButton (dropped the bespoke tooltip-wrapped button); tabs moved to a bordered toolbar strip with the variant={active?'Primary':'Secondary'} fill={active?'Solid':'Soft'} pattern from PolicyListViewer and now show per-tab counts; the centered "lines + label" month divider replaced with a left-aligned group label (Cinny group-label pattern); thumbnail tiles moved hover/focus styling to CSS :hover/:focus-visible (no JS hover state) and into MediaGallery.css.ts; file rows + grid tokenized. Docking fix (round 3) — the core of the finding: the gallery was a position: fixed overlay floating over the timeline, mounted from RoomViewHeader. It is now a docked flex sibling in the room layout row, exactly like MembersDrawer: open state lifted to a mediaGalleryAtom (mirrors bookmarksPanelAtom), rendered in Room.tsx with a vertical Line separator on desktop and key={room.roomId} to reset per room; the CSS is static-width on desktop and only position: fixed; inset: 0 full-screen on mobile (identical strategy to MembersDrawer.css). It now shares the row with the timeline instead of overlapping it. MembersDrawer uses a vanilla-extract class with width: toRem(266) and is placed by the layout system, not position: fixed. 54px width discrepancy also breaks visual rhythm if both panels could be open
N13 ScheduledMessagesTray ScheduledMessagesTray.tsx 108126 Collapsible tray header is <Box as="button"> with cursor: 'pointer' inline style and no folds variant — no hover state, no focus ring — FIXED: replaced with folds <Button variant="Secondary" fill="None" radii="0"> using before/after icon props (gains design-system hover/focus) All clickable header/toggle elements in the room view use folds <Button> or <IconButton> with explicit variants for hover/focus; <Box as="button"> with no variant is used nowhere else
N14 ForwardMessageDialog ForwardMessageDialog.tsx 137154 Dialog uses <Modal> but has no <Header> component and no close <IconButton> — only way to close is clicking outside — FIXED: added a folds <Header variant="Surface" size="500"> with the title + close <IconButton radii="300">, matching every other modal Every other modal using <Modal> or <Box role="dialog"> includes a <Header> with a close <IconButton> in the top-right (EditHistoryModal, LeaveRoomPrompt, ScheduleMessageModal, RemindMeDialog, etc.)
N15 ScheduleMessageModal ScheduleMessageModal.tsx 180193 Modal root is <Box as="form" role="dialog"> with manually assembled borderRadius: config.radii.R400/boxShadowFIXED: shell is now <Dialog as="form" variant="Surface">; removed inline surface styles ForwardMessageDialog uses folds <Modal size="400"> with R500 radius; the R400 vs R500 mismatch is visible when both dialogs appear in the same session
N16 Presence Picker SettingsTab.tsx 118144 Presence trigger dot is raw <button> with position: absolute; bottom: 2; right: 2 inline and no folds focus ring; no tooltip — FIXED: wrapped the trigger in a folds TooltipProvider (shows "Status: …"); replaced the undefined var(--bg-surface) with color.Background.Container. Kept the absolute-positioned <button> (it overlays the avatar corner; a full IconButton would be too large for the dot). Every other sidebar icon button uses folds IconButton with SidebarItemTooltip and TooltipProvider
N17 Presence Picker SettingsTab.tsx 8086 PresencePicker FocusTrap missing escapeDeactivates: stopPropagation and isKeyForward/isKeyBackwardFIXED: added all three options, matching the theme selector / sort menus Every other PopOut+FocusTrap+Menu combo supplies both (theme selector General.tsx:143160, SettingsSelect, sort menus) — without it Escape bubbles past the trap and arrow-key navigation is absent
N18 Profile Selects Profile.tsx 547575 / 816848 ProfileStatus auto-clear and ProfileTimezone selectors are native <select> elements with hardcoded colorScheme: 'dark' — will render in dark mode on light themes General.tsx uses folds SettingsSelect<T> (Button+PopOut+Menu) for all dropdowns; colorScheme: 'dark' breaks light/custom theme appearance
N19 Presence Labels useUserPresence.ts vs SettingsTab.tsx 5562 / 3642 PresenceBadge tooltip shows "Active / Busy / Away"; PresencePicker options read "Online / Idle / Do Not Disturb / Invisible" — a DND user shows tooltip "Busy", not "Do Not Disturb" — FIXED: aligned usePresenceLabel reader vocabulary to the setter (online→"Online", unavailable→"Idle", offline→"Offline") Within the same Lotus feature set the user-facing vocabulary is inconsistent between the setter UI and the reader tooltip
N20 Notification Presets Notifications.tsx 57107 Gaming/Work/Sleep preset buttons are bare <button> elements with Lotus-specific CSS vars (--border-interactive-normal, --bg-surface-low) not defined in all themes — FIXED: converted to folds <Button variant="Secondary" fill="Soft" radii="300"> (auto height) wrapping the emoji/label/description column; undefined vars removed Grouped preset/action buttons elsewhere use folds Chip variant="Primary/Secondary" outlined radii="Pill" (e.g., Composer Toolbar toggles in General.tsx:11001113)
N21 Notification Sound Selects SystemNotification.tsx 111305 Message sound, invite sound, and quiet-hours time pickers are bare <select>/<input type="time"> with colorScheme: 'dark' workaround All other dropdowns in settings use the Button+PopOut+Menu+MenuItem folds pattern; the native select renders OS-styled on all platforms
N22 DM Preview Virtualizer RoomNavItem.tsx / Direct.tsx 608627 / 232 DM preview adds a second text row to each DM item, making it taller than 38px, but useVirtualizer in Direct.tsx still uses estimateSize: () => 38 — causes layout jump/overlap on initial render — FIXED: bumped estimateSize to 52 (the two-line DM-row height) so the initial estimate matches the common case; measureElement still corrects each row exactly Non-DM rooms in Home.tsx also estimate 38px; DM items with a preview are now a different height, creating two visual densities in the same nav column
N23 RoomServerACL RoomServerACL.tsx 100115 / 298309 Server-name text input is a raw <input type="text"> with inline style object; "Allow IP literal addresses" is a raw <input type="checkbox"> with style={{ width: 16, height: 16 }}FIXED: text input → folds <Input variant={error?'Critical':'Secondary'}>; checkbox → folds <Checkbox variant="Primary"> All other text/boolean controls in room settings use folds Input and Checkbox components (RoomAddress.tsx:163, RoomAddress.tsx:330)
N24 PolicyListViewer PolicyListViewer.tsx 245264 Room-ID add input is a raw <input type="text"> with manually replicated folds token values — FIXED: replaced with folds <Input variant={error?'Critical':'Secondary'} size="400" radii="300"> Native pattern: folds <Input variant="Secondary" size="300" radii="300"> — no inline style needed
N25 ExportRoomHistory Inputs ExportRoomHistory.tsx 258292 Both date range pickers are raw <input type="date"> with inline styles — FIXED: replaced with folds <Input type="date" variant="Secondary" size="400" radii="300"> Native pattern: folds Input component; <input type="date"> renders OS-native date picker, unstyled relative to the rest of the settings panel
N26 RoomShareInvite QR RoomShareInvite.tsx 6673 QR code <img> has no onError handler and no loading state — broken-image placeholder shown when the external API is unreachable — FIXED: added loading="lazy" + onError that swaps to a folds "QR code unavailable" placeholder card Cinny avatar components and MediaGallery use onError handlers; this is the only settings element making a request to a third-party server with no graceful degradation

🟡 Minor — Cosmetic / Token Discipline

# Area File Lines Issue Native Pattern
N27 GIF Picker GifPicker.tsx 103110 FocusTrap omits returnFocusOnDeactivate: false — focus returns to GIF button on dismiss instead of staying in the editor — FIXED: added returnFocusOnDeactivate: false (matches EmojiBoard) EmojiBoard in RoomInput.tsx:978 explicitly sets returnFocusOnDeactivate={false}; GIF picker dismiss behaviour is inconsistent with emoji picker
N28 Character Counter RoomInput.tsx 11591174 Composer character counter rendered with color: 'var(--tc-surface-low)' and raw pixel padding — a CSS variable not used anywhere else in the codebase — FIXED: removed undefined var and raw opacity; now <Text priority="300"> with config.space.S100 padding Use color.* folds tokens or priority="300" on a Text component
N29 PollCreator Modal PollCreator.tsx 103116 Modal root is <Box as="form" role="dialog" aria-modal="true"> with manually assembled surface styles instead of folds <Dialog variant="Surface">FIXED: shell is now <Dialog as="form" variant="Surface">; removed inline surface styles MessageDeleteItem and MessageReportItem in Message.tsx:506,635 use <Dialog variant="Surface"> inside OverlayCenter > FocusTrap
N30 Playback Speed Chip AudioContent.tsx 163189 Speed chip uses variant="SurfaceVariant" radii="Pill" while adjacent Play/Pause chip uses variant="Secondary" radii="300" — mismatched shape and variant within the same leftControl row — FIXED: changed speed chip to variant="Secondary" radii="300" Controls grouped in the same row should share variant and radii
N31 Collapsible Message Toggle MsgTypeRenderers.tsx 97105 "Read more ↓" / "Show less ↑" uses <Button size="300" variant="Secondary" fill="None"> — visually a padded form button — FIXED: replaced with the native flush inline-button pattern (background:none;border:none;padding:0) + <Text size="T200"> tinted color.Primary.Main, matching (edited) in FallbackContent Inline text toggles in message content (e.g. (edited) in FallbackContent.tsx:74) use bare <button> with background: none; border: none; padding: 0 to stay flush with text
N32 ReadReceiptAvatars Pill ReadReceiptAvatars.tsx 95103 Pill border is '1px solid rgba(0,212,255,0.30)' hardcoded raw rgba string; borderRadius: '999px' not a folds radii token; padding in raw pixels — FIXED: replaced with config.borderWidth.B300, config.radii.Pill, config.space.S100/S200 Use color.* folds tokens and config.radii.Pill / config.space.S*
N33 ReadReceiptAvatars Class ReadReceiptAvatars.tsx 67 className="receipt-pill-btn" references a class never definedFIXED: removed dead className All custom CSS goes through co-located vanilla-extract *.css.ts files
N34 EventReaders Header Size EventReaders.tsx 70 Header size="600" (56px tall) while all peer message-action modals use size="500" (48px) — FIXED: changed to size="500" EditHistoryModal, LeaveRoomPrompt, MessageDeleteItem, MessageReportItem all use size="500"; size="600" is reserved for full-page panel headers
N35 EventReaders Close Button EventReaders.tsx 96 Close IconButton missing explicit radii="300" prop — FIXED: added radii="300" Every peer modal close button explicitly sets radii="300" (EditHistoryModal:184, LeaveRoomPrompt:75, MessageDeleteItem:517)
N36 EventReaders Header Border EventReaders.tsx 7277 Lotus-mode header sets borderBottom: '1px solid var(--lt-border-color)' as a CSS shorthand string — FIXED: changed to borderBottomWidth: config.borderWidth.B300 Native modals use borderBottomWidth: config.borderWidth.B300 to avoid overriding the border-color set by the folds variant system
N37 EventReaders Timestamp EventReaders.tsx 143151 Lotus path sets fontSize: '0.72rem' inline — a raw relative unit between folds T200 and T100 scale steps — FIXED: removed raw fontSize, added priority="300" Use folds Text size="T200" priority="300" for subdued secondary text
N38 BookmarksPanel Header BookmarksPanel.tsx 155196 Header uses variant="Surface" and close button uses size="300" radii="300"; also has a SurfaceVariant search bar strip with no equivalent in any native drawer — FIXED (full redesign): rebuilt the whole "Saved Messages" panel to match the canonical MembersDrawer — co-located BookmarksPanel.css.ts (toRem(266) + max-width:750px full-screen media query, replacing the old position:absolute; zIndex:100 mobile "modal" that had no backdrop/escape), variant="Background" header, room avatars on each item (was a generic hash icon), priority tokens replacing all raw opacity hacks, the borderLeft:3px accent removed, and Escape-to-close added. MembersDrawer header uses variant="Background" and default-size close button; the extra search+count strip creates a structurally different component family
N39 Forward Menu Icon Message.tsx 1150 Forward context menu item's after icon has no size="100" prop — FIXED: added size="100" to the ArrowRight icon Every other after-icon in the same menu block explicitly uses size="100" (Reply, Reaction, Edit, Remind Me, Bookmark); missing size causes the Forward icon to render larger
N40 ProfileDecoration Remove Button ProfileDecoration.tsx 185 "Remove" link is a raw <button> with background: 'none'; color: 'var(--tc-surface-low-contrast)' — an undefined CSS variable — FIXED: replaced with <Button variant="Critical" fill="None" size="300" radii="300"> Use folds <Button variant="Critical" fill="None"> or a Text-styled inline link
N41 PresenceBadge / UserNotes Saving UserRoomProfile.tsx 240244 "Saving…" indicator is <Text opacity={0.5}> without a spinner — FIXED: now shows a folds <Spinner variant="Success" fill="Solid" size="100"> beside the "Saving…" text Every other save operation in Profile.tsx shows a folds <Spinner variant="Success" fill="Solid" size="300"> alongside the save button
N42 Character Counter Convention UserRoomProfile.tsx vs Profile.tsx 243 / 479490 UserPrivateNotes shows remaining count "N left", appears only under 100; ProfileStatus shows "current / 64" always with color progression Two Lotus features in the same settings flow use different counter conventions; neither matches a pre-existing Cinny pattern
N43 Night Light Slider General.tsx 554565 Night Light intensity slider is a raw <input type="range"> with no accentColor token — renders in browser-default blue on all themes — FIXED: added accentColor: color.Primary.Main; the intensity label opacity hack also replaced with priority="300" The Gate Threshold slider at General.tsx:1456 at minimum sets accentColor: 'var(--accent-orange)'; the Night Light slider does neither
N44 Mention Highlight & Boot Button General.tsx 597677 <input type="color"> for mention highlight uses raw pixel dimensions (width: '36px', height: '28px', borderRadius: '4px'); Reset and Boot buttons are bare <button> with Lotus CSS vars — PARTIALLY FIXED: the mention-highlight Reset button (renders on all themes) is now a folds <Button variant="Secondary" fill="Soft">, removing the undefined --border-interactive-normal var. The Boot button is deliberately kept as-is: it only renders when lotusTerminal is active, i.e. exactly when the --accent-orange* TDS vars are defined. The <input type="color"> itself is tracked separately as N69. Adjacent settings controls use folds IconButton/Button; there is no other <input type="color"> in the Cinny settings UI
N45 SettingsSelect vs SelectTheme General.tsx 126 vs 197 SettingsSelect trigger uses variant="Secondary" while SelectTheme uses variant="Primary" outlined fill="Soft" for the same Button+PopOut dropdown pattern — adjacent rows in the same Appearance section have different visual weight — FIXED: SelectTheme trigger changed to variant="Secondary" to match SettingsSelect Dropdown triggers should share the same variant within the same settings section
N46 RoomInsights SectionHeader RoomInsights.tsx 2437 SectionHeader adds textTransform: 'uppercase', letterSpacing: '0.06em', opacity: 0.6 to Text size="L400"FIXED: simplified to <Text size="L400" priority="300"> Every other settings panel uses bare <Text size="L400">Label</Text> with no transforms (General.tsx:5272, ExportRoomHistory.tsx:220,246)
N47 RoomInsights Chart Radii RoomInsights.tsx 350356 / 415436 Bar chart uses borderRadius: 3 and histogram bars use borderRadius: '2px 2px 0 0' as raw pixel integers — FIXED: replaced with config.radii.R300 All other rounded corners use config.radii.* tokens
N48 RoomInsights Font Size RoomInsights.tsx 448 Hour-axis labels set style={{ fontSize: 9 }} as a raw pixel integer — overrides the folds Text size="T200" applied on the same element — FIXED: removed raw style={{ fontSize: 9 }} Use only folds Text size props; never override with raw fontSize
N49 RoomInsights Emoji Icons RoomInsights.tsx 4165 / 292295 StatTile uses literal Unicode emoji (🖼️ 🎬 🎵 📎) in <Text size="H4"> as icons — FIXED: StatTile now takes an icon: IconSrc and renders <Icon> using Icons.Photo/VideoCamera/Headphone/File All other iconographic elements use <Icon src={Icons.*} /> from folds — emoji rendering varies between Windows/macOS/Linux and cannot be tinted by the theme
N50 RoomInsights Warning Banner RoomInsights.tsx 168192 Disclaimer banner uses raw <Box style={{ border: color.Warning.Main, background: color.Warning.Container }}>FIXED: replaced with <SequenceCard variant="SurfaceVariant"> with <Icon> colored via color.Warning.Main Settings panel informational cards use <SequenceCard variant="SurfaceVariant"> throughout RoomServerACL, ExportRoomHistory, PolicyListViewer
N51 ExportRoomHistory Progress ExportRoomHistory.tsx 311314 Export progress shows as a plain Text string ("Exporting… N messages") — WON'T FIX (deliberate): unlike BackupRestore (which has a known total to drive a determinate ProgressBar), export has no known total — it counts messages as they stream. The operation already shows a folds Spinner in the button plus a live count, which is the correct affordance for an indeterminate task. BackupRestore.tsx:72,90 uses a folds <ProgressBar variant="Secondary" size="300"> for the same kind of long async operation
N52 MessageQuickReactions Empty Return Message.tsx 160 if (recentEmojis.length === 0) return <span />; — injects an invisible DOM node into the hover action bar flex container — FIXED: changed to return null Universal convention for empty renders in Cinny is return null; 144+ instances across the codebase; the empty <span> can affect flex spacing

Round 2 — Additional Feature Areas

🔴 Additional Major Findings

N53 — PTT Badge (Lotus Terminal path): Raw <div> tree with --lt-* CSS vars instead of folds <Chip>

  • File: src/app/features/call/CallControls.tsx, lines 242282
  • Status: OPEN
  • Issue: When lotusTerminal is true the PTT badge renders as a bare <Box> with inline styles referencing --lt-accent-green-dim, --lt-accent-green-border, --lt-accent-green — variables absent outside TDS mode — hardcoded rem padding, borderRadius: '99px' (non-token), a raw monospace fontFamily string, non-token letterSpacing, and a raw animation: CSS string for the live-pulse dot. The live dot is a raw <span> with inline style.
  • Root Cause: Two entirely separate component trees for the same badge depending on a theme boolean. The non-terminal path (lines 284301) uses the correct <Chip variant="Success"|"Warning" fill="Soft" radii="400" outlined>.
  • Fix: Remove the terminal branch. The standard <Chip> path already exists and TDS theming can be applied via the CSS variable layer without a separate component tree.

N54 — PiP Mute Overlay Badges: Raw <div> instead of folds <Badge>/<Chip>

  • File: src/app/components/CallEmbedProvider.tsx, lines 438477
  • Status: FIXED — replaced hardcoded borderRadius/padding/fontSize with config.radii.R300, config.space.S100/S200 tokens; replaced raw <span> text with folds <Text size="T200">; color now applied to the Icon/Text via color.Critical/Warning.Main. The dark translucent scrim (rgba(0,0,0,0.65)) is deliberately retained: these badges overlay arbitrary video, where a theme Chip/Badge surface token would not guarantee legibility. They are also non-interactive (pointerEvents: 'none'), so an interactive Chip (a <button>) is semantically wrong.
  • Issue: Both the "You muted" (bottom-left) and "All muted" (top-right) PiP badges are raw <div> elements with hardcoded background: 'rgba(0,0,0,0.65)', backdropFilter: 'blur(4px)', borderRadius: '6px', padding: '3px 7px', fontSize: '12px'. Color is set as color: color.Critical.Main directly on the wrapper <div>, not via a folds variant prop. Text is <span style={{ fontSize: '11px', fontWeight: 600 }}>.
  • Root Cause: CallView.tsx line 127 uses <Badge variant="Critical" fill="Solid" size="400"> in the same file for the "N Live" indicator — the native pattern exists and is unused here.

N55 — Chat Background / Seasonal Theme Selected State Uses color.Critical.Main (Error Red)

  • File: src/app/features/settings/general/General.tsx, lines 16601661 and 17261728
  • Status: FIXED — replaced all 4 instances of color.Critical.Main with color.Primary.Main in General.tsx
  • Issue: The selected-state border for both ChatBgGrid and SeasonalBgGrid is border: \2px solid ${color.Critical.Main}`and the label color is alsocolor.Critical.Main. color.Critical.Main` is the semantic token for destructive/error states — it is used for "Leave Room", "Delete Message", "Report Room" in the same file. A normal selection indicator rendered in error red is semantically wrong and visually alarming.
  • Root Cause: Wrong semantic token for an active/selected state.
  • Fix: Replace color.Critical.Main with color.Primary.Main (or color.Success.Main to match how other settings selections are styled) for both the border and label color.

N56 — Report Modal Category Dropdown: Native <select> Instead of folds Chip+PopOut+Menu

  • File: src/app/features/room/ReportRoomModal.tsx lines 138163; src/app/features/room/ReportUserModal.tsx lines 144169
  • Status: FIXED — extracted a shared ReportCategorySelect component (src/app/features/room/ReportCategorySelect.tsx) using the folds Button trigger + PopOut + FocusTrap + Menu + MenuItem pattern (with escapeDeactivates/arrow-key nav, matching OrderButton); both modals now use it instead of the native <select>.
  • Issue: Both report modals render the "Category" field as <Box as="select"> with hand-rolled inline styles (padding, border, background, color, fontSize, fontFamily). No other selector in the message-action modal context uses <select> — the established pattern for all dropdowns in both message modals and search filters is Chip onClick → setMenuAnchor → PopOut → FocusTrap → Menu → MenuItem (reference: OrderButton in SearchFilters.tsx lines 63114).

🟠 Additional Moderate Findings

# Area File Lines Issue Native Pattern
N57 PiP Fullscreen Button CallEmbedProvider.tsx 929951 PiP fullscreen toggle is a raw <button> with background: 'rgba(0,0,0,0.65)', color: '#fff', fontSize: '13px', Unicode ⛶/⊡ glyph — no focus ring, no tooltip — FIXED (token discipline): borderRadius/padding/gap replaced with config.radii.R300 + config.space.* tokens (also on the "Return to call" label). The dark scrim and #fff text are deliberately kept for legibility over arbitrary video; the glyph stays because folds has no fullscreen icon. aria-label/title tooltip already present. Controls.tsx fullscreen button uses <IconButton variant="Surface" fill="Soft" radii="400" size="400" outlined> with <TooltipProvider>; hardcoded #fff fails on light themes
N58 Screenshare Confirm Popup CallControls.tsx 303360 "Share your screen?" popup is a raw <Box> with --bg-surface/--bg-surface-border vars (undefined outside TDS), borderRadius: '0.75rem', boxShadow: '0 8px 32px rgba(...)', no FocusTrap Cinny's confirmation dialogs use folds <Menu> + <FocusTrap> + <PopOut>; the non-FocusTrap popup is not keyboard-accessible
N59 ML Noise Suppression Panel General.tsx 13031487 Sub-panel uses var(--border-color), var(--bg-card), var(--bg-input) (undefined in folds default theme), raw <details>/<summary> (UA-styled), accentColor: 'var(--accent-orange)' (TDS-only) All other settings sub-sections use <SettingTile> rows inside <SequenceCard>; no other settings component uses <details>
N60 Knock Badge on Members Button RoomViewHeader.tsx 744782 Knock count badge wrapped in extra <div style={{ position: 'relative' }}> with hardcoded fontSize: '9px', minWidth: '14px', height: '14px', padding: '0 3px' overriding folds size="200"FIXED: removed wrapper div, put position: 'relative' directly on the IconButton, <Badge size="400"> with toRem(3) insets and <Text size="L400"> — now matches the Pinned Messages badge pattern exactly Pinned Messages badge (same header, lines 651677) uses position: 'relative' directly on <IconButton> + toRem() for inset; no extra wrapper div
N61 Knock Member Rows MembersDrawer.tsx 441487 Knock requester rows use raw <Box> with manually duplicated padding; no <MenuItem> wrapper → no hover/focus/active states — WON'T FIX (deliberate): unlike a MemberItem (a clickable navigation row), a knock row contains two action buttons (Approve / Deny) and is not itself clickable. Wrapping it in <MenuItem> (a <button>) would nest interactive controls inside a button — invalid HTML/ARIA. The row has no interactive state to express. Every joined/invited member uses <MemberItem> which wraps <MenuItem variant="Background" radii="400"> with baked-in spacing and all interactive states
N62 Unverified Device Banner RoomInput.tsx 860883 Warning callout above composer uses inline background: color.Warning.Container, borderLeft: '3px solid color.Warning.Main' — a custom left-border accent pattern not present anywhere else in the folds system — FIXED: replaced the borderLeft: '3px' accent with a standard full border using color.Warning.ContainerLine + config.borderWidth.B300; removed the opacity hacks (folds OnContainer already meets contrast) Warning indicators in the same codebase use <Chip variant="Warning"> or <Badge variant="Warning">; the 3px left-border card pattern has no folds equivalent
N63 Report Modals — Box Instead of Dialog ReportRoomModal.tsx / ReportUserModal.tsx 97110 / 103116 Both modals render as <Box as="form" role="dialog"> with inline background/borderRadius/boxShadow; use config.radii.R400 (rounder) vs native Dialog which uses R300FIXED: both shells are now <Dialog as="form" variant="Surface">; removed inline surface styles (Dialog provides background/radius/shadow) Native MessageReportItem at Message.tsx:634 and all other Cinny message-action modals use <Dialog variant="Surface">
N64 EditHistoryModal — <Modal> vs <Dialog> EditHistoryModal.tsx 166 Uses <Modal variant="Surface" size="500"> while sibling message-action modals (DeleteMessageItem:505, MessageReportItem:634) all use <Dialog variant="Surface"> — different widths and internal padding <Dialog variant="Surface"> is the established modal shell for all message-triggered dialogs
N65 EditHistoryModal — No "Load More" EditHistoryModal.tsx 253259 When hasMore is true the modal shows passive <Text>"Showing the 50 most recent edits"</Text> with no action; older edits are inaccessible — FIXED: implemented real pagination — edits accumulate across next_batch fetches (de-duped by event id, re-sorted by ts), with a folds <Button>Load more</Button> (spinner while loading) replacing the passive text RoomActivityLog.tsx:425 and MessageSearch.tsx:129 both render a folds <Button size="300" variant="Secondary">Load more</Button> to fetch the next page
N66 DateRangeButton — Native <input type="date"> SearchFilters.tsx 558589 "From" and "To" date fields are raw <input type="date"> with inline style overrides including fontSize: '0.82rem'FIXED: replaced both with folds <Input type="date" variant="SurfaceVariant" size="300" radii="300">; removed now-unused color import SelectRoomButton (same file, line 224) and SelectSenderButton (line 424) both use folds <Input size="300" radii="300">; the date inputs are the only native browser inputs in the search filter row
N67 SeasonalEffect / NightLight Z-Index Order SeasonalEffect.tsx / App.tsx 759 / 6277 SeasonalEffect mounts at zIndex: 9999; NightLightOverlay at zIndex: 9998. Seasonal particles render above Night Light so they are never tinted. SeasonalEffect also shares z-index: 9999 with the skip-to-content link in ClientLayout.tsxFIXED: lowered SeasonalEffect overlay to zIndex: 9997 (below Night Light at 9998 and modals at 9999), so Night Light now tints the particles and dialogs are never obscured Expected UX: Night Light tints all visible content including effects; requires either a higher Night Light z-index or a lower SeasonalEffect z-index
N68 Syntax Highlighting — --lt-accent-* Vars in Non-TDS Themes syntaxHighlight.ts 313323 tokenStyle() returns var(--lt-accent-cyan/green/orange/purple, hardcoded-fallback)--lt-* vars only exist in TDS mode; fallbacks are Monokai dark colors that have poor contrast on light themes and no relationship to the existing --prism-* variables in ReactPrism.cssFIXED: tokenStyle() now maps to the --prism-* family (keyword/selector/boolean/atrule/comment) which has proper light/dark/TDS palettes; comment uses --prism-comment instead of an opacity hack ReactPrism.css uses --prism-keyword, --prism-selector etc. which switch correctly between light and dark palettes; syntax highlighting should use the same variable family
N69 Mention Highlight — <input type="color"> Instead of HexColorPickerPopOut General.tsx 644675 Raw <input type="color"> with hardcoded pixel dimensions; OS-native color picker chrome renders completely differently from the rest of settings UI — FIXED: replaced with <HexColorPickerPopOut> + <HexColorPicker> (react-colorful) behind a folds <Button> trigger showing a color swatch; the picker's built-in onRemove replaces the separate Reset button PowersEditor.tsx:125143 establishes <HexColorPickerPopOut picker={<HexColorPicker ...>}> as the codebase's color-picking pattern; Reset button should be <Button size="300" variant="Secondary" radii="300">
N70 ChatBgGrid / SeasonalBgGrid — Raw <button> Elements General.tsx 16481689 / 17111742 Both pickers use raw HTML <button> elements with hardcoded width: toRem(76), height: toRem(50/56), borderRadius: toRem(8), border: 2px solid rgba(...) — no focus ring via folds, no variant prop, no hover state from the design system — FIXED: chrome (radius, border, hover, keyboard :focus-visible ring, selected state via data-selected) moved to a shared BgSwatch.css.ts using config/color tokens; only the per-swatch size + live preview background remain inline (these are inherently custom preview tiles, not folds MenuItem/Chip candidates) Native Cinny theme pickers use folds <MenuItem> or <Chip> which respond to theme and provide focus/hover states automatically

🟡 Additional Minor Findings

# Area File Lines Issue Native Pattern
N71 Call Prescreen Text CallView.tsx 6385 ChannelFullMessage and AlreadyInCallMessage use <Text style={{ color: color.Critical/Warning.Main }}> inline instead of folds <Badge variant="Critical/Warning">WON'T FIX (deliberate): these are full, centered explanatory sentences ("Channel Full (N/M) — Wait for someone to leave…"), not short labels. A Badge is for compact chips like "N Live"; wrapping a sentence in one is visually wrong. They already use folds color.* tokens. The sibling LivekitServerMissingMessage/NoPermissionMessage use the same (un-flagged) pattern. The "N Live" badge directly above (line 127) correctly uses <Badge variant="Critical" fill="Solid" size="400">
N72 Mute MenuItem Icon RoomNavItem.tsx 454466 "Mute" <MenuItem> places bell-mute icon as a raw child node instead of using the before prop — FIXED: moved Icons.BellMute to before prop Every other <MenuItem> in both RoomNavItemMenu and RoomMenu places its leading icon in the before prop
N73 Pending Requests Header MembersDrawer.tsx 415422 "Pending Requests" section header is bare <Text> with inline padding instead of className={css.MembersGroupLabel}FIXED: now uses className={css.MembersGroupLabel} like every other section header Power-level group labels at lines 506519 use className={css.MembersGroupLabel} for all other section headers in the same virtualizer list
N74 Emoji Prefix Span RoomNavItem.tsx 730736 Emoji prefix rendered as raw <span style={{ fontSize: '1.15em', lineHeight: 1 }}> inside a <Text> node — FIXED: removed the emoji-splitting span; the room name (including any leading emoji) now renders directly inside <Text> All other nav item text uses folds <Text size="Inherit"> or similar — no raw <span> with em-based font-size override exists elsewhere in the sidebar
N75 Room Name Override / Star Indicators RoomNavItem.tsx 741757 Pencil and star indicator icons are embedded inside the name <Box as="span">, giving them the same visual baseline as the room name text — WON'T FIX (deliberate): an inline favorite-star / local-name marker adjacent to the name is a deliberate, common design (cf. Element/Slack pinned-name markers). Moving them to the far right would collide with the unread/notification indicators already there and risks layout regressions. Low value, real regression risk. Native sidebar status indicators (unread count, notification mode icon) are placed to the far right of the item, never inside the name text span group
N76 Report Modals — Extra Cancel Button ReportRoomModal.tsx / ReportUserModal.tsx 189191 / 195197 Both custom report modals include a "Cancel" <Button> in the footer row — FIXED: removed the Cancel button; dismissal is via the header × / click-outside, matching MessageReportItem Native MessageReportItem (Message.tsx:675691) has no Cancel button — dismissal is via × header button or click-outside only
N77 Search Filter Inline Lambdas SearchFilters.tsx 480, 625 SelectSenderButton and DateRangeButton trigger chips use inline onClick arrow functions — WON'T FIX (deliberate): purely a code-style nit with zero user-facing or behavioural impact. Inline arrow handlers are idiomatic React and used throughout this very file; extracting them yields no functional benefit. OrderButton (line 58) and SelectRoomButton (line 195) both extract a named const handleOpenMenu: MouseEventHandler<HTMLButtonElement> handler — bypassing the type annotation in the inline form
N78 HasLink Chip Active Color SearchFilters.tsx 755 HasLink active state uses variant="Primary" (blue); all boolean scope-toggle chips in the same bar use variant="Success" (green) with outlinedFIXED: changed to variant={containsUrl ? 'Success' : 'SurfaceVariant'} outlined={!!containsUrl} variant="Success" outlined is the established active-state pattern for boolean toggles in the filter bar
N79 Server Notice Chip Radii RoomViewHeader.tsx 570 <Chip size="400" radii="Pill">Pill radii on a room-type label — FIXED: changed to radii="300" Room/space type labels in lobby (RoomItem.tsx:83, SpaceItem.tsx:63) use radii="300"; radii="Pill" is for filter/tag chips only
N80 Server Support Contact Layout About.tsx 172239 Homeserver support contacts rendered as raw <Box direction="Column"> with <Text as="a"> pairs — custom label/link layout — WON'T FIX (deliberate): a contact is role → {matrix_id?, email?, …} (one-to-many links per role), which doesn't map onto SettingTile's single title/description/after slots without contortion. The current layout already uses folds Box/Text/SequenceCard + tokens and Text as="a" (a valid folds pattern); no undefined vars or raw HTML chrome. All other <SequenceCard> content in About.tsx and General.tsx uses <SettingTile title="..." description="..." after={...}> as the content unit
N81 Background Picker Grid — No Responsive Layout General.tsx 17071742 Fixed width: toRem(76) flex-wrap cells with no minWidth floor or CSS grid auto-fill — SeasonalBgGrid's 13 items produce a visually lopsided orphan last row at any viewport width Cinny's native grids use grid-template-columns: repeat(auto-fill, minmax(N, 1fr)) or equivalent for responsive fill
N82 Join/Leave Sounds Auto-Preview General.tsx 15921609 Selecting a sound in the dropdown immediately plays a preview, but no UI affordance (button label, description text, or "▶ Preview" button) communicates this to the user Settings tiles with side effects on selection (theme picker, chat background) show a live visual preview or a dedicated control explaining the side effect

Round 3 — Rich Topic Editor, RemindMe Dialog, Composer Toolbar, Voice Recorder, Uploads, Location, Mention Highlight

🔴 Additional Major Findings

N83 — Rich Topic Formatting Toolbar: Raw <button> Elements with Fully Inline Styles

  • File: src/app/features/common-settings/general/RoomProfile.tsx, lines 335358
  • Status: FIXED — replaced raw <button> elements with <Button size="300" radii="300" variant="Secondary" fill="Soft"> with styled <Text> children for B/I/S/code labels
  • Issue: The four formatting buttons (B, I, S, `) in the room topic editor are plain HTML <button> elements with entirely inline styles: manual border, borderRadius, background, color, cursor, fontSize, fontWeight, fontStyle, fontFamily, lineHeight. They bypass the folds design token system completely — no variant, size, or radii props, no theme-reactive hover/focus states.
  • Root Cause: Custom addition without referencing folds primitives.
  • Fix: Replace with <IconButton type="button" size="300" radii="300" variant="Surface" fill="Soft"> matching the emoji-picker trigger immediately above them at line 285, which already uses the correct pattern.

N84 — Topic Preview in Room Settings Renders Plain Text Instead of formatted_body

  • File: src/app/features/common-settings/general/RoomProfile.tsx, lines 457461
  • Status: FIXED — read-mode topic now checks topic.format === 'org.matrix.custom.html' and renders parse(sanitizeCustomHtml(topic.formatted_body)), matching RoomTopicViewer and all other display sites
  • Issue: The read-mode topic display wraps topic.topic (the plain-text field) in <Linkify> and never reads formatted_body. However buildTopicContent() (lines 8289) intentionally stores both topic and formatted_body under org.matrix.custom.html. After the user saves a formatted topic, the preview panel immediately shows the stripped plain-text version — the formatting appears to disappear within the same settings panel.
  • Root Cause: The existing RoomTopicViewer component (src/app/components/room-topic-viewer/RoomTopicViewer.tsx:2451) already checks topic.format === 'org.matrix.custom.html' and pipes formatted_body through sanitizeCustomHtml. This component is used everywhere else (RoomIntro, LobbyHero, RoomItem, Invites, etc.) but not in Room Settings.
  • Fix: Replace the inline plain-text render with <RoomTopicViewer topic={roomTopic}> to match all other display sites.

🟠 Additional Moderate Findings

# Area File Lines Issue Native Pattern
N85 RemindMe Dialog Shell RemindMeDialog.tsx 6981 Dialog shell is <Box role="dialog"> with background, borderRadius, boxShadow, overflow all set as inline styles using token lookups. Corner radius is config.radii.R400 which differs from the R300 embedded in <Dialog variant="Surface">FIXED: shell replaced with <Dialog variant="Surface" style={modalStyle}>; removed the inline background/borderRadius/boxShadow/overflow and the now-unused color import All small message-action dialogs (LeaveRoomPrompt, LogoutDialog, JoinAddressPrompt, PowerChip, DeleteMessageItem) use <Dialog variant="Surface" style={modalStyle}> as the shell
N86 RemindMe Preset Buttons RemindMeDialog.tsx 111117 The four preset time choices (20 min, 1 hr, 3 hr, tomorrow) use <MenuItem size="300" radii="300">MenuItem is a navigation primitive tied to menu/menubar ARIA roles; placing it inside role="dialog" is an invalid ARIA combination — FIXED: each preset is now a folds <Button variant="Secondary" fill="Soft" radii="300">, resolving the invalid menuitem-in-dialog ARIA Dialog action choices use <Button> (delete/leave/logout dialogs) or <Chip> (selection choices). No other dialog in the codebase uses MenuItem for action items
N87 Composer Toolbar Toggle Pattern General.tsx 11001114 Per-button toolbar toggles (Format, Emoji, Sticker, GIF, Location, Poll, Voice, Schedule) use <Chip variant="Primary"/"Secondary" radii="Pill"> in a wrap grid — a compact chip-toggle grid inside a SettingTile, different from every adjacent row The three sibling tiles in the same Editor() function (ENTER for Newline, Markdown, Formatting Toolbar) all use <SettingTile after={<Switch variant="Primary">}>. 15+ other binary settings in the file use the Switch pattern
N88 Voice Recorder Recording State VoiceMessageRecorder.tsx 195, 206, 240, 276 Recording container background is var(--bg-surface-variant), the live pulse dot is var(--tc-danger-normal), waveform bars are var(--tc-primary-normal) — custom Lotus CSS vars that may not exist in folds themes, falling back to transparent/black — FIXED: replaced with color.SurfaceVariant.Container, color.Critical.Main, color.Primary.Main Native message components use JS-accessible color.* tokens that are always populated regardless of theme class
N89 Voice Recorder Preview Audio VoiceMessageRecorder.tsx 282283 Preview state renders bare <audio src={previewUrl} controls> — native browser element with inconsistent cross-browser chrome — FIXED: replaced with <audio ref> + folds <IconButton> play/pause toggle; onEnded resets playing state Native audio messages use folds Attachment/AttachmentContent layout wrappers; pre-send preview should use <IconButton> play/pause controls
N90 Mention Highlight Contrast Formula App.tsx 3640 Auto-computed text color (black/white) uses simplified luma (0.299r + 0.587g + 0.114b)/255 > 0.5 — not WCAG 2.1 relative luminance (which requires gamma linearization) — FIXED: replaced with WCAG 2.1 relative luminance formula using ((c+0.055)/1.055)^2.4 gamma linearization; threshold moved from 0.5 to 0.179 Folds color.*.OnContainer tokens are manually curated to pass WCAG AA 4.5:1 contrast ratios; custom computation must match this guarantee

🟡 Additional Minor Findings

# Area File Lines Issue Native Pattern
N91 Upload Card Caption Input UploadCardRenderer.tsx 356376 Caption input is raw <input type="text"> with hardcoded inline CSS using Lotus-specific vars not in folds — FIXED: replaced with folds <Input variant="Secondary" size="300" radii="300"> Other text inputs in the UI use folds <Input size="300" radii="300"> with folds-token props for all sizing and color
N92 Location "Open Location" Button MsgTypeRenderers.tsx 534547 "Open Location" action link uses <Chip as="a"> — compact badge-sized element — FIXED: replaced with <Button as="a" variant="Secondary" fill="Solid" radii="300" size="400"> matching FileContent pattern FileContent.tsx uses <Button variant="Secondary" fill="Solid" radii="300" size="400"> for "Open File"/"Open PDF"
N93 Location Coordinates Text MsgTypeRenderers.tsx 532 <Text size="T300" style={{ opacity: 0.65 }}> — hardcoded non-standard opacity — FIXED: replaced with <Text size="T300" priority="300"> Secondary text uses folds priority prop; 0.65 is outside the token scale
N94 Mention Highlight Border Invisible App.tsx 41 --mention-highlight-border is set to the same value as --mention-highlight-bg — the border is invisible — FIXED: border is now rgba(r,g,b,0.5) — same hue as the background at 50% opacity, always visible In folds, color.*.ContainerLine is always a lighter/muted sibling of color.*.Container, providing the 1px outline that gives mention chips visual definition