```
Data table (compact, row-only separators, for dense data):
```html
…
```
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
Markdown supported.
```
Labels are amber-coloured and uppercase. Inputs: green border, focus → amber border + glow pulse.
### Modals
```html
Modal Title
```
- `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
…
…
```
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
📋
42Open
```
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
```
Collapse state persists across page reloads via `sessionStorage`.
### Boot Sequence
```html
```
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?)
/* 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"
/* Table utilities */
lt.tableNav.init('table-id') // j/k/Enter keyboard navigation
lt.sortTable.init('table-id') // click-to-sort on data-sort-key headers
/* 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
```
`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
```
### 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
```
---
## 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 ✓)