{% raw %} # Styling and templates Styling is vanilla CSS driven by design tokens, and pages are composed from a shared base template with per-page extension blocks. See also [Frontend and components](/docs/architecture-frontend.html). > Audience: administrators and maintainers. These pages are hidden from members and guests, in the sidebar, the search index, and the documentation export. ## Vanilla CSS There is **no preprocessor (no SCSS or LESS), no CSS-in-JS, and no utility framework**. Styles are hand-written CSS files in `static/css/`. Layout uses CSS Grid and Flexbox; transitions use native CSS animation. Consistency comes from design tokens, not from a framework. ## Design tokens `variables.css` defines the single source of theme truth as CSS custom properties on `:root`. Every other stylesheet references these instead of hardcoding values: | Group | Examples | |-------|----------| | Background | `--bg-primary`, `--bg-secondary`, `--bg-card`, `--bg-input` | | Accent | `--accent`, `--accent-hover`, `--accent-light` | | Text | `--text-primary`, `--text-secondary`, `--text-muted` | | Status | `--success`, `--warning`, `--danger`, `--info` | | Spacing | `--space-xs` ... `--space-2xl` | | Radius / shadow | `--radius`, `--radius-lg`, `--shadow`, `--shadow-lg` | | Fonts | `--font-sans`, `--font-mono` | | Layout | `--nav-height`, `--sidebar-width`, `--max-content` | | Topics | `--topic-devlog`, `--topic-showcase`, `--topic-question`, ... | ## File organization `base.html` always loads the core set: `variables.css`, `base.css`, `components.css`, the syntax-highlighting theme, then `markdown.css`, `mention.css`, `attachments.css`, `lightbox.css`, `media.css`, and `engagement.css`. Page-specific stylesheets (`feed.css`, `post.css`, `gists.css`, `profile.css`, and so on) are loaded per page via the `{% block extra_head %}` block, so a page pays only for the CSS it needs. ## Naming conventions - **Components**: `dp-`-prefixed, BEM-like (`.dp-upload-btn`, `.dp-upload-chip`). - **Layout / utility**: short utility classes (`.hidden`, `.text-muted`, `.line-clamp-3`). - **State**: shared state classes toggled by JavaScript (`.visible`, `.active`, `.voted`, `.dragover`). - **Feature-scoped**: prefixed by widget (`.dialog-*`, `.context-menu-*`, `.toast-*`, `.rendered-content`). ## Templates All routers import the shared `templates` from `devplacepy.templating`; they never instantiate `Jinja2Templates` per router, because globals (`avatar_url`, `get_unread_count`, `get_user_projects`, `format_date`, and others) are registered on that single instance. Pages extend `base.html` and use two extension points: page CSS in `{% block extra_head %}` and page JS in `{% block extra_js %}`. CDN scripts in `base.html` (marked, highlight.js, emoji-picker-element) use `defer` or `type="module"` for the same `domcontentloaded` reason described in [Frontend and components](/docs/architecture-frontend.html). Repeated markup is factored into partials and macros: - `_avatar_link.html` and `_user_link.html` are included with `_user`, `_size`, and `_size_class` locals for clickable avatars and usernames. - A modal macro renders the standard `.modal-overlay` / `.modal-card` structure. ## The modal pattern `Application.js` `initModals()` toggles a `.visible` class on a `.modal-overlay`; the CSS rule `.modal-overlay.visible { display: flex; }` handles visibility. Triggers usually have `href="#"`, so click handlers call `e.preventDefault()`. The `.modal-close` button is wired generically, so no inline JavaScript is needed per modal. ## How the frontend is served There is no separate front-end application or client-side router: every page is HTML rendered by the backend, then enhanced in place. How the static assets reach the browser differs between development and production. **Development** (`make dev`, or bare-metal `make prod` without nginx): the FastAPI app mounts `/static` through `UploadStaticFiles` and serves all CSS, JavaScript, and uploads itself. Everything is delivered by the application process. **Production** (Docker Compose): an nginx front door sits in front of the app and serves the static layer directly from the bind-mounted `devplacepy/static` directory on disk, never touching the application: - `/static/v/` (CSS and JavaScript) is served from disk with a one-year immutable cache; the `` path segment is the server boot timestamp, so a deploy busts every asset URL while keeping returning visitors fully cached between deploys. The unversioned `/static/` fallback keeps a short one-hour cache. See [Static asset caching](/docs/static-caching.html). - `/static/uploads/` is served from disk, but nginx **re-applies** `Content-Disposition: attachment` and `X-Content-Type-Options: nosniff`, because the app's own `UploadStaticFiles` sets that download header and a direct nginx handler would otherwise bypass it (a stored-XSS defense for uploaded SVG/HTML). - `/avatar/` is proxied to the app, because avatars are generated SVGs, not files on disk. - Everything else (`/`, the rendered pages and the JSON API) is proxied to the app, with an optional micro-cache. Rendered pages always come from the backend; in production the CSS, JavaScript, and uploads are served by nginx from disk for cache efficiency. See [nginx and networking](/docs/production-nginx.html) for the full route map. {% endraw %}