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):
- `EmojiPickerElement.js` is the single wrapper around the vendored element: `load()` lazily imports the module on first open, `create(onSelect)` builds an `<emoji-picker>` wired to the vendored `data.json` and calls back with the chosen unicode. Never instantiate `<emoji-picker>` or hardcode its data source anywhere else.
- `EmojiPicker.js` is the textarea consumer: it wraps the element in a toggle panel and inserts unicode at cursor position
- Added to all `.comment-form textarea` and `.emoji-picker-target` elements
- `ReactionBar.js` is the second consumer: the `+` button in `_reaction_bar.html` mounts the same element into `.reaction-picker` so any emoji can be used as a reaction
- 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_composer_form.html` - the create-post form (topic selector, content/title, project select, attachments, poll builder, footer). Locals: `_composer_topic` (preselected topic, default `random`), `_composer_project` (preselected project uid or `""`). Wrapped in the `modal()` macro by `feed.html` (Create New Post) and `project_detail.html` (owner-only Post an update, preset to `devlog` + the project). Never fork a second copy of this form.
- `_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`.
- `_quiz_actions.html` - the hub's **New quiz** / **Create quiz with Devii** card. Needs `user`; the Devii button carries `data-devii-prompt` (the platform-wide prompt-seeding attribute read by `DeviiTerminal.bindTriggers`).
- `_quiz_scoreboard.html` - the cross-quiz scoreboard rail plus the **Your progress** card. Needs `scoreboard`, `viewer_standing`, `viewer_progress`, `user`.
- `_quiz_card.html` - one quiz listing card. Needs `item` (a `QuizListItemOut`-shaped dict) and `user`.
- `_quiz_question.html` - one question in builder-preview or player mode. Locals: `_question`, `_mode` (`builder`|`player`), plus `_attempt_url` and `answer_max_chars` in player mode.
- `_quiz_answer_review.html` - one reviewed answer on the results screen. Local: `_question`.
- `_quiz_settings_fields.html` - the shared quiz settings fieldset. Optional local `_quiz` seeds the current values.
- `_report_button.html` - the Report control (and Block, when `_owner_name` is given) for any content action bar. Locals: `_type`, `_uid`, `_owner` (owner uid - the control hides on your own content), `_owner_name` (optional), `_class` (the surrounding surface's button class). Included at fifteen sites; a new content surface MUST include it (see the root `CLAUDE.md` rule).
- `_report_dialog.html` - the one report dialog, included once in `base.html` for signed-in users and driven by `ReportDialog.js`. Reasons come from the `REPORT_REASONS` Jinja global; never hand-roll a second dialog.
- `_maturity_gate.html` - the interstitial rendered in place of a maturity-labelled item. Local: `_level`. Guard the include with the `maturity_hidden(level, user)` global; the item's `maturity` is attached by `enrich_items`/`load_detail`.

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. All CSS conventions (tokens, `--z-*` stacking bands, breakpoints, reduced motion) are in `devplacepy/static/css/CLAUDE.md`.

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.
