# Portal Styling Guide (Template)
Use this document when building new portals so they match the visual and UX style of the reference portal (e.g. FreePBX / TM VOIP Extensions Portal). Replace placeholders like `[Portal Name]` with your portal’s name where relevant.
---
## 1. Design philosophy
- **Dark theme:** Dark backgrounds with light text; no light-mode variant in this guide.
- **Accent:** Single accent (teal/cyan gradient) for primary actions, links, and highlights.
- **Clarity:** Clear hierarchy (cards, sections, labels), consistent spacing, readable typography.
- **Consistency:** Same tokens, components, and patterns across all pages (login, app, modals).
---
## 2. Design tokens (CSS variables)
Define these in `:root` (or in a shared CSS file) and use them everywhere instead of hard-coded colors.
### Colors
| Token | Value | Usage |
|-------|--------|--------|
| `--bg-primary` | `#0a0e14` | Page background |
| `--bg-secondary` | `#11151c` | Header, secondary surfaces |
| `--bg-tertiary` | `#1a1f2b` | Inputs, table header, hover states |
| `--bg-card` | `#151a24` | Cards, modals |
| `--accent-primary` | `#00d4aa` | Primary accent (teal) |
| `--accent-secondary` | `#00b894` | Accent variant, secondary accent |
| `--accent-glow` | `rgba(0, 212, 170, 0.15)` | Focus rings, subtle highlights |
| `--text-primary` | `#e6e8eb` | Main text |
| `--text-secondary` | `#8b949e` | Labels, secondary text |
| `--text-muted` | `#5c6370` | Placeholders, disabled, hints |
| `--border-color` | `#2d333b` | Borders (cards, inputs, tables) |
| `--danger` | `#ff6b6b` | Errors, delete, destructive actions |
| `--danger-glow` | `rgba(255, 107, 107, 0.15)` | Danger focus/hover background |
| `--warning` | `#ffd93d` | Warnings |
| `--success` | `#00d4aa` | Success (can match accent) |
| `--gradient-accent` | `linear-gradient(135deg, #00d4aa 0%, #00b894 50%, #00cec9 100%)` | Primary buttons, logo text |
### Example `:root` block
```css
:root {
--bg-primary: #0a0e14;
--bg-secondary: #11151c;
--bg-tertiary: #1a1f2b;
--bg-card: #151a24;
--accent-primary: #00d4aa;
--accent-secondary: #00b894;
--accent-glow: rgba(0, 212, 170, 0.15);
--text-primary: #e6e8eb;
--text-secondary: #8b949e;
--text-muted: #5c6370;
--border-color: #2d333b;
--danger: #ff6b6b;
--danger-glow: rgba(255, 107, 107, 0.15);
--warning: #ffd93d;
--success: #00d4aa;
--gradient-accent: linear-gradient(135deg, #00d4aa 0%, #00b894 50%, #00cec9 100%);
}
```
---
## 3. Typography
- **Body / UI font:** `'Outfit', -apple-system, BlinkMacSystemFont, sans-serif`
- **Monospace (data, code, IDs):** `'JetBrains Mono', monospace`
Load from Google Fonts:
```html
```
- **Body:** `color: var(--text-primary);` `line-height: 1.6;`
- **Labels:** `font-size: 0.85rem;` `font-weight: 500;` `color: var(--text-secondary);` optional `text-transform: uppercase;` `letter-spacing: 0.5px;`
- **Card/section titles:** `font-size: 1.1rem;` `font-weight: 600;`
- **Table header:** `font-size: 0.75rem–0.8rem;` `font-weight: 600;` `text-transform: uppercase;` `letter-spacing: 0.5px;` `color: var(--text-secondary);`
- **Table body:** `font-size: 0.9rem;` monospace for IDs/codes
---
## 4. Page layout
### Global
- **Reset:** `* { margin: 0; padding: 0; box-sizing: border-box; }`
- **Body:** `background: var(--bg-primary);` `color: var(--text-primary);` `min-height: 100vh;` `font-family: 'Outfit', ...`
### Background treatment (optional)
Subtle gradient overlay for depth:
```css
body::before {
content: '';
position: fixed;
inset: 0;
background:
radial-gradient(circle at 20% 20%, rgba(0, 212, 170, 0.03) 0%, transparent 50%),
radial-gradient(circle at 80% 80%, rgba(0, 184, 148, 0.03) 0%, transparent 50%),
linear-gradient(180deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
pointer-events: none;
z-index: -1;
}
```
### Header (fixed)
- **Container:** `background: var(--bg-secondary);` `border-bottom: 1px solid var(--border-color);` `position: fixed; top: 0; left: 0; right: 0; z-index: 1000;` optional `backdrop-filter: blur(10px);`
- **Top row:** Logo left; status/user/actions right; `padding: 1rem 2rem;` `display: flex; align-items: center; justify-content: space-between;`
- **Tabs row:** Under the top row; `background: var(--bg-tertiary);` `padding: 0.5rem 2rem;` `border-top: 1px solid var(--border-color);` horizontal flex, gap, overflow-x auto for small screens
### Main content
- **Container:** `max-width: 1400px;` `margin: 0 auto;` `padding: 2rem;` `padding-top: calc(2rem + 140px);` (offset for fixed header + tabs). On mobile reduce padding and increase top offset if header stacks.
---
## 5. Logo
- **Wrapper:** flex, `align-items: center;` `gap: 0.75rem;`
- **Icon:** Square (e.g. 40×40px), `background: var(--gradient-accent);` `border-radius: 10px;` optional `box-shadow: 0 4px 20px var(--accent-glow);` emoji or icon inside.
- **Title (h1):** `font-size: 1.5rem;` `font-weight: 600;` `background: var(--gradient-accent);` `-webkit-background-clip: text;` `background-clip: text;` `-webkit-text-fill-color: transparent;`
---
## 6. Tabs (main navigation)
- **Tab button (default):** `padding: 0.75rem 1.5rem;` `background: transparent;` `border: none;` `color: var(--text-secondary);` `font-size: 0.95rem;` `font-weight: 500;` `border-radius: 8px;` flex with icon + label, `gap: 0.5rem;`
- **Hover:** `color: var(--text-primary);` `background: var(--bg-tertiary);`
- **Active:** `background: var(--gradient-accent);` `color: var(--bg-primary);`
- **Tab content:** `display: none;` by default; `.tab-content.active { display: block; }` optional fade-in animation.
---
## 7. Cards
- **Base:** `background: var(--bg-card);` `border: 1px solid var(--border-color);` `border-radius: 16px;` `padding: 1.5rem;` `margin-bottom: 1.5rem;`
- **Card header:** flex, `justify-content: space-between;` `align-items: center;` `margin-bottom: 1.5rem;` `padding-bottom: 1rem;` `border-bottom: 1px solid var(--border-color);`
- **Card title:** `font-size: 1.1rem;` `font-weight: 600;` flex with icon + text, `gap: 0.5rem;`
---
## 8. Buttons
- **Base:** `padding: 0.75rem 1.5rem;` `border-radius: 8px;` `font-size: 0.9rem;` `font-weight: 500;` inline-flex, `align-items: center;` `gap: 0.5rem;` `transition: all 0.2s ease;`
- **Primary:** `background: var(--gradient-accent);` `color: var(--bg-primary);` `border: none;` hover: slight `translateY(-2px);` `box-shadow: 0 4px 20px var(--accent-glow);`
- **Secondary:** `background: var(--bg-tertiary);` `border: 1px solid var(--border-color);` `color: var(--text-primary);` hover: `border-color: var(--accent-primary);`
- **Danger:** `background: transparent;` `border: 1px solid var(--danger);` `color: var(--danger);` hover: `background: var(--danger);` `color: white;`
- **Disabled:** `opacity: 0.5;` `cursor: not-allowed;`
- **Icon-only (small):** e.g. 28×28px, `border-radius: 6px;` same semantic colors (e.g. `.btn-remove` with `--danger`).
---
## 9. Forms
- **Grid:** `display: grid;` `grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));` `gap: 1rem;`
- **Form group:** flex column, `gap: 0.5rem;`
- **Label:** `font-size: 0.85rem;` `font-weight: 500;` `color: var(--text-secondary);` optional uppercase + letter-spacing
- **Input / select:** `padding: 0.75rem 1rem;` `background: var(--bg-tertiary);` `border: 1px solid var(--border-color);` `border-radius: 8px;` `color: var(--text-primary);` `font-size: 0.9rem;` monospace for IDs/codes
- **Focus:** `outline: none;` `border-color: var(--accent-primary);` `box-shadow: 0 0 0 3px var(--accent-glow);`
- **Placeholder:** `color: var(--text-muted);`
- **Read-only:** `background: var(--bg-secondary);` `cursor: default;`
- **Checkbox:** `accent-color: var(--accent-primary);` (or custom size e.g. 18×18px)
- **Password field:** wrapper with toggle button; input `padding-right: 3rem;` so toggle doesn’t overlap text.
---
## 10. Tables
- **Container:** `overflow-x: auto;` `border-radius: 12px;` `border: 1px solid var(--border-color);`
- **Table:** `width: 100%;` `border-collapse: collapse;`
- **th / td:** `padding: 0.6rem 0.75rem;` `text-align: left;` `border-bottom: 1px solid var(--border-color);` `color: var(--text-primary);`
- **th:** `background: var(--bg-tertiary);` `font-size: 0.75rem;` `font-weight: 600;` `text-transform: uppercase;` `letter-spacing: 0.5px;`
- **tbody tr hover:** `background-color: var(--bg-tertiary);`
- **Last row:** `tr:last-child td { border-bottom: none; }`
- **Data cells:** `font-family: 'JetBrains Mono', monospace;` `font-size: 0.9rem;`
- **Actions column:** right-aligned; min-width for action buttons; `.table-actions { display: flex; gap: 1rem; flex-wrap: wrap; }`
- **Data table variant:** `.data-table` with alternating row background (e.g. `nth-child(even)` subtle `rgba(255,255,255,0.02)`) and same hover.
---
## 11. Badges and status
- **Status badge (e.g. connection):** flex, `align-items: center;` `gap: 0.5rem;` `padding: 0.5rem 1rem;` `background: var(--bg-tertiary);` `border-radius: 20px;` `font-size: 0.85rem;` `border: 1px solid var(--border-color);`
- **Status dot:** 8×8px circle; `.connected { background: var(--success); }` `.error { background: var(--danger); }` optional pulse animation
- **Pill badge (e.g. extension ID):** `padding: 0.25rem 0.75rem;` `background: var(--accent-glow);` `color: var(--accent-primary);` `border-radius: 20px;` `font-weight: 500;`
- **Tech badge (e.g. PJSIP/SIP):** small, `border-radius: 4px;` `font-size: 0.75rem;` `font-weight: 600;` `text-transform: uppercase;` distinct colors per type (e.g. PJSIP blue, SIP purple)
---
## 12. Search and filters
- **Search box:** wrapper relative; input `padding: 0.6rem 1rem 0.6rem 2.5rem;` `width: 250px;` same colors as form inputs; optional `::before` search icon (e.g. 🔍) `left: 0.75rem;`
- **Filter checkbox:** inline-flex, `align-items: center;` `gap: 0.4rem;` `color: var(--text-secondary);` `accent-color: var(--accent-primary);`
---
## 13. Empty and loading states
- **Empty state:** `text-align: center;` `padding: 3rem;` `color: var(--text-muted);`
- **No results:** same idea, `padding: 2rem;`
- **Loading:** flex center, `padding: 2rem;` spinner (e.g. 32×32px border, `border-top-color: var(--accent-primary);` `animation: spin 1s linear infinite;`)
---
## 14. Modals
- **Overlay:** `position: fixed;` `inset: 0;` `background: rgba(0,0,0,0.7);` `z-index: 1000;` flex center; `display: none;` `.active { display: flex; }`
- **Dialog:** `background: var(--bg-card);` `border: 1px solid var(--border-color);` `border-radius: 16px;` `padding: 2rem;` `max-width: 500px;` `width: 90%;` optional scale-in animation
- **Modal header:** flex, `align-items: center;` `gap: 1rem;` `margin-bottom: 1.5rem;`
- **Modal icon:** e.g. 48×48px, `background: var(--accent-glow);` `border-radius: 12px;` centered content
- **Modal title:** `font-size: 1.25rem;` `font-weight: 600;`
- **Detail sections:** `margin-bottom: 2rem;` section title `font-size: 1.1rem;` `color: var(--accent-primary);` `border-bottom: 1px solid var(--border-color);` `padding-bottom: 0.5rem;`
- **Details grid:** `grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));` `gap: 1rem;` each item: label (small, secondary) + value (primary)
---
## 15. Toasts (notifications)
- **Container:** `position: fixed;` `top: 1rem;` `right: 1rem;` `z-index: 1100;` flex column, `gap: 0.5rem;`
- **Toast:** `padding: 1rem 1.5rem;` `background: var(--bg-card);` `border: 1px solid var(--border-color);` `border-radius: 8px;` flex, `align-items: center;` `gap: 0.75rem;` `max-width: 400px;` slide-in animation
- **Success:** `border-left: 3px solid var(--success);`
- **Error:** `border-left: 3px solid var(--danger);`
---
## 16. Pagination
- **Wrapper:** flex, `justify-content: center;` `align-items: center;` `gap: 0.5rem;` `padding: 1rem;` `border-top: 1px solid var(--border-color);`
- **Button:** `padding: 0.5rem 0.75rem;` `background: var(--bg-tertiary);` `border: 1px solid var(--border-color);` `border-radius: 6px;` `color: var(--text-primary);` `font-size: 0.85rem;` `min-width: 36px;`
- **Hover:** `background: var(--accent-glow);` `border-color: var(--accent-primary);` `color: var(--accent-primary);`
- **Active page:** `background: var(--accent-primary);` `color: var(--bg-primary);`
- **Disabled:** `opacity: 0.4;` `cursor: not-allowed;`
- **Info text:** `color: var(--text-secondary);` `font-size: 0.85rem;` between prev/next
---
## 17. Login page
- **Layout:** full viewport, flex center; `padding: 2rem;`
- **Card:** same tokens as app cards; `max-width: 400px;` `padding: 2.5rem;` `border-radius: 16px;` `box-shadow: 0 8px 32px rgba(0,0,0,0.3);`
- **Logo:** centered; icon (e.g. 64×64px) with gradient + glow; title with gradient text
- **Form:** same form-group and input styles as app; full-width primary submit button
- **Error message:** `color: var(--danger);` `font-size: 0.9rem;` above or below form
- Use the same `:root` variables and body background so login and app feel like one product.
---
## 18. Responsive
- **Breakpoint:** e.g. `@media (max-width: 768px)`
- **Header top:** `flex-direction: column;` `gap: 1rem;` `text-align: center;` reduce padding
- **Tabs:** reduce horizontal padding; allow horizontal scroll if needed
- **Container:** reduce padding; increase `padding-top` if header height grows (e.g. `calc(1rem + 180px)`)
- **Form grid:** `grid-template-columns: 1fr;` for single column on small screens
---
## 19. Checklist for a new portal
- [ ] Copy or recreate the `:root` design tokens.
- [ ] Load **Outfit** and **JetBrains Mono** (or same weights).
- [ ] Use the same header structure: logo + status/user + actions, then tabs.
- [ ] Use `.card`, `.card-header`, `.card-title` for sections.
- [ ] Use `.btn`, `.btn-primary`, `.btn-secondary`, `.btn-danger` for actions.
- [ ] Use `.form-grid`, `.form-group`, and input/select styles for forms.
- [ ] Use `.table-container`, table, `.data-table` and th/td styles for lists.
- [ ] Use same modal overlay/dialog and toast styles.
- [ ] Use same empty, loading, and error states.
- [ ] Apply same login page layout and token usage.
- [ ] Test at 768px width for basic responsive behavior.
- [ ] Replace [Portal Name] and any product-specific labels in this guide for your portal.
---
## 20. Reference files (this repo)
| File | Purpose |
|------|--------|
| `static/css/main.css` | Full implementation of tokens, layout, components |
| `app/templates/base.html` | App shell: fonts, header, tabs, container, toasts |
| `app/templates/login.html` | Login layout and inline tokens (can be moved to main.css) |
| `app/templates/tabs/_dashboard.html` | Example: cards, stats, table container |
| `app/templates/tabs/_users.html` | Example: card, form-grid, form-group, buttons, table |
For a new portal, you can copy `main.css` and adapt it (e.g. change `:root` if you need a different accent), then build your base template and pages to use the same class names and structure described above.