Wagenge FC

Design System

App

Foundation

Colors

Semantic surface tokens layered with opacity-based dark-mode variants. No separate dark-mode named swatches — use dark: modifiers throughout.

Surfaces

white

Light base

gray-50

Subtle tint

gray-100

Hover / well

gray-900

Dark card

gray-950

Dark base

Brand

#800000

Primary CTA

#6a0000

Hover

#800000/20

Tint bg

red-900

Sidebar / nav

red-700

Alt primary

Status Accents

emerald-500

Approved

green-600

Attending

amber-500

Pending / warn

red-600

Danger

blue-500

Info

violet-500

Training

Dark Mode Border Tokens

border-white/10

Primary — cards, modals, nav

border-white/8

Subtle — outer card borders

border-white/15

Stronger — button outlines

border-white/5

Faintest — inner dividers

Foundation

Typography

Two families: Barlow Condensed (font-display) for headings and stats; Inter (default) for all UI text. Mono for code and data values.

Barlow Condensed — font-display

Wagenge FC — Match Day

Saturday 5 July · League

Card Heading / Section Title

Basket Name / Subheading

Inter — font-sans (default)

Subheading — 16px semibold

Body strong — 14px medium

Body default — 14px regular

Supporting / meta — 12px

SECTION LABEL — 10px bold uppercase tracked

MICRO LABEL — 9px sidebar group headers

Mono — font-mono

bg-gray-950 · border-white/10 · rounded-2xl · #800000

GET /design · 23:55 · KSH 2,500.00

<!-- Display heading -->
<h2 class="font-display text-2xl font-bold text-gray-900 dark:text-white">...</h2>
<!-- Section label (all-caps, tracked) -->
<p class="text-[10px] font-bold uppercase tracking-[0.15em] text-gray-400 dark:text-gray-500">...</p>
<!-- Body -->
<p class="text-sm text-gray-600 dark:text-gray-300">...</p>
<!-- Muted / meta -->
<p class="text-xs text-gray-500 dark:text-gray-400">...</p>

Foundation

Spacing & Radii

Consistent padding and corner-radius tokens used throughout. Pick the right radius for the element's role, not its size.

Corner Radii

rounded-lg (8px)

Small buttons, tags

rounded-xl (12px)

Buttons, inputs, month cells

rounded-2xl (16px)

Cards, modals, toasts

rounded-full (9999px)

Badges, avatars, dots

Padding Tokens

px-2 py-0.5

Micro — tight badges

px-3 py-1.5

Small — compact buttons

px-4 py-2

Medium — secondary actions

px-5 py-2.5

Default — primary buttons

px-4 py-3 / px-5 py-4

Card body — inner content

px-5 pt-5 pb-4

Card header — with bottom border

p-5 / p-6

Card padded — all sides

Components

Badges & Pills

Muted opacity tints — never solid fills. Status badges use ring-1 ring-inset for a clean boundary. Category pills use no ring and a square rounded shape.

Status Badges

Approved Voting open Pending Rejected Suspended Info Closed

Match Category Pills

League Friendlies Tournament Training Unknown

Role & Eligibility

Admin ✓ Player Fan Eligible Ineligible Exception

Count & Notification Badges

3
5 2 12
<!-- Status badge (rounded-full, ring-inset) -->
<span class="inline-flex items-center rounded-full px-2.5 py-1
             text-[10px] font-bold uppercase tracking-[0.12em]
             ring-1 ring-inset
             bg-emerald-500/10 text-emerald-700 ring-emerald-500/25
             dark:text-emerald-400">Approved</span>

<!-- Category pill (rounded, no ring) -->
<span class="block truncate rounded px-1.5 py-px
             text-[11px] font-semibold leading-tight
             bg-amber-400/25 text-amber-800 dark:text-amber-300">League</span>

