Slircdocs Waiting list

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.

Dark
CanvasPanelTextFillAccentLinksDanger
Light
CanvasPanelTextFillAccentLinksDanger

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.

TokenDarkLightUse
--bg#11111b#eff1f5Canvas, inputs.
--bg-glow#1e1e2e → canvas#fff → canvasRadial background, including boot.
--panel#181825#e6e9efChrome and glass tint.
--panel-hi#313244#e6e9efRaised controls.
--secondary-fillvar(--panel-hi)#ccd0daSecondary button fill. Light uses Surface0 because panel-hi equals panel.
--secondary-fill-hoverPanel-hi, 15% text#bcc0ccSecondary button hover and focus.
--linevar(--panel-hi)#ccd0daNeutral borders; solid Surface0 in light.
--text#cdd6f4#4c4f69Main text.
--muted#7f849c#65687eSecondary copy; contrast-adjusted Latte Subtext0.
--brand-fill#a6e3a1#a6e3a1Primary buttons and composer circles.
--on-brand#11111b#4c4f69Dark labels/glyphs on lime.
--brand#a6e3a1#237719Accent ink, focus, presence, success.
--accentvar(--brand)var(--brand)Nick accent alias.
--mine#94e2d5#2b726aLinks, code, personal state.
--danger#f38ba8#af425aErrors and destructive actions; same hue as dark pink, softer than Latte Red.
--warning#f9e2af#856000Pending and verification states.
--attention#fab387#a34b09Agent input-needed state.
--shadow#0008#4c4f691aShared floating surfaces.
--mention-fillBrand at 16%#c3e9c3Normal mention fill.
--mention-inkvar(--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.

TokenStackUse
--uisystem-ui
-apple-system
BlinkMacSystemFont
"Segoe UI"
sans-serif
Chrome, headings, buttons, menus, cards.
--mono"JetBrains Mono"
ui-monospace
"Cascadia Code"
"SFMono-Regular"
Menlo
monospace
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).

TokenrempxUse
--text-2xs.6875rem11pxBadges and counts.
--text-xs.75rem12pxTimestamps, hints, status, tooltips.
--text-sm.875rem14pxUI body, inputs, menus, button labels.
--text-base1rem16pxMessages.
--text-lg1.25rem20pxSheet and dialog heads.
--text-xl1.5rem24pxPage 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.

TokenrempxUse
--space-1.25rem4pxIcon to label, chip padding.
--space-2.5rem8pxControl gaps and padding.
--space-3.75rem12pxCompact panels, menu padding.
--space-41rem16pxStandard panels and cards.
--space-51.5rem24pxSection gaps, dialog padding.
--space-62rem32pxLarge 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.

TokenValueUse
--radius-xs.25remCode chips, tiny marks.
--radius-sm.4remChips, copy buttons.
--radius-md.55remButtons, inputs, menu items.
--radius-lg.75remMenus, popovers, cards.
--radius-xl1remDialogs, toasts.
--radius-pill999pxPills, badges, composer.
--radius-round50%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.

TokenDurationUse
--dur-micro.1sHover color and opacity.
--dur-fast.15sMenus, dialogs, toggles.
--dur-base.2sSheets, slides, toasts.
--dur-entrance.3sMessage entrance.
--dur-reveal.5sBanners 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.

TokenDarkLightUse
--shadow-sm0 .3rem .9rem
#0006
0 .2rem .6rem
var(--shadow)
Typing row, chips, inline popovers.
--shadow-md0 .5rem 1.5rem
#0009
0 .5rem 1.5rem
var(--shadow)
Menus, tooltips, link popover, new-below pill.
--shadow-lg0 1.5rem 5rem
#000b
0 1rem 3rem
var(--shadow)
Command palette, header DM list, dialogs.

State fills

Light needs solid Latte fills where dark can use brand alphas.

TokenDarkLightUse
--active-fillBrand at 13%#d9eddcActive channel or DM row.
--active-ringtransparent#2377192eInset 1px border on the active row.
--mention-me-fillLink teal at 22%#b8e7b6Mentions of you.
--avatar-fillvar(--panel-hi)#ccd0daAvatar 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.

TokenValueUse
--disabled-opacity.5Disabled 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.

Dark surfaceSlirc
Light surfaceSlirc

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.

StopDarkLight
Green, start #41F79B #2ED882
Green, mid #4EF3A8 #38D68E
Green, highlight #76F7BC #55DD9F
Blue, start #43A3F9 #2F89DB
Blue, deep #5C8DF9 #4172DD
Blue, highlight #75B0F9 #5492DF
AssetUse
logo.svgVector mark.
logo.webp, logo.pngRaster mark; boot and compact branding.
wordmark.svg, wordmark@2x.pngStandalone white lettering for dark surfaces.
brand@2x.pngDark-surface lockup.
brand-light@2x.pngLight lockup, derived from the dark asset with white lettering changed to #4c4f69 and the mark darkened to light-surface contrast.
logo-light.webpLight mark: logo.webp with HSL lightness at 84% and saturation at 75%. Light theme swaps it in with content: url().
icons/slirc-light.svgLight service bot mark, same treatment as the logo.
icons/ui/nav-{home,dms}-light.svgLight mobile nav Home and DMs icons with the light gradient stops. Monochrome nav icons keep the darkening filter.
icons/bots/*-light.svgHelper 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.

24×24 viewBox, 2px stroke, round caps/joins, currentColor.

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.

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

Delete channel?

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.

Check your email

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.

Could not reach the server. Retrying…

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.

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

09:41adaAsk @ben or @you about v1.4.2. Read notes
Continuation messages share the speaker run.
bun run docs
Today
New since your last visit

msg, 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

09:42benShip today, not tomorrow. Run bun test first, then read the release notes
Docs build before every deploy.
Agreed, adding it to the checklist.
const ok = await fetch("/api") // ping
if (!ok) return false
  old line
  new line

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.

14:00remind
Ship the docs build before Friday
View in #design
14:01remindYour reminders (2): 1. In 20 minutes, 14:21 · stretch 2. Tomorrow at 9:00 · "Ship the docs build before Friday" · View in #design To cancel one: @remind cancel 1

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.

09:44slircCould not join #ops: invite only.
09:45adaPushing the fix nowfailed: connection lost
09:45adaAnd the docssending

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

@sam can you check this?

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.

Deploy is green, finally. Run just deploy or see setup:
just deploy

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.

Thinking

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

👍3Ada, Ben, and you
Channel link copied
Reminder: stretch

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.

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.

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.

🎉2Ben and you
Docs build before every deploy

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

OnlineAwayBusyFocus4Unread
StateMarkupTokenUse
Onlinedata-status="online"--brandDot on the avatar's lower right.
Away, offlinedata-status="away", "offline"--mutedSame dot, muted.
Busydata-status="busy"--dangerSame dot, danger.
Focus, break, resting, vacationdata-status="focus" and othersEmoji on --avatar-status-ring🎯 ☕ 💤 🌴 badge in place of the dot.
Unreadunread-badge--brand fill, --bg textCount 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

ben09:42

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.

ben09:42

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.