{% extends "base.html" %} {% from 'macros/a11y.html' import skip_link %} {% block body %} {{ skip_link("#main-content") }} {# Contract: ~/.claude/skills/ux-architect/components/app-shell.md (UX-031) #} {# v0.62 CSS refactor: inline Tailwind → semantic .dz-app-* / .dz-sidebar-* / .dz-topbar-* classes (components/fragments.css). Sidebar open/collapsed state still lives on the root via `data-dz-sidebar="open"` (Alpine `:data-` bind) — CSS attribute selectors drive both the slide transform on the sidebar AND the lg:pl-64 padding offset on the content area. Active nav link styling keys off `aria-current="page"` (already emitted) so the visual state and accessibility attribute share one source of truth. #}
{# Main content wrapper — padding offsets for the sidebar on desktop are applied via `[data-dz-sidebar="open"] .dz-app-content` rule. #}
{# Top bar (mobile + desktop when sidebar collapsed) #} {% block navbar %}
{# Mobile: open drawer #} {# Desktop collapsed: expand sidebar #}
{{ app_name | default("Dazzle") }}
{% if is_authenticated %} {{ user_name or user_email }} {% endif %} {# Dark mode toggle — #938: gated on `[ui] dark_mode_toggle`. Defaults to true; projects with a deliberately light-only brand (paper / academic) set the flag to false to hide the toggle everywhere. The Alpine `applyDark()` is still wired so existing keyboard / programmatic callers don't break, but with no button to invoke it the surface is gone. #} {% if dark_mode_toggle_enabled() %} {% endif %}
{% endblock navbar %} {# Page content — data-dz-* attrs feed the v0.61.0 analytics bus #}
{# Surface-level purpose subtitle (UX-048). Rendered above the content block as a muted intro line. Per-persona override resolved in page_routes._render_response. #} {% if page_purpose %}

{{ page_purpose }}

{% endif %} {% block content %}{% endblock %}
{# Overlay backdrop — mobile only, shown when sidebar open #} {# Sidebar #} {% block sidebar %} {% endblock sidebar %}
{% endblock %}