URBS Card Recharge
Design System
Three-layer token architecture for a 1920×1080 light / split-theme touchscreen kiosk. Complete color palette (green primary, teal accent), typography, spacing, and 11 production-ready screens.
Colors
Three-layer token architecture: raw hex values → semantic roles → component-specific assignments. Light background with green primary (#2ECE78) and teal accent (#00B4AE).
Primitive Raw palette — never used directly in components
Semantic Role-based tokens — use these in layouts
Component State tokens — used inside component styles
Typography
Single typeface: Outfit. Variable weight from 300 to 800. Self-hosted WOFF2, latin subset. All sizes are pixel-perfect for the 1920×1080 viewport. Minimum 14px for secondary labels.
Spacing
4-point base grid. All spacing values are multiples of 4px. Minimum touch target size: 48×48px (WCAG 2.5.5). Screen padding: 64px.
Spacing scale
- Base unit
- 4px
- Smallest
- space-1 · 4px
- Largest
- space-16 · 64px
- Touch target (min)
- 48 × 48px · WCAG 2.5.5
- Screen padding
- 64px
- Bar color
- #2ECE78
Radius & Borders
Corner radius scale from 6px (smallest) to 16px (cards). Border widths: 1px (subtle), 1.5px (balance pill), 2px (interactive), 3px (selected state).
Border weights
Shadows
Subtle elevation system. Cards use minimal shadows. Selected state adds accent glow: 0 4px 20px rgba(46,206,120,0.12).
Elevation scale
- Stage BG
- #FFFFFF
- Card BG
- #FFFFFF
- Card radius
- 12px
- xs
- 0 1px 2px rgba(10,20,30,0.06)
- sm
- 0 2px 6px rgba(10,20,30,0.08)
- md
- 0 6px 14px rgba(10,20,30,0.10)
- lg
- 0 10px 28px rgba(10,20,30,0.12)
- Selected glow
- 0 4px 20px rgba(46,206,120,0.12)
Iconography
Font Awesome 6 Free (Solid) — the exact set the kiosk ships with. Inline SVGs inheriting
currentColor.
All 18 glyphs match the production app 1:1. Sized 12–48px, paired with visible labels on all primary controls.
Kiosk Product iconography — 18 glyphs shipped in the totem
Kiosk glyphs
- Size
- 24px
- Light color
- #1A2332
- Dark color
- #FFFFFF
- Stroke
- fill=currentColor
DS site Docs chrome — not used inside the kiosk product
TopBar
Position: fixed top, z-index 100, transparent background. LEFT: URBS logo + balance pill. RIGHT: language buttons (PT/EN/ES) + exit button. Light surface for most screens, dark surface for full-dark states (ScreenSaver / ExitRemoveCard).
TopBar surfaces
- Position
- fixed top 0 · z-100
- Padding
- clamp 14-24px · 20-48px
- Background
- transparent (on screen BG)
- Logo
- urbs-logo · h:44px
- Balance label
- "Available balance" · uppercase 11-13px
- Balance value
- 15-22px · fw:800 · tabular-nums
- Lang button (active)
- bg #00B4AE · fw:700 · no border
- Lang button size
- min-h:48px · radius 10
- Lang inactive (light)
- bg white 92% · 1.5px #C8CDD5
- Lang inactive (dark)
- bg white 10% · 1.5px white 25%
- Exit button
- bg #1A2332 · white text · fw:700
- Exit icon
- right-from-bracket · 12-17px
- Dark-surface note
- Exit hidden on ScreenSaver · ExitRemoveCard
---
import KioskButton from '@/components/kiosk/Button.astro';
import KioskIcon from '@/components/icons/KioskIcon.astro';
---
{/* Light surface (TopBar on most screens) */}
<KioskButton variant="primary" size="md" label="Exit">
Exit
<KioskIcon slot="trailing" name="right-from-bracket" size={16} />
</KioskButton>
{/* Dark surface (ScreenSaver / ExitRemoveCard wrapper) */}
<KioskButton variant="primary-dark" size="md" label="Exit">
Exit
<KioskIcon slot="trailing" name="right-from-bracket" size={16} />
</KioskButton>
{/* Secondary action — ghost on light / dark surfaces */}
<KioskButton variant="ghost-light" size="md">Skip</KioskButton>
<KioskButton variant="ghost-dark" size="md">Skip</KioskButton> | Prop | Type | Default | Description |
|---|---|---|---|
variant | 'primary' | 'primary-dark' | 'ghost-light' | 'ghost-dark' | 'primary' | Navy fill for TopBar/Exit; dark variant adds a subtle border to read over dark panels. |
size | 'sm' | 'md' | 'md' | md = kiosk full (min-h touch 48px · 17px text) · sm = compact preview (40px · 14px). |
type | 'button' | 'submit' | 'reset' | 'button' | Native button `type` attribute. |
label | string | — | Accessible label (maps to `aria-label`). |
class | string | — | Extra classes appended to the root element. |
slot: leading
slot
| — | — | Leading icon slot. |
slot: (default)
slot
| — | — | Button label / children. |
slot: trailing
slot
| — | — | Trailing icon slot. |
-
variant- Type
'primary' | 'primary-dark' | 'ghost-light' | 'ghost-dark'- Default
'primary'- Desc
- Navy fill for TopBar/Exit; dark variant adds a subtle border to read over dark panels.
-
size- Type
'sm' | 'md'- Default
'md'- Desc
- md = kiosk full (min-h touch 48px · 17px text) · sm = compact preview (40px · 14px).
-
type- Type
'button' | 'submit' | 'reset'- Default
'button'- Desc
- Native button `type` attribute.
-
label- Type
string- Default
—- Desc
- Accessible label (maps to `aria-label`).
-
class- Type
string- Default
—- Desc
- Extra classes appended to the root element.
-
slot: leadingslot- Type
—- Default
—- Desc
- Leading icon slot.
-
slot: (default)slot- Type
—- Default
—- Desc
- Button label / children.
-
slot: trailingslot- Type
—- Default
—- Desc
- Trailing icon slot.
Balance Pill
TopBar indicator that appears once the user's card is read. Two value states: OK (green) and Low (red, below R$ 4.70 — the ticket price). Ships on both light screens and the dark-panel screens (onboarding, value-selection, screen-saver) — flip the stage above to compare.
Balance states
- BG light
- rgba(255,255,255,0.95)
- BG dark
- rgba(255,255,255,0.06)
- Border light
- 1.5px solid #E5E8ED
- Border dark
- 1.5px solid rgba(255,255,255,0.15)
- Radius
- 10px
- Label
- 11px · 500 · #6B7B8D
- Value OK
- 18px · 800 · #2ECE78
- Value Low
- 18px · 800 · #E5484D
---
import BalancePill from '@/components/kiosk/BalancePill.astro';
---
{/* OK state — above ticket price (R$ 4.70) */}
<BalancePill value="R$ 50.00" surface="light" state="ok" size="md" />
<BalancePill value="R$ 50.00" surface="dark" state="ok" size="md" />
{/* Low state — below ticket price, app renders a warning alongside */}
<BalancePill value="R$ 2.50" surface="light" state="low" size="md" />
<BalancePill value="R$ 2.50" surface="dark" state="low" size="md" />
{/* Custom label (defaults to "Available balance") */}
<BalancePill label="Saldo" value="R$ 50.00" surface="light" /> | Prop | Type | Default | Description |
|---|---|---|---|
value
required
| string | — | Pre-formatted monetary value — e.g. `"R$ 50.00"`. Uses tabular-nums so digits align. |
label | string | 'Available balance' | Uppercase label above the value. Kept short; truncation is consumer responsibility. |
surface | 'light' | 'dark' | 'light' | Background the pill sits on — controls the inactive border + backdrop blur. |
state | 'ok' | 'low' | 'ok' | ok → green value; low → red value (caller renders the "Low balance" warning alongside). |
size | 'sm' | 'md' | 'md' | md = kiosk full (label 13 · value 22) · sm = compact preview (label 11 · value 18). |
class | string | — | Extra classes appended to the root element. |
-
valuerequired- Type
string- Default
—- Desc
- Pre-formatted monetary value — e.g. `"R$ 50.00"`. Uses tabular-nums so digits align.
-
label- Type
string- Default
'Available balance'- Desc
- Uppercase label above the value. Kept short; truncation is consumer responsibility.
-
surface- Type
'light' | 'dark'- Default
'light'- Desc
- Background the pill sits on — controls the inactive border + backdrop blur.
-
state- Type
'ok' | 'low'- Default
'ok'- Desc
- ok → green value; low → red value (caller renders the "Low balance" warning alongside).
-
size- Type
'sm' | 'md'- Default
'md'- Desc
- md = kiosk full (label 13 · value 22) · sm = compact preview (label 11 · value 18).
-
class- Type
string- Default
—- Desc
- Extra classes appended to the root element.
Cards
White surface cards with a 2px border and an embedded 64×64 icon tile. Default: 2px #E5E8ED. Selected: 3px #2ECE78 + green glow shadow and a floating check badge. Ships on the PaymentMethod screen, where two cards are pinned to a fixed pixel height so they stay aligned side by side.
Payment method
- BG
- #FFFFFF
- Border default
- 2px solid #E5E8ED
- Border selected
- 3px solid #2ECE78
- Radius
- 12px
- Height
- 260px (pinned for side-by-side alignment)
- Icon tile
- 64×64 · radius 12 · bg #F0F2F5 / rgba(46,206,120,0.12)
- Icon glyph
- money-bill · credit-card · 28px
- Title · Subtitle
- 28px/700 · 14px/regular
- Shadow default
- 0 2px 8px rgba(0,0,0,0.03)
- Glow selected
- 0 4px 20px rgba(46,206,120,0.12)
- Check badge
- 32px circle, #2ECE78
- Ships on
- PaymentMethod screen
---
import Card from '@/components/kiosk/Card.astro';
import KioskIcon from '@/components/icons/KioskIcon.astro';
---
{/* Default · Selected — height auto (grows with content) */}
<Card variant="default">
<KioskIcon slot="icon" name="money-bill" size={28} class="text-fg-muted" />
Cash
<span slot="subtitle">Insert banknotes</span>
</Card>
<Card variant="selected">
<KioskIcon slot="icon" name="credit-card" size={28} class="text-primary" />
Card
<span slot="subtitle">Tap contactless card</span>
<KioskIcon slot="check-icon" name="check" size={14} />
</Card>
{/* PaymentMethod case — two cards pinned at 260px so copy of
different lengths still aligns side by side */}
<Card variant="default" height={260}>
<KioskIcon slot="icon" name="money-bill" size={28} class="text-fg-muted" />
Cash
<span slot="subtitle">Insert banknotes</span>
</Card>
<Card variant="selected" height={260}>
<KioskIcon slot="icon" name="credit-card" size={28} class="text-primary" />
Card
<span slot="subtitle">Tap contactless card</span>
<KioskIcon slot="check-icon" name="check" size={14} />
</Card>
{/* Consumer pattern — wrap in a <button> for radio-style selection */}
<button type="button" role="radio" aria-checked={selected} onClick={handleSelect}>
<Card variant={selected ? 'selected' : 'default'} height={260}>
<KioskIcon slot="icon" name="credit-card" size={28} />
Card
<span slot="subtitle">Tap contactless</span>
</Card>
</button> | Prop | Type | Default | Description |
|---|---|---|---|
variant | 'default' | 'selected' | 'default' | default → 2px border + soft shadow · selected → 3px primary border + green-glow shadow + check badge. |
height | number | 'auto' | 'auto' | auto → grows with content · number → pinned pixel height (e.g. 260 for PaymentMethod's aligned pair). |
class | string | — | Extra classes appended to the root element. |
slot: icon
slot
| — | — | Icon glyph, consumer-sized (~28px to match the kiosk). Card renders the 64×64 tile wrapper automatically. |
slot: (default)
slot
| — | — | Card title. |
slot: subtitle
slot
| — | — | Secondary copy under the title. |
slot: check-icon
slot
| — | — | Custom check icon for the selected-state badge (defaults to an inline SVG). |
-
variant- Type
'default' | 'selected'- Default
'default'- Desc
- default → 2px border + soft shadow · selected → 3px primary border + green-glow shadow + check badge.
-
height- Type
number | 'auto'- Default
'auto'- Desc
- auto → grows with content · number → pinned pixel height (e.g. 260 for PaymentMethod's aligned pair).
-
class- Type
string- Default
—- Desc
- Extra classes appended to the root element.
-
slot: iconslot- Type
—- Default
—- Desc
- Icon glyph, consumer-sized (~28px to match the kiosk). Card renders the 64×64 tile wrapper automatically.
-
slot: (default)slot- Type
—- Default
—- Desc
- Card title.
-
slot: subtitleslot- Type
—- Default
—- Desc
- Secondary copy under the title.
-
slot: check-iconslot- Type
—- Default
—- Desc
- Custom check icon for the selected-state badge (defaults to an inline SVG).
Numpad + Ticket Presets
Right panel of the ValueSelection screen. Ticket preset rows + 3×4 numpad on dark panel background (rgba(28,42,58,0.98)). Keys have subtle white borders; delete key in red. Full-width layout.
ValueSelection — right panel (dark)
- Key height
- 56px
- Key BG
- rgba(28,42,58,0.85)
- Key border
- 1px solid rgba(255,255,255,0.15)
- Key radius
- 8px
- Key font
- 22px w500 #FFFFFF
- Delete BG
- rgba(229,72,77,0.7)
- Delete border
- 1px solid rgba(229,72,77,0.85)
- Grid gap
- 8px
- Preset default BG
- rgba(28,42,58,0.85)
- Preset sel BG
- rgba(46,206,120,0.25)
- Preset sel border
- 1px solid rgba(46,206,120,0.5)
- Preset price
- #2ECE78
Processing Steps
Four sequential steps with three visually distinct states: done (light green bg + green check), active (light teal bg + teal spinner), pending (white bg + gray dot + opacity 0.4). Real labels from TotemContext.tsx. Matches Processing.tsx in production.
Step states
- Done BG
- rgba(46,206,120,0.04)
- Done border
- 1px rgba(46,206,120,0.15)
- Active BG
- rgba(0,180,174,0.06)
- Active border
- 1px rgba(0,180,174,0.2)
- Pending BG
- #FFFFFF · opacity:0.4
- Pending border
- 1px #E5E8ED
- Padding
- 10px 20px
- Radius
- 8px
- Label
- 15px · active:600 / else:400
- Icons
- circle-check · spinner · 16px
Transaction Feed
Live banknote log on dark panel. Items show icon (green check), label, and amount. Max 6 visible, animated entry from top.
Transaction items
- Item BG
- rgba(28,42,58,0.06)
- Item border
- 1px rgba(0,180,174,0.15)
- Item radius
- 8px
- Icon color
- #2ECE78
- Amount color
- #2ECE78 · fw:700
- Animation
- y: -10→0, opacity 0→1
Language Buttons
Three languages: PT / EN / ES. Active state: teal bg, white text + flag. Inactive: subtle bg with border. The app renders light variant on TopBar and dark variant on ScreenSaver / Onboarding (right-panel) contexts — flip the stage to compare.
Language selector
- Active BG
- #00B4AE
- Active color
- #FFFFFF · fw:700
- Inactive BG light
- rgba(255,255,255,0.92)
- Inactive BG dark
- rgba(255,255,255,0.10)
- Inactive color light
- #526679 · fw:500
- Inactive color dark
- rgba(255,255,255,0.65)
- Border light
- 1.5px solid #C8CDD5
- Border dark
- 1.5px solid rgba(255,255,255,0.25)
- Radius
- 10px
- Flag
- 22×16 · rounded 3px
- Touch target
- ≥ 48px height (kiosk)
---
import LanguageButton from '@/components/kiosk/LanguageButton.astro';
---
{/* Light surface (TopBar) — active = current language */}
<LanguageButton code="pt" active surface="light" size="md" />
<LanguageButton code="en" surface="light" size="md" />
<LanguageButton code="es" surface="light" size="md" />
{/* Dark surface (ScreenSaver · Onboarding right-panel) */}
<LanguageButton code="pt" active surface="dark" size="md" />
<LanguageButton code="en" surface="dark" size="md" />
<LanguageButton code="es" surface="dark" size="md" />
{/* Compact preview (docs / mobile) — size sm */}
<LanguageButton code="pt" active surface="light" size="sm" /> | Prop | Type | Default | Description |
|---|---|---|---|
code
required
| 'pt' | 'en' | 'es' | — | Language this button represents. Drives label (PT/EN/ES) and flag asset. |
active | boolean | false | True when this is the currently-selected language (teal fill, white label, no border). |
surface | 'light' | 'dark' | 'light' | Background the button sits on — controls the inactive styling. |
size | 'sm' | 'md' | 'md' | md = kiosk full (1920px) · sm = DS preview / mobile. |
class | string | — | Extra classes appended to the root element. |
-
coderequired- Type
'pt' | 'en' | 'es'- Default
—- Desc
- Language this button represents. Drives label (PT/EN/ES) and flag asset.
-
active- Type
boolean- Default
false- Desc
- True when this is the currently-selected language (teal fill, white label, no border).
-
surface- Type
'light' | 'dark'- Default
'light'- Desc
- Background the button sits on — controls the inactive styling.
-
size- Type
'sm' | 'md'- Default
'md'- Desc
- md = kiosk full (1920px) · sm = DS preview / mobile.
-
class- Type
string- Default
—- Desc
- Extra classes appended to the root element.
Screen Layouts
Three base templates sit on the Curitiba cityscape background. The split-layout and centered-layout stack semi-transparent white panels over the bg (so the city shows through at 8%); the dark-layout uses the same image under a solid navy overlay at 98% opacity. 1920×1080 base, scaled via Math.min(w/1920, h/1080). Idle timeout: 30s dev / 3min prod.
Math.min(w/1920, h/1080)/images/kiosk-bg.webp (Curitiba skyline)position:fixed above viewport64px all sidesToken Export
Style Dictionary–compatible JSON and CSS custom properties. All values match the production app (urbs-card-recharge-totem.vercel.app).
{
"color": {
"primary": { "value": "#2ECE78" },
"primary-dark": { "value": "#25A862" },
"accent": { "value": "#00B4AE" },
"bg": { "value": "#F0F2F5" },
"surface": { "value": "#FFFFFF" },
"dark-panel": { "value": "#1C2A3A" },
"text": { "value": "#1A2332" },
"text-secondary": { "value": "#526679" },
"text-muted": { "value": "#5E6F81" },
"border": { "value": "#E5E8ED" },
"success": { "value": "#2ECE78" },
"error": { "value": "#E5484D" },
"warning": { "value": "#F5A623" }
},
"spacing": {
"1": { "value": "4px" },
"2": { "value": "8px" },
"3": { "value": "12px" },
"4": { "value": "16px" },
"6": { "value": "24px" },
"8": { "value": "32px" },
"12": { "value": "48px" },
"16": { "value": "64px" }
},
"radius": {
"xs": { "value": "6px" },
"sm": { "value": "8px" },
"md": { "value": "10px" },
"lg": { "value": "12px" },
"xl": { "value": "14px" },
"2xl": { "value": "16px" },
"full": { "value": "999px" }
}
} :root {
--color-primary: #2ECE78;
--color-primary-dark: #25A862;
--color-accent: #00B4AE;
--color-bg: #F0F2F5;
--color-surface: #FFFFFF;
--color-dark-panel: #1C2A3A;
--color-text: #1A2332;
--color-text-secondary: #526679;
--color-text-muted: #5E6F81;
--color-border: #E5E8ED;
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-6: 24px;
--space-8: 32px;
--space-12: 48px;
--space-16: 64px;
--radius-xs: 6px;
--radius-sm: 8px;
--radius-md: 10px;
--radius-lg: 12px;
--radius-xl: 14px;
--radius-2xl: 16px;
--radius-full: 999px;
}
Source of truth:
rico-mello/urbs-design-system
→ src/styles/tokens.css