2026-03-14 21:08:57 -04:00
# LotusGuild Terminal Design System v1.0
Unified layout, styling, and JavaScript utilities for all LotusGuild web applications.
**Applies to: ** Tinker Tickets (PHP) · PULSE (Node.js) · GANDALF (Flask)
---
## Quick Start
2026-03-14 21:27:31 -04:00
**Core files ** (needed by every app):
2026-03-14 21:08:57 -04:00
```
base.css → all variables, components, animations, responsive rules
base.js → toast, modal, tabs, CSRF, fetch helpers, keyboard shortcuts
base.html → full component reference (static demo, not for production)
```
2026-03-14 21:27:31 -04:00
**Platform-specific helpers ** (one per backend):
```
php/layout.php → PHP base layout with CSP nonce + CSRF injection
python/auth.py → Flask @require_auth / @require_admin decorators
python/base.html → Jinja2 base template with block inheritance
node/middleware.js → Express requireAuth, csrfMiddleware, cspNonce, etc.
```
2026-03-14 21:08:57 -04:00
Then load them in your HTML:
``` html
< link rel = "stylesheet" href = "/web_template/base.css" >
<!-- app - specific overrides after: -->
< link rel = "stylesheet" href = "/assets/css/app.css" >
< script src = "/web_template/base.js" > < / script >
<!-- app - specific JS after: -->
< script nonce = "NONCE" src = "/assets/js/app.js?v=YYYYMMDD" > < / script >
```
---
## Aesthetic at a Glance
| Property | Value |
|---|---|
| Background | `#0a0a0a` (near-black) |
| Primary accent | `#00ff41` (phosphor green) |
| Secondary accent | `#ffb000` (amber) |
| Tertiary accent | `#00ffff` (cyan) |
| Error / critical | `#ff4444` (red) |
| Font | `'Courier New', 'Consolas', 'Monaco', 'Menlo', monospace` |
| Border radius | `0` — no rounded corners ever |
| Box shadow | Glow halos only (`0 0 Xpx color` ) — no drop shadows |
| Borders | `2px solid` standard · `3px double` for outer frames / modals |
| CRT effects | Scanline animation · screen flicker at 30s · binary corner watermark |
---
## CSS Prefix
All classes use the `.lt-` prefix (*LotusGuild Terminal*) to prevent collisions with app-specific CSS.
``` css
. lt-btn /* buttons */
. lt-card /* cards */
. lt-frame /* ASCII outer frames */
. lt-modal-overlay /* modals */
. lt-toast /* toasts */
. lt-status- * /* status badges */
. lt-p1 … . lt-p5 /* priority badges */
/* etc. */
```
---
## CSS Custom Properties
All colour, spacing, and layout values are CSS custom properties on `:root` .
Override them per-app in your `app.css` :
``` css
/* Example: app-specific accent on top of base */
: root {
--terminal-amber : #ffd000 ; /* slightly brighter amber */
}
```
### Key Variables
``` css
/* Backgrounds */
--bg-primary : # 0a0a0a
--bg-secondary : # 1a1a1a
--bg-tertiary : # 2a2a2a
--bg-terminal : # 001a00
/* Terminal colours */
--terminal-green : # 00ff41
--terminal-amber : # ffb000
--terminal-cyan : # 00ffff
--terminal-red : # ff4444
/* Glow stacks (text-shadow) */
--glow-green
--glow-green-intense
--glow-amber
--glow-amber-intense
--glow-cyan
--glow-red
/* Box-shadow halos */
--box-glow-green
--box-glow-amber
--box-glow-red
/* Z-index ladder */
--z-dropdown : 100 --z-modal : 500 --z-toast : 800 --z-overlay : 9999
```
---
## Components
### Buttons
``` html
< button class = "lt-btn" > Default< / button >
< button class = "lt-btn lt-btn-primary" > Primary< / button >
< button class = "lt-btn lt-btn-danger" > Delete< / button >
< button class = "lt-btn lt-btn-sm" > Small< / button >
< button class = "lt-btn lt-btn-ghost" > Ghost< / button >
< a href = "/create" class = "lt-btn lt-btn-primary" > Link Button< / a >
```
All buttons:
- Transparent background, green border, `[ text ]` bracket decoration via `::before` /`::after`
- Hover → amber colour, `translateY(-2px)` lift, amber glow
- `border-radius: 0` — terminal style, no rounding
### Status Badges
``` html
< span class = "lt-status lt-status-open" > Open< / span >
< span class = "lt-status lt-status-in-progress" > In Progress< / span >
< span class = "lt-status lt-status-closed" > Closed< / span >
< span class = "lt-status lt-status-online" > Online< / span >
< span class = "lt-status lt-status-failed" > Failed< / span >
```
Spinning `◐` indicator on `lt-status-in-progress` and `lt-status-running` .
### Priority Badges
``` html
< span class = "lt-priority lt-p1" > P1 Critical< / span >
< span class = "lt-priority lt-p2" > P2 High< / span >
< span class = "lt-priority lt-p3" > P3 Med< / span >
< span class = "lt-priority lt-p4" > P4 Low< / span >
< span class = "lt-priority lt-p5" > P5 Min< / span >
```
P1 has a pulsing glow animation.
### Chips (compact inline status)
``` html
< span class = "lt-chip lt-chip-ok" > OK< / span >
< span class = "lt-chip lt-chip-warn" > Warn< / span >
< span class = "lt-chip lt-chip-critical" > Critical< / span >
< span class = "lt-chip lt-chip-info" > Info< / span >
```
### Inline Messages
``` html
< div class = "lt-msg lt-msg-error" > Something went wrong< / div >
< div class = "lt-msg lt-msg-success" > Saved successfully< / div >
< div class = "lt-msg lt-msg-warning" > Rate limit approaching< / div >
< div class = "lt-msg lt-msg-info" > Auto-refresh every 30s< / div >
```
### ASCII Frames
Outer frame (double-line, for major sections):
``` html
< div class = "lt-frame" >
< span class = "lt-frame-bl" > ╚< / span >
< span class = "lt-frame-br" > ╝< / span >
< div class = "lt-section-header" > Section Title< / div >
< div class = "lt-section-body" >
<!-- content -->
< / div >
< / div >
```
Inner frame (single-line, for sub-panels):
``` html
< div class = "lt-frame-inner" >
< div class = "lt-subsection-header" > Sub Section< / div >
<!-- content -->
< / div >
```
The `::before` / `::after` pseudo-elements draw the top-left `╔` /`┌` and top-right `╗` /`┐` corners automatically. Bottom corners require explicit `<span>` children because CSS cannot target `:nth-pseudo-element` .
### Cards
``` html
< div class = "lt-card" >
< div class = "lt-card-title" > Card Title< / div >
<!-- content -->
< / div >
<!-- Grid of cards -->
< div class = "lt-grid" > <!-- auto - fill, min 300px -->
< div class = "lt-grid-2" > <!-- 2 columns -->
< div class = "lt-grid-3" > <!-- 3 columns -->
< div class = "lt-grid-4" > <!-- 4 columns -->
```
### Tables
Full-border table (simple data, terminal look):
``` html
< div class = "lt-table-wrap" >
< table class = "lt-table" id = "my-table" >
< thead > < tr >
< th data-sort-key = "name" > Name< / th >
< th data-sort-key = "status" > Status< / th >
< / tr > < / thead >
< tbody >
< tr class = "lt-row-p1 lt-row-critical" >
< td > ticket-001< / td >
< td > < span class = "lt-status lt-status-open" > Open< / span > < / td >
< / tr >
< / tbody >
< / table >
< / div >
<!-- Wire sorting -->
< script > lt . sortTable . init ( 'my-table' ) ; < / script >
```
Data table (compact, row-only separators, for dense data):
``` html
< table class = "lt-data-table" > …< / table >
```
Row classes: `lt-row-p1` – `lt-row-p5` (priority left border) · `lt-row-critical` · `lt-row-warning` · `lt-row-selected` (keyboard nav highlight)
### Forms
``` html
< div class = "lt-form-group" >
< label class = "lt-label lt-label-required" for = "title" > Title< / label >
< input id = "title" type = "text" class = "lt-input" placeholder = "…" >
< span class = "lt-form-hint" > Markdown supported.< / span >
< / div >
< div class = "lt-form-group" >
< label class = "lt-label" for = "priority" > Priority< / label >
< select id = "priority" class = "lt-select" >
< option > P1 — Critical< / option >
< / select >
< / div >
< div class = "lt-form-group" >
< label class = "lt-label" >
< input type = "checkbox" class = "lt-checkbox" > Confidential
< / label >
< / div >
<!-- Search with prompt prefix -->
< div class = "lt-search" >
< input type = "search" class = "lt-input lt-search-input" placeholder = "Search…" >
< / div >
```
Labels are amber-coloured and uppercase. Inputs: green border, focus → amber border + glow pulse.
### Modals
``` html
<!-- Trigger -->
< button class = "lt-btn" data-modal-open = "my-modal" > Open Modal< / button >
<!-- Modal -->
< div id = "my-modal" class = "lt-modal-overlay" aria-hidden = "true" >
< div class = "lt-modal" >
< div class = "lt-modal-header" >
< span class = "lt-modal-title" > Modal Title< / span >
< button class = "lt-modal-close" data-modal-close aria-label = "Close" > ✕< / button >
< / div >
< div class = "lt-modal-body" >
<!-- content -->
< / div >
< div class = "lt-modal-footer" >
< button class = "lt-btn lt-btn-ghost" data-modal-close > Cancel< / button >
< button class = "lt-btn lt-btn-primary" > Confirm< / button >
< / div >
< / div >
< / div >
```
- `data-modal-open="id"` → opens the overlay
- `data-modal-close` → closes the nearest `.lt-modal-overlay`
- Click on backdrop (`.lt-modal-overlay` ) → closes automatically
- ESC key → closes all open modals
### Tabs
``` html
< div class = "lt-tabs" >
< button class = "lt-tab active" data-tab = "panel-a" > Tab A< / button >
< button class = "lt-tab" data-tab = "panel-b" > Tab B< / button >
< / div >
< div id = "panel-a" class = "lt-tab-panel active" > …< / div >
< div id = "panel-b" class = "lt-tab-panel" > …< / div >
< script > lt . tabs . init ( ) ; < / script >
```
Active tab persists across page reloads via `localStorage` .
### Toast Notifications
``` js
lt . toast . success ( 'Ticket #123 saved' ) ;
lt . toast . error ( 'Connection refused' , 5000 ) ; // custom duration ms
lt . toast . warning ( 'Rate limit 80% used' ) ;
lt . toast . info ( 'Auto-refresh triggered' ) ;
```
Toasts appear bottom-right, stack vertically, auto-dismiss (default 3.5s), include an audible beep.
### Stats Widgets
``` html
< div class = "lt-stats-grid" >
< div class = "lt-stat-card" data-filter-key = "status" data-filter-val = "Open" >
< span class = "lt-stat-icon" > 📋< / span >
< div class = "lt-stat-info" >
< span class = "lt-stat-value" > 42< / span >
< span class = "lt-stat-label" > Open< / span >
< / div >
< / div >
< / div >
```
Clicking a stat card calls `window.lt_onStatFilter(key, val)` — implement this function in your app JS to wire it to your filter logic.
### Sidebar
``` html
< aside class = "lt-sidebar" id = "lt-sidebar" >
< div class = "lt-sidebar-header" >
Filters
< button class = "lt-sidebar-toggle" data-sidebar-toggle = "lt-sidebar" > ◀< / button >
< / div >
< div class = "lt-sidebar-body" >
< div class = "lt-filter-group" >
< span class = "lt-filter-label" > Category< / span >
< label class = "lt-filter-option" >
< input type = "checkbox" class = "lt-checkbox" > Hardware
< / label >
< / div >
< / div >
< / aside >
```
Collapse state persists across page reloads via `sessionStorage` .
### Boot Sequence
``` html
< div id = "lt-boot" class = "lt-boot-overlay" data-app-name = "MY APP" style = "display:none" >
< pre id = "lt-boot-text" class = "lt-boot-text" > < / pre >
< / div >
```
Runs once per session. Shows animated ASCII boot sequence. Add `data-app-name` to customise the banner text.
---
## JavaScript API (`window.lt`)
``` js
/* HTML safety */
lt . escHtml ( str ) // escape for innerHTML
/* Toasts */
lt . toast . success ( msg , durationMs ? )
lt . toast . error ( msg , durationMs ? )
lt . toast . warning ( msg , durationMs ? )
lt . toast . info ( msg , durationMs ? )
2026-03-14 21:27:31 -04:00
/* Audio */
lt . beep ( 'success' | 'error' | 'info' ) // Web Audio API beep; silent-fails if unavailable
// Note: toasts automatically call lt.beep() — only call directly for non-toast events
2026-03-14 21:08:57 -04:00
/* Modals */
lt . modal . open ( 'modal-id' )
lt . modal . close ( 'modal-id' )
lt . modal . closeAll ( )
/* Tabs */
lt . tabs . init ( )
lt . tabs . switch ( 'panel-id' )
/* Boot */
lt . boot . run ( 'APP NAME' , forceFlag ? )
/* Keyboard shortcuts */
lt . keys . on ( 'ctrl+s' , handler )
lt . keys . off ( 'ctrl+s' )
lt . keys . initDefaults ( ) // ESC close, Ctrl+K focus search, ? help
/* Sidebar */
lt . sidebar . init ( )
/* CSRF */
lt . csrf . headers ( ) // returns { 'X-CSRF-Token': token } or {}
/* Fetch helpers */
await lt . api . get ( '/api/tickets' )
await lt . api . post ( '/api/update_ticket.php' , { id : 1 , status : 'Closed' } )
await lt . api . put ( '/api/workflows/5' , payload )
await lt . api . delete ( '/api/comment' , { id : 9 } )
// All throw Error on non-2xx with the server's error message
/* Time */
lt . time . ago ( isoString ) // "5m ago"
lt . time . uptime ( seconds ) // "14d 6h 3m"
lt . time . format ( isoString ) // locale datetime string
/* Bytes */
lt . bytes . format ( 1234567 ) // "1.18 MB"
2026-03-14 21:27:31 -04:00
/* Table utilities — must be called explicitly (not auto-initialized) */
lt . tableNav . init ( 'table-id' ) // j/k/Enter keyboard navigation on <tbody> rows
lt . sortTable . init ( 'table-id' ) // click-to-sort on <th data-sort-key> headers
2026-03-14 21:08:57 -04:00
/* Stats widget filtering */
lt . statsFilter . init ( ) // wires data-filter-key clicks
/* Auto-refresh */
lt . autoRefresh . start ( fn , 30000 ) // call fn every 30 s
lt . autoRefresh . stop ( )
lt . autoRefresh . now ( ) // trigger immediately + restart timer
```
---
## CSRF Integration
### PHP (Tinker Tickets)
``` php
<!-- In your view , using SecurityHeadersMiddleware nonce : -->
< script nonce = " <?php echo $nonce ; ?> " >
window . CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>' ;
window . APP_TIMEZONE = '<?php echo $config[' TIMEZONE ']; ?>' ;
</ script >
```
`lt.api.*` automatically includes the token as `X-CSRF-Token` header.
### Node.js (PULSE)
``` js
// server.js: generate token and pass to template
res . render ( 'index' , { csrfToken : req . session . csrfToken } ) ;
```
``` html
<!-- EJS template -->
< script nonce = "<%= nonce %>" > window . CSRF _TOKEN = '<%= csrfToken %>' ; < / script >
```
### Flask (GANDALF)
GANDALF uses Authelia SSO with `SameSite=Strict` cookies so CSRF tokens are not required on API endpoints. For new apps using Flask-WTF:
``` html
< script nonce = "{{ nonce }}" > window . CSRF _TOKEN = '{{ csrf_token() }}' ; < / script >
```
---
## Auth Integration Patterns
### PHP — Authelia headers
``` php
// AuthMiddleware.php
$user = [
'username' => $_SERVER [ 'HTTP_REMOTE_USER' ] ? ? '' ,
'name' => $_SERVER [ 'HTTP_REMOTE_NAME' ] ? ? '' ,
'email' => $_SERVER [ 'HTTP_REMOTE_EMAIL' ] ? ? '' ,
'groups' => explode ( ',' , $_SERVER [ 'HTTP_REMOTE_GROUPS' ] ? ? '' ),
'is_admin' => in_array ( 'admin' , explode ( ',' , $_SERVER [ 'HTTP_REMOTE_GROUPS' ] ? ? '' )),
];
```
### Python/Flask — `@require_auth` decorator
``` python
def require_auth ( f ) :
@wraps ( f )
def wrapper ( * args , * * kwargs ) :
user = request . headers . get ( ' Remote-User ' , ' ' )
if not user :
return ' 401 Not Authenticated ' , 401
groups = [ g . strip ( ) for g in request . headers . get ( ' Remote-Groups ' , ' ' ) . split ( ' , ' ) ]
if ' admin ' not in groups :
return ' 403 Forbidden ' , 403
return f ( * args , * * kwargs )
return wrapper
```
### Node.js — Express middleware
``` js
function requireAuth ( req , res , next ) {
const user = req . headers [ 'remote-user' ] ;
if ( ! user ) return res . status ( 401 ) . json ( { error : 'Not authenticated' } ) ;
req . user = {
username : user ,
name : req . headers [ 'remote-name' ] || user ,
email : req . headers [ 'remote-email' ] || '' ,
groups : ( req . headers [ 'remote-groups' ] || '' ) . split ( ',' ) . map ( s => s . trim ( ) ) ,
isAdmin : ( req . headers [ 'remote-groups' ] || '' ) . split ( ',' ) . includes ( 'admin' ) ,
} ;
next ( ) ;
}
```
---
## Framework Skeleton Files
| File | Framework | Description |
|---|---|---|
| `php/layout.php` | PHP / Tinker Tickets | Complete page layout with CSP nonce, CSRF, session |
| `python/base.html` | Flask / Jinja2 | Jinja2 template extending pattern |
| `python/auth.py` | Flask | `@require_auth` decorator + `_get_user()` |
| `node/middleware.js` | Node.js / Express | Auth + CSRF + CSP nonce middleware |
---
## Directory Layout for New Apps
```
my-app/
├── assets/
│ ├── css/
│ │ └── app.css # Extends base.css — app-specific rules only
│ ├── js/
│ │ └── app.js # App logic — uses lt.* from base.js
│ └── images/
│ └── favicon.png
├── (php|python|node) files…
└── README.md # Link back to web_template/README.md for styling docs
```
---
## Responsive Breakpoints
| Breakpoint | Width | Changes |
|---|---|---|
| XL | > 1400px | Full layout, 4-col grids |
| LG | ≤ 1400px | 3-col grids, 4-col stats |
| MD | ≤ 1200px | 2-col grids, narrow sidebar |
| SM (tablet) | ≤ 1024px | Sidebar stacks below content |
| XS (phone) | ≤ 768px | Nav hidden, single-col |
| XXS | ≤ 480px | Minimal padding, 2-col stats |
---
## Key Design Rules
1. **No rounded corners ** — `border-radius: 0` everywhere
2. **No drop shadows ** — only glow halos (`box-shadow: 0 0 Xpx color` )
3. **No sans-serif fonts ** — 100% monospace stack
4. **ASCII borders ** — box-drawing chars via `::before` /`::after` , never images
5. **All uppercase ** — headings, labels, nav links, button text
6. * * `[ brackets ]` ** — buttons and nav links wrap their text in `[ ]`
7. **Green = primary ** — informational / active state
8. **Amber = secondary ** — headings, highlights, hover state
9. **Cyan = tertiary ** — uplinks, info, POE active, prompts
10. **Red = critical ** — errors, P1 priority, offline state
11. **Glow on everything critical ** — `text-shadow` glow stacks for P1 and errors
12. **Amber form labels ** — labels are amber, inputs are green
13. **Scanlines always on ** — the `body::before` CRT overlay is non-negotiable
---
## Accessibility Notes
- All interactive elements have `:focus-visible` outlines (amber, 2px)
- `@media (prefers-reduced-motion)` disables all animations
- Screen-reader utility class: `.lt-sr-only`
- Modals set `aria-hidden` and trap focus
- Status elements use semantic colour + text (not colour alone)
- Minimum contrast: text-muted `#00bb33` on `#0a0a0a` ≈ 5.3:1 (WCAG AA ✓)