Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
18 KiB
Authelia Portal — LotusGuild Terminal Design System Integration
This document covers everything that had to be written from scratch to theme the
Authelia authentication portal using the LotusGuild Terminal Design System v1.2.
It is a companion to base.css / base.js for future integrations with third-party
applications that use their own component frameworks.
Why a separate file at all?
base.css targets exclusively .lt-* class names — the design system's own prefix.
Authelia's frontend is a React single-page application built on
Material UI (MUI) v5, which generates its own stable class names like
.MuiCard-root, .MuiButton-containedPrimary, etc.
Bridging the two requires a translation layer: the same design tokens and visual
patterns from base.css, re-expressed against MUI's class structure.
Additionally, Authelia v4.x (latest: 4.39.x) does not support native CSS
injection — server.asset_path only handles logo.png and favicon.ico.
The CSS is injected via nginx sub_filter in Nginx Proxy Manager, which rewrites
Authelia's HTML on the fly to include a <link> tag before </head>.
Deployment architecture
Browser
└─► https://auth.lotusguild.org (NPM — LXC 139)
├─ GET /custom.css ──────── served from /data/custom_assets/authelia-custom.css
│ (nginx alias, no upstream request)
└─ GET /* ──────────────── proxied to Authelia (LXC 167 :9091)
HTML response is rewritten by sub_filter:
</head> → <link rel="stylesheet" href="/custom.css"></head>
NPM config changed: /data/nginx/proxy_host/29.conf (auth.lotusguild.org proxy host)
# Serve LotusGuild Terminal custom CSS
location = /custom.css {
alias /data/custom_assets/authelia-custom.css;
add_header Content-Type "text/css";
add_header Cache-Control "public, max-age=3600";
}
location / {
...existing headers...
# Prevent upstream gzip so sub_filter can inspect the body
proxy_set_header Accept-Encoding "";
# Inject CSS link before </head>
sub_filter '</head>' '<link rel="stylesheet" href="/custom.css"></head>';
sub_filter_once on;
include /etc/nginx/conf.d/include/proxy.conf;
}
Warning: NPM regenerates proxy_host conf files when you edit a proxy host in the UI. If host 29 is ever modified through NPM, re-apply the
sub_filterblock and the/custom.csslocation manually, or set the config via NPM's "Advanced" tab before saving.
Authelia config changed: /etc/authelia/configuration.yml — added asset_path
under server: so Authelia serves the resized logo and favicon:
server:
address: tcp://0.0.0.0:9091
asset_path: /etc/authelia/assets
Assets in /etc/authelia/assets/:
logo.png— 256 × 256 PNG resized from Tinker Ticketsassets/images/favicon.pngfavicon.ico— 32 × 32 ICO from the same source
What is NOT in base.css and had to be written
The sections below are ordered as they appear in authelia-custom.css.
1. Re-declaring design tokens with !important
In base.css: :root { ... } is declared once, without !important.
What had to be added: The entire :root block is repeated in custom.css because
Authelia's HTML is a self-contained SPA — base.css is never loaded. More critically,
MUI injects its own inline styles and CSS-in-JS rules at very high specificity.
Every property that needs to override MUI requires !important. base.css never
uses !important because it owns its namespace; here we are guests in MUI's DOM.
/* base.css does NOT use !important anywhere */
body { background-color: var(--bg-primary); }
/* custom.css must fight MUI's inline styles */
body { background-color: var(--bg-primary) !important; }
The token values themselves are identical to base.css — only the override
mechanism is new.
2. Universal box-sizing reset with !important
In base.css: *, *::before, *::after { box-sizing: border-box; } — no flag needed
because base.css loads first.
What had to be added:
*, *::before, *::after {
box-sizing: border-box !important;
}
MUI components set box-sizing inline on some elements. Without the flag the
clip-path geometry breaks on inputs and buttons.
3. MUI typography selectors
In base.css: Typography rules target h1–h6, a, p, body — standard HTML
elements. No MUI class names exist in the design system.
What had to be added: Authelia renders text almost exclusively through MUI's
<Typography> component, which produces elements with .MuiTypography-root and
variant classes. Without targeting these, all text inherits MUI's Roboto font and
light grey colour instead of JetBrains Mono and --text-primary.
.MuiTypography-root,
.MuiInputBase-input,
.MuiFormLabel-root,
.MuiFormHelperText-root,
label, p, span, div {
font-family: var(--font-mono) !important;
color: var(--text-primary) !important;
}
h1, h2, h3, h4, h5, h6,
.MuiTypography-h5,
.MuiTypography-h6 {
color: var(--accent-orange) !important;
text-shadow: var(--glow-orange) !important;
...
}
4. .MuiCard-root / .MuiPaper-root — the login card
In base.css: .lt-card implements the terminal card with clip-path, border,
background, and the ::before corner triangle accent.
What had to be added: Authelia wraps its login form in a MUI <Card> (which
extends <Paper>). Neither selector exists in base.css. The visual result is
identical to .lt-card — the clip-path polygon, cyan border, box-glow, and
corner triangle are all copied — but they target different class names and require
!important throughout to override MUI's elevation shadows and border-radius: 4px.
.MuiCard-root,
.MuiPaper-root {
background: var(--bg-card) !important;
border: 1px solid var(--border-color) !important;
border-radius: 0 !important; /* ← overrides MUI default */
clip-path: polygon(...) !important; /* same geometry as .lt-card */
...
}
.MuiCard-root::before,
.MuiPaper-root::before {
/* same corner triangle as .lt-card::before */
}
5. Logo element — img[alt="Authelia"]
In base.css: No equivalent. The design system has no logo slot component.
What had to be added: Authelia renders the portal logo as
<img alt="Authelia" src="./static/media/logo.png">. This selector targets it
specifically to apply the cyan drop-shadow and orange hover glow. Without it the
logo renders without any terminal aesthetic treatment.
img[alt="Authelia"] {
filter: drop-shadow(0 0 12px rgba(0,212,255,0.4)) !important;
transition: filter 0.25s ease !important;
}
img[alt="Authelia"]:hover {
filter: drop-shadow(0 0 18px rgba(255,107,0,0.5)) !important;
}
6. MUI outlined input family
In base.css: .lt-input, .lt-select, .lt-textarea — custom elements with
clip-path, terminal background, and cyan focus ring.
What had to be added: MUI's outlined text field is a composition of three separate elements that each need individual targeting:
| MUI class | What it controls | Equivalent in base.css |
|---|---|---|
.MuiOutlinedInput-root |
Outer wrapper — background, clip-path | .lt-input outer |
.MuiOutlinedInput-notchedOutline |
The SVG border element | .lt-input border |
.MuiInputBase-input |
The actual <input> inside |
.lt-input inner text |
.MuiInputLabel-root |
Floating label | .lt-label |
.MuiFormHelperText-root |
Error / hint text below field | .lt-form-hint |
MUI splits the border rendering into a separate SVG <fieldset> element
(.MuiOutlinedInput-notchedOutline), which requires its own border-color override.
base.css has no equivalent splitting since .lt-input is a single element with a
CSS border.
The .Mui-focused and .Mui-error state classes also need explicit overrides because
MUI applies its own colour through CSS-in-JS with high specificity.
.MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline {
border-color: var(--accent-cyan) !important;
box-shadow: var(--box-glow-cyan) !important;
}
.MuiFormHelperText-root.Mui-error {
color: var(--accent-red) !important;
text-shadow: var(--glow-red) !important;
}
Additionally, caret-color is set explicitly on the input because MUI does not
expose this through its theme — the blinking text cursor would otherwise be white.
7. MUI button family
In base.css: .lt-btn, .lt-btn-primary, .lt-btn-danger, .lt-btn-ghost —
all using clip-path hexagon cuts, transparent backgrounds, and border+glow styling.
What had to be added: MUI uses four separate button variant classes. The primary
(sign-in) button is .MuiButton-containedPrimary which by default renders as a
solid filled rectangle. The entire button appearance — transparent background,
border, clip-path, text-transform, letter-spacing — has to be explicitly overridden
because MUI's contained variant applies an opaque background-color inline.
/* MUI default: background-color: #1976d2; border-radius: 4px; box-shadow: ... */
.MuiButton-containedPrimary {
background: transparent !important; /* fight the inline fill */
border: 1px solid var(--accent-orange-border) !important;
border-radius: 0 !important;
clip-path: polygon(...) !important;
...
}
Text/link buttons (.MuiButton-text) are separate from contained buttons in MUI but
map to the same .lt-btn-ghost pattern in base.css.
8. MUI checkbox and switch
In base.css: .lt-checkbox, .lt-switch — fully custom-drawn using CSS only.
What had to be added: Authelia uses MUI's <Checkbox> (remember-me) and
<Switch> (settings toggles). These are SVG-based components. Because MUI controls
their colour through injected CSS variables, the only reliable override is to target
.Mui-checked state classes and use !important. There is no equivalent split
between track and thumb in base.css.
.MuiSwitch-switchBase.Mui-checked { color: var(--accent-cyan) !important; }
.MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track {
background-color: var(--accent-cyan) !important;
opacity: 0.5 !important;
}
9. MUI alert / notification banners
In base.css: .lt-inline-* for inline messages (.lt-inline-success,
.lt-inline-error, etc.) and .lt-toast-* for toasts. These use a left border
accent pattern.
What had to be added: Authelia uses MUI <Alert> for login errors, success
messages, and 2FA prompts. MUI Alert has its own severity system with four variant
classes (.MuiAlert-standardError, etc.). Each severity needs explicit colour,
background, and border-color overrides. The border-left: 3px solid currentColor
pattern is adapted from base.css's inline message style but applied to MUI's
class names with clip-path: none !important to cancel MUI's rounded corners.
10. MUI select, menu, and popover
In base.css: .lt-select for the select element itself; no dropdown overlay
component exists since the design system uses native <select>.
What had to be added: MUI's <Select> renders its dropdown as a <Paper>
element portalled to <body> with class .MuiMenu-paper. Without targeting this
separately, the dropdown popup inherits the browser default white background and
Roboto font, appearing completely unstyled. .MuiMenuItem-root hover and selected
states also need individual rules.
11. OTP / 2FA digit inputs — input[type="tel"], input[type="number"]
In base.css: No equivalent. The design system has no numeric code input.
What had to be added: Authelia's one-time-password entry uses <input type="tel">
elements arranged in a row. These need oversized, centered, cyan-glowing characters
with wide letter-spacing to render like a retro code display.
input[type="tel"],
input[type="number"] {
font-size: 1.2rem !important;
font-weight: 700 !important;
color: var(--accent-cyan) !important;
text-shadow: var(--glow-cyan) !important;
letter-spacing: 0.15em !important;
text-align: center !important;
caret-color: var(--accent-orange) !important;
}
12. MUI stepper — 2FA flow breadcrumb
In base.css: .lt-stepper, .lt-step, .lt-step-num — a custom CSS stepper
for multi-step wizard flows.
What had to be added: Authelia renders a MUI <Stepper> at the top of the 2FA
flow showing steps like "Username → Password → TOTP". MUI Stepper uses distinct
classes for label, icon, and connector that don't map 1-to-1 to .lt-step-*.
The step icon is an SVG circle rendered by React so it can only be tinted via
color and filter: drop-shadow(...). The connector line between steps is a
separate <hr> with class .MuiStepConnector-line.
.MuiStepIcon-root.Mui-active {
color: var(--accent-orange) !important;
filter: drop-shadow(0 0 6px rgba(255,107,0,0.6)) !important;
}
.MuiStepIcon-root.Mui-completed {
color: var(--accent-green) !important;
}
13. MUI linear and circular progress
In base.css: .lt-progress / .lt-progress-bar — a standard CSS progress bar
using width transitions. .lt-spinner — a rotating pseudo-element spinner.
What had to be added: Authelia uses MUI <LinearProgress> during API calls and
<CircularProgress> as the main loading spinner. MUI Linear Progress animates its
own internal elements (.MuiLinearProgress-bar) with keyframe animations; it cannot
be replaced with the .lt-progress pattern. The cyan-to-orange gradient and
box-shadow glow are new here — base.css's progress bar uses a solid
--accent-orange fill.
.MuiLinearProgress-bar {
background: linear-gradient(90deg, var(--accent-cyan), var(--accent-orange)) !important;
box-shadow: 0 0 8px rgba(0,212,255,0.5) !important;
}
14. MUI tooltip
In base.css: .lt-tooltip — a CSS-only tooltip shown via :hover on a
[data-tooltip] attribute.
What had to be added: MUI <Tooltip> portals its bubble to <body> with class
.MuiTooltip-tooltip. It is a separate DOM node, not a pseudo-element, so
.lt-tooltip styles cannot reach it. A standalone rule sets the terminal background,
monospace font, and squared border.
15. *:focus-visible global ring
In base.css: Focus rings are defined individually per component
(.lt-btn:focus-visible, .lt-input:focus-visible, etc.).
What had to be added: A single global rule is more practical here because Authelia contains many interactive MUI elements not known at design time, and adding per-component rules for every possible focusable element would be brittle.
*:focus-visible {
outline: 2px solid var(--accent-cyan) !important;
outline-offset: 2px !important;
box-shadow: var(--box-glow-cyan) !important;
}
16. Branding footer watermark — #root::after
In base.css: No equivalent. The design system does not add page-level
watermarks.
What had to be added: Authelia has no footer. The #root::after pseudo-element
attaches a fixed-position text watermark reading
LOTUSGUILD SECURE PORTAL // AUTH.LOTUSGUILD.ORG at the bottom of the viewport.
This uses --text-muted, --font-mono, uppercase, and wide letter-spacing to match
the terminal aesthetic without adding any HTML.
#root::after {
content: 'LOTUSGUILD SECURE PORTAL // AUTH.LOTUSGUILD.ORG';
position: fixed;
bottom: 12px;
left: 50%;
transform: translateX(-50%);
font-size: 0.55rem;
letter-spacing: 0.18em;
color: var(--text-muted);
opacity: 0.6;
pointer-events: none;
}
Patterns shared with base.css (not new)
The following are not documented above because they are direct re-uses of
existing base.css patterns, only re-targeted to new selectors:
| Pattern | base.css source |
Re-used in custom.css for |
|---|---|---|
| Dot-grid background | html { background-image: radial-gradient(...) } |
body |
| Scanlines overlay | body::before |
body::before (same rule, !important added) |
| Corner vignette | body::after |
body::after (same rule, !important added) |
| Card clip-path polygon | .lt-card { clip-path: polygon(...) } |
.MuiCard-root |
| Card corner triangle | .lt-card::before |
.MuiCard-root::before |
| Button clip-path hexagon | .lt-btn { clip-path: polygon(...) } |
.MuiButton-containedPrimary |
| Input clip-path cut | .lt-input { clip-path: polygon(...) } |
.MuiOutlinedInput-root |
| Scrollbar | ::-webkit-scrollbar-* section 45 |
identical, border-radius: 0 added |
| Link colours | a { color: var(--accent-cyan) } |
a (same, !important added) |
| Divider | .lt-divider border-color |
.MuiDivider-root |
Notes for future third-party integrations
-
Always check for
!importantrequirements. CSS-in-JS frameworks (MUI, Emotion, styled-components) inject styles at runtime with high specificity. Without!important, most design token overrides will lose to injected inline styles. -
The split border problem. MUI (and many component libraries) separate the border from the element it visually belongs to. Always inspect the actual DOM before writing selectors — what looks like one element is often three.
-
Portalled overlays. Dropdowns, tooltips, and modals in MUI are rendered into a separate DOM tree (
<body>portal). Scoped selectors inside a card or form won't reach them. Target their root classes (.MuiMenu-paper,.MuiTooltip-tooltip) directly. -
State classes. MUI uses
.Mui-focused,.Mui-checked,.Mui-error,.Mui-active,.Mui-completed,.Mui-disabledas state modifiers. These are the equivalent of:focus,:checked, etc. but applied by JavaScript — pseudo-class selectors alone will sometimes not fire. -
SVG-based components. Checkboxes, radio buttons, step icons, and spinners are SVGs injected by React. They cannot be styled with
background,border, orclip-path. Usecolor(SVGcurrentColorinheritance) andfilter: drop-shadow()instead.