This file defines the visual language for every Lenny screen (desktop receiver and mobile sender). Follow it for all UI work, including screens and components that don't exist yet: quick connect menu, settings, onboarding, dialogs, and so on. When something isn't covered here, extend the rules below rather than falling back to Material defaults.
Every interactive or grouping element is a flat, solid-colored "sticker": a thick dark outline, chunky rounded corners, and a hard offset shadow with zero blur. The sticker sits slightly above a plain dark page and physically presses down into its shadow when tapped.
- No gradients, no blur, no soft elevation. Shadows are solid ink rectangles offset down-right, with
blurRadius: 0andspreadRadius: 0. Never use Materialelevation. - Every card, button, input, chip, switch, and tile has a 3px ink outline (
#07081A). - Every sticker has an offset shadow in the same ink color, sized by role (see §4).
- Press = sink. On press, the element translates down-right by its shadow offset and the shadow shrinks to 0 (80ms, ease). On release it springs back.
- One color = one meaning. Accent colors are functional, never decorative (see §3.2). If a color doesn't carry its meaning, the element uses a neutral surface.
- Text on accent fills is always ink (
#07081A), never white. - Corners are chunky rounded (14–20px). Full pills are reserved for status chips, switches, and segmented controls.
- Background is always the plain
pagecolor (no dot grid or other texture, decided 2026-09-28), never an app-bar color band. There is no colored AppBar; the header sits directly on the page.
| Token | Hex | Use |
|---|---|---|
ink |
#07081A |
Outlines, shadows, text on accent fills |
page |
#1A1D38 |
Page background |
surface |
#262A4D |
Cards, neutral buttons in the header, chips |
well |
#0F1126 |
Recessed areas inside cards: inputs, off toggles, stat tiles, slider track, preview frame |
text |
#F4F1E8 |
Primary text (warm cream, not pure white) |
textMuted |
#B7B9D6 |
Labels, captions, secondary text |
textFaint |
#9A9DC8 |
Placeholder content inside wells only |
| Token | Hex | Meaning (the ONLY thing it's used for) |
|---|---|---|
yellow |
#FFD23F |
Primary action: Connect, Start, Save, the main CTA of a screen. Max one per view. |
mint |
#2EE6C5 |
On / selected state: active toggles, selected segment, switch-on track |
green |
#4ADE80 |
Live / streaming status only: status dots, LIVE badge |
coral |
#FF6B6B |
Stop / destructive / disconnected: Disconnect, Stop stream, delete, error status |
lilac |
#A78BFA |
Values & info: slider fill, info badges, "waiting" status, platform tags |
Don't introduce new accent colors. If a new meaning is needed, discuss first.
| Token | Value |
|---|---|
borderWidth |
3px everywhere |
radiusCard |
20px |
radiusButton |
16px |
radiusInput |
14px |
radiusTile |
14px (stat tiles, small wells, swatches) |
radiusChipSmall |
12px (header copy chips, icon buttons) |
radiusPill |
999px (status chips, switches, segmented controls only) |
| Element | Offset |
|---|---|
| Hero/preview card | 8px |
| Cards | 6px |
| Primary / destructive buttons | 5px |
| Regular buttons, toggles, segmented control | 4px |
| Inputs, chips, switches, small icon buttons, slider track/thumb | 3px |
| Tiny status dot (optional) | 2px |
| Stat tiles inside a card | none (outline only) |
- Page padding: 32px desktop, 20–24px mobile.
- Gap between cards: 16px (column), 24–28px (between major regions).
- Card inner padding: 18px (24px on large/showcase cards).
- Gap inside cards: 14px between rows; 10–12px between sibling buttons.
- Touch targets ≥ 44px tall. Standard button height 50–54px; primary full-width button 56px.
Use the google_fonts package.
| Role | Font | Weight | Size |
|---|---|---|---|
| Wordmark "Lenny" | Bricolage Grotesque | 800 | 46 desktop / 38 mobile, letter-spacing −0.02em, with a 3px hard ink text shadow |
| Headings / hero status ("Streaming") | Bricolage Grotesque | 700–800 | 22–28 |
| Body, button labels | DM Sans | 700 for buttons, 400–500 for body | 16–18 buttons, 14–16 body |
| Section labels | JetBrains Mono | 700 | 12, UPPERCASE, letter-spacing 0.08em, textMuted |
| Technical values (IPs, ports, fps, kbps, ms) | JetBrains Mono | 500–700 | 13–18 |
Rule: anything a user might copy or read as a number (IP, port, bitrate, latency) is monospace.
Plain page, nothing drawn on it. (A dot grid was dropped on 2026-09-28: the user didn't like it.)
surface fill, 3px ink border, radius 20, 6px shadow, padding 18. Starts with a section label (mono, uppercase, muted), e.g. CAMERA, FOCUS & LIGHT, EXPOSURE, STREAM, CONNECTION. Group related controls into one card; don't leave controls floating on the page.
| Variant | Fill | Text | Shadow |
|---|---|---|---|
| Primary | yellow |
ink | 5px |
| Destructive | coral |
ink | 5px |
| Neutral | well (inside cards) or surface (on page) |
text |
4px |
| Icon button | surface |
text |
3px, 44–48px square, radius 12–14, needs a tooltip/semantic label |
All buttons use the press-sink behavior. Disabled: keep the shape, drop the shadow to 0, fill well, text textMuted, no press animation.
- Off:
wellfill,textlabel. - On:
mintfill, ink label, and a leading bold check icon (✓, stroke ~3). - Same size/shape/shadow in both states; only fill, text color, and icon change.
Pill track 68×38, 3px border, 3px shadow. Knob: 24px text-colored circle with 3px ink border.
- Off: track
well, knob left. - On: track
mint, knob right. Animate knob position (~120ms). Label with icon sits on the left of the row, switch on the right.
One pill container (3px border, 4px shadow, clip). Segments split equally, separated by a 3px ink vertical divider. Selected segment mint with ink text; others well with text. Height 50–52.
Label above (DM Sans 700, 14, text). Field: well fill, 3px border, radius 14, 3px shadow, height 50, horizontal padding 14, monospace for technical values. Focused: shadow grows to 4px and border stays ink (no colored focus glow). Disabled/locked (e.g. while streaming): textMuted value, no shadow.
Pill, height 38–44, surface (on page) or well (in card), 3px border, 3px shadow. Leading 12–14px dot with 3px ink border in the status color: green = streaming, lilac = waiting/connecting, coral = disconnected/error. Label in DM Sans 700.
Small pill (height 32), 3px border, 3px shadow, mono 13 bold. LIVE badge = green fill + ink text + small ink dot. Info badges (camera side, resolution) = surface fill + text.
Button styled as chip: surface, radius 12, height 44, 3px shadow, mono 14 bold, value + trailing copy icon. Tapping copies and briefly swaps the icon to a check.
Track height 22, 3px border, pill, 3px shadow. Filled portion lilac, remainder well, with a hard edge between them (no gradient blend). Thumb: 34px text circle, 3px ink border, 3px shadow. Small sun icon at the left end, large sun icon at the right end, in textMuted.
Inside a card: well fill, 3px border, radius 14, no shadow, padding 8×12. Label DM Sans 12 textMuted, value JetBrains Mono 18 bold. Lay out in a 2-column grid (desktop sidebar) or wrap.
Hero card: well fill, 8px shadow, radius 20, padding 16. Inside it, the video sits in a 4:3 box with a 3px ink border and radius 14, letterboxed and centered. Badges overlay the top corners at 18px inset.
Treat them as cards floating above the page: surface, 3px border, radius 20, 8px shadow. Dim the page behind with a flat ink at ~60% opacity (no blur). List rows inside are well tiles or neutral buttons with the press-sink behavior. The single main action in a dialog is yellow; the cancel is neutral; destructive is coral.
Line icons, stroke width 2.5 (3 for check marks), round caps and joins, colored with the current text color. Sizes: 16–20 inline, 48–56 for empty/placeholder states. No filled icons, no emoji, no colored icon backgrounds unless the icon sits inside a sticker.
No scrolling. Every screen fits its window: a maximized desktop window, a phone in portrait or landscape. The camera preview gives up space first; anything optional is folded behind a button.
Desktop receiver: wordmark + platform tag + status chip on the left of the header, window buttons on the right. Below: the preview hero on the left (top-aligned, its real aspect ratio), cards on the right in two columns on wide windows (Connection → Camera → Focus & exposure | Video → Stream). Not connected, the Connection card shows the QR code first; Manual (IPs + port), USB ADB and USB tether sit behind one segmented control and unfold one at a time (tap the selected one again to fold it).
Mobile sender: header row (wordmark left, settings icon button right) → status hero card (big green dot, "Streaming" heading, mono "to IP · port" subline) → Connection card (Scan QR + Find PCs, the found PCs, then Manual / USB ADB / USB tether folded behind one segmented control; only Disconnect while connected) → Camera card (segmented control, toggle grid, torch switch row, exposure slider). Single column, full width minus page padding.
- Build one reusable
Sticker/PressableStickerwidget and use it everywhere rather than restyling Material widgets piecemeal:BoxDecoration(color: fill, border: Border.all(color: ink, width: 3), borderRadius: BorderRadius.circular(r), boxShadow: [BoxShadow(color: ink, offset: Offset(o, o), blurRadius: 0)]).- Pressed state: track pressed via
GestureDetector/Listener(onTapDown/onTapUp/onTapCancel), thenAnimatedContainer(80ms,Curves.easeOut) withtransform: Matrix4.translationValues(o, o, 0)and shadow offsetOffset.zero.
- Put all values from §3 in a single
LennyTokensclass (colors, radii, shadow offsets, spacing) and aThemeDatawithuseMaterial3: truebut with ripples disabled (splashFactory: NoSplash.splashFactory, transparent highlight/hover overlays). Press feedback comes from the sink, not ink splashes. - Override Material components (
FilledButton,SegmentedButton,Switch,Slider,TextField) only if they can hit the spec exactly; otherwise use custom widgets. Stock Material shapes, elevation, and ripples must not leak through. - Slider: custom
SliderThemewith a custom track shape (bordered pill, hard-edged lilac fill) and thumb shape (bordered circle with offset shadow), or a custom widget. - Background: scaffold
backgroundColor=page, nothing painted over it. - Keep text contrast ≥ 4.5:1:
text/textMutedonpage,surface, orwell; ink on every accent.
- No Material elevation, blurred shadows, ripples, or tinted surface overlays.
- No gradients (the slider's two-color fill is a hard stop, not a blend).
- No thin 1px outlines or borderless elements.
- No white text on accent fills.
- No accent used for decoration, and no more than one yellow action per view.
- No pure white (
#FFFFFF) or pure black backgrounds. - No fully rounded pill buttons for regular actions (pills are for status chips, switches, and segmented controls only).
- No colored app bar band; the header lives on the page.