The narrow layout

The application has one shell with two shapes. Above 768px it is a permanent sidebar beside a content column. At or below, the sidebar becomes a modal drawer, tables become cards, and the header sheds everything that is not needed to move around.

One breakpoint

MOBILE_BREAKPOINT_PX in src/app/core/services/viewport.service.ts is the only breakpoint the shell has. It appears in three places that cannot see each other:

WhereWhat it is
ViewportServicethe TypeScript signal components read
@media (max-width: 768px)the CSS in _common.scss and the shell components
playwright.config.ts mobile projectPixel 7, 412×915

These must agree. A shell that reflows at 768px with a table that reflows at 640px has a band where the sidebar is a drawer and the table is still a table, and nobody looks at 700px. scripts/check-responsive.mjs fails the build on a media query that does not match, unless the file is listed in its ALLOWED_OTHER_BREAKPOINTS with a reason.

A component may legitimately have its own breakpoint when the reason is its own content — a wide chart, a parameter grid that wants two columns while it can have them. That is what the allow-list is for. It is not for the shell.

Why the breakpoint exists in TypeScript at all

CSS can move the sidebar off-canvas. It cannot tell the component that the sidebar is now a dialog — that it takes role="dialog", traps focus, closes on Escape, and must be inert while hidden so it is not a run of invisible tab stops. That behaviour needs the breakpoint in code, and once it is in code it has to be the same number as the one in the stylesheet.

What changes below the breakpoint

The sidebar becomes a drawer. Off-canvas, over a backdrop, closed by default. It closes on a backdrop press, on Escape, on its own close button, and on navigating — that last one matters, because otherwise the page the user just asked for renders behind the menu they asked with.

SidebarService keeps isCollapsed and isDrawerOpen as separate state deliberately: someone who collapsed the sidebar on a desktop should still find the drawer closed on a phone, and get their collapsed column back on rotating to landscape rather than an overlay they never opened.

Tables become cards. DataTableComponent hides the header row and renders each cell as a label: value pair, reading the label from a data-label attribute. This is CSS off one attribute rather than a second template, so a column added to columns() appears in both layouts and cannot be added to one only.

The header sheds what does not fit. Business date, render time, the guide, the user's name and the language selector are all reachable elsewhere and are hidden rather than crushed. Navigation, search and sign-out stay.

Spacing tightens. --content-padding drops from 2rem to 12px and --header-height from 64px to 56px. Both are tokens, so a deployment can set them — see DOCS/CUSTOMIZATION.md.

Rules that are enforced, not suggested

npm run check:responsive fails the build on:

  1. A media query that is not the shell breakpoint, outside the allow-list.
  2. height: 100vh, anywhere. Mobile browsers measure vh against the viewport with the URL bar retracted, so 100vh is taller than the screen and the bottom of the page sits under the browser chrome. Use 100dvh.
  3. An unbounded fixed width above 320px, which forces a horizontal scroll on the narrowest supported device. A fixed width paired with a max-width in the same rule is fine and is not flagged.

npm run test:e2e:mobile covers what a regex cannot see: that the drawer is genuinely modal, that focus moves into it, that inert is applied while it is closed, that a tap on a link dismisses it, that the page does not scroll sideways, and that every header control clears a 44px touch target.

Adding a screen

Most screens need nothing. They sit in the content column, inherit --content-padding, and use DataTableComponent for lists — which is already responsive.

What to avoid:

  • A fixed pixel width wider than 320px without a max-width.
  • 100vh. Use 100dvh.
  • A new breakpoint. Use the shell's, or justify a component-specific one in the allow-list.
  • A control smaller than 44×44 that a finger has to hit.

If a screen genuinely cannot work as a single column — a comparison view, a wide reconciliation grid — put it in a horizontally scrollable container of its own rather than letting the page scroll. The page scrolling sideways is the defect; a table that scrolls inside its own box is not.