This file documents CDN libraries, the emoji picker, the modal system, and shared template partials under `templates/`. Claude Code auto-loads it whenever a file under this directory is read or edited.

## Templates and frontend (general rules)

- All routers MUST import the shared `templates` from `devplacepy.templating`. Do NOT instantiate `Jinja2Templates` per router - globals (`avatar_url`, `get_unread_count`, `get_user_projects`, `format_date`) are registered on that single instance.
- Templates extend `base.html`, add page CSS via `{% block extra_head %}`, page JS via `{% block extra_js %}`.
- **Nav active state is the `nav_active` Jinja global**, never an inline path check. `nav_active(request, *prefixes, exact=False)` (`templating.py`) returns `"active"` (or `""`) when `request.url.path` equals a prefix or is a sub-path of it (`exact=True` matches only the exact path, e.g. the admin Dashboard root). Every topnav/sidebar link uses it (`class="topnav-link {{ nav_active(request, '/projects') }}"`); do NOT hand-write `{% if request.url.path == ... or request.url.path.startswith(...) %}active{% endif %}`.

## CDN Libraries

Loaded via `<script>` tags in `base.html`. ALL must use `defer` to avoid blocking DOMContentLoaded:

```html
<script defer src="https://cdn.jsdelivr.net/npm/marked/lib/marked.umd.js"></script>
<script defer src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/highlight.min.js"></script>
<script defer src="/static/vendor/purify.min.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/emoji-picker-element@^1/index.js"></script>
<script defer src="/static/js/ContentRenderer.js"></script>
<script defer src="/static/js/EmojiPicker.js"></script>
<script type="module" src="/static/js/Application.js"></script>
```

**Never use `<script>` without `defer` for CDN libraries** - they block HTML parsing and cause `wait_until="domcontentloaded"` to timeout in Playwright tests.

## Emoji Picker

Uses `emoji-picker-element` web component (Discord-style, searchable, skin tones):
- `EmojiPicker.js` wraps it with a toggle button and inserts unicode at cursor position
- Added to all `.comment-form textarea` and `.emoji-picker-target` elements
- The old `&#x1F600;` emoji button has been removed from all templates

## Modal System

`Application.js` `initModals()` toggles a `.visible` CSS class on `.modal-overlay`; the CSS rule `.modal-overlay.visible { display: flex; }` handles visibility:

```javascript
// CORRECT - toggle the .visible class on the modal:
modal.classList.add("visible");    // show
modal.classList.remove("visible"); // hide
```

Triggers usually have `href="#"`, so call `e.preventDefault()` in click handlers. Always call `e.preventDefault()` on `[data-modal]` click handlers since trigger elements often have `href="#"`:

```javascript
trigger.addEventListener("click", (e) => {
    e.preventDefault();
    modal.style.display = "flex";
});
```

`.modal-close` is wired generically by `Application.js` - no inline JS needed in templates for basic modals.

Do NOT hand-write the overlay/header markup. Use the shared macro in `templates/_macros.html`:

```jinja
{% from "_macros.html" import modal %}
{% call modal('create-post-modal', 'Create New Post') %}{# wide=true for modal-card-wide #}
    <form ...> ... <div class="modal-footer">...</div> </form>
{% endcall %}
```

## Shared template partials

Reuse these via `{% set _x = ... %}{% include %}` (the `_avatar_link.html` convention) instead of copy-pasting markup:

- `_post_votes.html` - post +/- vote bar. Locals: `_uid`, `_my_vote`, `_count`.
- `_star_vote.html` - project/gist star button. Locals: `_type` (`project`|`gist`), `_uid`, `_my_vote`, `_count`, `_btn_class`, optional `_stop` (adds `data-stop-propagation`). The star glyph (`☆`→`★` when `.voted`) comes from the `vote-star` CSS class via `::before` (`base.css`) - do not put a literal star in markup.
- `_post_header.html` - post author/avatar/time header (`.post-header`). Locals: `_author`, `_time`.
- `_topic_selector.html` - topic radio group. Locals: `_topics`, `_selected`.

Vote button styles live ONCE in `feed.css` (`.post-action-btn`) - never redefine them in `post.css`. Page-specific CSS goes in a `static/css/*.css` file referenced from `{% block extra_head %}`, never an inline `<style>` block.

For clickable avatars/usernames, reuse `templates/_avatar_link.html` and `templates/_user_link.html` via `{% include %}` with `_user`, `_size`, `_size_class` locals.

The three public listings (`/feed`, `/gists`, `/projects`) share one search box: the `templates/_sidebar_search.html` partial (included at the top of `.sidebar-card` with `_action`/`_placeholder`/`_hidden` locals) plus the `database.text_search_clause(table, search, fields, author_field)` data helper. Each listing matches its text fields PLUS the author username: pass `author_field="user_uid"` and the helper resolves matching usernames to uids via `database.get_uids_by_username_match(search)` and OR-includes `user_uid IN (...)` (no JOIN, no separate `ilike`). Any new listing with a left filter panel reuses both - never re-inline an `ilike` search clause, never add a JOIN for author matching, and never hand-roll another search form. See `devplacepy/routers/CLAUDE.md` -> Listing search.

## Modal pattern

`Application.js` `initModals()` toggles a `.visible` CSS class on `.modal-overlay`; the CSS rule `.modal-overlay.visible { display: flex; }` handles visibility. Triggers usually have `href="#"`, so call `e.preventDefault()` in click handlers. `.modal-close` is wired generically - no inline JS needed.
