История коммитов

.
refactor(admin): move the language pages to Twig
/admin/languages and its management page become @admin/languages.twig and
@admin/languages-manage.twig; the controller returns ViewResponse and drops
the Render dependency.

- the whole johncms config no longer reaches the template: the language list
arrives as rows of code, name and active
- entries of the remote language catalog are normalised in the controller, so
the template reads the same keys whatever the source
- the PHP closure building the install/update/delete forms is now a macro
.
refactor(admin): move the advertising pages to Twig
The list, the form and the confirmation of /admin/ads become @admin/ads.twig,
@admin/ad-form.twig and @admin/ad-confirm.twig; the controller returns
ViewResponse and no longer depends on Render.

- the PHP closure that glued the reorder forms together is now a Twig macro
- the option lists of the radio groups moved to the controller, and the place
filters of the list are built from the same source
- the two hints carrying a <br> inside their msgid are printed with |raw
.
feat(console): generate ide-twig.json for IDE template navigation
The Twig namespaces are a convention of the application, so an IDE cannot
resolve @namespace template names on its own. The new twig:ide-config command
writes ide-twig.json from TemplatePathRegistry, the same source the runtime
loader uses.

- twig:ide-config [--theme=default] [--check]
- ide-twig.json is committed; the verification gate runs --check so a module
added to the configuration cannot leave it stale
.
refactor(admin): move the counters pages to Twig
Five .phtml become @admin/counters.twig, counter-{view,form,preview,delete-confirm}.twig, and the error branch moves to @admin/pages/result.twig. The list builds its up and down forms with a macro instead of a closure that wrote HTML by hand.

The snippets keep being printed as they are on the view and the preview pages: that is what the pages are for — an administrator checks there that the counter really runs before saving it.
.
refactor(admin): move the file integrity pages to Twig
Three .phtml become @admin/file-integrity{,-snapshot-confirm,-scan-result}.twig, and the two outcomes of a scan move to @admin/pages/result.twig. The message about a consistent snapshot carries a <br>, so it is handed over as Markup.

The active item of the sidebar keeps travelling as sec_menu, the same key the Plates pages used: the panel is migrated page by page, and the menu contract stays put.
.
refactor(admin): give the panel a base layout of its own
The layouts of the panel extended the base of the public site, which would have to be parametrised along three axes at once — the set of icons, the build entry point and the shape of the title. @admin/layouts/base.twig carries its own head instead: the admin bundle, the single favicon the panel needs, and a title without the site name appended, the way the Plates panel always titled its pages.

body_class becomes a block, so the guest layout keeps the plain body it had. The base of the theme no longer claims the admin layouts extend it.
.
refactor(admin): give the admin panel its Twig layouts and move its sources into the theme
The chrome of the panel becomes @admin/layouts/{default,guest}.twig on top of @theme/layouts/base.twig, so the head, the color scheme and the flatpickr locale live in one place and only the chrome and the build entry differ between the two areas. The menu of the panel becomes @admin/components/sidebar-admin-menu.twig, and @admin gains pages/result.twig and pages/errors/{403,404}.twig for the callers that will name them.

The admin theme needed no debug and sidebar-user-menu of its own: what differed were bugs of the public copies (data-parent instead of data-bs-parent, a Bootstrap 4 close button), fixed in the shared components, while the position of the debug line became a parameter.

The counters of the sidebar move to AdminRuntime::counters(), published as admin_counters(): four queries that AdminControllerContext ran on every request to the panel are now paid for by the pages that print them. Seven more counter calls that the Plates sidebar computed and never printed are gone. AdminControllerContext keeps feeding the Plates sidebar until the last .phtml of the panel is migrated.

themes/admin/src moves to themes/default/src/admin as it is — two independent asset sets, deduplication deliberately postponed; theme.php and vite.config.js point at the new path and the build was checked. The sprite of the default theme gained the tool icon and the ckeditor hi/tk locales, so the admin layouts resolve every asset through the theme chain. The dashboard is the pilot page of the panel on Twig.
.
refactor(login, registration, consent, mail): move the last public pages to Twig
16 .phtml templates become 15 pages and 3 components. The login and registration forms now use the components of the theme (alert, field-errors, consent-checkbox), the tabs of the mail became @mail/public/components/nav.twig, and both editors became module components. Every controller of the four modules returns ViewResponse.

MessageItemDTO::$text and ConversationItemDTO::$previewText return Twig\Markup. LogoutController stops running HTTP_REFERER through FILTER_SANITIZE_SPECIAL_CHARS — the environment escapes the output — and RegistrationController hands its cookies to ViewResponse instead of building a Response.

Fixed on the way: MailMessagePreviewService passed an int into SmiliesRendererInterface::render(bool) and killed the conversation list with a TypeError. The defect sat in the phpstan baseline and never showed, because the demo database carries no mail.
.
refactor(forum): move the public pages to Twig
32 .phtml templates become 26 pages and 6 components under templates/public. Duplicates collapse on the way: who and voted_users shared one user row and became visitors.twig and poll-voters.twig, new_topics served three controllers and became topic-list.twig, header and footer became components, and the quick reply form of a topic became a macro instead of two copies.

Every controller returns ViewResponse; ForumErrorRenderer gained viewResponse() and keeps render(Render, ...) only for ForumAccessMiddleware, which must return a Response. ForumMessage::$post_text, ViewPostDTO::$body, the search results and the post text of the file listing now return Twig\Markup; htmlentities over the poll form values and htmlspecialchars over the topic name are gone.

Nullable flags of the topic and the message (closed, deleted, pinned, has_poll, deleted_by, editor_name, edit_count) get accessors that type them: a null attribute of an Eloquent model falls through to the method of the same name and throws. The trap is written down in .agents/twig-migration.md.

Dead code removed on the way: reply_message declared a preview it never printed, so the purifier, the media embed and the smilies renderer left both reply controllers. The heading of the add-poll form said 'Add File'.
.
refactor(downloads): move the public pages to Twig
27 .phtml templates become 24 pages and 3 components under templates/public; delete_file and delete_additional were byte-identical and merged into confirm-delete.twig. Every controller returns ViewResponse; FileCommentsController and both middlewares stay on Plates (the legacy Comments prints its own page, middleware must return a Response).

Escaping moves to output: ~20 htmlspecialchars calls dropped, including the ones that wrote escaped data to the database (name_link, link_text, format). DownloadFile::$about_html and the comment texts of the review page now return Twig\Markup, the 'Prohibited file type!<br>...' message too. Dead FilePresenter::$filtered_name removed and preview_text no longer passes htmlentities.

Template headers declare their variables with @var and fully qualified class names; the rule is written down in .agents/twig-migration.md. The phpstan baseline is regenerated: the ignore counts of the module shifted.