<!-- Notification dot badge -->
<span class="absolute -right-1 -top-1 flex h-4 w-4 items-center justify-center
             rounded-full bg-red-500 text-[9px] font-bold text-white
             ring-2 ring-white dark:ring-gray-950">3</span>

Components

Buttons

Filled buttons carry shadow-sm and lift to shadow-md on hover. Corners: rounded-xl (sm: rounded-lg). Disabled: opacity-40 cursor-not-allowed.

Filled — Intent Variants

Secondary — Light Surface

Secondary — Dark Surface (modals, dark cards)

Icon & Ghost Buttons

Sizes

With Icons

Loading / In-progress (submit-button controller)

<!-- Primary / Brand -->
<button class="inline-flex items-center gap-2 rounded-xl
               bg-[#800000] px-5 py-2.5 text-sm font-bold text-white
               shadow-sm transition-all hover:bg-[#6a0000] hover:shadow-md
               focus:outline-none focus:ring-2 focus:ring-[#800000]/50
               focus:ring-offset-2 focus:ring-offset-white dark:focus:ring-offset-gray-950">

<!-- Secondary light surface -->
<button class="inline-flex items-center gap-2 rounded-xl
               border border-gray-200 bg-white px-5 py-2.5
               text-sm font-semibold text-gray-700 shadow-sm transition-all
               hover:border-gray-300 hover:bg-gray-50 hover:shadow
               dark:border-white/10 dark:bg-white/[0.04] dark:text-gray-200
               dark:hover:border-white/20 dark:hover:bg-white/[0.08]">

<!-- Danger outlined -->
<button class="inline-flex items-center gap-2 rounded-xl
               border border-red-200 bg-red-50 px-5 py-2.5
               text-sm font-semibold text-red-700 shadow-sm transition-all
               hover:border-red-300 hover:bg-red-100
               dark:border-red-500/30 dark:bg-red-500/10 dark:text-red-400">

<!-- Icon ghost -->
<button class="flex h-9 w-9 items-center justify-center rounded-xl
               text-gray-400 transition-colors
               hover:bg-gray-100 hover:text-gray-600
               dark:hover:bg-white/10 dark:hover:text-gray-200">

<!-- Loading (submit-button Stimulus controller) -->
<button data-submit-button-target="button">
  <span data-submit-button-target="text">Save</span>
  <svg data-submit-button-target="spinner" class="hidden h-4 w-4 animate-spin">...</svg>
</button>

Components

Form Elements

All fields use rounded-xl. Focus ring: focus:ring-2 focus:ring-red-500/20. Labels always use the 10px uppercase tracked pattern.

Light Surface

Dark Surface (modal)

KSH

Form Group — Validation States

Helper text goes here

Looks good!

Enter a valid email address

Checkboxes

<!-- Standard input -->
<label class="mb-1.5 block text-[10px] font-bold uppercase tracking-[0.12em] text-gray-500 dark:text-gray-400">Label</label>
<input type="text"
       class="w-full rounded-xl border border-gray-200 bg-white px-3.5 py-2.5 text-sm
              text-gray-900 shadow-sm transition
              focus:border-red-500/60 focus:outline-none focus:ring-2 focus:ring-red-500/20
              dark:border-white/10 dark:bg-white/[0.06] dark:text-white">

<!-- Error state -->
<input class="... border-red-400 ring-2 ring-red-400/20 dark:border-red-500/60">
<p class="mt-1.5 text-xs text-red-600 dark:text-red-400">Error message</p>

Components

Avatars

Use the user_avatar(user, size:) helper. For players, user_avatar_with_eligibility_badge adds a coloured status dot. Default background is bg-red-900.

Sizes

BK

xs

BK

sm

BK

md

BK

lg

KM

with img

Eligibility Badge (player only)

BK

Eligible

BK

Eligible (manual)

BK

Partial

BK

Ineligible

Badge colors: bg-emerald-500 eligible · bg-blue-500 manual exception · bg-yellow-500 partial · bg-red-500 ineligible

User Row (name + role combo)

BK

Brian Kimani

Player

JW

Joyce Wanjiru

Admin

TO

Tom Odhiambo

Fan

<!-- ERB helpers (recommended) -->
<%= user_avatar(@user, size: "md") %>
<%= user_avatar(@user, size: "lg") %>
<%= user_avatar_with_eligibility_badge(@user, size: "md") %>
<!-- size: "xs" | "sm" | "md" | "lg" -->

<!-- Raw HTML — initials avatar -->
<div class="flex h-10 w-10 shrink-0 items-center justify-center
            rounded-full bg-red-900 text-sm font-bold text-white shadow-md">
  BK
</div>
<!-- Sizes: xs=h-6·w-6·text-xs  sm=h-8·w-8·text-sm  md=h-10·w-10·text-sm  lg=h-16·w-16·text-xl -->

<!-- Raw HTML — avatar + eligibility badge -->
<div class="relative shrink-0">
  <div class="flex h-10 w-10 items-center justify-center
              rounded-full bg-red-900 text-sm font-bold text-white shadow-md">BK</div>
  <span class="absolute -bottom-0.5 -right-0.5 h-4 w-4 rounded-full
               ring-2 ring-white dark:ring-gray-900 bg-emerald-500"></span>
</div>
<!-- Badge sizes: xs=h-2.5·w-2.5  sm=h-3.5·w-3.5  md=h-4·w-4  lg=h-5·w-5 -->
<!-- Badge colors: bg-emerald-500 eligible · bg-blue-500 manual exception
                   bg-yellow-500 partial   · bg-red-500  ineligible -->

<!-- Raw HTML — user row (avatar + name + meta) -->
<div class="flex items-center gap-3">
  <div class="relative shrink-0">
    <div class="flex h-9 w-9 items-center justify-center
                rounded-full bg-red-900 text-sm font-bold text-white shadow-sm">BK</div>
    <!-- badge here if player -->
  </div>
  <div>
    <p class="text-sm font-semibold text-gray-900 dark:text-white">Brian Kimani</p>
    <p class="text-xs text-gray-500 dark:text-gray-400">Player</p>
  </div>
</div>

Layout

Cards

Always rounded-2xl. Two surface depths: card (gray-900) and deep/modal (gray-950). Inner dividers use thinner tokens than the outer border.

Standard card

Match

Attendance

Voting open

Submit your availability for Saturday's match against Kahawa FC.

Modal / deep card

Admin

Start Attendance Voting

Card body content goes here.

Profile hero card

BK

Brian Kimani

#7

"Kimo"

Approved Admin ✓ Striker

Admin / accent-bordered card

Admin Only

Contribution Exemptions

Mark basket months as exempt from contributions.

Card body…

<!-- Standard card -->
<div class="overflow-hidden rounded-2xl border border-gray-100 bg-white
            dark:border-white/10 dark:bg-gray-900">
  <div class="border-b border-gray-100 px-5 py-4 dark:border-white/5">header</div>
  <div class="px-5 py-4">body</div>
</div>

<!-- Modal / deep card -->
<div class="overflow-hidden rounded-2xl border border-white/10 bg-gray-950 shadow-2xl">
  <div class="border-b border-white/5 px-5 py-4">header</div>
  <div class="px-5 py-4">body</div>
  <div class="border-t border-white/5 flex justify-end gap-2 px-5 py-4">footer</div>
</div>

<!-- Profile hero (crimson strip) -->
<div class="overflow-hidden rounded-2xl border border-gray-200 bg-white
            dark:border-gray-800 dark:bg-gray-950">
  <div class="h-1 bg-gradient-to-r from-[#800000] via-[#800000]/50 to-transparent"></div>
  ...
</div>

Layout

Lists

Two patterns: divide-y for compact data rows, space-y-3 card grid for rich items. Hover uses hover:bg-gray-50 / dark:hover:bg-gray-900.

Divide-y List (members, players)

BK

Brian Kimani

Player

Eligible
JW

Joyce Wanjiru

Admin

N/A
TO

Tom Odhiambo

Fan

N/A

Card List (matches)

Wagenge FC vs Kahawa FC

League · Sat 5 Jul

Attending

Wagenge FC vs Park FC

Friendlies · Sun 6 Jul

Not responded

Wagenge FC vs Garden FC

Tournament · Mon 7 Jul

Not attending
<!-- Divide-y list wrapper -->
<div class="overflow-hidden rounded-2xl border border-gray-100 bg-white
            dark:border-white/10 dark:bg-gray-900">
  <!-- Row (add border-t on all but first) -->
  <div class="flex items-center gap-3 px-5 py-3.5 transition-colors
              hover:bg-gray-50 dark:hover:bg-white/[0.03]
              border-t border-gray-100 dark:border-white/5">...</div>
</div>

<!-- Card list item -->
<div class="flex items-center gap-4 rounded-2xl border border-gray-100 bg-white
            px-5 py-4 transition-all
            hover:border-gray-200 hover:shadow-sm
            dark:border-white/10 dark:bg-gray-900">...</div>

Feedback

Toasts & Flash

Auto-dismiss after 5s via auto-dismiss Stimulus controller. Fixed overlay container: #flash-messages. Rendered by shared/_flash_message.

Success

This is a sample success toast message.

Error

This is a sample error toast message.

Warning

This is a sample warning toast message.

Info

This is a sample info toast message.

Inline Flash (inside panels / modals — no shadow, no dismiss)

That date has already passed. Please select a future date.

Changes saved successfully.

You have unsubmitted changes.

<!-- Turbo Stream toast -->
<%= turbo_stream.append "flash-messages" do %>
  <%= render "shared/flash_message", type: :notice, message: "Saved!" %>
<% end %>
<!-- Types: :notice | :alert | :warning | :info -->

<!-- Inline flash (error) -->
<div class="flex items-start gap-2 rounded-xl
            border border-red-500/20 bg-red-500/10 px-3 py-2.5">
  <!-- icon -->
  <p class="text-sm text-red-700 dark:text-red-300">Message</p>
</div>

Feedback

Empty States

Used when a list or section has no data. Always center-aligned with a descriptive icon and an optional CTA.

Card empty state

No matches yet

When a match is scheduled it will appear here.

Dashed border (permissions / settings)

No permission

You don't have access to manage this section.

Inline empty (inside a list)

Recent transactions

No transactions recorded.

Contextual color (eligible players)

No eligible players

All players are currently ineligible.

<!-- Card empty state (standard) -->
<div class="rounded-2xl border border-gray-100 bg-white px-6 py-12 text-center
            dark:border-white/10 dark:bg-gray-900">
  <div class="mx-auto mb-4 flex h-12 w-12 items-center justify-center
              rounded-full bg-gray-100 dark:bg-gray-800">
    <!-- svg icon in text-gray-400 dark:text-gray-500 -->
  </div>
  <p class="text-sm font-semibold text-gray-700 dark:text-gray-300">No matches yet</p>
  <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Descriptive helper text.</p>
  <!-- optional CTA button -->
</div>

<!-- Dashed border empty state (permissions / settings sections) -->
<div class="rounded-2xl border border-dashed border-gray-200 py-12 text-center
            dark:border-gray-700">
  <!-- same inner structure -->
</div>

<!-- Inline empty (inside an existing card list) -->
<div class="px-5 py-10 text-center">
  <p class="text-sm text-gray-400 dark:text-gray-500">No transactions recorded.</p>
</div>

<!-- Contextual tinted empty state (colour matches the data type) -->
<div class="rounded-2xl border border-emerald-100 bg-emerald-50/60 px-6 py-10 text-center
            dark:border-emerald-500/20 dark:bg-emerald-500/[0.06]">
  <div class="mx-auto mb-3 flex h-11 w-11 items-center justify-center
              rounded-full bg-emerald-100 dark:bg-emerald-900/40">
    <!-- svg icon in text-emerald-600 dark:text-emerald-400 -->
  </div>
  <p class="text-sm font-semibold text-emerald-700 dark:text-emerald-300">Label</p>
  <p class="mt-1 text-xs text-emerald-600/70 dark:text-emerald-400/60">Description</p>
</div>
<!-- Swap emerald → amber / red / blue to match the section context -->

Patterns

Stats & Metrics

Numbers use font-display font-bold for visual weight. Group related stats in a 3-column strip.

3-Column Stat Strip (attendance)

78%

Attendance

14

Attended

18

Total matches

Payment Status Summary

8

Paid

months

2

Partial

months

2

Unpaid

months

Year Summary Card (brand-tinted)

2026 Summary

Total contributed

KSH 28,500

Transactions

14

Active baskets

3

Dashboard Stat Buttons (attendance voting)

<!-- 3-column stat strip -->
<div class="grid grid-cols-3 gap-3">
  <div class="rounded-2xl border border-gray-200 bg-white p-4 text-center
              dark:border-gray-800 dark:bg-gray-950">
    <p class="font-display text-2xl font-bold text-gray-900 dark:text-white">78%</p>
    <p class="mt-0.5 text-xs text-gray-500 dark:text-white/45">Attendance</p>
  </div>
</div>

<!-- Year summary card (brand-tinted) -->
<div class="rounded-2xl border border-[#800000]/20 bg-[#800000]/5 p-5
            dark:border-[#800000]/30 dark:bg-[#800000]/15">
  <p class="text-xs font-semibold uppercase tracking-[0.2em]
            text-[#800000] dark:text-red-400/80">2026 Summary</p>
  <div class="mt-4 grid grid-cols-3 gap-4">
    <div>
      <p class="text-xs text-[#800000]/60 dark:text-red-400/55">Label</p>
      <p class="mt-0.5 font-display text-xl font-bold text-[#800000] dark:text-red-300">Value</p>
    </div>
  </div>
</div>

<!-- Dashboard stat button (attendance voting count) -->
<button class="rounded-xl border px-2 py-2.5 text-center transition-all hover:brightness-105
               border-emerald-100 bg-emerald-50
               dark:border-emerald-800/50 dark:bg-emerald-900/10">
  <p class="font-display text-xl font-bold leading-none text-gray-900 dark:text-white">12</p>
  <p class="mt-1 text-[9px] font-bold uppercase tracking-[0.12em]
            text-emerald-600 dark:text-emerald-400">Attending</p>
</button>

<!-- Colour variants for stat buttons -->
<!-- Attending  : border-emerald-100 bg-emerald-50  dark:border-emerald-800/50 dark:bg-emerald-900/10  text-emerald-600 -->
<!-- Not attend : border-red-100    bg-red-50      dark:border-red-800/50    dark:bg-red-900/10     text-red-600    -->
<!-- Pending    : border-amber-100  bg-amber-50   dark:border-amber-800/50  dark:bg-amber-900/10   text-amber-600  -->
<!-- Total      : border-gray-100   bg-white      dark:border-gray-700      dark:bg-gray-900       text-gray-500   -->

Patterns

Voting & Toggle UI

Attendance and role toggle buttons. Unselected state: muted dark surface. Selected state: full-opacity brand color with shadow-sm.

Unselected

Selected — attending + role

Role

<!-- Vote toggle button — unselected -->
<button class="flex flex-col items-center justify-center gap-1.5
               rounded-xl border border-white/15 bg-white/[0.06] px-3 py-3
               text-gray-300 transition-colors
               hover:border-green-500/30 hover:bg-green-500/10">
  <!-- icon -->
  <span class="text-xs font-semibold">Attending</span>
</button>

<!-- Vote toggle button — selected (attending) -->
<button class="flex flex-col items-center justify-center gap-1.5
               rounded-xl border border-green-500 bg-green-600 px-3 py-3
               text-white shadow-sm">
  <!-- icon -->
  <span class="text-xs font-bold">Attending</span>
</button>

<!-- Vote toggle button — selected (not attending) -->
<button class="... border border-red-500 bg-red-600 text-white shadow-sm">

<!-- Role picker pill grid -->
<div class="rounded-xl border border-white/10 bg-white/5 px-4 py-3">
  <p class="mb-2 text-[10px] font-bold uppercase tracking-[0.12em] text-gray-500">Role</p>
  <div class="grid grid-cols-3 gap-1.5">
    <!-- Active -->
    <button class="rounded-lg border bg-[#800000] border-[#800000]
                   px-2 py-1.5 text-xs font-bold text-white">Striker</button>
    <!-- Inactive -->
    <button class="rounded-lg border border-white/10 bg-white/[0.04]
                   px-2 py-1.5 text-xs text-gray-400 hover:bg-white/10">Midfielder</button>
  </div>
</div>

Patterns

Modals & Overlays

Bottom-sheet on mobile (rounded-t-2xl), centered dialog on sm+. Backdrop: bg-black/70. Panel surface: bg-gray-950.

Admin

Confirm action

This action will permanently remove the match and all associated attendance records. This cannot be undone.

<!-- Backdrop + panel (Stimulus modal controller) -->
<div class="fixed inset-0 z-50 flex items-end sm:items-center justify-center
            bg-black/70 p-0 sm:p-4"
     data-controller="modal">
  <div class="bg-white dark:bg-gray-950
              rounded-t-2xl sm:rounded-2xl
              shadow-2xl border border-gray-200/60 dark:border-white/5
              w-full sm:max-w-md">

    <!-- Header -->
    <div class="flex items-center justify-between
                border-b border-gray-100 dark:border-white/5 px-5 py-4">
      <h3 class="text-base font-bold text-gray-900 dark:text-white">Title</h3>
      <button data-action="modal#close"
              class="flex h-8 w-8 items-center justify-center rounded-xl
                     text-gray-400 hover:bg-gray-100 hover:text-gray-600
                     dark:hover:bg-white/10 dark:hover:text-white">✕</button>
    </div>

    <!-- Body -->
    <div class="px-5 py-5">...</div>

    <!-- Footer -->
    <div class="flex justify-end gap-2
                border-t border-gray-100 dark:border-white/5 px-5 py-4">
      <button class="[secondary]">Cancel</button>
      <button class="[primary]">Confirm</button>
    </div>
  </div>
</div>

Patterns

Accordion

Powered by the accordion Stimulus controller. The chevron icon rotates 180° on open via data-accordion-target="icon".

Use the accordion controller to toggle this open. The chevron rotates 180° and content slides in. Works nested inside cards.

Standalone (inside a card, e.g. exemptions)

<div data-controller="accordion">
  <button type="button"
          class="flex w-full items-center justify-between gap-3 px-5 py-4
                 text-left transition-colors hover:bg-gray-50 dark:hover:bg-white/[0.03]"
          data-action="click->accordion#toggle"
          aria-expanded="false">
    <span class="text-sm font-semibold text-gray-900 dark:text-white">
      Heading
    </span>
    <svg class="h-4 w-4 text-gray-400 transition-transform"
         data-accordion-target="icon" ...>
      <path d="M19 9l-7 7-7-7"/>
    </svg>
  </button>
  <div class="hidden px-5 py-4 border-t border-gray-100 dark:border-white/5"
       data-accordion-target="content">
    Content...
  </div>
</div>

Wagenge FC Design System · GET /design · Public route · No auth required