Skip to content

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:

  1. Account pages -- the glass-card style with semantic background and border:

    <div class="alert alert-error">
        <svg><!-- icon --></svg>
        <div class="alert-content">
            <div class="alert-title">Title</div>
            <div class="alert-text">Description</div>
        </div>
    </div>
    

  2. Dashboard pages -- Tailwind-based with a left border accent, driven by Django messages:

    <div class="p-4 rounded-lg bg-red-900/20 border-l-4 border-red-500 text-red-300">
    

Both follow the same semantic colour tokens (success=green, error=red, warning=amber, info=blue/teal).

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).

  1. Grid -- subtle 60px grid lines at 3% teal opacity, scrolling diagonally over 20 seconds
  2. Glow orbs -- two large radial-gradient circles (teal top-right, coral bottom-left) pulsing on an 8-second cycle
  3. Hexagons -- six floating hexagonal SVG shapes with 15-24 second float cycles, hidden on mobile (@media max-width: 768px)
  4. 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

  1. Create templates/account/new_page.html
  2. Extend the base: {% extends "account/base_account.html" %}
  3. Set the title block: {% block title %}Page Title - RotaCC{% endblock %}
  4. 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

  1. Create the template extending the frontend base
  2. Use Tailwind classes and design-system CSS classes
  3. 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