title: Design system
description: Slirc themes, branding, icons, typography, and app components.
crumb: Reference
lead: The current app uses Catppuccin Mocha and Latte, with Slirc lime in both. This reference documents shipped styles and their source files.

<link rel="stylesheet" href="/style.css" />
<style>
html, body { height: auto; overflow: visible; }
body { height: auto; font: 1rem/1.6 var(--ui); }
.topbar .brand { display: flex; min-width: 0; min-height: 0; }
.topbar .menu-toggle { border: 0; border-radius: .55rem; background: transparent; color: var(--muted); padding: 0; }
.ds-example { display: grid; gap: .75rem; margin: 1rem 0; padding: 1rem; border: 1px solid var(--line); border-radius: .7rem; background: color-mix(in srgb, var(--panel) 45%, transparent); }
.ds-row { display: flex; flex-wrap: wrap; align-items: center; gap: .6rem; }
.ds-example h2, .ds-example h3 { padding: 0; border: 0; }
.ds-source { color: var(--muted); font-size: .875rem; }
.ds-theme-preview .profile-dialog-card { flex: 1 1 12rem; width: auto; max-width: 20rem; gap: var(--space-2); padding: var(--space-4); border-radius: var(--radius-lg); font-size: var(--text-base); box-shadow: none; }
.ds-theme-preview .field-hint { font-size: var(--text-sm); }
.ds { max-width: 56rem; }
.ds p, .ds ul, .ds ol, .ds table, .ds .callout { max-width: none; }
.ds table { display: block; overflow-x: auto; scrollbar-width: none; }
.ds table::-webkit-scrollbar { display: none; }
.ds-stack code { display: inline-block; padding-block: .3rem; line-height: 1.55; }
.ds table code { white-space: nowrap; }
.ds-swatch { display: inline-block; width: 1.5rem; height: 1.5rem; border: 1px solid var(--line); border-radius: .3rem; vertical-align: middle; }
/* Selector and both palettes share one row on desktop and wrap on narrower screens. */
.ds-theme-preview { display: flex; flex-wrap: wrap; align-items: stretch; gap: var(--space-3); }
.ds-theme-preview .ds-palette { flex: 0 1 auto; min-width: 0; padding: var(--space-3); }
.ds-theme-preview .ds-palette .ds-row { gap: .5rem .35rem; }
.ds-palette { padding: var(--space-4); border: 1px solid var(--line); border-radius: var(--radius-lg); }
.ds-palette > strong { display: block; margin-bottom: var(--space-3); }
.ds-palette .ds-row > span { display: grid; justify-items: center; text-align: center; gap: var(--space-1); font-size: var(--text-xs); }
.ds-palette-dark { background: #11111b; color: #cdd6f4; --line: #313244; }
.ds-palette-light { background: #eff1f5; color: #4c4f69; --line: #ccd0da; }
.ds-chat { font-family: "JetBrains Mono", ui-monospace, "Cascadia Code", "SFMono-Regular", Menlo, monospace; }
/* Fixed app surfaces shown in flow; mention styling mirrors the current shell override. */
.ds-example > .profile-dialog-card + .settings-toggle { width: min(36rem, 100%); }
.ds-example .composer { position: static; width: auto; left: auto; bottom: auto; translate: none; }
.ds-example .msg .body .mention { background: var(--mention-fill); color: var(--mention-ink); padding: 0; font: inherit; opacity: 1; }
.ds-example .glass-toast, .ds-example .reaction-tooltip { position: static; translate: none; }
/* Floating menus, popovers, and phone chrome shown in flow instead of at their runtime anchors. */
.ds-example :is(.channel-menu, .channel-flyout, .message-menu, .reaction-picker, .command-palette, .link-popover, .header-dm-list, .thread-tooltip, .typing-row, .bot-thinking, .new-below, .mobile-nav) { position: static; inset: auto; translate: none; margin: 0; }
.ds-example .header-dm-list { width: max-content; }
.ds-example .mobile-nav { width: min(24rem, 100%); }
.ds-example .command-palette { width: min(24rem, 100%); } /* the app caps it at 384px on desktop (popups.ts) */
.ds-example .new-below { justify-self: start; }
.ds-example .glass-toast { justify-self: start; max-width: 34rem; }
.ds-example .add-prompt { margin-left: 0; }
.ds-example .sheet-backdrop { position: relative; inset: auto; z-index: auto; width: min(24rem, 100%); min-height: 18rem; overflow: hidden; border-radius: var(--radius-lg, 1rem); }
.ds-example .ds-title { display: block; margin: .8rem 0 .5rem; font: 700 1.25rem/1.3 var(--ui); }
.ds-example .delete-confirmation .ds-title { margin-top: 0; }
.ds-example .join-heading { font-weight: 700; }
.ds-example > .notice-card, .ds-example > .auth-card { width: min(26rem, 100%); }
.ds-branding { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.ds-branding > div { padding: 1rem; border-radius: .7rem; display: grid; gap: 1rem; }
.ds-branding img { max-width: 100%; height: auto; }
.ds-branding .ds-mark { width: 3rem; }
.ds-branding .ds-lockup { width: 14rem; }
@media (max-width: 500px) { .ds-branding { grid-template-columns: 1fr; } }
/* Glass needs something behind it to read as glass: examples that blur sit on a dusk cloud photo
   (Diana @didsss, Pexels 10064781, Pexels license), cropped to 1600x1000 and tinted per theme. */
.ds-example:has(.channel-menu, .message-menu, .command-palette, .link-popover, .header-dm-list, .sheet-backdrop, .glass-toast, .mobile-nav, .composer, .typing-row, .reaction-tooltip, .thread-tooltip, .notify-banner, .new-below) { background: linear-gradient(#11111b59, #11111b59), url("/docs/glass-clouds.webp") center / cover; }
:root[data-theme="light"] .ds-example:has(.channel-menu, .message-menu, .command-palette, .link-popover, .header-dm-list, .sheet-backdrop, .glass-toast, .mobile-nav, .composer, .typing-row, .reaction-tooltip, .thread-tooltip, .notify-banner, .new-below) { background: linear-gradient(#eff1f526, #eff1f526), url("/docs/glass-clouds.webp") center / cover; }
</style>
<div class="ds">
<h2>Source files</h2>
<p>The app shell is assembled from <code>app/partials/*.html</code> into <code>app/public/index.html</code> (generated; never edit directly). Alpine components live in <code>app/client/</code> and ship as <code>/client.js</code>. Shared tokens and component styles are in <code>app/public/style.css</code>; the shell partials also carry effective style overrides. <code>app/public/theme.js</code> applies the appearance preference before CSS and first paint.</p>
<p>Examples and tables on this page are hand-written. Linking <code>/style.css</code> keeps the previews accurate but does not update prose or markup; edit this page when components change.</p>
<h2>Appearance</h2>
<p>Settings → Appearance offers System, Light, and Dark. System is the default and follows the device preference live; an explicit choice overrides it. The choice saves to your account and follows you across devices on their next load. <code>localStorage</code> under <code>slirc-theme</code> supplies the first paint and signed-out pages; after sign-in, the account preference takes over. Changes apply immediately. Failed saves keep the local choice without an error dialog. Blocked storage still allows changes on the current page. Invalid or missing preferences use System.</p>
<p><code>theme.js</code> sets <code>data-theme</code> and <code>data-theme-preference</code> on <code>&lt;html&gt;</code>, native <code>color-scheme</code>, and the browser theme color before the stylesheet loads. Theme selection must not wait for sign-in or Alpine.</p>
<div class="ds-example ds-theme-preview"><div class="profile-dialog-card"><div class="form-fields"><div class="form-field"><label>Preview theme<select aria-describedby="theme-preview-hint" onfocus="this.value = document.documentElement.dataset.theme || 'dark'" onchange="document.documentElement.dataset.theme = this.value"><option value="dark">Dark</option><option value="light">Light</option></select></label><p class="field-hint" id="theme-preview-hint">Doesn&#39;t change your settings.</p></div></div></div><div class="ds-palette ds-palette-dark"><strong>Dark</strong><div class="ds-row"><span><span class="ds-swatch" style="background:#11111b"></span>Canvas</span><span><span class="ds-swatch" style="background:#181825"></span>Panel</span><span><span class="ds-swatch" style="background:#cdd6f4"></span>Text</span><span><span class="ds-swatch" style="background:#a6e3a1"></span>Fill</span><span><span class="ds-swatch" style="background:#a6e3a1"></span>Accent</span><span><span class="ds-swatch" style="background:#94e2d5"></span>Links</span><span><span class="ds-swatch" style="background:#f38ba8"></span>Danger</span></div></div><div class="ds-palette ds-palette-light"><strong>Light</strong><div class="ds-row"><span><span class="ds-swatch" style="background:#eff1f5"></span>Canvas</span><span><span class="ds-swatch" style="background:#e6e9ef"></span>Panel</span><span><span class="ds-swatch" style="background:#4c4f69"></span>Text</span><span><span class="ds-swatch" style="background:#a6e3a1"></span>Fill</span><span><span class="ds-swatch" style="background:#237719"></span>Accent</span><span><span class="ds-swatch" style="background:#2b726a"></span>Links</span><span><span class="ds-swatch" style="background:#af425a"></span>Danger</span></div></div></div>
<h2>Color and surfaces</h2>
<p>Dark uses Catppuccin Mocha; light uses Latte neutrals and Slirc lime. Brand fills and readable accent ink are separate: pastel lime works as a fill with dark text, not as small text on a light canvas. Keep lime under roughly ten percent of a typical screen.</p>
<table><thead><tr><th>Token</th><th>Dark</th><th>Light</th><th>Use</th></tr></thead><tbody>
<tr><td><code>--bg</code></td><td><code>#11111b</code></td><td><code>#eff1f5</code></td><td>Canvas, inputs.</td></tr>
<tr><td><code>--bg-glow</code></td><td>#1e1e2e → canvas</td><td>#fff → canvas</td><td>Radial background, including boot.</td></tr>
<tr><td><code>--panel</code></td><td><code>#181825</code></td><td><code>#e6e9ef</code></td><td>Chrome and glass tint.</td></tr>
<tr><td><code>--panel-hi</code></td><td><code>#313244</code></td><td><code>#e6e9ef</code></td><td>Raised controls.</td></tr>
<tr><td><code>--secondary-fill</code></td><td><code>var(--panel-hi)</code></td><td><code>#ccd0da</code></td><td>Secondary button fill. Light uses Surface0 because panel-hi equals panel.</td></tr>
<tr><td><code>--secondary-fill-hover</code></td><td>Panel-hi, 15% text</td><td><code>#bcc0cc</code></td><td>Secondary button hover and focus.</td></tr>
<tr><td><code>--line</code></td><td><code>var(--panel-hi)</code></td><td><code>#ccd0da</code></td><td>Neutral borders; solid Surface0 in light.</td></tr>
<tr><td><code>--text</code></td><td><code>#cdd6f4</code></td><td><code>#4c4f69</code></td><td>Main text.</td></tr>
<tr><td><code>--muted</code></td><td><code>#7f849c</code></td><td><code>#65687e</code></td><td>Secondary copy; contrast-adjusted Latte Subtext0.</td></tr>
<tr><td><code>--brand-fill</code></td><td><code>#a6e3a1</code></td><td><code>#a6e3a1</code></td><td>Primary buttons and composer circles.</td></tr>
<tr><td><code>--on-brand</code></td><td><code>#11111b</code></td><td><code>#4c4f69</code></td><td>Dark labels/glyphs on lime.</td></tr>
<tr><td><code>--brand</code></td><td><code>#a6e3a1</code></td><td><code>#237719</code></td><td>Accent ink, focus, presence, success.</td></tr>
<tr><td><code>--accent</code></td><td><code>var(--brand)</code></td><td><code>var(--brand)</code></td><td>Nick accent alias.</td></tr>
<tr><td><code>--mine</code></td><td><code>#94e2d5</code></td><td><code>#2b726a</code></td><td>Links, code, personal state.</td></tr>
<tr><td><code>--danger</code></td><td><code>#f38ba8</code></td><td><code>#af425a</code></td><td>Errors and destructive actions; same hue as dark pink, softer than Latte Red.</td></tr>
<tr><td><code>--warning</code></td><td><code>#f9e2af</code></td><td><code>#856000</code></td><td>Pending and verification states.</td></tr>
<tr><td><code>--attention</code></td><td><code>#fab387</code></td><td><code>#a34b09</code></td><td>Agent input-needed state.</td></tr>
<tr><td><code>--shadow</code></td><td><code>#0008</code></td><td><code>#4c4f691a</code></td><td>Shared floating surfaces.</td></tr>
<tr><td><code>--mention-fill</code></td><td>Brand at 16%</td><td><code>#c3e9c3</code></td><td>Normal mention fill.</td></tr>
<tr><td><code>--mention-ink</code></td><td><code>var(--brand)</code></td><td><code>var(--text)</code></td><td>Normal mention label.</td></tr>
</tbody></table>
<p>Light active channels use <code>#d9eddc</code> with an inset 1px green border at 18% opacity (<code>#2377192e</code>). Normal mentions use <code>#c3e9c3</code>; personal mentions <code>#b8e7b6</code>. Links stay underlined, and primary, destructive, and focus styles keep full-strength outlines. Light link teal has 5.00:1 contrast on Base and 4.64:1 on Mantle; check contrast before reusing it on Surface0 controls. The lightbox and embedded players stay dark.</p>
<h2>Typography, spacing, and layout</h2>
<p>New and edited components use the shared tokens in <code>app/public/style.css</code> instead of literal values; existing components migrate when touched. A literal needs a concrete layout or accessibility reason.</p>
<h3>Fonts</h3>
<p>JetBrains Mono is used when installed; no webfont is loaded. Desktop nicks have a system-font override.</p>
<table><thead><tr><th>Token</th><th>Stack</th><th>Use</th></tr></thead><tbody>
<tr><td><code>--ui</code></td><td class="ds-stack"><code>system-ui<br>-apple-system<br>BlinkMacSystemFont<br>"Segoe UI"<br>sans-serif</code></td><td>Chrome, headings, buttons, menus, cards.</td></tr>
<tr><td><code>--mono</code></td><td class="ds-stack"><code>"JetBrains Mono"<br>ui-monospace<br>"Cascadia Code"<br>"SFMono-Regular"<br>Menlo<br>monospace</code></td><td>Messages, composer, code, fences.</td></tr>
</tbody></table>
<p>Line height is 1.45 for UI body and about 1.55 for messages.</p>
<h3>Type</h3>
<p>Six steps, with no 13px step on purpose. The metadata floor is <code>--text-xs</code> (12px); only badge counts go down to <code>--text-2xs</code> (11px).</p>
<table><thead><tr><th>Token</th><th>rem</th><th>px</th><th>Use</th></tr></thead><tbody>
<tr><td><code>--text-2xs</code></td><td><code>.6875rem</code></td><td><code>11px</code></td><td>Badges and counts.</td></tr>
<tr><td><code>--text-xs</code></td><td><code>.75rem</code></td><td><code>12px</code></td><td>Timestamps, hints, status, tooltips.</td></tr>
<tr><td><code>--text-sm</code></td><td><code>.875rem</code></td><td><code>14px</code></td><td>UI body, inputs, menus, button labels.</td></tr>
<tr><td><code>--text-base</code></td><td><code>1rem</code></td><td><code>16px</code></td><td>Messages.</td></tr>
<tr><td><code>--text-lg</code></td><td><code>1.25rem</code></td><td><code>20px</code></td><td>Sheet and dialog heads.</td></tr>
<tr><td><code>--text-xl</code></td><td><code>1.5rem</code></td><td><code>24px</code></td><td>Page and display heads.</td></tr>
</tbody></table>
<p>Tooltips, hints, and status use <code>--text-xs</code> (12px); button labels use <code>--text-sm</code> (14px). Exceptions: glyphs sized to a fixed container (avatar initials, badge digits), display sizes of 1.75rem and 2rem, and em sizes inside code. Light timestamps use lighter weight, not faded opacity.</p>
<h3>Spacing</h3>
<p>Gaps and padding step through six values. Pick the smallest step that separates two groups; skip a step to mark a new group.</p>
<table><thead><tr><th>Token</th><th>rem</th><th>px</th><th>Use</th></tr></thead><tbody>
<tr><td><code>--space-1</code></td><td><code>.25rem</code></td><td><code>4px</code></td><td>Icon to label, chip padding.</td></tr>
<tr><td><code>--space-2</code></td><td><code>.5rem</code></td><td><code>8px</code></td><td>Control gaps and padding.</td></tr>
<tr><td><code>--space-3</code></td><td><code>.75rem</code></td><td><code>12px</code></td><td>Compact panels, menu padding.</td></tr>
<tr><td><code>--space-4</code></td><td><code>1rem</code></td><td><code>16px</code></td><td>Standard panels and cards.</td></tr>
<tr><td><code>--space-5</code></td><td><code>1.5rem</code></td><td><code>24px</code></td><td>Section gaps, dialog padding.</td></tr>
<tr><td><code>--space-6</code></td><td><code>2rem</code></td><td><code>32px</code></td><td>Large separation between page groups.</td></tr>
</tbody></table>
<p>Use rem for type, gaps, padding, and radii; px for 1–2px borders and focus outlines. Touch targets stay at least 2.75rem (44px at the default root size); safe-area insets remain additive.</p>
<h3>Radius</h3>
<p>Radius grows with the surface, and a nested surface uses a smaller radius than its container.</p>
<table><thead><tr><th>Token</th><th>Value</th><th>Use</th></tr></thead><tbody>
<tr><td><code>--radius-xs</code></td><td><code>.25rem</code></td><td>Code chips, tiny marks.</td></tr>
<tr><td><code>--radius-sm</code></td><td><code>.4rem</code></td><td>Chips, copy buttons.</td></tr>
<tr><td><code>--radius-md</code></td><td><code>.55rem</code></td><td>Buttons, inputs, menu items.</td></tr>
<tr><td><code>--radius-lg</code></td><td><code>.75rem</code></td><td>Menus, popovers, cards.</td></tr>
<tr><td><code>--radius-xl</code></td><td><code>1rem</code></td><td>Dialogs, toasts.</td></tr>
<tr><td><code>--radius-pill</code></td><td><code>999px</code></td><td>Pills, badges, composer.</td></tr>
<tr><td><code>--radius-round</code></td><td><code>50%</code></td><td>Avatars and circular buttons.</td></tr>
</tbody></table>
<h3>Motion</h3>
<p>Every transition uses <code>--ease-standard</code>, <code>cubic-bezier(.2,.7,.2,1)</code>. Loops such as spinners and pulses keep their own durations.</p>
<table><thead><tr><th>Token</th><th>Duration</th><th>Use</th></tr></thead><tbody>
<tr><td><code>--dur-micro</code></td><td><code>.1s</code></td><td>Hover color and opacity.</td></tr>
<tr><td><code>--dur-fast</code></td><td><code>.15s</code></td><td>Menus, dialogs, toggles.</td></tr>
<tr><td><code>--dur-base</code></td><td><code>.2s</code></td><td>Sheets, slides, toasts.</td></tr>
<tr><td><code>--dur-entrance</code></td><td><code>.3s</code></td><td>Message entrance.</td></tr>
<tr><td><code>--dur-reveal</code></td><td><code>.5s</code></td><td>Banners and boot.</td></tr>
</tbody></table>
<h3>Shadows</h3>
<p>Elevation has three steps. Dark uses black alphas strong enough to lift glass off the canvas; light uses <code>var(--shadow)</code> (<code>#4c4f691a</code>), a Latte text tint that stays quiet on pale surfaces.</p>
<table><thead><tr><th>Token</th><th>Dark</th><th>Light</th><th>Use</th></tr></thead><tbody>
<tr><td><code>--shadow-sm</code></td><td class="ds-stack"><code>0 .3rem .9rem<br>#0006</code></td><td class="ds-stack"><code>0 .2rem .6rem<br>var(--shadow)</code></td><td>Typing row, chips, inline popovers.</td></tr>
<tr><td><code>--shadow-md</code></td><td class="ds-stack"><code>0 .5rem 1.5rem<br>#0009</code></td><td class="ds-stack"><code>0 .5rem 1.5rem<br>var(--shadow)</code></td><td>Menus, tooltips, link popover, new-below pill.</td></tr>
<tr><td><code>--shadow-lg</code></td><td class="ds-stack"><code>0 1.5rem 5rem<br>#000b</code></td><td class="ds-stack"><code>0 1rem 3rem<br>var(--shadow)</code></td><td>Command palette, header DM list, dialogs.</td></tr>
</tbody></table>
<h3>State fills</h3>
<p>Light needs solid Latte fills where dark can use brand alphas.</p>
<table><thead><tr><th>Token</th><th>Dark</th><th>Light</th><th>Use</th></tr></thead><tbody>
<tr><td><code>--active-fill</code></td><td>Brand at 13%</td><td><code>#d9eddc</code></td><td>Active channel or DM row.</td></tr>
<tr><td><code>--active-ring</code></td><td><code>transparent</code></td><td><code>#2377192e</code></td><td>Inset 1px border on the active row.</td></tr>
<tr><td><code>--mention-me-fill</code></td><td>Link teal at 22%</td><td><code>#b8e7b6</code></td><td>Mentions of you.</td></tr>
<tr><td><code>--avatar-fill</code></td><td><code>var(--panel-hi)</code></td><td><code>#ccd0da</code></td><td>Avatar fallback behind initials and photos.</td></tr>
</tbody></table>
<h3>Disabled</h3>
<p>Disabled controls fade and keep their layout and label. Busy is a separate state that hides the label behind a ring and keeps the control's size.</p>
<table><thead><tr><th>Token</th><th>Value</th><th>Use</th></tr></thead><tbody>
<tr><td><code>--disabled-opacity</code></td><td><code>.5</code></td><td>Disabled buttons, inputs, and menu items in both themes.</td></tr>
</tbody></table>
<h3>Layout</h3>
<p>Desktop has a 15rem conversation rail and an 860px maximum message column. Phone layout depends on coarse pointers, not width alone. Header height is <code>calc(3.625rem + env(safe-area-inset-top, 0px))</code>; phone navigation is <code>--nav-h: 4.1rem</code>; <code>--composer-space</code> is measured at runtime by <code>app/client/composer-editor.ts</code>, with CSS fallbacks before measurement.</p>
<h2>Branding</h2>
<p>Use the same lime mark in both themes. Light surfaces need dark lettering, dark surfaces white lettering. Preserve aspect ratio; do not invert the whole lockup. Informative logos get Slirc alt text; decorative marks beside visible branding get empty alt text.</p>
<div class="ds-branding"><div style="background:#11111b;color:#cdd6f4"><strong>Dark surface</strong><img class="ds-mark" src="/logo.webp" alt="" style="content:normal" /><img class="ds-lockup" src="/brand@2x.png" alt="Slirc" style="content:normal" /></div><div style="background:#eff1f5;color:#4c4f69"><strong>Light surface</strong><img class="ds-mark" src="/logo-light.webp" alt="" /><img class="ds-lockup" src="/brand-light@2x.png" alt="Slirc" /></div></div>
<p>The mark runs a green-to-blue gradient. Light keeps the same hues at 84% lightness and 75% saturation, so the edge holds on the pale canvas without turning neon; dark uses the original stops from <code>logo.svg</code>.</p>
<table><thead><tr><th>Stop</th><th>Dark</th><th>Light</th></tr></thead><tbody>
<tr><td>Green, start</td><td><span class="ds-swatch" style="background:#41F79B"></span> <code>#41F79B</code></td><td><span class="ds-swatch" style="background:#2ED882"></span> <code>#2ED882</code></td></tr>
<tr><td>Green, mid</td><td><span class="ds-swatch" style="background:#4EF3A8"></span> <code>#4EF3A8</code></td><td><span class="ds-swatch" style="background:#38D68E"></span> <code>#38D68E</code></td></tr>
<tr><td>Green, highlight</td><td><span class="ds-swatch" style="background:#76F7BC"></span> <code>#76F7BC</code></td><td><span class="ds-swatch" style="background:#55DD9F"></span> <code>#55DD9F</code></td></tr>
<tr><td>Blue, start</td><td><span class="ds-swatch" style="background:#43A3F9"></span> <code>#43A3F9</code></td><td><span class="ds-swatch" style="background:#2F89DB"></span> <code>#2F89DB</code></td></tr>
<tr><td>Blue, deep</td><td><span class="ds-swatch" style="background:#5C8DF9"></span> <code>#5C8DF9</code></td><td><span class="ds-swatch" style="background:#4172DD"></span> <code>#4172DD</code></td></tr>
<tr><td>Blue, highlight</td><td><span class="ds-swatch" style="background:#75B0F9"></span> <code>#75B0F9</code></td><td><span class="ds-swatch" style="background:#5492DF"></span> <code>#5492DF</code></td></tr>
</tbody></table>
<table><thead><tr><th>Asset</th><th>Use</th></tr></thead><tbody>
<tr><td><a href="/logo.svg">logo.svg</a></td><td>Vector mark.</td></tr>
<tr><td><a href="/logo.webp">logo.webp</a>, <a href="/logo.png">logo.png</a></td><td>Raster mark; boot and compact branding.</td></tr>
<tr><td><a href="/wordmark.svg">wordmark.svg</a>, <a href="/wordmark@2x.png">wordmark@2x.png</a></td><td>Standalone white lettering for dark surfaces.</td></tr>
<tr><td><a href="/brand@2x.png">brand@2x.png</a></td><td>Dark-surface lockup.</td></tr>
<tr><td><a href="/brand-light@2x.png">brand-light@2x.png</a></td><td>Light lockup, derived from the dark asset with white lettering changed to #4c4f69 and the mark darkened to light-surface contrast.</td></tr>
<tr><td><a href="/logo-light.webp">logo-light.webp</a></td><td>Light mark: <code>logo.webp</code> with HSL lightness at 84% and saturation at 75%. Light theme swaps it in with <code>content: url()</code>.</td></tr>
<tr><td><a href="/icons/slirc-light.svg">icons/slirc-light.svg</a></td><td>Light service bot mark, same treatment as the logo.</td></tr>
<tr><td><code>icons/ui/nav-{home,dms}-light.svg</code></td><td>Light mobile nav Home and DMs icons with the light gradient stops. Monochrome nav icons keep the darkening filter.</td></tr>
<tr><td><code>icons/bots/*-light.svg</code></td><td>Helper bot marks in pastel Latte for light: help <code>#5fd27c</code>, remind <code>#fb9a62</code>, poll <code>#b48af4</code>. Dark keeps the Mocha data-URI marks.</td></tr>
</tbody></table>
<h2>Icons</h2>
<p>UI controls use <a href="https://lucide.dev/">Lucide</a>, distributed under the <a href="https://lucide.dev/license">ISC licence</a>. The curated local sprite is <code>app/public/icons/lucide/sprite.svg</code>; <code>lucideIcon()</code> in <code>app/client/presentation.ts</code> emits trusted SVG markup. A name is usable only when its symbol exists in the sprite; missing names render empty.</p>
<div class="ds-example ds-row"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="/icons/lucide/sprite.svg#plus"></use></svg><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="/icons/lucide/sprite.svg#arrow-up"></use></svg><span>24×24 viewBox, 2px stroke, round caps/joins, currentColor.</span></div>
<p>Icon-only buttons need an accessible label; their SVG is decorative. Use Lucide for new controls. Text glyphs and emoji are not control-icon substitutes.</p>
<h2>Glass, layering, and motion</h2>
<p><code>--blur-light</code> is blur(3px), <code>--blur-medium</code> blur(9px), <code>--blur-heavy</code> blur(18px). Glass pairs blur with a panel tint for text contrast. If blur cannot reach content behind a panel, check ancestor filters and opacity for backdrop-root boundaries. Floating glass surfaces share one tint, <code>--glass-tint</code> (panel at 74%), and <code>--glass-filter</code>: in dark it blurs and darkens what is behind (<code>saturate(1.2) brightness(.75)</code>) so a panel always sits below the content it covers; light only blurs and uses quieter shadows. Fixed chrome (header, docked rooms rail, members panel, composer, mobile nav) shares <code>--glass-chrome</code> (panel at 60%); dialogs stay near-solid at 96%.</p>
<p>The overlay ladder lives in a second <code>:root</code> block: content 1, header 20, navigation menus 90/91, dialog 100, boot screen 102, <code>--z-tooltip</code> 120. Native dialogs use the browser top layer.</p>
<p>Motion tokens are under Typography, spacing, and layout. Component durations: <code>--dur-boot-pop</code> 450ms, <code>--dur-boot-exit</code> 150ms, <code>--dur-reaction</code> 380ms; dialogs use <code>--dur-fast</code>. Boot and reaction animations play once. Reduced motion removes nonessential entrances and transitions. Shared CSS carries view-transition and arrival rules the current shell disables; document effective shell behavior, not every dormant rule.</p>
<h2>Components</h2>
<h3>Buttons and loading</h3>
<div class="ds-example ds-row"><button class="btn-primary" type="button">Save changes</button><button class="btn-secondary" type="button">Cancel</button><button class="btn-danger" type="button">Delete</button><button class="btn-primary" type="button" disabled>Disabled</button><button class="btn-primary busy" type="button" aria-busy="true" aria-label="Saving">Save</button><button class="link-button" type="button">Resend</button></div>
<div class="ds-example ds-row"><button class="btn-secondary busy" type="button" aria-busy="true" aria-label="Sending">Send</button><button class="btn-danger busy" type="button" aria-busy="true" aria-label="Deleting">Delete</button><p class="profile-note loading"><span class="spinner" aria-hidden="true"></span>Loading agents…</p></div>
<p class="ds-source">Shared button classes: <code>btn-primary</code>, <code>btn-secondary</code>, <code>btn-danger</code>, <code>link-button</code>. Resting buttons have no borders. Busy (<code>.busy</code> with <code>aria-busy</code>) hides the label and shows a ring in the button's ink, so it keeps its width; disabled stays distinct; keyboard focus has an explicit full-strength outline. A standalone <code>spinner</code> leads a loading line in <code>profile-note loading</code>. Purpose-specific controls exist beyond these classes.</p>
<h3>Native forms and notifications</h3>
<div class="ds-example"><div class="profile-dialog-card"><div class="form-fields"><label>Display name<input type="text" value="Ada" /></label><div class="form-field"><label>Appearance<select aria-describedby="example-appearance-hint"><option>System</option><option>Light</option><option>Dark</option></select></label><p class="field-hint" id="example-appearance-hint">Syncs with your account.</p></div><p class="form-error" role="alert"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="/icons/lucide/sprite.svg#circle-alert"></use></svg><span>Could not save. Try again.</span></p><label class="form-consent"><input type="checkbox" checked /><span>Confirm resend</span></label></div></div><button class="settings-toggle" type="button" role="switch" aria-checked="true"><span><strong>Notifications</strong><small>On</small></span><span>Change</span></button></div>
<p class="ds-source">Native inputs, selects, and checkboxes use the effective color scheme. <code>form-fields</code> spaces fields; <code>form-field</code> groups a control with its hint. <code>form-error</code> pairs a tinted error surface with a decorative icon. <code>form-consent</code> keeps a native checkbox beside its label in a bordered row. Input borders stay quiet until focus; modal forms focus their first useful field. <code>settings-toggle</code> is a labelled switch; <code>prefs-toggle</code> is a disclosure, not a switch. Appearance is a native select, not a custom picker.</p>
<h3>Dialog</h3>
<div class="ds-example"><section class="notice-card delete-confirmation"><strong class="ds-title">Delete channel?</strong><p>Deleting #design removes its threads and history for everyone.</p><div class="pair-actions"><button class="btn-secondary" type="button">Cancel</button><button class="btn-danger" type="button">Delete</button></div></section></div>
<p class="ds-source">Real modal markup is <code>&lt;dialog class="custom-modal"&gt;</code> with a <code>notice-card</code> or form card; only the card is shown here, and its <code>h2</code> title is shown as plain text so it stays out of this page's contents. Native top-layer dialogs cancel on Escape, trap Tab among visible enabled controls, and restore the invoker. Destructive confirmation initially focuses safe Cancel. No alert/confirm/prompt.</p>
<h3>Cards and notices</h3>
<div class="ds-example"><section class="profile-dialog-card profile-screen-card admission-card" aria-label="Join Slirc"><div class="invite-heading"><p class="join-eyebrow">Invitation</p><p class="join-heading">Join Slirc</p><p class="join-note">Pick a display name and you're in. No password.</p></div><div class="form-fields"><label>Display name<input type="text" value="sam" /></label></div><div class="admission-actions"><button type="button" class="btn-primary">Join Slirc</button></div></section></div>
<p class="ds-source">Invitation and admission pages use <code>profile-dialog-card profile-screen-card admission-card</code>: an <code>invite-heading</code> with <code>join-eyebrow</code>, <code>join-heading</code>, and <code>join-note</code>, then the form and <code>admission-actions</code>. The real heading is an <code>h1</code>.</p>
<div class="ds-example"><section class="auth-card" aria-label="Sign in to Slirc"><img class="login-logo login-wordmark" src="/brand@2x.png" width="233" height="73" alt="Slirc" /><div class="auth-step auth-view"><div><form onsubmit="return false"><label>Email<input type="email" value="ada@example.com" /></label><button type="submit" class="btn-primary">Send sign-in link</button></form><div class="auth-divider" role="separator">or</div><button type="button" class="auth-secondary"><span class="menu-icon" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="/icons/lucide/sprite.svg#smartphone"></use></svg></span><span>Link with another device</span></button></div></div></section></div>
<p class="ds-source"><code>auth-card</code> sits in <code>custom-modal auth-modal</code> and shows one <code>auth-step</code> at a time; <code>collapsed</code> hides the others. <code>auth-divider</code> draws the rule around its label; <code>auth-secondary</code> is the bordered alternative action.</p>
<div class="ds-example"><section class="notice-card"><svg class="notice-icon" viewBox="0 0 24 24" aria-hidden="true"><use href="/icons/lucide/sprite.svg#mail"></use></svg><strong class="ds-title">Check your email</strong><p>We sent a sign-in link to ada@example.com.</p><div class="pair-actions"><button class="btn-secondary" type="button">Use another email</button></div></section></div>
<p class="ds-source"><code>notice-card</code> is the centered informational card: optional <code>notice-icon</code>, title, muted copy, then <code>pair-actions</code>. <code>delete-confirmation</code> left-aligns it for destructive dialogs.</p>
<div class="ds-example"><div class="notify-banner"><p>Could not reach the server. Retrying…</p></div></div>
<p class="ds-source"><code>notify-banner</code> is the transcript banner for errors and connection notes; it sticks to the top of the messages. <code>notify-banner-close</code> adds a dismiss. Connection and delivery errors live here; command and input feedback uses an auto toast.</p>
<div class="ds-example"><p class="invitation-empty">No invitations yet. Invite someone from a channel or <a href="/invitations">by email</a>.</p><div class="invitation-notice-error"><p class="profile-error" role="alert">Could not load invitations.</p><button type="button" class="btn-secondary">Retry</button></div></div>
<p class="ds-source">Empty states use a compact informative notice: a subtle bordered panel with muted copy (<code>invitation-empty</code>), or a light danger tint with its Retry inside for a load failure (<code>invitation-notice-error</code>). No stripe, no divider, no illustration.</p>
<h3>Messages, mentions, reactions, and code</h3>
<div class="ds-example ds-chat"><div class="msg"><span class="message-avatar"><span aria-hidden="true" style="background:#cba6f7">A</span></span><span class="time">09:41</span><span class="nick"><span>ada</span></span><span class="body"><span class="message-copy"><span class="body-seg"><span>Ask </span><span class="mention" role="button" tabindex="0">@ben</span><span> or </span><span class="mention me" role="button" tabindex="0">@you</span><span> about </span><code>v1.4.2</code><span>. </span><a href="/docs/messages">Read notes</a></span></span><span class="reaction-chips"><button class="reaction-chip" type="button" aria-label="Thumbs up, 3">👍<small>3</small></button><button class="reaction-chip mine" type="button" aria-label="Celebration, 1">🎉<small>1</small></button></span></span></div><div class="msg cont"><span class="body"><span class="message-copy"><span class="body-seg"><span>Continuation messages share the speaker run.</span></span></span></span></div><div class="msg"><span class="body"><pre class="fence"><code>bun run docs</code></pre></span></div><div class="date-divider">Today</div><div class="date-divider new-divider">New since your last visit</div></div>
<p class="ds-source"><code>msg</code>, <code>msg cont</code>, <code>message-entry</code>, <code>message-avatar</code>, <code>time</code>, <code>nick</code>, <code>body</code>. Desktop actions are <code>message-add</code> and <code>message-more</code>; phone uses long press. Mentions open the person card. Reactions carry counts, and <code>mine</code> marks your reaction. <code>fence</code> scrolls horizontally and Glow highlights escaped code. Message-segment markup must not insert whitespace: pre-wrap exposes it.</p>
<h3>Message outputs</h3>
<div class="ds-example ds-chat"><div class="msg"><span class="message-avatar"><span aria-hidden="true" style="background:#89b4fa">B</span></span><span class="time">09:42</span><span class="nick"><span>ben</span></span><span class="body"><span class="message-copy"><span class="body-seg"><span><span>Ship </span></span><span><strong>today</strong></span><span><span>, not </span></span><span><em>tomorrow</em></span><span><span>. Run </span></span><span><code>bun test</code></span><span><span> first, then read </span></span><span><a class="message-link" href="/docs/messages">the release notes</a></span></span></span></span></div><div class="msg cont"><span class="body"><span class="message-copy"><span class="body-seg"><span><blockquote>Docs build before every deploy.</blockquote></span><span><span>Agreed, adding it to the checklist.</span></span></span></span></span></div><div class="msg cont"><span class="body"><span class="message-copy"><span class="body-seg"><span><pre class="fence"><code language="js"><strong>const</strong> <b>ok</b> <i>=</i> <strong>await</strong> <b>fetch</b><i>(</i><em>"/api"</em><i>)</i> <sup>// ping</sup>
<strong>if</strong> <i>(</i><i>!</i>ok<i>)</i> <strong>return</strong> <strong>false</strong></code></pre></span></span></span></span></div><div class="msg cont"><span class="body"><span class="message-copy"><span class="body-seg"><span><pre class="fence"><code language="diff"><del>  old line</del>
<ins>  <strong>new</strong> line</ins></code></pre></span></span></span></span></div></div>
<p class="ds-source">Message Markdown renders <code>**bold**</code> as <code>strong</code>, <code>*italic*</code> as <code>em</code>, backticks as <code>code</code>, links as <code>message-link</code>, <code>&gt; </code> lines as <code>blockquote</code>, and fences as <code>pre.fence</code>. Glow highlights fences with plain elements, styled under <code>.fence code</code>: <code>strong</code>/<code>b</code> keywords and names in brand ink, <code>em</code> strings and numbers, <code>label</code> and <code>dfn</code> in link teal, <code>i</code> punctuation and <code>sup</code> comments muted, <code>ins</code> added lines in brand ink, <code>del</code> removed lines in faded danger.</p>
<div class="ds-example ds-chat"><div class="msg"><span class="message-avatar"><span aria-hidden="true" style="background:#fab387">R</span></span><span class="time">14:00</span><span class="nick"><span>remind</span></span><span class="body"><span class="message-copy"><span class="body-seg"><span><blockquote>Ship the docs build before Friday</blockquote></span><span><a class="message-link" href="/#design">View in #design</a></span></span></span></span></div><div class="msg"><span class="message-avatar"><span aria-hidden="true" style="background:#fab387">R</span></span><span class="time">14:01</span><span class="nick"><span>remind</span></span><span class="body"><span class="message-copy"><span class="body-seg"><span><span>Your reminders (2):
1. In 20 minutes, 14:21 · stretch
2. Tomorrow at 9:00 · "Ship the docs build before Friday" · </span></span><span><a class="message-link" href="/#design">View in #design</a></span><span><span>
To cancel one: @remind cancel 1</span></span></span></span></span></div></div>
<p class="ds-source">A fired Remind me reminder arrives in the remind DM as a quote of the message, then a link back to it. Links to a message on this instance show a short label: <strong>View in #channel</strong>, <strong>View in thread</strong>, or <strong>View in DM with nick</strong>; other URLs show in full. <code>@remind list</code> answers with one numbered line per reminder, joined by <code>·</code>, in a single message; pre-wrap keeps its line breaks.</p>
<div class="ds-example ds-chat"><div class="msg error"><span class="time">09:44</span><span class="nick"><span>slirc</span></span><span class="body"><span class="message-copy"><span class="body-seg"><span><span>Could not join #ops: invite only.</span></span></span></span></span></div><div class="msg"><span class="message-avatar"><span aria-hidden="true" style="background:#cba6f7">A</span></span><span class="time">09:45</span><span class="nick"><span>ada</span></span><span class="body"><span class="message-copy"><span class="body-seg"><span><span>Pushing the fix now</span></span></span></span><span class="outbox-status" data-state="failed"><span>failed: connection lost</span> <button type="button" class="outbox-retry">Retry</button> <button type="button" class="outbox-discard">Discard</button></span></span></div><div class="msg pending"><span class="time">09:45</span><span class="nick"><span>ada</span></span><span class="body"><span class="message-copy"><span class="body-seg"><span><span>And the docs</span></span></span></span><span class="outbox-status" data-state="sending"><span>sending</span><span class="typing-dots" aria-hidden="true"><i></i><i></i><i></i></span></span></span></div></div>
<p class="ds-source"><code>msg error</code> colors a whole row in danger ink. A message that did not send keeps its text and gets <code>outbox-status</code> with <code>data-state</code>: saving, queued, and sending are muted with dots; failed and uncertain are danger ink with underlined <code>outbox-retry</code> and <code>outbox-discard</code> text buttons. Uncertain asks for consent first, since a resend may duplicate.</p>
<h3>Composer</h3>
<div class="ds-example"><form class="composer" role="group" aria-label="Message composer" onsubmit="return false"><div class="composer-editor"><div class="composer-input" contenteditable="true" role="textbox" aria-multiline="true" aria-label="Message #design" data-placeholder="Type or / for commands"></div></div><button class="composer-plus" type="button" aria-label="Add photo"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="/icons/lucide/sprite.svg#plus"></use></svg></button><button class="composer-send" type="button" aria-label="Send message"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="/icons/lucide/sprite.svg#arrow-up"></use></svg></button></form></div>
<p class="ds-source">Lime circles with dark glyphs in both themes. Phone hits are at least 44px. The composer grows for multiline drafts; phone geometry overrides the desktop pill. Opening a conversation must not open the phone keyboard. Autocomplete, Markdown shortcuts, pending uploads, and edit state belong to the composer feature; Cmd/Ctrl+B, Cmd/Ctrl+I, and Cmd/Ctrl+Alt+C wrap the selection or caret word in Markdown.</p>
<h3>Composer states</h3>
<div class="ds-example ds-chat"><div class="add-prompt" data-add-prompt="sam"><p class="add-prompt-text"><span><span>@sam isn't in #design. </span></span><span><span class="add-prompt-who"><span class="mention-avatar" aria-hidden="true"><img src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20100%20100%22%3E%3Crect%20width%3D%22100%22%20height%3D%22100%22%20rx%3D%2250%22%20fill%3D%22%2394e2d5%22%2F%3E%3Ctext%20x%3D%2250%22%20y%3D%2254%22%20fill%3D%22%2311111b%22%20font-family%3D%22system-ui%2Csans-serif%22%20font-size%3D%2248%22%20font-weight%3D%22750%22%20text-anchor%3D%22middle%22%20dominant-baseline%3D%22middle%22%3ES%3C%2Ftext%3E%3C%2Fsvg%3E" alt="" /></span><span>@sam</span></span></span><span class="add-prompt-private" tabindex="0" role="img" aria-label="Only visible to you" data-tooltip="Only visible to you"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="/icons/lucide/sprite.svg#eye-off"></use></svg></span></p><div class="add-prompt-actions"><button type="button" class="add-prompt-add">Add</button><button type="button" class="add-prompt-dismiss">Dismiss</button></div></div><form class="composer" role="group" aria-label="Message composer" onsubmit="return false"><div class="composer-editor"><div class="composer-input" contenteditable="true" role="textbox" aria-multiline="true" aria-label="Message #design" data-placeholder="Type or / for commands">@sam can you check this?</div></div><button class="composer-plus" type="button" aria-label="Add photo"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="/icons/lucide/sprite.svg#plus"></use></svg></button><button class="composer-send" type="button" aria-label="Send message"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="/icons/lucide/sprite.svg#arrow-up"></use></svg></button></form></div>
<p class="ds-source">An inline note sits under the message that caused it, above the composer, and only you see it (the <code>add-prompt-private</code> eye). <code>add-prompt</code> has a brand left rule, an <code>add-prompt-who</code> mention pill, and pill actions: <code>add-prompt-add</code> in brand tint, <code>add-prompt-dismiss</code> quiet. Both disable while the add is busy; once done, only Dismiss stays.</p>
<div class="ds-example"><form class="composer tall" role="group" aria-label="Message composer" onsubmit="return false"><div class="composer-editor"><div class="composer-input" contenteditable="true" role="textbox" aria-multiline="true" aria-label="Message #design" data-placeholder="Type or / for commands">Deploy is <strong data-markdown="**green**">green</strong>, <em data-markdown="*finally*">finally</em>. Run <code data-markdown="`just deploy`">just deploy</code> or see <a href="/docs/setup" data-markdown="[setup](/docs/setup)">setup</a>:<pre class="fence" data-markdown="```sh"><code language="sh">just deploy</code></pre></div></div><button class="composer-plus" type="button" aria-label="Add photo"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="/icons/lucide/sprite.svg#plus"></use></svg></button><button class="composer-send" type="button" aria-label="Send message"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="/icons/lucide/sprite.svg#arrow-up"></use></svg></button></form></div>
<p class="ds-source">The draft renders its Markdown live inside <code>composer-input</code>: <code>strong</code>, <code>em</code>, <code>code</code>, <code>a</code>, and <code>pre.fence</code> elements, each keeping its source in <code>data-markdown</code> so the sent text is exact. Inline code and links use link teal; fences get a bordered panel. <code>tall</code> switches the composer from pill to card.</p>
<div class="ds-example"><div class="typing-row"><span class="typing-avatars"><span class="typing-avatar" role="img" tabindex="0" aria-label="ben is typing" data-tooltip="ben"><img src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20100%20100%22%3E%3Crect%20width%3D%22100%22%20height%3D%22100%22%20rx%3D%2250%22%20fill%3D%22%2389b4fa%22%2F%3E%3Ctext%20x%3D%2250%22%20y%3D%2254%22%20fill%3D%22%2311111b%22%20font-family%3D%22system-ui%2Csans-serif%22%20font-size%3D%2248%22%20font-weight%3D%22750%22%20text-anchor%3D%22middle%22%20dominant-baseline%3D%22middle%22%3EB%3C%2Ftext%3E%3C%2Fsvg%3E" alt="" /></span><span class="typing-avatar" role="img" tabindex="0" aria-label="sam is typing" data-tooltip="sam"><img src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20100%20100%22%3E%3Crect%20width%3D%22100%22%20height%3D%22100%22%20rx%3D%2250%22%20fill%3D%22%2394e2d5%22%2F%3E%3Ctext%20x%3D%2250%22%20y%3D%2254%22%20fill%3D%22%2311111b%22%20font-family%3D%22system-ui%2Csans-serif%22%20font-size%3D%2248%22%20font-weight%3D%22750%22%20text-anchor%3D%22middle%22%20dominant-baseline%3D%22middle%22%3ES%3C%2Ftext%3E%3C%2Fsvg%3E" alt="" /></span></span><span class="typing-dots" aria-hidden="true"><i></i><i></i><i></i></span></div><div class="bot-thinking" role="status">Thinking <span class="thinking-dots" aria-hidden="true"><i></i><i></i><i></i></span></div></div>
<p class="ds-source"><code>typing-row</code> floats above the composer with up to three <code>typing-avatar</code> faces and bouncing <code>typing-dots</code>; an <code>sr-only</code> status line carries the text. <code>bot-thinking</code> is the muted pill shown while a bot works on an answer, aligned with message avatars, with fading <code>thinking-dots</code>. Neither shifts layout; reduced motion stills the dots.</p>
<h3>Channels, members, and routed account navigation</h3>
<div class="ds-example"><div class="channel-row"><button class="room active" type="button"><span class="room-mark channel" aria-hidden="true">#</span><span class="room-name">design</span><span class="unread-badge">4</span></button></div><button class="member-row" type="button"><span class="room-mark peer-avatar avatar-status-host" data-status="online" aria-hidden="true">A</span><span class="member-copy"><span class="member-nick">ada</span><small>Admin</small></span></button><nav class="profile-tabs account-tabs" aria-label="Account pages"><a href="/profile">Profile</a><a class="active" href="/settings" aria-current="page">Settings</a></nav></div>
<p class="ds-source">Channels use <code>channel-row</code>, <code>room</code>, and <code>room active</code>. Presence uses <code>avatar-status-host</code> plus <code>data-status</code> and needs accessible text. <code>admin-badge</code> uses the user-star symbol; <code>agent-mark</code> labels bots and agents. Account pages use routed links in <code>account-tabs</code>, not profile tab buttons.</p>
<h3>Tooltips and toast</h3>
<div class="ds-example"><span class="reaction-tooltip"><strong>👍<span>3</span></strong><small>Ada, Ben, and you</small></span><div class="glass-toast" role="status"><span>Channel link copied</span><button type="button" class="notify-banner-close" aria-label="Dismiss notification" onclick="this.closest('.glass-toast').remove()"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="/icons/lucide/sprite.svg#x"></use></svg></button></div><div class="glass-toast sticky" role="status"><svg class="toast-icon" viewBox="0 0 24 24" aria-hidden="true"><use href="/icons/lucide/sprite.svg#bell"></use></svg><span>Reminder: stretch</span><button type="button" class="toast-action">Open</button><button type="button" class="toast-action quiet" onclick="this.closest('.glass-toast').remove()">Dismiss</button></div></div>
<p class="ds-source">Generic <code>slirc-tooltip</code> lives in the shell partial and opens on hover and keyboard focus. <code>reaction-tooltip</code> and <code>thread-tooltip</code> remain separate variants. The shared tooltip uses a low-alpha panel tint for text contrast.</p>
<p class="ds-source"><code>glass-toast</code> pills sit above the composer in <code>toast-region</code>, in two kinds. Connection and delivery errors stay in the transcript banner; command and input feedback, like an unknown command or a missing argument, uses an auto toast.</p>
<p class="ds-source"><strong>Auto</strong> (the default, <code>note</code>) confirms something that just happened, or answers a command or input the app could not take; it shows even while the banner is up. A lighter layer drains toward the dismiss button over 5s, plus 1s per 30 characters of text up to 9s, and the toast closes when it empties; hover or keyboard focus pauses it. With reduced motion the layer stays still and the toast still closes on time. A newer auto toast replaces the current one.</p>
<p class="ds-source"><strong>Sticky</strong> (<code>glass-toast sticky</code>, <code>sticky</code>) is for something that needs the person, like a reminder that fired while they were elsewhere. No timer: the same glass as other toasts, a bell, a <code>toast-action</code> button and a quiet <strong>Dismiss</strong>, and it stays until one is chosen. It fades in once over 160ms, without movement under reduced motion, and never flashes. Its state is separate, so an auto toast never replaces or pushes it away; when both show, sticky sits above auto. Several sticky toasts stack newest first, up to three; after that one row says how many more wait, with <strong>Open</strong> and <strong>Dismiss all</strong>. <strong>Open</strong> goes to the conversation and clears every toast for it. Toasts live in the app shell, so they show on every screen: above the composer in a conversation, at the bottom of the screen (above the phone nav) elsewhere.</p>
<h3>Menus and popovers</h3>
<div class="ds-example ds-row" style="align-items:flex-start"><div class="channel-menu nav-menu" role="menu" aria-label="Options for #design"><div><button type="button"><span class="menu-icon" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="/icons/lucide/sprite.svg#star"></use></svg></span><span>Star</span></button><div><button type="button"><span class="menu-icon" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="/icons/lucide/sprite.svg#bell"></use></svg></span><span>Mute</span></button><div class="menu-divider"></div><button type="button" aria-expanded="true"><span class="menu-icon" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="/icons/lucide/sprite.svg#copy"></use></svg></span><span>Copy</span><span class="menu-caret"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="/icons/lucide/sprite.svg#chevron-right"></use></svg></span></button><button type="button"><span class="menu-icon" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="/icons/lucide/sprite.svg#settings"></use></svg></span><span>Channel settings…</span></button></div></div></div><div class="channel-flyout" role="menu" aria-label="Copy options"><button type="button" role="menuitem"><span class="menu-icon" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="/icons/lucide/sprite.svg#hash"></use></svg></span>Name</button><button type="button" role="menuitem"><span class="menu-icon" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="/icons/lucide/sprite.svg#link"></use></svg></span>Link</button></div><div class="channel-menu nav-menu" role="menu" aria-label="Options for ben"><div><button type="button"><span class="menu-icon" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="/icons/lucide/sprite.svg#star"></use></svg></span><span>Star</span></button><div><button type="button"><span class="menu-icon" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="/icons/lucide/sprite.svg#user"></use></svg></span><span>View profile</span></button><button type="button"><span class="menu-icon" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="/icons/lucide/sprite.svg#bell"></use></svg></span><span>Mute</span></button><button type="button" class="danger"><span class="menu-icon" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="/icons/lucide/sprite.svg#trash"></use></svg></span><span>Close DM</span></button></div></div></div></div>
<p class="ds-source">The sidebar kebab opens <code>channel-menu nav-menu</code>: <code>menu-icon</code> plus label rows, <code>menu-divider</code> between groups, <code>menu-caret</code> on an item with a submenu. Copy opens <code>channel-flyout</code> beside it on hover or click. <code>danger</code> items (Close DM, Delete) use danger ink and go last.</p>
<div class="ds-example ds-row" style="align-items:flex-start"><div class="message-menu"><div><button type="button"><span class="menu-icon" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="/icons/lucide/sprite.svg#pencil"></use></svg></span>Edit</button><button type="button"><span class="menu-icon" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="/icons/lucide/sprite.svg#corner-down-right"></use></svg></span>Reply in thread</button><button type="button"><span class="menu-icon" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="/icons/lucide/sprite.svg#bell"></use></svg></span>Remind me</button><button type="button" class="danger"><span class="menu-icon" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="/icons/lucide/sprite.svg#trash"></use></svg></span>Delete</button></div></div><div class="message-menu"><div class="remind-presets" role="group" aria-label="Remind me"><button type="button">In 20 minutes</button><button type="button">In 1 hour</button><button type="button">Tomorrow at 9:00</button><button type="button">Custom…</button></div></div></div>
<p class="ds-source">The message ⋮ (<code>message-more</code>) opens <code>message-menu</code>; <code>popover-up</code> flips it when there is no room below. <strong>Remind me</strong> swaps the list for <code>remind-presets</code> in place; <strong>Custom…</strong> opens <code>remind-form</code> with a date and time field, Back, and Set reminder. Phone shows the same items in the message sheet.</p>
<div class="ds-example ds-row" style="align-items:flex-start"><div class="reaction-picker"><div class="reaction-picker-presets"><button type="button" class="picker-reaction" aria-label="React with 👍"><span class="pr-emoji">👍</span></button><button type="button" class="picker-reaction" aria-label="React with ❤️"><span class="pr-emoji">❤️</span></button><button type="button" class="picker-reaction" aria-label="React with 😂"><span class="pr-emoji">😂</span></button><button type="button" class="picker-reaction" aria-label="React with 🔥"><span class="pr-emoji">🔥</span></button><button type="button" class="picker-reaction" aria-label="React with 👀"><span class="pr-emoji">👀</span></button><button type="button" class="picker-reaction reaction-search-open" aria-label="Add reaction"><span class="pr-icon" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="/icons/lucide/sprite.svg#smile-plus"></use></svg></span></button></div></div><div class="reaction-picker searching"><div class="reaction-search"><input type="search" class="picker-search-input reaction-search-input" placeholder="Find an emoji…" aria-label="Find an emoji" value="heart" /><div class="reaction-search-grid" role="listbox" aria-label="Emoji"><button type="button" role="option" class="active" aria-selected="true" aria-label=":heart:">❤️</button><button type="button" role="option" aria-selected="false" aria-label=":hearts:">♥️</button><button type="button" role="option" aria-selected="false" aria-label=":heartbeat:">💓</button><button type="button" role="option" aria-selected="false" aria-label=":heart_eyes:">😍</button><button type="button" role="option" aria-selected="false" aria-label=":heartpulse:">💗</button><button type="button" role="option" aria-selected="false" aria-label=":heart_hands:">🫶</button><button type="button" role="option" aria-selected="false" aria-label=":heart_on_fire:">❤️‍🔥</button><button type="button" role="option" aria-selected="false" aria-label=":heart_eyes_cat:">😻</button><button type="button" role="option" aria-selected="false" aria-label=":heart_decoration:">💟</button><button type="button" role="option" aria-selected="false" aria-label=":blue_heart:">💙</button><button type="button" role="option" aria-selected="false" aria-label=":gift_heart:">💝</button><button type="button" role="option" aria-selected="false" aria-label=":grey_heart:">🩶</button><button type="button" role="option" aria-selected="false" aria-label=":pink_heart:">🩷</button><button type="button" role="option" aria-selected="false" aria-label=":two_hearts:">💕</button><button type="button" role="option" aria-selected="false" aria-label=":black_heart:">🖤</button><button type="button" role="option" aria-selected="false" aria-label=":brown_heart:">🤎</button></div><p class="reaction-search-code">:heart:</p></div></div></div>
<p class="ds-source">The smile button on a message opens <code>reaction-picker</code>: <code>picker-reaction</code> presets, then <strong>Add reaction</strong> (<code>reaction-search-open</code>, Lucide smile-plus). Add reaction turns the same popover into <code>reaction-picker searching</code>: <code>reaction-search-input</code> takes focus, <code>reaction-search-grid</code> shows eight columns with <code>active</code> following the arrow keys, and <code>reaction-search-code</code> names the highlighted shortcode. Enter or a click reacts; Escape goes back to the presets. An empty field shows the presets, popular picks, and custom emoji.</p>
<div class="ds-example"><div class="command-palette" role="listbox" aria-label="Commands"><button type="button" class="active" role="option" aria-selected="true"><code>/remind</code><span>Ask the room&#39;s remind helper</span></button><button type="button" role="option" aria-selected="false"><code>/threads</code><span>Start a thread from the latest message</span></button><button type="button" role="option" aria-selected="false"><code>/giphy</code><span>Find and send a GIF</span></button></div></div>
<p class="ds-source">Typing <code>/</code> opens <code>command-palette</code> above the composer: a glass list of <code>code</code> command names in brand ink with muted descriptions; <code>active</code> follows the arrow keys. <code>mention-palette</code> and <code>emoji-palette</code> reuse it for @ and : completion.</p>
<div class="ds-example ds-row" style="align-items:flex-start"><div class="link-popover" role="group" aria-label="Link actions"><a href="https://example.com" target="_blank" rel="noopener noreferrer"><span><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="/icons/lucide/sprite.svg#external-link"></use></svg></span><span>Go to</span></a><button type="button"><span><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="/icons/lucide/sprite.svg#pencil"></use></svg></span><span>Edit</span></button></div><div class="header-dms"><div class="header-dm-list"><button type="button"><img src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20100%20100%22%3E%3Crect%20width%3D%22100%22%20height%3D%22100%22%20rx%3D%2250%22%20fill%3D%22%2389b4fa%22%2F%3E%3Ctext%20x%3D%2250%22%20y%3D%2254%22%20fill%3D%22%2311111b%22%20font-family%3D%22system-ui%2Csans-serif%22%20font-size%3D%2248%22%20font-weight%3D%22750%22%20text-anchor%3D%22middle%22%20dominant-baseline%3D%22middle%22%3EB%3C%2Ftext%3E%3C%2Fsvg%3E" alt="" /><span>ben</span><small>2</small></button><button type="button"><img src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20100%20100%22%3E%3Crect%20width%3D%22100%22%20height%3D%22100%22%20rx%3D%2250%22%20fill%3D%22%2394e2d5%22%2F%3E%3Ctext%20x%3D%2250%22%20y%3D%2254%22%20fill%3D%22%2311111b%22%20font-family%3D%22system-ui%2Csans-serif%22%20font-size%3D%2248%22%20font-weight%3D%22750%22%20text-anchor%3D%22middle%22%20dominant-baseline%3D%22middle%22%3ES%3C%2Ftext%3E%3C%2Fsvg%3E" alt="" /><span>sam</span><small>1</small></button></div></div></div>
<p class="ds-source">Clicking a link in a message opens <code>link-popover</code> with Go to, plus Edit on your own messages. <code>header-dm-list</code> drops from the header DM avatars and lists DMs with unread counts in <code>small</code>.</p>
<div class="ds-example ds-row" style="align-items:flex-start"><span class="reaction-tooltip"><strong>🎉<span>2</span></strong><small>Ben and you</small></span><div class="thread-tooltip">Docs build before every deploy</div></div>
<p class="ds-source"><code>reaction-tooltip</code> names who reacted; phone hides it and uses the sheet. <code>thread-tooltip</code> previews a thread from its sidebar row, with <code>above</code> to flip it. Both ignore the pointer.</p>
<h3>Presence and unread</h3>
<div class="ds-example ds-row"><span class="room-mark peer-avatar avatar-status-host" data-status="online" aria-hidden="true">A</span><span>Online</span><span class="room-mark peer-avatar avatar-status-host" data-status="away" aria-hidden="true">B</span><span>Away</span><span class="room-mark peer-avatar avatar-status-host" data-status="busy" aria-hidden="true">S</span><span>Busy</span><span class="room-mark peer-avatar avatar-status-host" data-status="focus" aria-hidden="true">K</span><span>Focus</span><span class="unread-badge">4</span><span>Unread</span></div>
<table><thead><tr><th>State</th><th>Markup</th><th>Token</th><th>Use</th></tr></thead><tbody>
<tr><td>Online</td><td><code>data-status="online"</code></td><td><code>--brand</code></td><td>Dot on the avatar's lower right.</td></tr>
<tr><td>Away, offline</td><td><code>data-status="away"</code>, <code>"offline"</code></td><td><code>--muted</code></td><td>Same dot, muted.</td></tr>
<tr><td>Busy</td><td><code>data-status="busy"</code></td><td><code>--danger</code></td><td>Same dot, danger.</td></tr>
<tr><td>Focus, break, resting, vacation</td><td><code>data-status="focus"</code> and others</td><td>Emoji on <code>--avatar-status-ring</code></td><td>🎯 ☕ 💤 🌴 badge in place of the dot.</td></tr>
<tr><td>Unread</td><td><code>unread-badge</code></td><td><code>--brand</code> fill, <code>--bg</code> text</td><td>Count pill at the end of a channel or DM row.</td></tr>
</tbody></table>
<p class="ds-source">The dot's ring is <code>--avatar-status-ring</code>, the surface behind the avatar (<code>--bg</code>, or <code>--panel</code> in the sidebar). Color is never the only signal: the avatar's label carries the status text.</p>
<h3>Mobile</h3>
<div class="ds-example"><div class="sheet-backdrop"><section class="action-sheet"><div class="sheet-head"><strong>ben</strong><span>09:42</span></div><div class="sheet-reactions"><button type="button" aria-label="React with 👍">👍</button><button type="button" aria-label="React with ❤️">❤️</button><button type="button" aria-label="React with 😂">😂</button><button type="button" aria-label="React with 🔥">🔥</button><button type="button" aria-label="React with 👀">👀</button><button type="button" class="reaction-search-open" aria-label="Add reaction"><span class="menu-icon" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="/icons/lucide/sprite.svg#smile-plus"></use></svg></span></button></div><div class="sheet-divider"></div><div class="sheet-actions"><button type="button"><span class="menu-icon" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="/icons/lucide/sprite.svg#pencil"></use></svg></span>Edit</button><button type="button"><span class="menu-icon" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="/icons/lucide/sprite.svg#corner-down-right"></use></svg></span>Reply in thread</button><button type="button"><span class="menu-icon" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="/icons/lucide/sprite.svg#bell"></use></svg></span>Remind me</button><button type="button" class="danger"><span class="menu-icon" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="/icons/lucide/sprite.svg#trash"></use></svg></span>Delete</button></div></section></div></div>
<p class="ds-source">Long press on phone opens <code>action-sheet</code> in <code>dialog.custom-modal.sheet-modal</code>, over <code>sheet-backdrop</code> (dim plus light blur). <code>sheet-head</code> names the message, <code>sheet-reactions</code> holds quick reactions and Add reaction, <code>sheet-actions</code> mirrors the message menu. Shown here inside a framed backdrop.</p>
<div class="ds-example"><div class="sheet-backdrop"><section class="action-sheet"><div class="sheet-head"><strong>ben</strong><span>09:42</span></div><div class="reaction-search sheet-reaction-search"><input type="search" class="picker-search-input reaction-search-input" placeholder="Find an emoji…" aria-label="Find an emoji" value="heart" /><div class="reaction-search-grid" role="listbox" aria-label="Emoji"><button type="button" role="option" class="active" aria-selected="true" aria-label=":heart:">❤️</button><button type="button" role="option" aria-selected="false" aria-label=":hearts:">♥️</button><button type="button" role="option" aria-selected="false" aria-label=":heartbeat:">💓</button><button type="button" role="option" aria-selected="false" aria-label=":heart_eyes:">😍</button><button type="button" role="option" aria-selected="false" aria-label=":heartpulse:">💗</button><button type="button" role="option" aria-selected="false" aria-label=":heart_hands:">🫶</button><button type="button" role="option" aria-selected="false" aria-label=":heart_on_fire:">❤️‍🔥</button><button type="button" role="option" aria-selected="false" aria-label=":heart_eyes_cat:">😻</button><button type="button" role="option" aria-selected="false" aria-label=":heart_decoration:">💟</button><button type="button" role="option" aria-selected="false" aria-label=":blue_heart:">💙</button><button type="button" role="option" aria-selected="false" aria-label=":gift_heart:">💝</button><button type="button" role="option" aria-selected="false" aria-label=":grey_heart:">🩶</button><button type="button" role="option" aria-selected="false" aria-label=":pink_heart:">🩷</button><button type="button" role="option" aria-selected="false" aria-label=":two_hearts:">💕</button><button type="button" role="option" aria-selected="false" aria-label=":black_heart:">🖤</button><button type="button" role="option" aria-selected="false" aria-label=":brown_heart:">🤎</button></div><button type="button" class="btn-secondary reaction-search-back">Back</button></div></section></div></div>
<p class="ds-source"><strong>Add reaction</strong> (<code>reaction-search-open</code>) after the quick reactions swaps the sheet actions for <code>sheet-reaction-search</code>: the same field and grid as the desktop picker, with 44px cells and Back. Back or Escape returns to the actions; a pick reacts and closes the sheet. The sheet rides above the keyboard while the field has focus.</p>
<div class="ds-example"><nav class="mobile-nav" aria-label="Phone navigation"><button type="button" class="active" aria-label="Home"><img class="menu-icon" src="/icons/ui/nav-home.svg" alt="" /><span class="nav-label">Home</span><span class="nav-dot" aria-hidden="true"></span></button><button type="button" aria-label="Direct messages"><img class="menu-icon" src="/icons/ui/nav-dms.svg" alt="" /><span class="nav-label">DMs</span></button><button type="button" aria-label="More"><img class="menu-icon" src="/icons/ui/nav-more.svg" alt="" /><span class="nav-label">More</span></button></nav><button type="button" class="new-below">New messages below</button></div>
<p class="ds-source"><code>mobile-nav</code> is the glass pill fixed at the bottom of phone screens: Home, DMs, and More, with <code>active</code> on the current tab and <code>nav-dot</code> for unread. It hides while the keyboard is open. <code>new-below</code> floats above the composer when new messages arrive out of view; tap to jump down. Both are fixed in the app and shown in flow here.</p>
<h2>Accessibility</h2>
<ul><li>Check text contrast against rendered backgrounds, including glass. Faint borders cannot be the only way to identify a control or keyboard focus.</li><li>Label icon-only controls.</li><li>Show keyboard focus with a solid accent outline.</li><li>Dialogs keep Tab within visible, enabled controls. Escape cancels; closing returns focus to the opener.</li><li>Preserve focus and text selection during rerenders.</li><li>Keep fixed phone controls clear of safe-area insets.</li><li>Use shortcode alt text for custom emoji. Show important state with text, icon, count, or position, not color alone.</li><li>Honor reduced motion. Allow text selection when a mouse or other fine pointer is available.</li></ul>
<h2>Build and docs</h2>
<p>This source page lives at <code>app/docs/design.md</code>; the public page and markdown copy are generated. After editing this page or a component, run <code>cd app &amp;&amp; bun run docs</code> and keep the generated <code>app/public/docs/</code> output. <code>bun test test/docs.test.ts</code> verifies the built pages. Cached app asset changes also need <code>bun run version</code> in <code>app/</code>; never hand-edit asset hashes. Public landing and docs surfaces reuse Slirc branding but are separate from the app shell.</p>
</div>
