Design System¶
RotaCC uses a dark-mode-first design system built around two ideas: clinical precision (clean, organised, professional) and vital energy (dynamic, alive, energetic). The system is implemented across three layers: CSS custom properties in design-system.css, a shared common.css stylesheet, and Tailwind utility classes configured in base.html.
Design tokens¶
All tokens are defined as CSS custom properties in frontend/static/css/design-system.css. The same values are mirrored in the Tailwind config inside frontend/templates/frontend/base.html and in the account pages' inline <style> block in templates/account/base_account.html.
Primary colours¶
:root {
--teal-deep: #0d9488; /* gradient start, active states */
--teal-light: #14b8a6; /* gradient end, links, focus rings */
--teal-glow: rgba(20, 184, 166, 0.4);
--coral: #f97316; /* secondary accent */
--coral-light: #fb923c; /* link hover, warm highlights */
}
| Token | Tailwind class | Usage |
|---|---|---|
--teal-deep |
brand-teal.deep |
Primary button gradient start, active nav, sidebar brand bar |
--teal-light |
brand-teal / teal-400 |
Primary button gradient end, links, focus rings |
--coral |
brand-coral / orange-500 |
Admin section accent, link hover colour |
--coral-light |
orange-400 |
Warm gradient end for brand text |
Background system¶
:root {
--slate-900: #0f172a; /* page background */
--slate-800: #1e293b; /* card background, sidebar */
--slate-700: #334155; /* borders, dividers */
--slate-600: #475569; /* footer, minor elements */
--slate-500: #64748b; /* placeholder text */
}
The page body uses bg-gradient-to-br from-slate-900 to-slate-800 (or the CSS equivalent linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #0f172a 100%)). Cards sit on --slate-800 at 80% opacity with a backdrop blur.
Text colours¶
--text-primary: #ffffff; /* headlines, important labels */
--text-secondary: #cbd5e1; /* form labels, nav items */
--text-muted: #94a3b8; /* descriptions, subtitles */
--text-subtle: #64748b; /* placeholder text, help text */
--text-dim: #475569; /* footer, minor elements */
Semantic colours¶
/* Success */
--success-bg: rgba(34, 197, 94, 0.1);
--success-border: rgba(34, 197, 94, 0.3);
--success-text: #4ade80;
/* Error */
--error-bg: rgba(239, 68, 68, 0.1);
--error-border: rgba(239, 68, 68, 0.3);
--error-text: #f87171;
/* Warning */
--warning-bg: rgba(245, 158, 11, 0.1);
--warning-border: rgba(245, 158, 11, 0.3);
--warning-text: #fbbf24;
/* Info */
--info-bg: rgba(59, 130, 246, 0.1);
--info-border: rgba(59, 130, 246, 0.3);
--info-text: #60a5fa;
Typography¶
Outfit is the display font for headings, buttons, and the brand logo. DM Sans is the body font for paragraphs, labels, and form text. Both are loaded from Google Fonts in every base template.
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=DM+Sans:wght@400;500;600&display=swap" rel="stylesheet">
Tailwind configuration in base.html registers them as font-display and font-body:
tailwind.config = {
theme: {
extend: {
fontFamily: {
'display': ['Outfit', 'sans-serif'],
'body': ['DM Sans', 'sans-serif'],
}
}
}
}
Type scale¶
| Element | Font | Size | Weight | Notes |
|---|---|---|---|---|
| Page title h1 | Outfit | 2rem (32px) | 700 | letter-spacing: -0.02em; brand span uses teal-to-coral gradient |
| Page title h2 | Outfit | 1.25rem (20px) | 600 | |
| Body text | DM Sans | 1rem (16px) | 400 | Default <body> font |
| Form label | DM Sans | 0.875rem (14px) | 500 | Colour #cbd5e1 |
| Help / error text | DM Sans | 0.75-0.8rem | 400 | Help: #64748b, error: #f87171 |
| Button text | Outfit | 0.875-1rem | 600 |
Gradient text (brand)¶
The "Rota" in "RotaCC" uses a teal-to-coral gradient applied via background-clip: text:
.text-gradient-brand {
background: linear-gradient(135deg, var(--teal-light) 0%, var(--coral-light) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
In account pages this is applied directly to .page-title h1 span. In the app shell, use the .text-gradient utility class from common.css.
Components¶
Cards (glassmorphism)¶
Cards use a translucent slate-800 background with a 20px backdrop blur, creating a frosted-glass effect over the background layers.
.card-glass {
background: rgba(30, 41, 59, 0.8);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(71, 85, 105, 0.5);
border-radius: 1.5rem; /* 24px */
box-shadow:
0 25px 50px -12px rgba(0, 0, 0, 0.5), /* depth shadow */
0 0 0 1px rgba(20, 184, 166, 0.1), /* teal glow ring */
inset 0 1px 0 rgba(255, 255, 255, 0.05); /* top highlight */
}
Account pages use .account-card (max-width 420px, padding 3rem). Dashboard pages use .card / .card-glass with .card-header and .card-body children.
Form elements¶
Text input:
.form-input {
padding: 0.875rem 1rem;
background: rgba(15, 23, 42, 0.6);
border: 1px solid rgba(71, 85, 105, 0.5);
border-radius: 0.75rem; /* 12px */
color: white;
font-size: 1rem;
font-family: 'DM Sans', sans-serif;
}
.form-input:focus {
border-color: var(--teal-light);
box-shadow: 0 0 0 3px rgba(20, 184, 166, 0.15), 0 0 20px rgba(20, 184, 166, 0.1);
background: rgba(15, 23, 42, 0.8);
}
.form-input.error {
border-color: #ef4444;
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.15);
}
Labels use .form-label or .form-label-styled: 14px, weight 500, colour #cbd5e1.
Checkboxes use accent-color: var(--teal-light) for the teal tint, wrapped in .checkbox-group (flex row, 8px gap).
Error text is 0.8rem, colour #f87171. Help text is 0.75rem, colour #64748b.
Buttons¶
Three button variants, all defined in both design-system.css (as .btn-primary, .btn-secondary, .btn-danger) and common.css:
Primary -- teal gradient with a glossy overlay:
.btn-primary {
background: linear-gradient(135deg, var(--teal-deep) 0%, var(--teal-light) 100%);
padding: 0.75rem 1.5rem;
border-radius: 0.75rem;
font-family: 'Outfit', sans-serif;
font-weight: 600;
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 12px 40px rgba(20, 184, 166, 0.4);
}
Secondary -- translucent slate:
.btn-secondary {
background: rgba(51, 65, 85, 0.5);
border: 1px solid rgba(71, 85, 105, 0.5);
color: var(--text-secondary);
}
Danger -- transparent with red border:
.btn-danger {
color: var(--error-text);
border: 1px solid var(--error-border);
background: transparent;
}
.btn-danger:hover { background: var(--error-bg); }
Account page buttons use the .submit-btn class instead, which is full-width and has an entrance animation.
Alerts¶
Two alert patterns exist in the codebase:
-
Account pages -- the glass-card style with semantic background and border:
-
Dashboard pages -- Tailwind-based with a left border accent, driven by Django messages:
Both follow the same semantic colour tokens (success=green, error=red, warning=amber, info=blue/teal).
Navigation sidebar¶
The main app uses a fixed sidebar (w-64, bg-slate-800/95, backdrop-blur-xl) with sectioned navigation:
- My Rota (teal accent) -- clinician pages
- Leave (cyan accent) -- leave management
- Swaps (amber accent) -- shift swap requests
- Administration (orange/coral accent) -- admin pages
Active links use bg-gradient-to-r from-teal-600 to-teal-500 for clinician pages and bg-gradient-to-r from-orange-600 to-orange-500 for admin pages. The admin accent colour visually separates admin and clinician contexts.
The sidebar includes a .sidebar-brand header bar with the teal gradient background and the Rota<span class="text-orange-300">CC</span> branding.
Motion and animation¶
Easing curve¶
The system uses one primary easing: cubic-bezier(0.16, 1, 0.3, 1) (stored as --ease-out-expo). This gives a natural deceleration feel.
Card entrance¶
Cards slide up 30px and fade in over 800ms:
.card-animate-in {
animation: cardAppear 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
opacity: 0;
transform: translateY(30px);
}
@keyframes cardAppear {
to { opacity: 1; transform: translateY(0); }
}
Staggered form reveals¶
Form groups animate in sequentially, 50ms apart:
.form-group-animated:nth-child(1) { animation-delay: 0.5s; }
.form-group-animated:nth-child(2) { animation-delay: 0.55s; }
.form-group-animated:nth-child(3) { animation-delay: 0.6s; }
/* ...continues at 50ms intervals */
The full entrance timeline for account pages:
| Time | Element |
|---|---|
| 0.0s | Card begins appearing |
| 0.2s | Logo icon |
| 0.3s | Page title h1 |
| 0.4s | Page title paragraph |
| 0.5s-0.7s | Form fields (staggered 50ms) |
| 0.75s | Submit button |
| 0.85s | Links below form |
Background effects¶
The account pages have a layered background with four animated elements. The main dashboard does not use these (it uses a plain gradient background instead).
- Grid -- subtle 60px grid lines at 3% teal opacity, scrolling diagonally over 20 seconds
- Glow orbs -- two large radial-gradient circles (teal top-right, coral bottom-left) pulsing on an 8-second cycle
- Hexagons -- six floating hexagonal SVG shapes with 15-24 second float cycles, hidden on mobile (
@media max-width: 768px) - Medical cross -- a static cross shape with a breathing glow animation on a 6-second cycle (account pages only)
Timing reference¶
| Animation | Duration | Easing |
|---|---|---|
| Card entrance | 800ms | cubic-bezier(0.16, 1, 0.3, 1) |
| Form stagger | 800ms each, 50ms offset | cubic-bezier(0.16, 1, 0.3, 1) |
| Button hover | 300ms | ease |
| Glow orb pulse | 8000ms | ease-in-out |
| Grid scroll | 20000ms | linear |
| Hexagon float | 15000-24000ms | ease-in-out |
Reduced motion¶
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
Template structure¶
The project has two distinct template hierarchies that share design tokens but differ in layout.
Account pages (login, signup, password reset)¶
templates/base.html # Email base (MJML)
templates/account/base_account.html # Account page base
extends: (standalone, includes all CSS inline)
templates/account/login.html # {% extends "account/base_account.html" %}
templates/account/signup.html
templates/account/logout.html
templates/account/password_reset.html
templates/account/password_reset_from_key.html
...
base_account.html is self-contained: it loads Tailwind via CDN, defines all CSS inline, renders the background effects (grid, orbs, hexagons, medical cross), and wraps content in a centred .account-card. Pages extend it and fill the {% block content %} block.
Dashboard pages (main app)¶
frontend/templates/frontend/base.html # App shell (sidebar + header)
frontend/templates/frontend/dashboard.html # {% extends "frontend/base.html" %}
frontend/templates/frontend/admin/*.html # Admin pages
frontend/templates/components/navigation.html # Sidebar nav partial
frontend/base.html provides:
- Tailwind CDN with custom config (brand colours, font families)
- Alpine.js (deferred) for reactive UI (sidebar toggle, dropdowns, modals)
- HTMX for dynamic content updates
- common.css (which imports design-system.css)
- Sidebar navigation, top header with user menu, Django messages area
- {% block page_title %}, {% block content %}, {% block extra_css %}, {% block extra_js %}
How to add a new account page¶
- Create
templates/account/new_page.html - Extend the base:
{% extends "account/base_account.html" %} - Set the title block:
{% block title %}Page Title - RotaCC{% endblock %} - Fill the content block with the standard structure:
{% block content %}
<!-- Logo -->
<div class="logo-container">
<div class="logo-icon">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<!-- appropriate icon -->
</svg>
</div>
</div>
<!-- Title -->
<div class="page-title">
<h1><span>Rota</span>CC</h1>
<p>Page subtitle</p>
</div>
<!-- Form -->
<form method="post">
{% csrf_token %}
<div class="form-group">
<label class="form-label" for="field">Label</label>
<input class="form-input" ...>
</div>
<button type="submit" class="submit-btn">Action</button>
</form>
<!-- Links -->
<div class="links">
<a href="...">Link text</a>
</div>
{% endblock %}
How to add a new dashboard page¶
- Create the template extending the frontend base
- Use Tailwind classes and design-system CSS classes
- Add the route in the appropriate
urls.py
Frontend libraries¶
Tailwind CSS (CDN)¶
Loaded from cdn.tailwindcss.com in both base templates. The inline tailwind.config in frontend/base.html registers custom colours and font families. Tailwind handles all spacing, layout, and most styling on dashboard pages.
Alpine.js¶
Used for reactive UI behaviour: sidebar toggle, user dropdown menu, modal show/hide, and notification dismissal. Always loaded with defer. Attributes used: x-data, x-show, x-cloak, @click, @click.away, x-transition.
HTMX¶
Loaded from cdn.jsdelivr.net/npm/htmx.org@1.9.10. Used for partial page updates without full reloads, particularly in shift management and calendar views. Attributes used: hx-get, hx-post, hx-target, hx-swap, hx-trigger.
CSS file structure¶
frontend/static/css/
design-system.css # Design tokens, component base styles
common.css # Imports design-system.css; adds overrides and utilities
common.css imports design-system.css at the top and then layers on additional styles for specific components (cards, buttons, alerts, calendar cells, environment banners) that bridge between the design tokens and Tailwind utilities.
Z-index layers¶
--z-background: 1; /* grid, hexagons, orbs */
--z-content: 10; /* main content, footer */
--z-header: 20; /* top header bar */
--z-sidebar: 30; /* sidebar navigation */
--z-modal-overlay: 50;
--z-modal: 51;
--z-tooltip: 60;
/* Environment banner: 9999 */
Colour contrast¶
All text/background combinations meet WCAG AA:
- White (
#ffffff) on slate-900 (#0f172a): 16.3:1 - Text secondary (
#cbd5e1) on slate-800 (#1e293b): 8.4:1 - Teal light (
#14b8a6) on slate-900 (#0f172a): 5.1:1