Slirc is in invite-only pre-launch. Chat works today, and personal agents are coming soon.
Reference
Design system
The current app uses Catppuccin Mocha and Latte, with Slirc lime in both. This reference documents shipped styles and their source files.
Source files
The app shell is assembled from app/partials/*.html into app/public/index.html (generated; never edit directly). Alpine components live in app/client/ and ship as /client.js. Shared tokens and component styles are in app/public/style.css; the shell partials also carry effective style overrides. app/public/theme.js applies the appearance preference before CSS and first paint.
Examples and tables on this page are hand-written. Linking /style.css keeps the previews accurate but does not update prose or markup; edit this page when components change.
Appearance
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. localStorage under slirc-theme 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.
theme.js sets data-theme and data-theme-preference on <html>, native color-scheme, and the browser theme color before the stylesheet loads. Theme selection must not wait for sign-in or Alpine.
Doesn't change your settings.
Color and surfaces
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.
| Token | Dark | Light | Use |
|---|---|---|---|
--bg | #11111b | #eff1f5 | Canvas, inputs. |
--bg-glow | #1e1e2e → canvas | #fff → canvas | Radial background, including boot. |
--panel | #181825 | #e6e9ef | Chrome and glass tint. |
--panel-hi | #313244 | #e6e9ef | Raised controls. |
--secondary-fill | var(--panel-hi) | #ccd0da | Secondary button fill. Light uses Surface0 because panel-hi equals panel. |
--secondary-fill-hover | Panel-hi, 15% text | #bcc0cc | Secondary button hover and focus. |
--line | var(--panel-hi) | #ccd0da | Neutral borders; solid Surface0 in light. |
--text | #cdd6f4 | #4c4f69 | Main text. |
--muted | #7f849c | #65687e | Secondary copy; contrast-adjusted Latte Subtext0. |
--brand-fill | #a6e3a1 | #a6e3a1 | Primary buttons and composer circles. |
--on-brand | #11111b | #4c4f69 | Dark labels/glyphs on lime. |
--brand | #a6e3a1 | #237719 | Accent ink, focus, presence, success. |
--accent | var(--brand) | var(--brand) | Nick accent alias. |
--mine | #94e2d5 | #2b726a | Links, code, personal state. |
--danger | #f38ba8 | #af425a | Errors and destructive actions; same hue as dark pink, softer than Latte Red. |
--warning | #f9e2af | #856000 | Pending and verification states. |
--attention | #fab387 | #a34b09 | Agent input-needed state. |
--shadow | #0008 | #4c4f691a | Shared floating surfaces. |
--mention-fill | Brand at 16% | #c3e9c3 | Normal mention fill. |
--mention-ink | var(--brand) | var(--text) | Normal mention label. |
Light active channels use #d9eddc with an inset 1px green border at 18% opacity (#2377192e). Normal mentions use #c3e9c3; personal mentions #b8e7b6. 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.
Typography, spacing, and layout
New and edited components use the shared tokens in app/public/style.css instead of literal values; existing components migrate when touched. A literal needs a concrete layout or accessibility reason.
Fonts
JetBrains Mono is used when installed; no webfont is loaded. Desktop nicks have a system-font override.
| Token | Stack | Use |
|---|---|---|
--ui | system-ui | Chrome, headings, buttons, menus, cards. |
--mono | "JetBrains Mono" | Messages, composer, code, fences. |
Line height is 1.45 for UI body and about 1.55 for messages.
Type
Six steps, with no 13px step on purpose. The metadata floor is --text-xs (12px); only badge counts go down to --text-2xs (11px).
| Token | rem | px | Use |
|---|---|---|---|
--text-2xs | .6875rem | 11px | Badges and counts. |
--text-xs | .75rem | 12px | Timestamps, hints, status, tooltips. |
--text-sm | .875rem | 14px | UI body, inputs, menus, button labels. |
--text-base | 1rem | 16px | Messages. |
--text-lg | 1.25rem | 20px | Sheet and dialog heads. |
--text-xl | 1.5rem | 24px | Page and display heads. |
Tooltips, hints, and status use --text-xs (12px); button labels use --text-sm (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.
Spacing
Gaps and padding step through six values. Pick the smallest step that separates two groups; skip a step to mark a new group.
| Token | rem | px | Use |
|---|---|---|---|
--space-1 | .25rem | 4px | Icon to label, chip padding. |
--space-2 | .5rem | 8px | Control gaps and padding. |
--space-3 | .75rem | 12px | Compact panels, menu padding. |
--space-4 | 1rem | 16px | Standard panels and cards. |
--space-5 | 1.5rem | 24px | Section gaps, dialog padding. |
--space-6 | 2rem | 32px | Large separation between page groups. |
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.
Radius
Radius grows with the surface, and a nested surface uses a smaller radius than its container.
| Token | Value | Use |
|---|---|---|
--radius-xs | .25rem | Code chips, tiny marks. |
--radius-sm | .4rem | Chips, copy buttons. |
--radius-md | .55rem | Buttons, inputs, menu items. |
--radius-lg | .75rem | Menus, popovers, cards. |
--radius-xl | 1rem | Dialogs, toasts. |
--radius-pill | 999px | Pills, badges, composer. |
--radius-round | 50% | Avatars and circular buttons. |
Motion
Every transition uses --ease-standard, cubic-bezier(.2,.7,.2,1). Loops such as spinners and pulses keep their own durations.
| Token | Duration | Use |
|---|---|---|
--dur-micro | .1s | Hover color and opacity. |
--dur-fast | .15s | Menus, dialogs, toggles. |
--dur-base | .2s | Sheets, slides, toasts. |
--dur-entrance | .3s | Message entrance. |
--dur-reveal | .5s | Banners and boot. |
Shadows
Elevation has three steps. Dark uses black alphas strong enough to lift glass off the canvas; light uses var(--shadow) (#4c4f691a), a Latte text tint that stays quiet on pale surfaces.
| Token | Dark | Light | Use |
|---|---|---|---|
--shadow-sm | 0 .3rem .9rem | 0 .2rem .6rem | Typing row, chips, inline popovers. |
--shadow-md | 0 .5rem 1.5rem | 0 .5rem 1.5rem | Menus, tooltips, link popover, new-below pill. |
--shadow-lg | 0 1.5rem 5rem | 0 1rem 3rem | Command palette, header DM list, dialogs. |
State fills
Light needs solid Latte fills where dark can use brand alphas.
| Token | Dark | Light | Use |
|---|---|---|---|
--active-fill | Brand at 13% | #d9eddc | Active channel or DM row. |
--active-ring | transparent | #2377192e | Inset 1px border on the active row. |
--mention-me-fill | Link teal at 22% | #b8e7b6 | Mentions of you. |
--avatar-fill | var(--panel-hi) | #ccd0da | Avatar fallback behind initials and photos. |
Disabled
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.
| Token | Value | Use |
|---|---|---|
--disabled-opacity | .5 | Disabled buttons, inputs, and menu items in both themes. |
Layout
Desktop has a 15rem conversation rail and an 860px maximum message column. Phone layout depends on coarse pointers, not width alone. Header height is calc(3.625rem + env(safe-area-inset-top, 0px)); phone navigation is --nav-h: 4.1rem; --composer-space is measured at runtime by app/client/composer-editor.ts, with CSS fallbacks before measurement.
Branding
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.




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 logo.svg.
| Stop | Dark | Light |
|---|---|---|
| Green, start | #41F79B | #2ED882 |
| Green, mid | #4EF3A8 | #38D68E |
| Green, highlight | #76F7BC | #55DD9F |
| Blue, start | #43A3F9 | #2F89DB |
| Blue, deep | #5C8DF9 | #4172DD |
| Blue, highlight | #75B0F9 | #5492DF |
| Asset | Use |
|---|---|
| logo.svg | Vector mark. |
| logo.webp, logo.png | Raster mark; boot and compact branding. |
| wordmark.svg, wordmark@2x.png | Standalone white lettering for dark surfaces. |
| brand@2x.png | Dark-surface lockup. |
| brand-light@2x.png | Light lockup, derived from the dark asset with white lettering changed to #4c4f69 and the mark darkened to light-surface contrast. |
| logo-light.webp | Light mark: logo.webp with HSL lightness at 84% and saturation at 75%. Light theme swaps it in with content: url(). |
| icons/slirc-light.svg | Light service bot mark, same treatment as the logo. |
icons/ui/nav-{home,dms}-light.svg | Light mobile nav Home and DMs icons with the light gradient stops. Monochrome nav icons keep the darkening filter. |
icons/bots/*-light.svg | Helper bot marks in pastel Latte for light: help #5fd27c, remind #fb9a62, poll #b48af4. Dark keeps the Mocha data-URI marks. |
Icons
UI controls use Lucide, distributed under the ISC licence. The curated local sprite is app/public/icons/lucide/sprite.svg; lucideIcon() in app/client/presentation.ts emits trusted SVG markup. A name is usable only when its symbol exists in the sprite; missing names render empty.
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.
Glass, layering, and motion
--blur-light is blur(3px), --blur-medium blur(9px), --blur-heavy 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, --glass-tint (panel at 74%), and --glass-filter: in dark it blurs and darkens what is behind (saturate(1.2) brightness(.75)) 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 --glass-chrome (panel at 60%); dialogs stay near-solid at 96%.
The overlay ladder lives in a second :root block: content 1, header 20, navigation menus 90/91, dialog 100, boot screen 102, --z-tooltip 120. Native dialogs use the browser top layer.
Motion tokens are under Typography, spacing, and layout. Component durations: --dur-boot-pop 450ms, --dur-boot-exit 150ms, --dur-reaction 380ms; dialogs use --dur-fast. 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.
Components
Buttons and loading
Loading agents…
Shared button classes: btn-primary, btn-secondary, btn-danger, link-button. Resting buttons have no borders. Busy (.busy with aria-busy) 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 spinner leads a loading line in profile-note loading. Purpose-specific controls exist beyond these classes.
Native forms and notifications
Syncs with your account.
Could not save. Try again.
Native inputs, selects, and checkboxes use the effective color scheme. form-fields spaces fields; form-field groups a control with its hint. form-error pairs a tinted error surface with a decorative icon. form-consent keeps a native checkbox beside its label in a bordered row. Input borders stay quiet until focus; modal forms focus their first useful field. settings-toggle is a labelled switch; prefs-toggle is a disclosure, not a switch. Appearance is a native select, not a custom picker.
Dialog
Deleting #design removes its threads and history for everyone.
Real modal markup is <dialog class="custom-modal"> with a notice-card or form card; only the card is shown here, and its h2 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.
Cards and notices
Invitation
Join Slirc
Pick a display name and you're in. No password.
Invitation and admission pages use profile-dialog-card profile-screen-card admission-card: an invite-heading with join-eyebrow, join-heading, and join-note, then the form and admission-actions. The real heading is an h1.

auth-card sits in custom-modal auth-modal and shows one auth-step at a time; collapsed hides the others. auth-divider draws the rule around its label; auth-secondary is the bordered alternative action.
We sent a sign-in link to ada@example.com.
notice-card is the centered informational card: optional notice-icon, title, muted copy, then pair-actions. delete-confirmation left-aligns it for destructive dialogs.
notify-banner is the transcript banner for errors and connection notes; it sticks to the top of the messages. notify-banner-close adds a dismiss. Connection and delivery errors live here; command and input feedback uses an auto toast.
No invitations yet. Invite someone from a channel or by email.
Could not load invitations.
Empty states use a compact informative notice: a subtle bordered panel with muted copy (invitation-empty), or a light danger tint with its Retry inside for a load failure (invitation-notice-error). No stripe, no divider, no illustration.
Messages, mentions, reactions, and code
bun run docsmsg, msg cont, message-entry, message-avatar, time, nick, body. Desktop actions are message-add and message-more; phone uses long press. Mentions open the person card. Reactions carry counts, and mine marks your reaction. fence scrolls horizontally and Glow highlights escaped code. Message-segment markup must not insert whitespace: pre-wrap exposes it.
Message outputs
Message Markdown renders **bold** as strong, *italic* as em, backticks as code, links as message-link, > lines as blockquote, and fences as pre.fence. Glow highlights fences with plain elements, styled under .fence code: strong/b keywords and names in brand ink, em strings and numbers, label and dfn in link teal, i punctuation and sup comments muted, ins added lines in brand ink, del removed lines in faded danger.
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: View in #channel, View in thread, or View in DM with nick; other URLs show in full. @remind list answers with one numbered line per reminder, joined by ·, in a single message; pre-wrap keeps its line breaks.
msg error colors a whole row in danger ink. A message that did not send keeps its text and gets outbox-status with data-state: saving, queued, and sending are muted with dots; failed and uncertain are danger ink with underlined outbox-retry and outbox-discard text buttons. Uncertain asks for consent first, since a resend may duplicate.
Composer
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.
Composer states
@sam isn't in #design. @sam
An inline note sits under the message that caused it, above the composer, and only you see it (the add-prompt-private eye). add-prompt has a brand left rule, an add-prompt-who mention pill, and pill actions: add-prompt-add in brand tint, add-prompt-dismiss quiet. Both disable while the add is busy; once done, only Dismiss stays.
The draft renders its Markdown live inside composer-input: strong, em, code, a, and pre.fence elements, each keeping its source in data-markdown so the sent text is exact. Inline code and links use link teal; fences get a bordered panel. tall switches the composer from pill to card.
typing-row floats above the composer with up to three typing-avatar faces and bouncing typing-dots; an sr-only status line carries the text. bot-thinking is the muted pill shown while a bot works on an answer, aligned with message avatars, with fading thinking-dots. Neither shifts layout; reduced motion stills the dots.
Channels, members, and routed account navigation
Channels use channel-row, room, and room active. Presence uses avatar-status-host plus data-status and needs accessible text. admin-badge uses the user-star symbol; agent-mark labels bots and agents. Account pages use routed links in account-tabs, not profile tab buttons.
Tooltips and toast
Generic slirc-tooltip lives in the shell partial and opens on hover and keyboard focus. reaction-tooltip and thread-tooltip remain separate variants. The shared tooltip uses a low-alpha panel tint for text contrast.
glass-toast pills sit above the composer in toast-region, 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.
Auto (the default, note) 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.
Sticky (glass-toast sticky, sticky) 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 toast-action button and a quiet Dismiss, 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 Open and Dismiss all. Open 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.
Menus and popovers
The sidebar kebab opens channel-menu nav-menu: menu-icon plus label rows, menu-divider between groups, menu-caret on an item with a submenu. Copy opens channel-flyout beside it on hover or click. danger items (Close DM, Delete) use danger ink and go last.
The message ⋮ (message-more) opens message-menu; popover-up flips it when there is no room below. Remind me swaps the list for remind-presets in place; Custom… opens remind-form with a date and time field, Back, and Set reminder. Phone shows the same items in the message sheet.
:heart:
The smile button on a message opens reaction-picker: picker-reaction presets, then Add reaction (reaction-search-open, Lucide smile-plus). Add reaction turns the same popover into reaction-picker searching: reaction-search-input takes focus, reaction-search-grid shows eight columns with active following the arrow keys, and reaction-search-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.
Typing / opens command-palette above the composer: a glass list of code command names in brand ink with muted descriptions; active follows the arrow keys. mention-palette and emoji-palette reuse it for @ and : completion.
Clicking a link in a message opens link-popover with Go to, plus Edit on your own messages. header-dm-list drops from the header DM avatars and lists DMs with unread counts in small.
reaction-tooltip names who reacted; phone hides it and uses the sheet. thread-tooltip previews a thread from its sidebar row, with above to flip it. Both ignore the pointer.
Presence and unread
| State | Markup | Token | Use |
|---|---|---|---|
| Online | data-status="online" | --brand | Dot on the avatar's lower right. |
| Away, offline | data-status="away", "offline" | --muted | Same dot, muted. |
| Busy | data-status="busy" | --danger | Same dot, danger. |
| Focus, break, resting, vacation | data-status="focus" and others | Emoji on --avatar-status-ring | 🎯 ☕ 💤 🌴 badge in place of the dot. |
| Unread | unread-badge | --brand fill, --bg text | Count pill at the end of a channel or DM row. |
The dot's ring is --avatar-status-ring, the surface behind the avatar (--bg, or --panel in the sidebar). Color is never the only signal: the avatar's label carries the status text.
Mobile
Long press on phone opens action-sheet in dialog.custom-modal.sheet-modal, over sheet-backdrop (dim plus light blur). sheet-head names the message, sheet-reactions holds quick reactions and Add reaction, sheet-actions mirrors the message menu. Shown here inside a framed backdrop.
Add reaction (reaction-search-open) after the quick reactions swaps the sheet actions for sheet-reaction-search: 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.
mobile-nav is the glass pill fixed at the bottom of phone screens: Home, DMs, and More, with active on the current tab and nav-dot for unread. It hides while the keyboard is open. new-below 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.
Accessibility
- Check text contrast against rendered backgrounds, including glass. Faint borders cannot be the only way to identify a control or keyboard focus.
- Label icon-only controls.
- Show keyboard focus with a solid accent outline.
- Dialogs keep Tab within visible, enabled controls. Escape cancels; closing returns focus to the opener.
- Preserve focus and text selection during rerenders.
- Keep fixed phone controls clear of safe-area insets.
- Use shortcode alt text for custom emoji. Show important state with text, icon, count, or position, not color alone.
- Honor reduced motion. Allow text selection when a mouse or other fine pointer is available.
Build and docs
This source page lives at app/docs/design.md; the public page and markdown copy are generated. After editing this page or a component, run cd app && bun run docs and keep the generated app/public/docs/ output. bun test test/docs.test.ts verifies the built pages. Cached app asset changes also need bun run version in app/; never hand-edit asset hashes. Public landing and docs surfaces reuse Slirc branding but are separate from the app shell.
