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

.
fix(view): keep layout variables from shadowing page data
A {% set %} in a layout lives in the context its blocks are rendered in, so
"notifications" set for the sidebar counters replaced the list of the
notifications page, and the page died on the first item.

Every variable a layout keeps for itself is now prefixed with layout_, and a test
holds the convention for the layouts still to be written.
.
refactor(notifications): move the notification pages to Twig
Two contracts change with them, and both serve every list page still to come:

- Pagination::render() returns Twig\Markup, so a template prints {{ pagination }}
with no raw filter. Plates keeps echoing it, since Markup is a string when used
as one.
- Notification::getMessageAttribute() returns Markup too: the event templates in
the configuration carry links, and under autoescape they would print as tags.

Whether the navigation is shown is decided by Pagination::hasPages() instead of
comparing the total against the per-page setting of the visitor, which is what
that comparison meant.
.
refactor(language): move the language modal to Twig
A fragment loaded into a modal, so the template names no layout at all — the
Twig counterpart of a Plates template that never called $this->layout().

The list of languages comes from the controller instead of the configuration
array the template used to read, and the checked radio is written as a condition
rather than as an echoed attribute string.
.
refactor(redirect): move the external link page to Twig
Escaping moves to the output: the controller no longer runs htmlspecialchars()
on the referer and the template no longer runs it on the URL, so the value is
escaped once, by the environment.

The host in the warning is still emphasised inside the translated sentence, so
that one string stays markup and says so.

The referer comes off the request instead of $_SERVER, and the page is covered
by the smoke set.
.
feat(view): render the home page with Twig
The first page served by the Twig environment, with the public layout it needs.

- ViewResponse: a controller returns the template and its data, the response
normalizer renders it. The renderer arrives as a closure, so a controller that
still returns a string does not assemble the environment.
- themes/default/templates/layouts/{base,default}.twig and the components of the
chrome, next to the existing .phtml ones.
- The layout no longer reaches for di(), the container or the database: the facts
it prints come from SiteRuntime and DebugPanelRuntime, built only when a
template calls them. Ads and analytics are handed over as Markup.
- The cookie banner is described by the consent module through the twig extension
tag, so the theme only lays it out.
- vite() takes an area instead of a path and resolves the entry point through the
manifests of the theme chain.
- themes/example moves to Twig and keeps documenting how an override is written.
- app.* facts are reachable under their snake_case names; an attribute a guest
does not carry is never read straight off the user model, since Eloquent
answers __call() and Twig then invokes it as a method.
- DEBUG and DEBUG_FOR_ALL are declared dynamic for PHPStan: their value belongs to
the installation. Four baseline entries that rested on that are gone.
.
feat(view): add the Twig engine alongside Plates
Twig 3 is installed and wired, but nothing renders through it yet: no .twig
template ships, so every page is still served by Plates.

- Themes are described by an optional themes/<name>/theme.php: a parent to
inherit from, the Vite entry points and free-form settings. A theme without a
manifest inherits from the default one, so existing themes keep working.
ThemeChainResolver expands a theme into the chain templates and assets are
looked up along.
- TemplatePathRegistry registers the namespaces by convention — @theme, @admin
and one per installed module — so a module needs no Twig configuration at all.
A module with unusual layout can add paths with the johncms.template_paths tag.
- One environment serves the whole of HTTP, with autoescape on, and
strict_variables and auto_reload following DEBUG. The compiled templates live
in data/cache/twig/web, outside the document root.
- Extensions: the app global (facts about the page, the user and the token
resolved only if a template asks), the translation helpers, asset(),
asset_exists(), vite(), avatar(), the value formatters, and plates(), which
lets a Twig page pull in a partial that has not moved yet.
- AssetResolver looks up a file along the theme chain and, unlike the Plates
extension, does not take the page down when it is missing outside DEBUG.
- New commands twig:lint (also part of the verification gate) and twig:compile.
- i18n:scan now walks .twig templates too, through a second scanner working on
the Twig AST — without it the strings of a migrated template would drop out of
the .pot files.
.
refactor(view): prepare the view layer for the Twig migration
Groundwork for running Twig alongside the current Plates engine.

- The admin area is no longer detected by reading $_SERVER['REQUEST_URI'] in
RenderEngineFactory and Assets: CurrentPage::isAdminArea() answers it off the
RequestStack. Render resolves the theme through a resolver instead of holding a
name, so the engine singleton no longer freezes the theme of the first request
handled by the process.
- Johncms\System\View\Theme renamed to Johncms\View\ColorScheme, with
getCurrentScheme()/isDarkScheme(); it is the color scheme of the page, and the
name is needed for the site theme.
- New Johncms\View\RendererInterface with PlatesRenderer and DelegatingRenderer,
which routes a template to its engine by the shape of its name (@ns/file.twig to
Twig, ns::file to Plates). Registered as RendererInterface; the Twig renderer
joins it once it exists.
.
refactor(mail): send the queue only from the scheduler
Kernel::terminate() no longer flushes the mail queue after the response, and the USE_CRON constant is removed with it: mail:send-pending is registered as a scheduled task and runs every minute, so the queue has one owner. The baseline entry for the always-true negation of USE_CRON is dropped too.

Also merges the two consecutive isWebRuntime() blocks in handle() into one.
.
docs: record the mail and request changes
CHANGELOG: mail is sent by the scheduler only (USE_CRON removed) and the request is no longer a container service. The architecture guide no longer presents the position of the request argument as a rule, since the invoker resolves it by type.
.
docs: record the request-out-of-the-container work
The migration plan and the documentation submodule: the request guide rewritten for the current API and the outdated request examples in the routing and consent guides fixed.