/* ==========================================================================
   AMPS UI  —  base + components for the redesigned interface.
   Cascade layers keep specificity predictable: later layer always wins,
   regardless of selector specificity. Author components flat & low.
   Load order:  tokens.css  ->  ui.css
   ========================================================================== */

@layer reset, base, layout, components, utilities;

/* ---- RESET --------------------------------------------------------------- */
@layer reset {
  *, *::before, *::after { box-sizing: border-box; }
  * { margin: 0; }
  html { -webkit-text-size-adjust: 100%; }
  body { min-height: 100dvh; line-height: var(--leading-normal); }
  img, svg, video { display: block; max-width: 100%; }
  input, button, textarea, select { font: inherit; color: inherit; }
  button { cursor: pointer; }
  table { border-collapse: collapse; }
  :where(a) { color: inherit; }
  /* The `hidden` attribute must always win. The UA rule is only [hidden]{display:none} at (0,1,0), so ANY
     component that sets its own display (.field__error/.alert = flex, .form-grid = grid, …) silently beats
     it and renders while "hidden". !important is the spec-intended enforcement — authors are not meant to
     override `hidden`. This supersedes the per-component .foo[hidden] patches dotted below. */
  [hidden] { display: none !important; }
  @media (prefers-reduced-motion: reduce) {
    *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
  }
}

/* ---- BASE ---------------------------------------------------------------- */
@layer base {
  body {
    font-family: var(--font-sans);
    font-size: var(--text-base);
    font-weight: var(--weight-normal);
    color: var(--color-text);
    background: var(--color-canvas);
    /* Grayscale AA so Archivo renders at its true weight on macOS — sub-pixel AA (the default)
       thickens the strokes and makes regular text read as bold. */
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
  }
  h1,h2,h3,h4 { line-height: var(--leading-tight); font-weight: var(--weight-semibold);
    text-wrap: balance; }   /* even heading line lengths, no runts (UX Movement: rags & runts) */
  p, .field__hint, .field__error, .alert__body { text-wrap: pretty; }  /* avoid single-word last lines */
  /* Large headings tighten tracking to counter optical looseness (only shows above ~24px). */
  h1 { font-size: var(--text-2xl); font-weight: var(--weight-bold); letter-spacing: var(--tracking-tight); }
  /* text-xl (24) is reserved for the in-app page title (.page-header h1); sections start a rung below
     so the page title is the only 24px heading — a clean size ladder, no weight-only separation. */
  h2 { font-size: var(--text-lg); }
  h3 { font-size: var(--text-md); }
  h4 { font-size: var(--text-base); }
  a { color: var(--color-text-link); text-decoration: none; }
  a:hover { text-decoration: underline; }
  :where(a, button, input, select, textarea, [tabindex]):focus-visible {
    outline: var(--focus-ring); outline-offset: 2px; border-radius: var(--radius-sm);
  }
  :where(input, select, textarea) { accent-color: var(--accent-control); }
  code, kbd, samp { font-family: var(--font-mono); }
}

/* ---- LAYOUT : app shell -------------------------------------------------- */
@layer layout {
  /* Skip link — first focusable element, off-screen until focused, then a pill over the chrome.
     The rail puts a long nav run before the content, so this is required (WCAG 2.4.1 Bypass Blocks). */
  .skip-link { position: fixed; z-index: 200; inset-block-start: var(--space-2); inset-inline-start: var(--space-2);
           padding: var(--space-2) var(--space-3); background: var(--color-surface); color: var(--color-text-link);
           border-radius: var(--radius-md); box-shadow: var(--shadow-md); font-weight: var(--weight-medium);
           transform: translateY(-150%); transition: transform var(--transition); }
  .skip-link:focus { transform: none; }

  /* Brand-coloured chrome (logo bar + nav) on the left, content on the right; no top bar on desktop.
     height:100dvh + overflow:hidden so the chrome stays put and only .shell__main scrolls. The logo
     sits in its own `brand` row atop the sidebar column, so on mobile it can detach into a
     full-width top bar while the nav drawer slides in independently. */
  .shell { display: grid; grid-template-columns: var(--shell-sidebar-w) 1fr;
           grid-template-rows: auto 1fr; height: 100dvh; overflow: hidden;
           grid-template-areas: "brand main" "sidebar main"; }
  /* Brand-coloured rail. The nav inside it flips to light-on-blue by overriding the component
     tokens HERE only (scoped), so the nav rules below need no changes. */
  .shell__sidebar { grid-area: sidebar; background: var(--color-sidebar);
           display: flex; flex-direction: column; gap: 0;
           padding: 0; overflow-y: auto;
           /* Light-on-brand, tuned so the rail recedes and content leads: quieter idle text and
              subtle washes for hover/active — no stark white punch-out. */
           --color-text: rgb(255 255 255 / .80);
           --color-text-muted: rgb(255 255 255 / .60);
           --color-surface: rgb(255 255 255 / .08);    /* subtle idle hover wash */
           --color-surface-hover: rgb(255 255 255 / .08);
           --color-accent: #ffffff;                     /* avatar / count badge stay solid white */
           --color-accent-hover: rgb(255 255 255 / .9);
           --color-text-inverse: var(--color-sidebar);  /* initials on the white avatar */
           --color-border: rgb(255 255 255 / .18);
           --color-border-subtle: rgb(255 255 255 / .12);
           --color-logo-mark: #ffffff; }
  .shell__main { grid-area: main; overflow: auto; }
  .shell__content { max-width: var(--content-max-width); margin-inline: auto;
           padding: var(--space-6) var(--space-6) var(--space-12); }

  /* Below 1024px the brand becomes a full-width top bar (logo + hamburger) and the nav becomes an
     off-canvas drawer — branding stays visible even with the drawer closed. */
  @media (max-width: 1023px) {
    .shell { grid-template-columns: 1fr; grid-template-areas: "brand" "main"; }
    .shell__sidebar { position: fixed; inset-block: 0; inset-inline-start: 0;
           width: var(--shell-sidebar-w); z-index: 50; box-shadow: var(--shadow-lg);
           transform: translateX(-100%); transition: transform var(--transition); }
    .shell[data-nav-open="true"] .shell__sidebar { transform: none; }
    .shell__scrim { display: none; position: fixed; inset: 0; z-index: 40;
           background: rgb(0 0 0 / .4); }
    .shell[data-nav-open="true"] .shell__scrim { display: block; }
  }
}

/* ---- COMPONENTS ---------------------------------------------------------- */
@layer components {

  /* Brand bar — its own grid cell (grid-area brand) atop the sidebar column. Brand-coloured with a
     toned near-white logo, LEFT-ALIGNED to the nav's 24px inset at its intrinsic size (not stretched
     to fill — that read as "crammed in"). The Dashboard divider below is a single bottom hairline. */
  .brand { grid-area: brand; display: flex; align-items: center; justify-content: flex-start;
           gap: var(--space-2); min-height: var(--shell-brand-h);
           padding-block: var(--space-3); padding-inline: var(--space-6);
           margin: 0; color: #ffffff; background: var(--color-sidebar);
           --color-logo: rgb(255 255 255 / .90);    /* toned near-white — softer than a pure-white punch-out */
           --color-logo-mark: var(--color-logo); }  /* "P" mark matches the wordmark — one uniform toned-white logo */
  .brand__home { display: flex; align-items: center; justify-content: flex-start; flex: 0 1 auto;
           min-inline-size: 0; color: var(--color-logo); }
  .brand__home:hover { text-decoration: none; }
  /* The hamburger lives in the brand bar, mobile only; white on the blue bar. `.brand .nav-toggle`
     (0,2,0) beats `.btn`'s display (components, 0,1,0) so it stays hidden on desktop. */
  .brand .nav-toggle { display: none; flex: none; color: #ffffff; }
  .brand .nav-toggle:hover { background: rgb(255 255 255 / .14); color: #ffffff; }
  /* Keyboard focus on the blue chrome: the global blue ring would be near-invisible, so use a
     white ring on both the brand bar and the sidebar nav (WCAG 2.2 visible-focus). */
  .brand :where(a, button):focus-visible,
  .shell__sidebar :where(a, button, [tabindex]):focus-visible { outline-color: #ffffff; }
  /* Inline SVG logo (not <img>) so its colours resolve: the wordmark uses currentColor and the "P"
     mark uses --color-logo-mark. Sized by a modest fixed HEIGHT at its intrinsic aspect (never
     stretched to fill the column — that read as "crammed in"); left-aligned via .brand above. */
  .brand__logo, .brand__logo svg { width: auto; height: 28px; display: block; }
  /* Mobile: brand becomes a full-width top bar — hamburger left, slightly shorter logo beside it. */
  @media (max-width: 1023px) {
    .brand .nav-toggle { display: inline-flex; }
    .brand__logo, .brand__logo svg { height: 24px; }
  }
  /* nav region runs full-bleed so the active highlight spans the whole rail width
     (admin-console convention — Fasthosts/Carbon/Azure); content is inset via item padding. */
  .shell__sidebar .nav-group { padding-inline: 0; }
  /* Nav starts flush under the brand band — no top gap (a gap would show as an empty strip above
     the fill when item 1 is hovered/active). Breathing room lives inside each row. Full-width-fill
     convention (WordPress/Azure/Carbon). */
  .shell__sidebar .nav-group:first-of-type { margin-block-start: 0; }
  .shell__sidebar .nav-group + .nav-group { margin-block-start: var(--space-3); }

  /* Icons — Lucide via the SVG sprite. Stroke = currentColor, so an icon takes the surrounding
     text colour automatically (incl. white on the active nav pill). */
  .icon { inline-size: 16px; block-size: 16px; flex: none; vertical-align: middle; }
  .nav-item .icon { inline-size: 18px; block-size: 18px; }

  /* Pinned sidebar footer (account / notifications / theme — Anthropic-console style) */
  /* Footer (global menu) items run full-bleed like the main nav — same 24px content inset,
     square, full-width hover/active. */
  .sidebar-footer { margin-block-start: auto; display: flex; flex-direction: column; gap: 2px;
           padding-block: var(--space-2); padding-inline: 0;
           border-block-start: 1px solid var(--color-border-subtle); }
  .sidebar-footer .nav-item, .sidebar-footer .user-button { padding-inline: var(--space-6);
           border-radius: 0; }
  .nav-item__meta { margin-inline-start: auto; }
  .count-badge { min-width: 1.25rem; height: 1.25rem; padding-inline: 6px;
           display: inline-flex; align-items: center; justify-content: center;
           border-radius: var(--radius-full); background: var(--color-accent);
           color: var(--color-text-inverse); font-size: var(--text-sm);
           font-weight: var(--weight-semibold); }

  /* User button (avatar + name, bottom of sidebar) */
  .user-button { all: unset; box-sizing: border-box; display: flex; align-items: center;
           gap: var(--space-3); width: 100%; padding: var(--space-2) var(--space-3);
           border-radius: var(--radius-md); cursor: pointer; color: var(--color-text);
           font-weight: var(--weight-medium); }
  .user-button:hover { background: var(--color-surface-hover); }
  /* Secondary identity caption under the user name (e.g. "Administrator") — a muted label. */
  .user-role { color: var(--color-text-muted); }
  /* Reusable circular avatar (Gravatar image); a tinted disc shows while it loads or if it fails.
     The L1 rail keeps its own smaller .r-avatar (26px); this is the larger, reusable size. */
  .avatar { inline-size: 40px; block-size: 40px; border-radius: var(--radius-full);
           object-fit: cover; background: var(--color-surface-sunken); flex: none; }
  .user-button .icon { transition: transform var(--transition); }
  .user-button[aria-expanded="true"] .icon { transform: rotate(180deg); }

  /* Global menu — opens upward from the user button (Create account / Log out). A floating
     popover over the nav (shadow + surface), inset from the rail edges. */
  .user-area { position: relative; }
  /* The popover is a light floating surface — re-assert the normal tokens so it doesn't inherit
     the sidebar's light-on-blue overrides. */
  .shell__sidebar .user-menu { --color-text: var(--slate-900); --color-surface: #ffffff;
           --color-surface-hover: var(--slate-100); --color-border-subtle: var(--slate-200); }
  .user-menu { position: absolute; inset-inline: var(--space-2); inset-block-end: calc(100% + var(--space-1));
           background: var(--color-surface); border: 1px solid var(--color-border-subtle);
           border-radius: var(--radius-md); box-shadow: var(--shadow-lg);
           padding: var(--space-1); display: flex; flex-direction: column; gap: 2px; z-index: 20; }
  .user-menu[hidden] { display: none; }
  .user-menu__item { display: flex; align-items: center; gap: var(--space-3);
           padding: var(--space-2) var(--space-3); border-radius: var(--radius-sm);
           color: var(--color-text); }
  .user-menu__item:hover { background: var(--color-surface-hover); text-decoration: none; }
  .user-menu__sep { height: 1px; background: var(--color-border-subtle); margin: var(--space-1) 0; }

  /* Navigation (sidebar) */
  .nav-group { display: flex; flex-direction: column; gap: 2px; }
  /* sentence-case, near-black, semibold — like UniFi (hierarchy via weight+size, not greying) */
  .nav-group__label { font-size: var(--text-sm); font-weight: var(--weight-medium);
           color: var(--color-text);
           padding: var(--space-3) var(--space-6) var(--space-1); }
  .nav-item { display: flex; align-items: center; gap: var(--space-3);
           padding: var(--space-2) var(--space-3); border-radius: var(--radius-md);
           color: var(--color-text); font-weight: var(--weight-normal);
           /* button-safe: a <button class="nav-item"> matches anchors */
           width: 100%; text-align: start; background: none; border: 0; cursor: pointer; }
  .nav-item:hover { background: var(--color-surface-hover); text-decoration: none; }
  /* On the brand rail, idle hover is a subtle white wash (--color-surface). */
  .shell__sidebar .nav-item:not([aria-current="page"]):hover,
  .shell__sidebar .user-button:hover { background: var(--color-surface); }
  .nav-item[aria-current="page"] { background: var(--color-accent);
           color: var(--color-text-inverse); }
  .nav-item[aria-current="page"]:hover { background: var(--color-accent-hover); }
  /* Active item on the navy rail: a solid brand-blue pill + full-white text (Vultr/Fasthosts style)
     so the brand blue echoes from the rail through to the content links/buttons/tabs. */
  .shell__sidebar .nav-item[aria-current="page"] { background: var(--color-brand);
           color: #ffffff; font-weight: var(--weight-medium); }
  .shell__sidebar .nav-item[aria-current="page"]:hover { background: var(--blue-700); }
  /* Sidebar primary items run full-bleed (square, 24px content inset) so hover/active fill the
     whole rail width. Includes the persistent Dashboard item (a direct sidebar child, above the
     context band). Footer items (the theme menu) keep the base inset rounded pill. */
  .shell__sidebar > .nav-item,
  .shell__sidebar .nav-group .nav-item { padding-inline: var(--space-6); border-radius: 0; }
  /* Dashboard = the home anchor: same height as the section items, set apart by a SINGLE hairline
     beneath it (same `--color-border` as the footer separator) — no top border, so there's no seam
     under the logo. Whitespace + the bottom divider separate it from the section list. */
  .shell__sidebar > .nav-item { padding-block: var(--space-2);
           border-block-end: 1px solid var(--color-border-subtle); }

  /* Expandable parent + second-level sub-nav (multi-level sidebar menus) */
  .nav-caret { margin-inline-start: auto; font-size: .75em; transition: transform var(--transition); }
  .nav-item--parent[aria-expanded="false"] .nav-caret { transform: rotate(-90deg); }
  .nav-item--parent[aria-expanded="false"] + .nav-sub { display: none; }
  .nav-sub { display: flex; flex-direction: column; gap: 1px;
           margin: 2px 0 var(--space-2);
           margin-inline-start: calc(var(--space-3) + 0.55em);   /* align rail under parent icon */
           padding-inline-start: var(--space-4);
           border-inline-start: 1px solid var(--color-border-subtle); }
  .nav-subitem { display: block; padding: var(--space-1) var(--space-3); border-radius: var(--radius-md);
           color: var(--color-text-muted); }
  .nav-subitem:hover { background: var(--color-surface-hover); color: var(--color-text); text-decoration: none; }
  .nav-subitem[aria-current="page"] { background: var(--color-accent); color: var(--color-text-inverse);
           font-weight: var(--weight-medium); }

  /* Breadcrumbs */
  .breadcrumbs { display: flex; flex-wrap: wrap; gap: var(--space-2);
           font-size: var(--text-sm); color: var(--color-text-subtle);   /* support tone */
           margin-block-end: var(--space-2);   /* breadcrumb names the same entity as the H1 → tight
                                                   coupling, distinctly less than the page-header's
                                                   space-5 gap below the title (proximity: within < between) */
           padding: 0; list-style: none; }  /* zero the UA <ol> indent so it lines up with the title */
  .breadcrumbs li + li::before { content: "/"; margin-inline-end: var(--space-2);
           color: var(--color-text-subtle); }
  .breadcrumbs li { list-style: none; }

  /* Page header. For a RECORD page it carries the entity identity: `__titles` stacks the record
     name (h1 + status badge) over a muted `.record-meta` line of key identifiers — static, not a
     link/nav (the record's sub-pages are the tabs below; global nav stays in the sidebar). */
  .page-header { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
           gap: var(--space-4); margin-block-end: var(--space-5);
           /* reserve the action-button row height whether or not actions are present, so the content
              below sits at the same place; the centred title just gains a little breathing room. */
           min-block-size: var(--control-h); }
  .page-header__titles { display: flex; flex-direction: column; gap: var(--space-1); min-inline-size: 0; }
  /* Record/page title sits at 24px — prominent but not shouty in a dense console (cf. the base
     h1 30px reserved for sparse/portal pages); clearly above the 16px card titles. */
  .page-header h1 { font-size: var(--text-xl); letter-spacing: var(--tracking-snug); }
  .page-header__actions { display: flex; flex-wrap: wrap; gap: var(--space-2); }
  .record-meta { color: var(--color-text-muted); font-size: var(--text-base); }
  /* When the identity line links back up to the record root, keep its muted look (continuity)
     and reveal the affordance on hover. */
  .record-meta a { color: inherit; text-decoration: none; }
  .record-meta a:hover { color: var(--color-text); text-decoration: underline; }
  /* Title-integrated hierarchy (nav option D): the parent sits IN the h1 as a clear link, so the
     way back is title-sized and unmissable — not a separate small control, and no new control type. */
  .title-up { color: var(--color-text-link); text-decoration: none; }
  .title-up:hover { text-decoration: underline; }
  .title-sep { color: var(--color-text-subtle); font-weight: var(--weight-normal); margin-inline: var(--space-2); }
  /* tabs that act as a record's sub-nav sit just under the header, above the tab body */
  .page-header + .tabs { margin-block-start: calc(-1 * var(--space-2)); }

  /* Tabs */
  .tabs { display: flex; flex-wrap: wrap; gap: var(--space-1);
           border-block-end: 1px solid var(--color-border-subtle); margin-block-end: var(--space-5);
           padding: 0; list-style: none; }
  .tab { padding: var(--space-3) var(--space-4); color: var(--color-text-muted);
           font-weight: var(--weight-medium); border-block-end: 2px solid transparent;
           margin-block-end: -1px; }
  .tab:hover { color: var(--color-text); text-decoration: none; }
  .tab[aria-current="page"] { color: var(--color-accent-text);
           border-block-end-color: var(--color-accent-text); }

  /* Drill-in "up one level" control (nav option A) — a high-salience way back that mirrors the
     tab click that drilled in; the breadcrumb alone is too quiet to be the primary exit. */
  .back-link { display: inline-flex; align-items: center; gap: var(--space-1);
           margin-inline-start: calc(-1 * var(--space-2)); margin-block-end: var(--space-3);
           padding: var(--space-1) var(--space-3) var(--space-1) var(--space-2);
           font-size: var(--text-base); font-weight: var(--weight-medium);
           color: var(--color-text-muted); border-radius: var(--radius-md); }
  .back-link:hover { color: var(--color-text-link); background: var(--color-surface-hover);
           text-decoration: none; }
  .back-link .icon { width: 1.05em; height: 1.05em; }

  /* Secondary sub-nav (nav option B) — a sub-area's own views shown UNDER the persistent primary
     tabs. Pill-shaped so it reads as clearly subordinate, not a competing second tab row. */
  .subnav { display: flex; flex-wrap: wrap; gap: var(--space-1); margin-block-end: var(--space-5); }
  .tabs + .subnav { margin-block-start: calc(-1 * var(--space-3)); }
  .subnav__item { padding: var(--space-1) var(--space-3); border-radius: var(--radius-md);
           font-weight: var(--weight-medium); color: var(--color-text-muted); }
  .subnav__item:hover { background: var(--color-surface-hover); color: var(--color-text);
           text-decoration: none; }
  .subnav__item[aria-current="page"] { background: var(--color-accent-subtle);
           color: var(--color-accent-text); }

  /* Toolbar above a table/list for that view's primary action(s). Actions are full-size `.btn`
     (never `--sm`) — a given function looks the same everywhere it appears. */
  .toolbar { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end;
           gap: var(--space-3); margin-block-end: var(--space-4); }
  /* Optional leading cluster (search + filters). Its auto end-margin pushes the trailing action(s)
     to the right — a bare-action toolbar (no `__start`) still sits right via justify-content. */
  .toolbar__start { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3);
           margin-inline-end: auto; }

  /* Segmented control — a boxed, mutually-exclusive toggle (list status filters, binary form modes).
     Same accent-selection language as .subnav; sized to line up with .btn / .input. */
  .segmented { display: inline-flex; align-items: stretch; gap: 2px; padding: 3px;
           background: var(--color-surface-sunken); border: 1px solid var(--color-border-subtle);
           border-radius: var(--radius-md); }
  .segmented__item { display: inline-flex; align-items: center; justify-content: center;
           min-height: calc(var(--control-h) - 8px); padding: 0 var(--space-3); border: 0; background: none; font: inherit;
           font-size: var(--text-base); font-weight: var(--weight-medium); color: var(--color-text-muted);
           border-radius: calc(var(--radius-md) - 2px); cursor: pointer; white-space: nowrap;
           text-decoration: none; }
  .segmented__item:hover { color: var(--color-text); }
  .segmented__item[aria-pressed="true"] { background: var(--color-accent-subtle);
           color: var(--color-accent-text); }

  /* Header action menu — a button that drops a popover of actions (e.g. "New ▾"). Lives in the
     page-header actions slot; anchored to its trigger, opening downward (cf. the sidebar
     user-menu which opens upward). */
  .menu { position: relative; display: inline-flex; }
  .menu__list { position: absolute; inset-block-start: calc(100% + var(--space-1)); inset-inline-end: 0;
           min-inline-size: 12rem; z-index: 30; padding: var(--space-1);
           background: var(--color-surface); border: 1px solid var(--color-border-subtle);
           border-radius: var(--radius-md); box-shadow: var(--shadow-lg); }
  .menu__list[hidden] { display: none; }
  /* the rail user menu sits at the bottom and spans the rail, so its popup opens upward */
  .r-usermenu, .r-thememenu { display: block; width: 100%; }
  .menu--up .menu__list { inset-block-start: auto; inset-block-end: calc(100% + var(--space-1)); }
  /* The sidebar user menu is fixed-positioned so it escapes the rail's overflow-x clip (a 56px strip
     would otherwise crop it) and renders over L2 + the page. It is always the expanded-rail content
     width, anchored bottom-left above the avatar button — so it fills L1 with symmetric insets when the
     rail is expanded, and overlays the content at that same width when the rail is a strip. */
  .r-usermenu .menu__list, .r-thememenu .menu__list { position: fixed;
           inset-block: auto calc(40px + var(--space-2) * 2 + var(--space-1));  /* clear the avatar button + gap */
           inset-inline: var(--space-2) auto;
           inline-size: calc(var(--shell-sidebar-w) - var(--space-2) * 2); min-inline-size: 0; }
  /* the theme menu sits one rail row (48px item + 2px gap) above the user menu, so its popup clears that row too */
  .r-thememenu:has(+ .r-usermenu) .menu__list { inset-block-end: calc(40px + var(--space-2) * 2 + var(--space-1) + 50px); }
  /* a radio menu shows its current choice with a trailing check, kept in place when unchecked so labels don't move */
  .menu__check { margin-inline-start: auto; }
  .menu__item[aria-checked="false"] .menu__check { visibility: hidden; }
  .menu__item { display: flex; align-items: center; gap: var(--space-3); width: 100%;
           padding: var(--space-2) var(--space-3); border: 0; background: none; text-align: start;
           cursor: pointer; font-family: inherit; font-weight: var(--weight-normal);
           border-radius: var(--radius-sm); color: var(--color-text); font-size: var(--text-base); }
  .menu__item:hover { background: var(--color-surface-hover); text-decoration: none; }
  /* the button's own trailing caret only, never an icon nested deeper (the rail's .ricon glyph) */
  .menu [data-menu-button] > .icon:last-child { transition: transform var(--transition); }
  .menu [data-menu-button][aria-expanded="true"] > .icon:last-child { transform: rotate(180deg); }
  .menu__item--danger { color: var(--color-danger-text); }

  /* Page-header actions collapse to a single "Actions ▾" menu on narrow screens (menu is cloned from
     the header buttons by shell2.js, so markup stays a plain list of .btn's). */
  .action-overflow { display: none; }
  @media (max-width: 640px) {
    .page-header__actions > .btn { display: none; }
    .page-header__actions > .action-overflow { display: inline-flex; }
  }

  /* Buttons — hierarchy by fill, not size */
  .btn { display: inline-flex; align-items: center; gap: var(--space-2);
           padding: 0 var(--space-4); border-radius: var(--radius-md);
           border: 1px solid transparent; font-weight: var(--weight-medium);
           line-height: var(--leading-snug); white-space: nowrap;
           transition: background var(--transition), border-color var(--transition), color var(--transition);
           min-height: var(--control-h); }
  .btn:hover { text-decoration: none; }
  .btn:disabled, .btn[aria-disabled="true"] { opacity: .55; cursor: not-allowed; }
  /* Submit-once busy state — a leading spinner while the action is in flight (the button is also
     disabled by JS so it can't be double-submitted). window.clearSubmit() clears it. */
  .btn.is-busy { cursor: default; opacity: 1; }   /* stay full-opacity while working — the spinner signals the state */
  .btn.is-busy .icon { display: none; }            /* hide the button's own leading icon so only the spinner shows */
  /* the icon's own 16px, so swapping icon for spinner leaves the button's width unchanged */
  .btn.is-busy::before { content: ""; inline-size: 16px; block-size: 16px; flex: none; border-radius: 50%;
           border: 2px solid currentColor; border-inline-end-color: transparent; animation: amps-spin .6s linear infinite; }
  @media (prefers-reduced-motion: reduce) { .btn.is-busy::before { animation-duration: 1.4s; } }
  .btn--primary { background: var(--color-accent); color: var(--color-text-inverse); }
  .btn--primary:hover { background: var(--color-accent-hover); }
  /* White with a 3:1 edge, so it reads as a real action on the canvas and on white cards alike.
     Colour stays neutral — blue is reserved for primary. */
  .btn--secondary { background: var(--color-btn-secondary-bg); color: var(--color-text);
           border-color: var(--color-border-strong); }
  .btn--secondary:hover { background: var(--color-btn-secondary-hover); }
  .btn--ghost { background: transparent; color: var(--color-text-muted); }
  .btn--ghost:hover { background: var(--color-surface-hover); color: var(--color-text); }
  .btn--danger { background: var(--color-danger-solid); color: var(--color-text-inverse); }
  .btn--danger:hover { background: color-mix(in srgb, var(--color-danger-solid) 85%, #000); }
  .btn--success { background: var(--color-success-solid); color: var(--color-text-inverse); }
  .btn--success:hover { background: color-mix(in srgb, var(--color-success-solid) 85%, #000); }
  .btn--sm { padding: var(--space-1) var(--space-3); min-height: 28px; font-size: var(--text-sm); }

  .button-bar { display: flex; flex-wrap: wrap; gap: var(--space-2);
           margin-block-start: var(--space-6); }
  .button-bar--end { justify-content: flex-end; }
  /* Enter submits the first submit button in the DOM, so the primary leads the markup and moves to the right edge here. */
  .button-bar--end > .btn--primary { order: 1; }

  /* Eased hover/active state changes so interactions don't snap (matches the focus/input easing
     already in place); reduced-motion is guarded globally in the reset layer. */
  .nav-item, .nav-subitem, .tab, .row-action, .data-table tbody tr, .user-menu__item, .menu__item {
           transition: background var(--transition), color var(--transition); }

  /* Card / panel — a firm border, tonal step and the smallest shadow, so white cards hold their edge on the pale canvas */
  .card { background: var(--color-surface); border: 1px solid var(--color-border);
           border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); }
  /* Reserve a consistent header height that fits a standard button, so a header carrying a right-aligned
     action (table-shell `action`) is the SAME height as a title-only one — one header height everywhere.
     min-block-size governs the height; the small block padding keeps a full-size button from inflating it. */
  .card__header { display: flex; align-items: center; justify-content: space-between;
           gap: var(--space-3); min-block-size: 3.25rem; padding: var(--space-1) var(--space-5);
           border-block-end: 1px solid var(--color-border-subtle); }
  /* A card header is a toolbar: an add-control here sits at intrinsic width, not the form default
     width:100% (which would fill the row, wrap the button below it, and inflate the header). */
  .card__header :is(.input, .select) { inline-size: auto; }
  /* Base text size, medium (500) — the body-size heading weight (contract: weight = structure; 600 reads
     heavy at 14px). It reads as a heading via that weight + the header divider + the muted labels beneath. */
  .card__title { font-size: var(--text-base); font-weight: var(--weight-medium); }
  .card__body { padding: var(--space-5); }
  /* Stacked cards: one canonical gap (centralised here, not per-page). Major section groups use
     `.page-section` (32px); consecutive cards sit closer at 20px. */
  .card + .card { margin-block-start: var(--space-5); }
  /* Page content is a vertical flow: every top-level block (a card, OR a <form>/<div> that wraps a
     card) is spaced by the flex gap, so inter-card spacing survives a form wrapper — the .card + .card
     adjacency selector alone broke the moment a card sat inside a form. Reset that adjacency margin for
     direct children here so the gap is not doubled; it still applies to cards nested inside a form. */
  .page-body { display: flex; flex-direction: column; gap: var(--space-5); }
  .page-body > .card + .card { margin-block-start: 0; }
  /* a bar after a top-level card already has the 20px gap, so it adds 4px to match the 24px it gets after a card inside a form */
  .page-body > * + .button-bar,
  .page-body > * + form > .button-bar:not(:not(input[type="hidden"]) ~ .button-bar) { margin-block-start: var(--space-1); }
  /* the flex gap owns spacing between direct children, so drop a top-level alert's own bottom margin —
     otherwise it stacks on the gap and the banner sits closer to the header than to the content below.
     Scoped to direct children, so an alert nested in a form (error-summary) keeps its own spacing. */
  .page-body > .alert { margin-block-end: 0; }
  /* Page intro / description — a leading paragraph above the content, given clear separation (32px) from
     the content below. Consistent whether it sits inside a form (no gap) or is a direct .page-body child
     (which already carries the 20px flex gap). */
  .page-intro { margin-block-end: var(--space-8); }
  .page-body > .page-intro { margin-block-end: var(--space-3); }   /* + the 20px flex gap = 32px total */
  /* availability checker (broadband/ethernet): the form intro and the results summary each need a gap
     before the block that follows (the field grid / the table) — the reset zeroes default <p> margins. */
  [data-availability] form > p:first-of-type,
  [data-availability] [data-availability-summary] { margin-block-end: var(--space-4); }
  .card-grid { display: grid; gap: var(--space-5);
           grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); }

  /* Stat tile */
  .stat__label { color: var(--color-text-subtle); }   /* support tone */
  .stat__value { font-size: var(--text-2xl); font-weight: var(--weight-bold);
           letter-spacing: var(--tracking-tight);
           font-variant-numeric: tabular-nums; margin-block-start: var(--space-1); }

  /* Status indicator — coloured dot + label (matches the portal's icon+label status,
     stays at body size so it doesn't read as a different-sized pill in a table) */
  .status { display: inline-flex; align-items: center; gap: var(--space-2); white-space: nowrap; }
  .status::before { content: ""; inline-size: .5rem; block-size: .5rem; border-radius: 50%;
           background: var(--color-text-subtle); flex: none; }
  .status--active::before  { background: var(--color-success-solid); }
  .status--inactive::before{ background: var(--color-text-subtle); }
  .status--pending::before { background: var(--color-warning-solid); }
  .status--suspended::before { background: var(--color-warning-solid); }
  .status--error::before   { background: var(--color-danger-solid); }

  /* Badges / status pills */
  .badge { display: inline-flex; align-items: center; gap: var(--space-1);
           padding: 2px var(--space-2); border-radius: var(--radius-md);
           font-size: var(--text-sm); font-weight: var(--weight-medium);
           border: 1px solid transparent; }
  .badge--success { background: var(--color-success-bg); color: var(--color-success-text);
           border-color: var(--color-success-border); }
  .badge--warning { background: var(--color-warning-bg); color: var(--color-warning-text);
           border-color: var(--color-warning-border); }
  .badge--danger { background: var(--color-danger-bg); color: var(--color-danger-text);
           border-color: var(--color-danger-border); }
  .badge--info { background: var(--color-info-bg); color: var(--color-info-text);
           border-color: var(--color-info-border); }
  .badge--neutral { background: var(--color-surface-sunken); color: var(--color-text-muted);
           border-color: var(--color-border-subtle); }

  /* Alerts / message boxes */
  .alert { display: flex; gap: var(--space-3); padding: var(--space-4); align-items: flex-start;
           border: 1px solid; border-radius: var(--radius-md); margin-block-end: var(--space-4); }
  .alert > .icon { flex: none; margin-block-start: 2px; }  /* align the (block) icon to the first text line */
  .alert__body { display: flex; flex-direction: column; gap: var(--space-1); }
  .alert--success { background: var(--color-success-bg); border-color: var(--color-success-border);
           color: var(--color-success-text); }
  .alert--warning { background: var(--color-warning-bg); border-color: var(--color-warning-border);
           color: var(--color-warning-text); }
  .alert--danger { background: var(--color-danger-bg); border-color: var(--color-danger-border);
           color: var(--color-danger-text); }
  .alert--info { background: var(--color-info-bg); border-color: var(--color-info-border);
           color: var(--color-info-text); }
  /* Page-top notice: a full-bleed variant for system-level messages (billing run, outage, maintenance).
     Rendered as the FIRST child of .shell__content, above .page-header — no top bar reopened. */
  .alert--banner { border-radius: 0; border-inline-width: 0; align-items: center;
           margin-block: calc(-1 * var(--space-6)) var(--space-5);
           margin-inline: calc(-1 * var(--space-6)); padding-inline: var(--space-6); }
  .alert--banner > .alert__body { flex-direction: row; align-items: center; gap: var(--space-2); flex: 1; }
  .alert__dismiss { margin-inline-start: auto; }
  /* Flash banners (JS-rendered, data-banner) are single-line messages — centre the icon / text / dismiss
     vertically, rather than the base .alert top-align that strands a single line above the taller dismiss. */
  .alert[data-banner] { align-items: center; }
  .alert[data-banner] > .icon { margin-block-start: 0; }

  /* Meter — a thin proportional fill (mailbox quota, disk usage, allocation). No JS, no charts;
     width via the --meter custom property (e.g. style="--meter: 64%"). */
  .meter { display: block; inline-size: 100%; block-size: 8px; border-radius: var(--radius-full);
           background: var(--color-surface-sunken); overflow: hidden; }
  .meter__fill { display: block; block-size: 100%; border-radius: inherit; inline-size: var(--meter, 0%);
           background: var(--color-accent); }
  .meter--ok   .meter__fill { background: var(--color-success-solid); }
  .meter--warn .meter__fill { background: var(--color-warning-solid); }
  .meter--over .meter__fill { background: var(--color-danger-solid); }
  .meter--fixed { inline-size: 12rem; }  /* a set width for inline use (e.g. beside a "3.9 GB of 5 GB" label) */

  /* ---- TABLE : the canonical data table (rebuild — see DESIGN.md "Tables") ------
     Wrapper = keyboard-focusable scroll region (WCAG reflow) + a container-query context + a
     vertical cap so the sticky thead engages. Widths are declared ONCE on <col>s under
     table-layout:fixed — no width:1%/max-width:0 hack. Wrap by default; truncate only opt-in. */
  /* positioned, so a cell's .visually-hidden text is clipped here instead of stretching the page */
  .table-scroll { container-type: inline-size; overflow: auto; position: relative;
           border: 1px solid var(--color-border-subtle); border-radius: var(--radius-lg);
           background: var(--color-surface); }   /* horizontal scroll only; no max-height → never scrolls vertically */
  .table-scroll:focus-visible { outline: var(--focus-ring); outline-offset: -2px; }

  /* THE COLUMN MODEL (research-backed, 2026-07). table-layout:auto sizes every column to its content,
     ONE `.cell-grow` column absorbs the slack, and a global cap + ellipsis (below) truncates the rest —
     so a table fills its pane, never wraps, and only scrolls horizontally as a last resort. Real
     <table> semantics are kept (no display:grid — that strips row/column a11y in Safari). Read-only
     tables no longer need per-column tuning; only editable/allocation grids keep the `.col--*` width
     tokens, via layout='fixed' below. See DESIGN.md §4 Tables. */
  .data-table { inline-size: 100%; table-layout: auto; border-collapse: separate; border-spacing: 0;
           font-size: var(--text-base); line-height: var(--leading-snug);
           --table-pad-y: var(--space-3); --table-pad-x: var(--space-4); }   /* density knob */
  /* Editable / allocation grids and live lists opt back into fixed layout + the width tokens (pass
     layout='fixed' to table-shell): even input / amount columns, and columns that hold still as you search. */
  .data-table--editable, .data-table[data-layout="fixed"] { table-layout: fixed; }
  .data-table[data-layout="fixed"] col.col--grow    { width: auto; }
  .data-table[data-layout="fixed"] col.col--fit     { width: var(--col-fit); }
  .data-table[data-layout="fixed"] col.col--select  { width: var(--col-select); }
  .data-table[data-layout="fixed"] col.col--id      { width: var(--col-id); }
  .data-table[data-layout="fixed"] col.col--date    { width: var(--col-date); }
  .data-table[data-layout="fixed"] col.col--time    { width: var(--col-time); }
  .data-table[data-layout="fixed"] col.col--price   { width: var(--col-price); }
  .data-table[data-layout="fixed"] col.col--actions { width: var(--col-actions); }
  .data-table[data-layout="fixed"] col.col--qty     { width: var(--col-qty); }
  .data-table[data-layout="fixed"] col.col--name    { width: var(--col-name); }
  .data-table[data-layout="fixed"] col.col--type    { width: var(--col-type); }
  .data-table[data-layout="fixed"] col.col--datetime { width: var(--col-datetime); }
  /* under fixed layout the grow column's 100% would outbid the col widths */
  .data-table[data-layout="fixed"] .cell-grow { inline-size: auto; }
  /* a fixed column can't fit every status name, so a long one wraps rather than being cut off */
  .data-table[data-layout="fixed"] .status { white-space: normal; }

  .data-table caption { text-align: start; padding: var(--table-pad-y) var(--table-pad-x);
           font-weight: var(--weight-medium); font-size: var(--text-md);
           border-block-end: 1px solid var(--color-border-subtle); }
  /* Truncate by default: content-fit up to --col-max, then ellipsis — headers too, so a long header
     clips instead of forcing its column wider (the old overflow bug). min-inline-size:0 lets a cell
     shrink below its content so the table always fits its pane. Opt into wrapping with .cell-wrap. */
  .data-table th, .data-table td { padding: var(--table-pad-y) var(--table-pad-x);
           text-align: start; vertical-align: middle;
           min-inline-size: 0; max-inline-size: var(--col-max, 18rem);
           overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* Rows with a stacked cell (a .cell-main over .cell-sub lines) top-align so a figure in another
     column lines up with the description's first line; simple single-line rows stay centred (above),
     so a taller cell like a 32px row-action doesn't leave the text stranded at the top. */
  .data-table tbody tr:has(.cell-sub, .cell-attr:not([hidden])) td { vertical-align: top; }
  /* Sticky column header — a structural label, so near-black + medium like a card title (contract:
     read-this structure), on the sunken surface with a box-shadow underline (no black bar). */
  .data-table thead th { position: sticky; inset-block-start: 0; z-index: 2;
           background: var(--color-surface-sunken); color: var(--color-text);
           font-weight: var(--weight-medium); white-space: nowrap;
           box-shadow: inset 0 -1px 0 var(--color-border); }
  .data-table tbody td { border-block-start: 1px solid var(--color-border-subtle); }
  .data-table tbody tr:first-child td { border-block-start: none; }  /* header draws its own line */

  /* Zebra is opt-in; the rule is "rules OR zebra, never neither" (row rules are the default). */
  .data-table--zebra tbody tr:nth-child(even) td { background: var(--color-surface-sunken); }

  /* ROW STATES — a low-saturation intent tint + a left-border rule (never colour-alone). Cascade order
     encodes precedence: hover < highlight < warning < error < selected. Muted uses the 4.5:1 token. */
  .data-table tbody tr:hover td { background: var(--color-surface-hover); }
  .data-table tbody tr[data-state="muted"] td { color: var(--color-text-muted); }
  .data-table tbody tr[data-state="highlight"] td { background: var(--color-surface-hover); }
  .data-table tbody tr[data-state="warning"] td { background: var(--color-warning-bg); }
  .data-table tbody tr[data-state="warning"] td:first-child { box-shadow: inset 3px 0 0 var(--color-warning-border); }
  .data-table tbody tr[data-state="error"] td { background: var(--color-danger-bg); }
  .data-table tbody tr[data-state="error"] td:first-child { box-shadow: inset 3px 0 0 var(--color-danger-border); }
  .data-table tbody tr[aria-selected="true"] td,
  .data-table tbody tr:has(input:checked) td { background: var(--color-accent-subtle); }
  /* Whole-cell pick target: wrap a non-input cell's content in <label class="cell-pick" for="{box-id}"> so a
     click anywhere in the cell toggles the row's native checkbox — no JS, no ARIA grid (Adrian Roselli). The
     :has(input:checked) tint above already syncs the highlight both ways. Never wrap a cell's own control. */
  .data-table td > label.cell-pick { display: block; cursor: pointer;
           margin: calc(-1 * var(--table-pad-y)) calc(-1 * var(--table-pad-x));
           padding: var(--table-pad-y) var(--table-pad-x); }
  /* Keyboard focus cue on the row (the checked tint alone doesn't show focus); selected rows keep their tint. */
  .data-table tbody tr:focus-within:not(:has(input:checked)) td { background: var(--color-surface-hover); }
  /* In-body group rows (distinct from tfoot grand totals): a section HEADER (.row-section) at the top of
     a group — e.g. product categories in a selection table — or a SUBTOTAL (.is-subtotal) at the bottom.
     Same bold, tonally-set full-width treatment. */
  .data-table tbody tr.is-subtotal td,
  .data-table tbody tr.row-section td { font-weight: var(--weight-medium);
           background: var(--color-surface-sunken); border-block-start: 1px solid var(--color-border); }
  .data-table .row-indent { padding-inline-start: var(--space-8); }   /* parent -> child services */

  /* TFOOT totals — each figure under its column; grand-total set off. */
  .data-table tfoot td { padding-block: var(--space-2); }
  .data-table tfoot tr:first-child td { padding-block-start: var(--space-3); }
  .data-table tfoot .totals-label { text-align: end; color: var(--color-text-subtle); }   /* support tone */
  .data-table tfoot .totals-grand td { border-block-start: 1px solid var(--color-border-subtle);
           padding-block-start: var(--space-3); font-weight: var(--weight-semibold); }
  .data-table tfoot .totals-grand .totals-label { color: var(--color-text); }

  /* Alignment as a data-TYPE on the column (header + body), not a width. */
  .data-table .num { text-align: end; font-variant-numeric: tabular-nums; }
  .data-table [data-align="end"] { text-align: end; }
  .data-table [data-align="center"] { text-align: center; }
  .data-table .nowrap { white-space: nowrap; }
  /* Primary/secondary lines in a cell: the main line leads by COLOUR (near-black), the sub line recedes
     to the subtle token — no weight, no size change (hierarchy contract: colour = read-this vs scaffold). */
  .data-table .cell-main { display: block; color: var(--color-text);
           overflow: hidden; text-overflow: ellipsis; }
  .data-table .cell-sub { display: block; color: var(--color-text-subtle); margin-block-start: 2px;
           overflow: hidden; text-overflow: ellipsis; }
  /* A labelled value ("Domain Name: google.com") and the attribute lines under it ("Billing Note: …").
     The label carries the meaning, so it leads by weight and the value recedes; an attribute wraps
     under its own value rather than ellipsing, so a note is read in full. */
  .data-table .cell-label { font-weight: var(--weight-medium); color: var(--color-text); }
  .data-table .cell-label .icon { display: inline-block; inline-size: 14px; block-size: 14px; color: var(--color-text-subtle);
           vertical-align: -2px; margin-inline-end: 6px; }
  /* an unlabelled value is the whole line, so it keeps the text colour */
  .data-table .cell-label:not([hidden]) + .cell-value { color: var(--color-text-subtle); }
  .data-table .cell-attr { display: flex; align-items: baseline; gap: .3em; margin-block-start: 2px; white-space: normal; }
  .data-table .cell-attr > .cell-label { flex: none; }
  .data-table .cell-attr > .cell-value { min-inline-size: 0; overflow-wrap: anywhere; }

  /* GROW — the ONE column that absorbs leftover width (mark its <th> and every <td>). It fills the
     slack and still truncates its own overflow when the table is tight, so the table fills its pane
     with no horizontal scroll. Reserve exactly one per table. */
  .data-table .cell-grow { inline-size: 100%; max-inline-size: none; }
  /* ACTIONS — a column of right-aligned icon buttons (.row-actions). Trims the cell padding so the
     column hugs the buttons rather than reserving a full text column's width. */
  .data-table .cell-actions { inline-size: 1%; padding-inline: var(--space-2); }

  /* OVERFLOW ESCAPE HATCHES (truncate is the default). cell-wrap lets a long-text column wrap instead
     of ellipsis; cell-clamp-2 wraps to two lines then ellipses; cell-break wraps long unbreakable
     tokens (IDs/URLs). All re-enable wrapping over the global nowrap. */
  .data-table .cell-wrap { white-space: normal; overflow: visible; text-overflow: clip; max-inline-size: none; }
  .data-table .cell-clamp-2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
           white-space: normal; overflow: hidden; text-overflow: clip; }
  .data-table .cell-break { white-space: normal; overflow-wrap: anywhere; text-overflow: clip; }

  /* KEY-COLUMN PIN — cells marked .col-pin stick through horizontal scroll (below the sticky thead).
     The right-edge shadow is a FULL-HEIGHT gradient pseudo-element (not a per-cell box-shadow, which
     leaves gaps at every row boundary): each cell's strip is contiguous, so the column reads as one
     continuous edge. It sits just outside the pinned cell, over the scrolled content. */
  .data-table .col-pin { position: sticky; inset-inline-start: 0; z-index: 1;
           background: var(--color-surface); }
  .data-table .col-pin::after { content: ""; position: absolute; inset-block: 0; inset-inline-end: 0;
           inline-size: 6px; transform: translateX(100%); pointer-events: none;
           background: linear-gradient(to right, var(--table-pin-shadow), transparent); }
  .data-table thead th.col-pin { z-index: 3; background: var(--color-surface-sunken); }
  .data-table tbody tr:hover td.col-pin { background: var(--color-surface-hover); }
  .data-table tbody tr[aria-selected="true"] td.col-pin,
  .data-table tbody tr:has(input:checked) td.col-pin { background: var(--color-accent-subtle); }

  /* COLUMN-PRIORITY hiding — as the CONTAINER narrows, drop lowest-priority columns first (col + cells). */
  @container (max-width: 60rem) {
    .data-table col[data-priority="3"] { width: 0; }
    .data-table [data-priority="3"] { display: none; }
  }
  @container (max-width: 44rem) {
    .data-table col[data-priority="2"] { width: 0; }
    .data-table [data-priority="2"] { display: none; }
  }

  /* sortable header: a real button fills the cell; active column reads in the brand accent. */
  .data-table th > button.sort { all: unset; display: flex; align-items: center; gap: var(--space-2);
           inline-size: 100%; cursor: pointer; }
  .data-table th[aria-sort] > button.sort { color: var(--color-accent-text); font-weight: var(--weight-medium); }
  .data-table .num button.sort, .data-table [data-align="end"] button.sort { justify-content: flex-end; }

  /* empty state */
  .data-table td.table-empty { padding: var(--space-12) var(--space-5); text-align: center;
           color: var(--color-text-muted); }

  /* Row actions — end-aligned fixed-square icon buttons so the column edge is stable. */
  .row-actions { display: flex; justify-content: flex-end; align-items: center; gap: var(--space-1); }
  .row-action { display: inline-flex; align-items: center; justify-content: center;
           inline-size: 32px; block-size: 32px; border-radius: var(--radius-md);
           border: 0; background: none; padding: 0; cursor: pointer; color: var(--color-text-muted);
           /* keep the full 32px hit/hover target but pull its layout box back to one text line, so the
              action never makes a row taller than its text would (overflow falls into the cell padding). */
           margin-block: calc((1lh - 32px) / 2); }
  .row-action:hover { color: var(--color-text); background: var(--color-surface-hover); text-decoration: none; }
  .row-action:disabled { opacity: .55; cursor: not-allowed; background: none; color: var(--color-text-muted); }
  /* Same trick for an inline input/select in a table cell: pin it to a compact 32px and collapse its
     LAYOUT box to one text line, so a row with an input is no taller than a plain text row (the overflow
     falls into the cell's vertical padding). */
  .data-table td :where(.input, .select) {
           min-height: 0; block-size: 32px; padding-block: 0; margin-block: calc((1lh - 32px) / 2); }
  .row-action--danger { color: var(--color-danger-text); }
  .row-action--danger:hover, .row-action--danger:focus-visible {
           color: var(--color-danger-text); background: var(--color-danger-bg); }

  /* Bulk-action bar — swaps in above the table when >=1 row is selected ("N selected -> actions"). */
  /* Bulk-action bar. The actions are a SEGMENTED group flush to the bar's top / bottom / right edges:
     no vertical or right padding, buttons stretch to full height and join (shared borders, square
     inner corners), and the bar's overflow:hidden clips the far corners to its radius. The count sits
     inset on the left. Buttons use an inset focus ring so the clip never cuts the outline. */
  .table-bulkbar { display: flex; align-items: stretch; gap: var(--space-4); flex-wrap: wrap;
           padding-block: 0; padding-inline: var(--space-4) 0; overflow: hidden;
           background: var(--color-accent-subtle); border: 1px solid var(--color-border-subtle);
           border-radius: var(--radius-lg); margin-block-end: var(--space-3); }
  .table-bulkbar[hidden] { display: none; }   /* display:flex above otherwise overrides the [hidden] attribute */
  .table-bulkbar__count { display: flex; align-items: center; font-weight: var(--weight-semibold);
           padding-block: var(--space-2); }
  .table-bulkbar__actions { display: flex; margin-inline-start: auto; }
  /* Segments are square with NO outer border — the bar's border is the only outer edge (no doubling);
     a single 1px seam divides adjacent segments. */
  .table-bulkbar__actions .btn { border-radius: 0; border-block: 0; border-inline-end: 0; border-inline-start: 0; }
  .table-bulkbar__actions .btn + .btn { border-inline-start: 1px solid var(--color-border); }
  .table-bulkbar__actions .btn:focus-visible { outline-offset: -2px; }

  /* LAST-RESORT card stacking — detail tables only (Safari drops table semantics under display overrides). */
  @container (max-width: 34rem) {
    .data-table.is-stackable thead { position: absolute; inline-size: 1px; block-size: 1px;
           overflow: hidden; clip-path: inset(50%); }
    .data-table.is-stackable tr { display: block; border-block-start: 1px solid var(--color-border-subtle); }
    .data-table.is-stackable td { display: flex; justify-content: space-between; gap: var(--space-4);
           border: 0; }
    .data-table.is-stackable td::before { content: attr(data-label);
           font-weight: var(--weight-medium); color: var(--color-text-muted); }
  }

  /* TOOLTIP — reveal for truncated cells etc. Positioned by Floating-UI (shell2.js); shown via [data-show].
     Reuses the legacy component, now token-driven + light/dark. */
  .tooltip { position: absolute; inset-block-start: 0; inset-inline-start: 0; z-index: 1000;
           inline-size: max-content; max-inline-size: 22rem; white-space: pre-line;  /* honour \n in multi-line (chart) tooltips */
           padding: var(--space-1) var(--space-2); border-radius: var(--radius-sm);
           background: var(--color-tooltip-bg); color: var(--color-tooltip-text);
           font-size: var(--text-sm); line-height: var(--leading-snug); box-shadow: var(--shadow-md);
           opacity: 0; visibility: hidden; pointer-events: none; transition: opacity var(--transition); }
  .tooltip[data-show] { opacity: 1; visibility: visible; }
  /* Rich variant (chart tooltips) — a title + per-series swatch/label/value rows, like a plotting
     library's tooltip. Fixed dark surface in both themes so the colour swatches always read. */
  .tooltip--rich { white-space: normal; background: rgb(17 24 39 / .94); color: #f9fafb;
           padding: var(--space-2) var(--space-3); }
  .tooltip__title { font-weight: var(--weight-medium); margin-block-end: 4px; }
  .tooltip__row { display: flex; align-items: center; gap: var(--space-2); }
  .tooltip__row + .tooltip__row { margin-block-start: 2px; }
  .tooltip__dot { inline-size: 9px; block-size: 9px; border-radius: 2px; flex: none; }
  .tooltip__val { margin-inline-start: auto; padding-inline-start: var(--space-4); font-variant-numeric: tabular-nums; }

  /* Pagination — record count, Previous/Next, then rows-per-page (select + explicit Apply). Nav items are
     .btn.btn--secondary LINKS in a <nav aria-label="Pagination"> (no JS, copyable URLs); an unavailable
     direction stays in place as <a role="link" aria-disabled="true"> without href, so Next never moves.
     Rows-per-page must NEVER auto-submit on change (WCAG 3.2.2) — Apply submits the surrounding GET form.
     The count grows to fill its line and the controls pack to the end, so any wrap keeps them right-aligned.
     Container queries, not media queries: the card's width depends on the rail/L2 nav, not the viewport. */
  .pagination { container-type: inline-size; display: flex; flex-wrap: wrap; align-items: center;
           justify-content: flex-end; gap: var(--space-3) var(--space-6); margin-block-start: var(--space-4);
           color: var(--color-text-muted); }
  .pagination__info { flex: 1 0 auto; margin: 0; white-space: nowrap; font-variant-numeric: tabular-nums; }
  .pagination__info b { color: var(--color-text); font-weight: var(--weight-medium); white-space: nowrap; }
  /* Equal columns so Previous and Next read as one balanced pair at every width. */
  .pagination__nav { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: var(--space-2); }
  .pagination__step { justify-content: center; gap: var(--space-1); }
  .pagination__step[aria-disabled="true"] { pointer-events: none; }
  .pagination__rows { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1) var(--space-2);
           white-space: nowrap; }
  @container (max-width: 30rem) {
    .pagination__info, .pagination__nav, .pagination__rows, .pagination__rows .input-group { flex: 1 1 100%; }
    .pagination__rows .input-group > .select { flex: 1 1 auto; }
    .pagination__info { white-space: normal; }   /* long totals wrap between the bold figures, never off the card */
    .pagination__step { padding-inline: var(--space-2); }
  }
  /* Narrowest phones: the words fit the half-width buttons, the chevrons don't. */
  @container (max-width: 13rem) { .pagination__step .icon { display: none; } }

  /* ---- FORMS ------------------------------------------------------------- */
  /* Fill the card. Columns size to content and reflow with the available width: min(100%, 24rem)
     gives one column in a narrow/auth card, two on a laptop, three-plus on a wide record card — no
     max-width cap, so a data form uses the whole card instead of a left-hand strip. */
  .form-grid { display: grid; gap: var(--space-4) var(--space-6);
           grid-template-columns: repeat(auto-fit, minmax(min(100%, 24rem), 1fr)); }
  .field { display: flex; flex-direction: column; gap: var(--space-1); }
  .field--full { grid-column: 1 / -1; }
  /* a lone checkbox has no label row, so in a grid it aligns to where the inputs sit (the bottom
     of the row) rather than floating at the top, misaligned with its neighbours */
  .form-grid .field:has(> .checkbox:only-child) { justify-content: flex-end; }
  /* Label is support text (contract): weight 400 in the subtle tone, so the near-black value leads. */
  .field__label { color: var(--color-text-subtle); }
  /* The label is the quiet support tone (so the value leads); the hint is one clear step quieter still — by
     COLOUR, not size — so it reads as the least-loud text in the field. */
  .field__hint { font-size: var(--text-base); color: var(--color-text-hint); }
  /* Read-only / derived value interleaved in an edit form (record id, computed status, timestamps) — a
     .field--static holds a <span class="field__label"> + a <p class="field__value"> of PLAIN TEXT, never a
     disabled input (disabled controls fail contrast and are skipped by AT — Roselli, GOV.UK summary list). */
  /* a long unbroken value (a checksum, a URL) wraps inside its column instead of widening the form */
  .field__value { color: var(--color-text); overflow-wrap: anywhere; }
  .field__error { display: flex; align-items: flex-start; gap: var(--space-1);
           font-size: var(--text-base); color: var(--color-danger-text);
           font-weight: var(--weight-medium); }
  /* icon + colour + text — never colour/text alone (UX Movement: form error mistakes); the icon is a
     block element (svg reset), so flex + a 2px nudge keeps it on the first text line, not stacked. */
  .field__error .icon { margin-block-start: 2px; color: var(--color-danger-solid); }
  .input, .select, .textarea {
           padding: var(--space-2) var(--space-3); background: var(--color-field-bg);
           border: 1px solid var(--color-field-border); border-radius: var(--radius-md);
           min-height: var(--control-h); width: 100%; color: var(--color-text);
           transition: border-color var(--transition), box-shadow var(--transition); }
  /* the height comes from --control-h, not padding, so a field never outgrows the button beside it */
  .input, .select { padding-block: 0; }
  .textarea--fixed { resize: none; }   /* opt out of the drag-resize handle */
  .input--search { width: 18rem; }  /* list-toolbar search box — one width across every list */
  /* The self-hosted Archivo subset declares the general-punctuation range but omits the bullet the
     browser masks passwords with, so the mask renders as .notdef boxes. Mask from a system font that
     has it — the typed value is hidden anyway, so the family only affects the dots. */
  input[type="password"] { font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
  /* native <select> chrome removed so it matches .input, with a custom chevron */
  .select { appearance: none; -webkit-appearance: none; cursor: pointer;
           padding-inline-end: calc(var(--space-3) + 1.25rem);
           background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='none' stroke='%2364748b' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E");
           background-repeat: no-repeat; background-position: right var(--space-3) center;
           background-size: 1rem; }
  /* Lighter chevron stroke (slate-400) in dark — the data-URI can't read a token, so the dark
     theme repaints it so it stays visible on the dark field. */
  [data-theme="dark"] .select { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='none' stroke='%2394a3b8' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E"); }
  @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) .select {
           background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='none' stroke='%2394a3b8' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E"); } }
  .input:disabled, .select:disabled, .textarea:disabled { background: var(--color-field-disabled);
           color: var(--color-text-muted); }
  .field:has([aria-invalid="true"]) :where(.input, .select, .textarea),
  :where(.input, .select, .textarea)[aria-invalid="true"] { border-color: var(--color-field-invalid); }
  /* Input focus: the border turns the accent (a solid 3:1 change — the part that satisfies WCAG 2.2
     Focus Appearance) + a soft ring that HUGS the field (no offset gap, unlike buttons/links). The
     transparent outline is the Windows High-Contrast / forced-colors fallback. accent-text adapts
     per theme so it clears 3:1 on both the white and dark field. (Stripe/Polaris/shadcn pattern.) */
  .input:focus-visible, .select:focus-visible, .textarea:focus-visible {
           outline: 2px solid transparent; outline-offset: 0;
           border-color: var(--color-accent-text);
           box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-accent-text) 30%, transparent); }
  :where(.input, .select, .textarea)[aria-invalid="true"]:focus-visible {
           border-color: var(--color-field-invalid);
           box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-field-invalid) 30%, transparent); }
  fieldset { border: 0; padding: 0; }
  legend { font-weight: var(--weight-medium); padding: 0; margin-block-end: var(--space-2); }

  /* Horizontal (label-left) form — the EDITABLE sibling of .detail-list; replaces the legacy
     `data-grid two-column has-title` form-as-table. Each .field becomes a label | control row. The label
     column shares the width with a fixed control column, so labels have room and values aren't stranded
     at the left of a wide card. Consecutive grids are whitespace groups: no headings, lines or bands. */
  .form-grid--horizontal { display: flex; flex-direction: column; gap: var(--space-2); max-width: 56rem; }
  .form-grid--horizontal:has(.field__hint) { gap: var(--space-4); }
  .form-grid--horizontal + .form-grid--horizontal { margin-block-start: var(--space-8); }
  /* intro text above a form gets the same gap as the form's own rows (the reset zeroes paragraph margins) */
  .card__body > p + .form-grid { margin-block-start: var(--space-4); }
  /* Every row is the SAME height (the control height) with its control vertically centred, so a
     checkbox / static / select row lines up with an input row and the form keeps an even vertical rhythm.
     The label sits in row 1, so a field error stacking below in column 2 never drags the label off it. */
  .form-grid--horizontal .field { display: grid; grid-template-columns: minmax(12rem, 1fr) minmax(0, 28rem);
           column-gap: var(--space-8); row-gap: var(--space-1); min-block-size: var(--control-h);
           align-items: center; align-content: center; }
  .form-grid--horizontal .field__label { grid-column: 1; grid-row: 1; }
  .form-grid--horizontal .field > :not(.field__label) { grid-column: 2; }
  /* a hint sits under its label and the control centres across both; a group with hints is a group of
     settings, so all its labels take full ink over the grey hints */
  .form-grid--horizontal .field > .field__hint { grid-column: 1; grid-row: 2; margin: 0; }
  .form-grid--horizontal .field:has(> .field__hint) > :not(.field__label, .field__hint, .field__error) { grid-row: 1 / span 2; }
  .form-grid--horizontal:has(.field__hint) .field__label { color: var(--color-text); }
  /* read-only values lead by weight as well as ink; typed input text stays regular */
  .form-grid--horizontal :is(.field__value:not(.field__value--empty), .input-group__addon) { font-weight: var(--weight-medium); color: var(--color-text); }
  /* a group whose only field is a select fills the control column, so its right edge lines up with the rest */
  .form-grid--horizontal .input-group:not(:has(> .input)) > .select { flex: 1 1 auto; min-inline-size: 0; }
  /* a setting that can't be changed dims its label and hint, like its checkbox */
  .form-grid--horizontal .field:has(> input[type="checkbox"]:disabled) > :not(input) { opacity: .55; }
  /* a bare checkbox/radio in the control column keeps the standard 1rem box, left-aligned like an input */
  .form-grid--horizontal .field--inline > input[type="checkbox"],
  .form-grid--horizontal .field--inline > input[type="radio"] {
           inline-size: 1rem; block-size: 1rem; margin: 0; justify-self: start; }
  /* a field--full row breaks out of the two-column rhythm to span the whole width — for a textarea (label
     above it) or a long consent checkbox (label beside it), neither of which suits the narrow label column */
  .form-grid--horizontal .field--full { display: block; }
  .form-grid--horizontal .field--full > .field__label { display: block; margin-block-end: var(--space-1); }
  /* an absent read-only value reads as muted, so a null row never looks like a broken empty field */
  .field__value--empty { color: var(--color-text-muted); }
  @media (max-width: 640px) {                        /* stack label over control on narrow screens */
    /* back to the base flex column, which ignores the grid placement above, so label, hint, control and error stack in source order */
    .form-grid--horizontal .field { display: flex; align-items: stretch; }
  }

  /* Form section — a titled group of fields (replaces the table <caption>); stack several per form. */
  .form-section { margin-block-end: var(--space-8); }
  .form-section:last-child { margin-block-end: 0; }
  .form-section__title { font-size: var(--text-base); font-weight: var(--weight-medium);
           color: var(--color-text); padding-block-end: var(--space-2); margin-block-end: var(--space-4);
           border-block-end: 1px solid var(--color-border-subtle); }
  /* The line being added — its input row (.line-edit) and its always-open detail row (.line-detail-row)
     form ONE group: the same accent tint and a continuous left rail across both, bracketed at the top and
     with no divider between them, so the detail reads as part of the line rather than a separate item. */
  .line-edit > td, .line-detail-row > td { background: var(--color-accent-subtle); }
  .line-edit > td:first-child,
  .line-detail-row > td:first-child { box-shadow: inset 3px 0 0 var(--color-accent-text); }
  .line-edit > td { border-block-start: 1px solid var(--color-accent-text); }
  .line-detail-row > td { border-block-start: none; padding: 0; }
  .line-detail { padding: var(--space-2) var(--space-5) var(--space-4) var(--space-6); }
  .line-detail .form-grid--horizontal { max-inline-size: 32rem; gap: var(--space-2); }
  /* labels sit right beside their inputs (content-width label column, tight gap) so the whole detail
     tucks under the Description column rather than sprawling across the table */
  .line-detail .form-grid--horizontal .field { grid-template-columns: max-content minmax(0, 1fr);
           column-gap: var(--space-4); }

  /* Checkbox drawn on the native input (still announced and submitted as a checkbox), because the native
     disabled box fades to near-invisible; this one keeps its 3:1 edge and dims like a disabled button. */
  :where(input[type="checkbox"]) { appearance: none; -webkit-appearance: none; inline-size: 1rem; block-size: 1rem;
           flex: none; margin: 0; cursor: pointer; border: 1.5px solid var(--color-field-border);
           border-radius: var(--radius-sm); background: var(--color-field-bg) center / 100% no-repeat; }
  input[type="checkbox"]:checked { border-color: var(--accent-control); background-color: var(--accent-control);
           background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8.5l3 3 6-6.5' fill='none' stroke='%23fff' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }
  input[type="checkbox"]:disabled { opacity: .55; cursor: not-allowed; }
  input[type="checkbox"]:disabled:not(:checked) { background-color: var(--color-surface-sunken); }
  /* forced colours drop the fill but keep the white tick image, so hand the box back to the system */
  @media (forced-colors: active) { input[type="checkbox"] { appearance: auto; } }

  /* Checkbox / radio with an inline label (a radio keeps --accent-control from the base layer). */
  .checkbox, .radio { display: inline-flex; align-items: center; gap: var(--space-2); cursor: pointer; }
  .checkbox > input, .radio > input { inline-size: 1rem; block-size: 1rem; flex: none; margin: 0; cursor: pointer; }
  .checkbox-group { display: flex; flex-direction: column; gap: var(--space-3); }  /* a stack of toggles */
  .radio-row { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4); }  /* options across a control column */

  /* Option list — a stack of selectable rows (radio/checkbox + title + optional description / end meta);
     the whole row is the <label> and the checked row highlights. (User groups, checkout choices.) */
  .option-list { display: flex; flex-direction: column; gap: var(--space-2); }
  .option { display: flex; align-items: flex-start; gap: var(--space-3); padding: var(--space-3) var(--space-4);
           border: 1px solid var(--color-border); border-radius: var(--radius-md); cursor: pointer;
           transition: background var(--transition), border-color var(--transition); }
  .option:hover { background: var(--color-surface-hover); }
  .option:has(> input:checked) { border-color: var(--color-accent-text); background: var(--color-accent-subtle); }
  .option > input { inline-size: 1rem; block-size: 1rem; flex: none; margin: 0; margin-block-start: 2px; cursor: pointer; }
  .option__body { display: flex; flex-direction: column; gap: 2px; min-inline-size: 0; }
  .option__title { font-weight: var(--weight-medium); }
  .option__desc { color: var(--color-text-muted); }
  .option__meta { margin-inline-start: auto; white-space: nowrap; color: var(--color-text-muted);
           font-variant-numeric: tabular-nums; }

  /* Input group — join an .input/.select with a static addon (prefix/suffix) or trailing button on one
     line: shared borders, only the outer corners rounded. (subdomain suffix, protocol prefix, search.) */
  .input-group { display: flex; align-items: stretch; }
  .input-group > * { position: relative; }
  .input-group > :not(:first-child) { border-start-start-radius: 0; border-end-start-radius: 0; margin-inline-start: -1px; }
  .input-group > :not(:last-child) { border-start-end-radius: 0; border-end-end-radius: 0; }
  .input-group > .input { flex: 1 1 auto; min-inline-size: 0; }
  /* a segment <select> (e.g. the http/https protocol) shrinks to its content instead of taking the
     .select full width, so the sibling .input keeps the room. */
  .input-group > .select { flex: none; inline-size: auto; }
  .input-group :focus-visible { z-index: 1; }        /* the focused segment's ring sits above its neighbours */
  .input-group__addon { display: inline-flex; align-items: center; white-space: nowrap;
           padding-inline: var(--space-3); background: var(--color-surface-sunken);
           border: 1px solid var(--color-field-border); border-radius: var(--radius-md);
           color: var(--color-text-muted); }   /* radius so the group's outer corners round (inner squared below) */

  /* key:value description list (read-only record detail — replaces the old "two-column
     data-grid" form-as-table). Left label column + value column on one row (GOV.UK Summary
     list / Carbon Structured list) — NOT top-stacked: "top-aligned labels are fastest" is
     form-filling guidance, not read-only display. Rows take the input height (~36px) and
     centre vertically, so a read-only value and an inline-edit field share one even rhythm. */
  /* minmax(8rem, …) floors the label column so short labels still get breathing room and values
     start at a consistent x; --space-8 gap keeps a long label off its value. */
  .detail-list { display: grid; grid-template-columns: minmax(8rem, max-content) 1fr;
           column-gap: var(--space-8); row-gap: var(--space-2);
           align-items: center; grid-auto-rows: minmax(var(--control-h), auto); }
  .detail-list dt { color: var(--color-text-subtle); }   /* label is the quieter, lighter element */
  .detail-list dd { margin: 0; }   /* value leads via near-black vs the lighter label — by colour, not weight */
  /* the panel is the query container; without this the rule below has none and never matches */
  .card__body:has(> .detail-list) { container-type: inline-size; }
  @container (max-width: 30rem) { .detail-list { grid-template-columns: 1fr;
           grid-auto-rows: auto; row-gap: 0; }
           .detail-list dt { margin-block-start: var(--space-2); }
           .detail-list dd { margin-block-end: var(--space-2); } }
}

/* ---- UTILITIES (thin layer; wins over components) ------------------------ */
@layer utilities {
  .stack { display: flex; flex-direction: column; }
  .stack > * + * { margin-block-start: var(--gap, var(--space-4)); }
  .cluster { display: flex; flex-wrap: wrap; gap: var(--gap, var(--space-3)); align-items: center; }
  .muted { color: var(--color-text-muted); }
  .text-end { text-align: end; }
  /* consistent vertical rhythm between major page sections (32px) */
  .page-section { margin-block-end: var(--space-8); }
  .page-section:last-child { margin-block-end: 0; }
  .visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden;
           clip-path: inset(50%); white-space: nowrap; }
  /* Display toggle for conditional fields (JS-driven). Distinct from .visually-hidden, which stays in
     the a11y tree — .is-hidden removes the element entirely. See DESIGN.md §4 "Conditional fields". */
  .is-hidden { display: none !important; }
}

/* ============================================================================
   TWO-LAYER RECORD NAVIGATION  (`.shell2` + `.r-*`)
   L1 rail = the full labelled navy sidebar on section/list pages; when a record
   opens (`data-context="record"`) it compresses to a 56px icon rail and an L2
   context panel (`.r-l2`) docks beside it. 56 + 216 = 272 (= the section rail),
   so the content column never reflows on drill-in. Light-on-dark in both themes.
   (See DESIGN.md §4 "Multi-level navigation".)
   ============================================================================ */
@layer components {
  .shell2 { display: grid; height: 100dvh; overflow: hidden;
            grid-template-columns: var(--shell-sidebar-w) 0px 1fr; grid-template-rows: var(--shell-brand-h) 1fr;
            grid-template-areas: "logo l2 main" "rail l2 main"; }
  .shell2[data-context="record"] { grid-template-columns: var(--shell-rail-compact) var(--shell-context-w) 1fr; }

  /* L1 logo cell — wordmark when expanded, "P" mark when compressed */
  .r-logo { grid-area: logo; background: var(--color-sidebar); display: flex; align-items: center;
            padding-inline: var(--space-6); overflow: hidden;
            color: rgb(255 255 255 / .92); --color-logo-mark: rgb(255 255 255 / .92); }
  .shell2[data-context="record"] .r-logo { padding-inline: 0; justify-content: center; }
  .r-logo__word { height: 26px; width: auto; }
  .r-logo__mark { height: 30px; width: auto; display: none; }
  .shell2[data-context="record"] .r-logo__word { display: none; }
  .shell2[data-context="record"] .r-logo__mark { display: block; }

  /* L1 icon rail */
  .r-rail { grid-area: rail; background: var(--color-sidebar); display: flex; flex-direction: column;
            padding: var(--space-2); gap: 2px; overflow-y: auto; overflow-x: hidden; }
  .shell2[data-context="record"] .r-rail { padding-inline: 8px; align-items: center; }
  /* Item has NO inline padding — the 40px .ricon is the first child, so its left edge is anchored at
     the rail's 8px padding in BOTH states. That keeps the icon at the same x expanded vs compressed
     (no horizontal jump); the .ricon's own 10px internal padding gives the icon breathing room. */
  .r-item { display: flex; align-items: center; gap: var(--space-3); width: 100%; padding: 4px 0;
            color: rgb(255 255 255 / .72); background: none; border: 0; cursor: pointer;
            text-decoration: none; border-radius: var(--radius-md); }
  .r-item .ricon { position: relative; flex: 0 0 auto; width: 40px; height: 40px; border-radius: var(--radius-md);
            display: flex; align-items: center; justify-content: center; transition: background var(--transition); }
  .r-item .icon { width: 20px; height: 20px; }
  /* Section icons are applied by CSS from a route-id-derived class (.r-item--<snake_id>), so the
     wrapper and the menu data stay icon-agnostic; the design system owns which glyph each section
     shows. Utility items (theme, notifications, avatar) keep an inline sprite <svg> instead — the
     theme menu's <use> is swapped between sun/moon/monitor by shell2.js. Mask rules are below. */
  /* Set the mask SUB-properties only, never the `mask:` shorthand — the shorthand resets mask-image to
     none, and this base rule out-specifies the per-section mask-image rules below, so it would erase them. */
  .r-item[class*="r-item--"] .ricon::before { content: ''; display: block; width: 20px; height: 20px;
            background: currentColor;
            -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
            -webkit-mask-position: center; mask-position: center;
            -webkit-mask-size: contain; mask-size: contain; }
  .r-item--customer_list .ricon::before { -webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22%23000%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M16%2021v-2a4%204%200%200%200-4-4H6a4%204%200%200%200-4%204v2%22%20%2F%3E%3Cpath%20d%3D%22M16%203.128a4%204%200%200%201%200%207.744%22%20%2F%3E%3Cpath%20d%3D%22M22%2021v-2a4%204%200%200%200-3-3.87%22%20%2F%3E%3Ccircle%20cx%3D%229%22%20cy%3D%227%22%20r%3D%224%22%20%2F%3E%3C%2Fsvg%3E"); mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22%23000%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M16%2021v-2a4%204%200%200%200-4-4H6a4%204%200%200%200-4%204v2%22%20%2F%3E%3Cpath%20d%3D%22M16%203.128a4%204%200%200%201%200%207.744%22%20%2F%3E%3Cpath%20d%3D%22M22%2021v-2a4%204%200%200%200-3-3.87%22%20%2F%3E%3Ccircle%20cx%3D%229%22%20cy%3D%227%22%20r%3D%224%22%20%2F%3E%3C%2Fsvg%3E"); }
  .r-item--customer_detail .ricon::before { -webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22%23000%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M16%2021v-2a4%204%200%200%200-4-4H6a4%204%200%200%200-4%204v2%22%20%2F%3E%3Cpath%20d%3D%22M16%203.128a4%204%200%200%201%200%207.744%22%20%2F%3E%3Cpath%20d%3D%22M22%2021v-2a4%204%200%200%200-3-3.87%22%20%2F%3E%3Ccircle%20cx%3D%229%22%20cy%3D%227%22%20r%3D%224%22%20%2F%3E%3C%2Fsvg%3E"); mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22%23000%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M16%2021v-2a4%204%200%200%200-4-4H6a4%204%200%200%200-4%204v2%22%20%2F%3E%3Cpath%20d%3D%22M16%203.128a4%204%200%200%201%200%207.744%22%20%2F%3E%3Cpath%20d%3D%22M22%2021v-2a4%204%200%200%200-3-3.87%22%20%2F%3E%3Ccircle%20cx%3D%229%22%20cy%3D%227%22%20r%3D%224%22%20%2F%3E%3C%2Fsvg%3E"); }
  .r-item--services .ricon::before { -webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22%23000%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M11%2021.73a2%202%200%200%200%202%200l7-4A2%202%200%200%200%2021%2016V8a2%202%200%200%200-1-1.73l-7-4a2%202%200%200%200-2%200l-7%204A2%202%200%200%200%203%208v8a2%202%200%200%200%201%201.73z%22%20%2F%3E%3Cpath%20d%3D%22M12%2022V12%22%20%2F%3E%3Cpolyline%20points%3D%223.29%207%2012%2012%2020.71%207%22%20%2F%3E%3Cpath%20d%3D%22m7.5%204.27%209%205.15%22%20%2F%3E%3C%2Fsvg%3E"); mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22%23000%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M11%2021.73a2%202%200%200%200%202%200l7-4A2%202%200%200%200%2021%2016V8a2%202%200%200%200-1-1.73l-7-4a2%202%200%200%200-2%200l-7%204A2%202%200%200%200%203%208v8a2%202%200%200%200%201%201.73z%22%20%2F%3E%3Cpath%20d%3D%22M12%2022V12%22%20%2F%3E%3Cpolyline%20points%3D%223.29%207%2012%2012%2020.71%207%22%20%2F%3E%3Cpath%20d%3D%22m7.5%204.27%209%205.15%22%20%2F%3E%3C%2Fsvg%3E"); }
  .r-item--order_list .ricon::before { -webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22%23000%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Ccircle%20cx%3D%228%22%20cy%3D%2221%22%20r%3D%221%22%20%2F%3E%3Ccircle%20cx%3D%2219%22%20cy%3D%2221%22%20r%3D%221%22%20%2F%3E%3Cpath%20d%3D%22M2.05%202.05h2l2.66%2012.42a2%202%200%200%200%202%201.58h9.78a2%202%200%200%200%201.95-1.57l1.65-7.43H5.12%22%20%2F%3E%3C%2Fsvg%3E"); mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22%23000%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Ccircle%20cx%3D%228%22%20cy%3D%2221%22%20r%3D%221%22%20%2F%3E%3Ccircle%20cx%3D%2219%22%20cy%3D%2221%22%20r%3D%221%22%20%2F%3E%3Cpath%20d%3D%22M2.05%202.05h2l2.66%2012.42a2%202%200%200%200%202%201.58h9.78a2%202%200%200%200%201.95-1.57l1.65-7.43H5.12%22%20%2F%3E%3C%2Fsvg%3E"); }
  .r-item--reports .ricon::before { -webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22%23000%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M3%203v16a2%202%200%200%200%202%202h16%22%20%2F%3E%3Cpath%20d%3D%22M18%2017V9%22%20%2F%3E%3Cpath%20d%3D%22M13%2017V5%22%20%2F%3E%3Cpath%20d%3D%22M8%2017v-3%22%20%2F%3E%3C%2Fsvg%3E"); mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22%23000%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M3%203v16a2%202%200%200%200%202%202h16%22%20%2F%3E%3Cpath%20d%3D%22M18%2017V9%22%20%2F%3E%3Cpath%20d%3D%22M13%2017V5%22%20%2F%3E%3Cpath%20d%3D%22M8%2017v-3%22%20%2F%3E%3C%2Fsvg%3E"); }
  .r-item--admin .ricon::before { -webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22%23000%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M20%2013c0%205-3.5%207.5-7.66%208.95a1%201%200%200%201-.67-.01C7.5%2020.5%204%2018%204%2013V6a1%201%200%200%201%201-1c2%200%204.5-1.2%206.24-2.72a1.17%201.17%200%200%201%201.52%200C14.51%203.81%2017%205%2019%205a1%201%200%200%201%201%201z%22%20%2F%3E%3C%2Fsvg%3E"); mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22%23000%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M20%2013c0%205-3.5%207.5-7.66%208.95a1%201%200%200%201-.67-.01C7.5%2020.5%204%2018%204%2013V6a1%201%200%200%201%201-1c2%200%204.5-1.2%206.24-2.72a1.17%201.17%200%200%201%201.52%200C14.51%203.81%2017%205%2019%205a1%201%200%200%201%201%201z%22%20%2F%3E%3C%2Fsvg%3E"); }
  .r-item--tools .ricon::before { -webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22%23000%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M14.7%206.3a1%201%200%200%200%200%201.4l1.6%201.6a1%201%200%200%200%201.4%200l3.106-3.105c.32-.322.863-.22.983.218a6%206%200%200%201-8.259%207.057l-7.91%207.91a1%201%200%200%201-2.999-3l7.91-7.91a6%206%200%200%201%207.057-8.259c.438.12.54.662.219.984z%22%20%2F%3E%3C%2Fsvg%3E"); mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22%23000%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M14.7%206.3a1%201%200%200%200%200%201.4l1.6%201.6a1%201%200%200%200%201.4%200l3.106-3.105c.32-.322.863-.22.983.218a6%206%200%200%201-8.259%207.057l-7.91%207.91a1%201%200%200%201-2.999-3l7.91-7.91a6%206%200%200%201%207.057-8.259c.438.12.54.662.219.984z%22%20%2F%3E%3C%2Fsvg%3E"); }
  .r-item .rlabel { font-size: var(--text-base); font-weight: var(--weight-medium); white-space: nowrap; }
  .r-item:hover { color: #fff; }
  /* Hover follows the active state's shape: the whole row when labelled, the icon tile when compact. It is
     a background-IMAGE wash over the active background-COLOR, so the current item still answers hover. */
  .r-item { --rail-hover-wash: linear-gradient(var(--color-rail-hover), var(--color-rail-hover)); }
  .shell2[data-context="section"] .r-item:hover { background-image: var(--rail-hover-wash); }
  .shell2[data-context="record"] .r-item:hover .ricon { background-image: var(--rail-hover-wash); }
  .shell2[data-context="record"] .r-item { justify-content: center; }
  /* display:none (not width:0) so the flex gap between icon and label collapses too — otherwise the
     gap keeps the icon off-centre in the 56px rail. */
  .shell2[data-context="record"] .r-item .rlabel { display: none; }
  /* The current section is the same solid control blue as the L2 pill and primary buttons — the whole row
     expanded, the icon tile compressed. */
  .r-item[aria-current="page"] { color: #fff; }
  .shell2[data-context="section"] .r-item[aria-current="page"] { background-color: var(--color-rail-active); }
  .shell2[data-context="record"] .r-item[aria-current="page"] .ricon { background-color: var(--color-rail-active); }
  .r-spacer { margin-block-start: auto; }
  /* a sidebar menu's caret says the row opens a menu; it flips with the menu's other carets when open */
  .r-item .r-caret { width: 16px; height: 16px; margin-inline: auto var(--space-2); flex: none; color: rgb(255 255 255 / .55); }
  .shell2[data-context="record"] .r-item .r-caret { display: none; }
  .r-dot { position: absolute; top: 7px; right: 7px; width: 8px; height: 8px; border-radius: var(--radius-full);
            background: var(--color-danger-solid); box-shadow: 0 0 0 2px var(--color-sidebar); }
  /* Gravatar image (falls back to the white disc if it fails to load); object-fit keeps it circular. */
  .r-avatar { width: 26px; height: 26px; border-radius: var(--radius-full); background: #fff; color: var(--color-brand);
            font-size: var(--text-sm); font-weight: var(--weight-bold); display: flex; align-items: center; justify-content: center;
            object-fit: cover; }

  /* L2 context panel — the wider second layer of the same navy menu chrome (light-on-dark), a step
     lighter than the rail with a faint seam, so it reads as part of the menu, not a content column. */
  .r-l2 { grid-area: l2; background: var(--color-context-panel); position: relative; z-index: 1;
          color: rgb(255 255 255 / .92); border-inline-start: 1px solid rgb(255 255 255 / .10);
          box-shadow: 8px 0 20px -12px rgb(2 6 23 / .5);
          display: flex; flex-direction: column; overflow: hidden; opacity: 1; }
  :root[data-theme="dark"] .r-l2 { border-inline-end: 1px solid rgb(255 255 255 / .08); }
  @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) .r-l2 { border-inline-end: 1px solid rgb(255 255 255 / .08); } }
  .r-rail :focus-visible, .r-l2 :focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
  .shell2[data-context="section"] .r-l2 { opacity: 0; }
  .r-back { display: flex; align-items: center; gap: var(--space-2); padding: var(--space-3) var(--space-4);
            font-weight: var(--weight-medium); color: rgb(255 255 255 / .92); text-decoration: none;
            border-block-end: 1px solid rgb(255 255 255 / .08); min-height: 48px; white-space: nowrap; }
  .r-back:hover { background: rgb(255 255 255 / .08); color: #fff; text-decoration: none; }
  .r-back .icon { width: 18px; height: 18px; color: rgb(255 255 255 / .6); }
  .r-ident { padding: var(--space-4); border-block-end: 1px solid rgb(255 255 255 / .08); }
  /* Clip the identity to one line each; the full text reveals via the shared tooltip (data-tooltip-truncate). */
  .r-ident__name { font-weight: var(--weight-medium); font-size: var(--text-base); color: rgb(255 255 255 / .95);
            min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .r-ident__meta { color: rgb(255 255 255 / .55); margin-block-start: 3px;
            min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .r-nav { padding: var(--space-3) var(--space-2); display: flex; flex-direction: column; gap: 1px; }
  .r-nav a, .r-nav button { display: flex; align-items: center; gap: var(--space-2); width: 100%;
            padding: var(--space-2) var(--space-3); border-radius: var(--radius-md); text-align: start;
            font-size: var(--text-base); font-weight: var(--weight-medium); color: rgb(255 255 255 / .72);
            background: none; border: 0; cursor: pointer; text-decoration: none; white-space: nowrap; }
  .r-nav a:hover, .r-nav button:hover { background: rgb(255 255 255 / .08); color: #fff; text-decoration: none; }
  .r-nav a[aria-current="page"] { background: var(--color-brand); color: #fff; }

  /* Content column. Positioned, so .visually-hidden text (absolute) is laid out in this scroller,
     not the page, which it would otherwise stretch below the fixed-height shell */
  .r-main { grid-area: main; overflow-y: auto; position: relative; }
  .r-view[hidden], .r-content[hidden], .r-tab[hidden] { display: none; }
  .service-row { cursor: pointer; }
  .service-row:hover { background: var(--color-surface-hover); }
  .service-row a { color: var(--color-text-link); text-decoration: none; font-weight: var(--weight-medium); }

  /* Mobile: L1 stays a 56px icon strip; L2 becomes an off-canvas drawer (no dead-end) */
  .r-l2toggle, .r-scrim { display: none; }
  @media (max-width: 1023px) {
    .shell2, .shell2[data-context="record"] { grid-template-columns: var(--shell-rail-compact) 1fr;
            grid-template-areas: "logo main" "rail main"; }
    /* On mobile the L1 rail is ALWAYS the compact icon strip — labels drop regardless of
       section/record, so a section (list) page doesn't cram the full labelled rail into 56px. */
    .shell2 .r-logo { padding-inline: 0; justify-content: center; }
    .shell2 .r-logo__word { display: none; }
    .shell2 .r-logo__mark { display: block; }
    .shell2 .r-rail { padding-inline: 8px; align-items: center; }
    .shell2 .r-item { justify-content: center; }
    .shell2 .r-item .rlabel, .shell2 .r-item .r-caret { display: none; }
    .shell2[data-context="section"] .r-item:hover { background: none; }
    .shell2[data-context="section"] .r-item:hover .ricon { background-image: var(--rail-hover-wash); }
    /* the active section reads as the icon tile in the strip, not the full-width row */
    .shell2[data-context="section"] .r-item[aria-current="page"] { background: none; }
    .shell2[data-context="section"] .r-item[aria-current="page"] .ricon { background-color: var(--color-rail-active); }
    .r-l2 { position: fixed; inset-block: 0; inset-inline-start: var(--shell-rail-compact); inline-size: min(280px, 80vw);
            z-index: 40; transform: translateX(calc(-100% - var(--shell-rail-compact))); transition: transform 160ms ease; }
    .shell2[data-l2open="true"] .r-l2 { transform: none; }
    .shell2[data-context="record"] .r-l2toggle { display: inline-flex; align-items: center; justify-content: center;
            position: fixed; z-index: 41; inset-block-start: var(--space-3); inset-inline-start: calc(var(--shell-rail-compact) + var(--space-3));
            inline-size: 40px; block-size: 40px; border-radius: var(--radius-md); border: 1px solid var(--color-border);
            background: var(--color-surface); color: var(--color-text); box-shadow: var(--shadow-sm); cursor: pointer; }
    .shell2[data-context="record"] .r-l2toggle .icon { width: 20px; height: 20px; }
    .shell2[data-context="record"][data-l2open="true"] .r-l2toggle { display: none; }
    .shell2[data-context="record"][data-l2open="true"] .r-scrim { display: block; position: fixed; inset: 0;
            inset-inline-start: var(--shell-rail-compact); background: rgb(2 6 23 / .45); z-index: 39; }
    .shell2[data-context="record"] .r-main .shell__content { padding-block-start: calc(var(--space-6) + 44px); }
  }
}

/* ============================================================================
   OVERLAYS & WIDGETS — modal dialog, auth layout, notes feed, autocomplete,
   loading spinner. (See DESIGN.md — legacy modal-layout/login-box/notes/SCA.)
   ============================================================================ */
@layer components {
  /* Modal — a native <dialog> (opened with showModal(), so centring + ::backdrop are free). Render
     content INLINE (not the legacy iframe). Open with [data-modal-open="#id"], close [data-modal-close]. */
  .modal { max-inline-size: min(32rem, calc(100vw - 2 * var(--space-6))); inline-size: 100%; padding: 0;
           margin: auto;   /* restore native dialog centring — the reset's `* { margin: 0 }` had killed it */
           border: 1px solid var(--color-border); border-radius: var(--radius-lg);
           background: var(--color-surface); color: var(--color-text); box-shadow: var(--shadow-lg); }
  .modal::backdrop { background: rgb(2 6 23 / .5); }
  /* wider inline form modal — a label-left form-grid (with date ranges / money pairs) needs more room
     than the default confirm width; the remote-iframe modal auto-sizes so this is for inline dialogs. */
  .modal--form { max-inline-size: min(42rem, calc(100vw - 2 * var(--space-6))); }
  .modal__header { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4);
           padding: var(--space-4) var(--space-5); border-block-end: 1px solid var(--color-border-subtle); }
  .modal__title { font-size: var(--text-lg); font-weight: var(--weight-semibold); letter-spacing: var(--tracking-snug); }
  .modal__body { padding: var(--space-5); }
  .modal__footer { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--space-2);
           padding: var(--space-4) var(--space-5); border-block-start: 1px solid var(--color-border-subtle); }
  /* Remote modal — the dialog body is an <iframe> loading a server-rendered route, auto-sized by
     shell2.js; the framed page styles itself with this same design system. */
  .modal--remote { padding: 0; overflow: hidden; }
  .modal__frame { display: block; inline-size: 100%; border: 0; }
  /* Load overlay — dims the page the instant a remote modal is triggered (same colour as the dialog
     ::backdrop, so the dim is continuous), with a DEBOUNCED spinner that only fades in for a slow route.
     shell2.js shows it on trigger, reveals the spinner after 500ms, and hides it once the frame loads. */
  .modal-loading { position: fixed; inset: 0; z-index: 1000; display: grid; place-items: center;
           background: rgb(2 6 23 / .5); }
  .modal-loading[hidden] { display: none; }
  .modal-loading .loading-panel { opacity: 0; transition: opacity .15s ease; color: #f9fafb; }
  .modal-loading.is-visible .loading-panel { opacity: 1; }
  /* larger, blue-on-white ring so it reads on the dark dim (base .spinner stays blue-on-grey for SCA) */
  .modal-loading .spinner { inline-size: 2.75rem; block-size: 2.75rem; border-width: 4px;
           border-color: #ffffff; border-block-start-color: var(--color-accent-text); }
  body.has-modal { overflow: hidden; }
  /* The modal-layout body is iframe content, not a full page — clear the base 100dvh floor, otherwise
     inside the frame `100dvh` == the frame's own height and scrollHeight never shrinks below it, so the
     auto-size locks the modal at the frame height instead of fitting the content. */
  .modal-page { min-block-size: 0; }

  /* Auth layout — a full-viewport centred card for login / password screens (no app shell). A roomy
     ~400px card with a CENTRED header (logo + title + intro) over a left-labelled form, on an 8pt
     rhythm; applies to every auth-layout page (LoginLDAP/LoginLocal + the forgot/reset forms). */
  .auth-layout { min-block-size: 100dvh; display: grid; place-items: center; padding: var(--space-6);
           background: var(--color-canvas); }
  .auth-card { inline-size: 100%; max-inline-size: 25rem; }        /* ~400px (Auth0/Stripe range) */
  .auth-card .card__body { padding: var(--space-8); }              /* roomier than the app default (20px) */
  /* Centred header: logo, title and intro share the centre axis; the form's labels stay left. The title
     keeps the base h1 size — 24px is reserved for the in-app page title (.page-header h1). */
  .auth-card h1 { text-align: center; margin: 0 0 var(--space-2); }
  .auth-intro { color: var(--color-text-muted); text-align: center; margin: 0 0 var(--space-8); }
  /* the sign-in card is light, so the logo takes the normal text colour rather than the rail's white */
  .auth-logo { display: block; color: var(--color-text); margin-block-end: var(--space-6); }
  .auth-logo .r-logo__word { height: 40px; width: auto; max-inline-size: 100%; display: block; margin-inline: auto; }
  .auth-logo .r-logo__mark { display: none; }
  .auth-card .btn--block { min-height: 44px; margin-block-start: var(--space-2); }  /* taller submit + a clear gap above */
  .auth-links { display: flex; justify-content: center; gap: var(--space-4);
           margin-block-start: var(--space-6); }
  .btn--block { width: 100%; justify-content: center; }            /* full-width button (auth submit) */

  /* Timeline feed — ONE pattern for both the attributed activity log (author + timestamp + body) and the dense
     product-updates changelog: line-separated, borderless rows on a semantic list, flush to the card padding (no
     per-row box, no list indent). Use <ul> (or <ol> for a reverse-chronological log) with <time datetime> —
     NOT role="feed" (that's for infinite scroll only; WAI-ARIA APG). */
  .note-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
  .note { padding-block: var(--space-3); border-block-end: 1px solid var(--color-border-subtle); }
  .note:last-child { border-block-end: 0; }
  .note__meta { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-2);
           margin-block-end: var(--space-1); }
  .note__author { font-weight: var(--weight-semibold); }
  .note__time { color: var(--color-text-subtle); }
  .note__body { color: var(--color-text); max-inline-size: var(--prose-max-width); }
  /* Audit / activity entries lead with the actor's .avatar; the meta + body stack in .note__main beside it.
     Scoped by :has so plain notes (AccountNotes) and the changelog keep their block layout. */
  .note:has(> .avatar) { display: flex; gap: var(--space-3); }
  .note > .avatar { margin-block-start: 2px; }   /* nudge the avatar onto the first text line */
  .note__main { min-inline-size: 0; }
  /* Changelog variant — the row lays its parts out on one line: fixed date column + a .badge type tag + text. */
  .note-list--changelog .note { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-3); }
  /* Fixed type-tag column so the tags align and every description starts at the same x. */
  .note-list--changelog .badge { flex: none; min-inline-size: 4rem; justify-content: center; }
  .note__date { flex: none; inline-size: 5.75rem; color: var(--color-text-subtle); font-variant-numeric: tabular-nums; }
  .note__text { flex: 1 1 0; min-inline-size: 0; color: var(--color-text); }

  /* Autocomplete — a results popover under a search input (JS builds .autocomplete + its <ul>/<li>). */
  .autocomplete-search { position: relative; }
  /* Grows to fit the widest result (min = input width, capped so it never runs off-screen); a longer
     result then ellipsis-truncates rather than wrapping. Anchored to the input's start edge. */
  .autocomplete { position: absolute; inset-block-start: calc(100% + var(--space-1)); inset-inline-start: 0; z-index: 30;
           min-inline-size: 100%; inline-size: max-content; max-inline-size: min(32rem, 90vw);
           padding: var(--space-1); background: var(--color-surface); border: 1px solid var(--color-border-subtle);
           border-radius: var(--radius-md); box-shadow: var(--shadow-lg); max-block-size: 16rem; overflow-y: auto; }
  .autocomplete ul { list-style: none; margin: 0; padding: 0; }
  .autocomplete li { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-2) var(--space-3);
           border-radius: var(--radius-sm); cursor: pointer; }
  .autocomplete__label { flex: 1 1 auto; min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .autocomplete li:hover,
  .autocomplete li.is-active,
  .autocomplete li[aria-selected="true"] { background: var(--color-surface-hover); }
  .autocomplete__meta { flex: none; margin-inline-start: auto; color: var(--color-text-subtle); white-space: nowrap; }
  .autocomplete__empty { padding: var(--space-2) var(--space-3); color: var(--color-text-subtle); }

  /* Searchable select — searchable-select.js enhances a native <select data-searchable> into a combobox
     that filters its own options client-side (bounded, server-rendered lists — account/nominal codes).
     The native select stays as the submitted value; the input reuses the .autocomplete popover above and
     carries the .select chevron so it still reads as a dropdown. */
  .select-search__input { padding-inline-end: calc(var(--space-3) + 1.25rem);
           background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='none' stroke='%2364748b' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E");
           background-repeat: no-repeat; background-position: right var(--space-3) center; background-size: 1rem; }
  [data-theme="dark"] .select-search__input { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='none' stroke='%2394a3b8' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E"); }
  @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) .select-search__input {
           background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='none' stroke='%2394a3b8' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E"); } }

  /* Loading spinner + centred interstitial panel (SCA / payment redirect). */
  .spinner { inline-size: 2rem; block-size: 2rem; border-radius: 50%;
           border: 3px solid var(--color-border); border-block-start-color: var(--color-accent-text);
           animation: amps-spin .7s linear infinite; }
  @keyframes amps-spin { to { transform: rotate(360deg); } }
  @media (prefers-reduced-motion: reduce) { .spinner { animation-duration: 2s; } }
  .loading-panel { display: flex; flex-direction: column; align-items: center; gap: var(--space-4);
           padding: var(--space-12); color: var(--color-text-muted); text-align: center; }
}

/* ============================================================================
   FINANCE / MONEY — tabular figures, discounted-price pairs, invoice/order
   totals blocks, money inputs. (See DESIGN.md — replaces col-price/format_price,
   .strikeout/.discount, invoice <tfoot>, order_summary.tpl, NetAmount inputs.)
   ============================================================================ */
@layer components {
  /* Inline money value — tabular figures so digits line up; never wraps mid-amount. */
  .money { font-variant-numeric: tabular-nums; white-space: nowrap; }
  .money--credit { color: var(--color-success-text); }   /* a credit / refund (money in) */
  .money--muted  { color: var(--color-text-subtle); }    /* a zero / non-emphasised amount */
  .money--negative { color: var(--color-danger-text); }  /* an over-allocation / negative remainder */

  /* Discounted-price pair (replaces legacy .strikeout + .discount): the crossed-out original
     beside the price that actually applies. */
  .price { display: inline-flex; align-items: baseline; gap: var(--space-2); }
  .price__was { text-decoration: line-through; color: var(--color-text-subtle);
           font-variant-numeric: tabular-nums; }
  .price__now { color: var(--color-success-text); font-weight: var(--weight-semibold);
           font-variant-numeric: tabular-nums; }

  /* Totals block — a right-aligned label→amount summary. Serves both the invoice <tfoot>
     (Net / VAT / Total) and the order-summary sidebar. Use a <dl> of .totals__row groups; the
     grand total gets a rule above + heavier weight. Amounts are tabular so they align. */
  .totals { display: flex; flex-direction: column; gap: var(--space-2); }
  .totals__row { display: flex; align-items: baseline; justify-content: space-between;
           gap: var(--space-6); }
  .totals__row dt { color: var(--color-text-muted); }
  .totals__row dd { font-variant-numeric: tabular-nums; text-align: end; }
  .totals__row--grand { margin-block-start: var(--space-1); padding-block-start: var(--space-3);
           border-block-start: 1px solid var(--color-border); }
  .totals__row--grand dt { color: var(--color-text); font-weight: var(--weight-semibold); }
  .totals__row--grand dd { font-weight: var(--weight-semibold); }
  /* A titled sub-group inside a summary card (e.g. "Ongoing charges" over "Due today"). */
  /* Group heading inside a summary card — base size (matches the rows it groups), set apart by weight
     + muted colour, not by a smaller size. */
  .totals__subhead { font-weight: var(--weight-semibold);
           color: var(--color-text-muted); margin-block-end: var(--space-1); }

  /* Money input — right-aligned tabular figures; pair with an .input-group "£" addon. Width-capped to
     hold up to 0000000.00; in a table cell the column rule still sizes it (that selector wins). */
  .input--money { inline-size: 8.5rem; max-inline-size: 100%; text-align: end; font-variant-numeric: tabular-nums; }
  .input-group > .input--money { flex: 0 0 auto; }   /* keep the capped width instead of growing to fill */
}

/* ============================================================================
   ORDERS / CHECKOUT — step progress, product/payment choice cards, the
   two-column order layout, attribute sub-lists, stacked sidebar buttons.
   (See DESIGN.md — replaces order_progress.tpl, .card-list, .order-layout,
   .order-sidebar, .attributes, .buttons.stacked.)
   ============================================================================ */
@layer components {
  /* Step progress — a numbered <ol> across the checkout flow: complete / current / upcoming.
     The connector before a step is filled once you've reached it. */
  .stepper { display: flex; list-style: none; margin: 0 0 var(--space-8); padding: 0; }
  .stepper__step { flex: 1; position: relative; display: flex; flex-direction: column;
           align-items: center; gap: var(--space-2); text-align: center; }
  .stepper__step:not(:first-child)::before { content: ""; position: absolute; inset-block-start: 15px;
           inset-inline-end: 50%; inline-size: 100%; block-size: 2px; background: var(--color-border); }
  .stepper__num { position: relative; z-index: 1; inline-size: 32px; block-size: 32px; border-radius: 50%;
           display: grid; place-items: center; background: var(--color-surface);
           border: 2px solid var(--color-border); color: var(--color-text-muted);
           font-size: var(--text-sm); font-weight: var(--weight-semibold); font-variant-numeric: tabular-nums; }
  .stepper__label { font-size: var(--text-sm); color: var(--color-text-muted); }
  .stepper__step.is-complete::before,
  .stepper__step.is-current::before { background: var(--color-accent); }
  .stepper__step.is-complete .stepper__num { background: var(--color-accent); border-color: var(--color-accent);
           color: var(--color-text-inverse); }
  .stepper__step.is-current .stepper__num { border-color: var(--color-accent); color: var(--color-accent-text); }
  .stepper__step.is-current .stepper__label { color: var(--color-text); font-weight: var(--weight-semibold); }
  /* Completed steps show a check instead of their number — done vs you-are-here at a glance. The check
     is absolutely centred over the (hidden) numeral so it can't become a second grid row and ride high. */
  .stepper__step.is-complete .stepper__num { color: transparent; }
  .stepper__step.is-complete .stepper__num::before { content: "\2713"; position: absolute; inset: 0;
           display: grid; place-items: center; color: var(--color-text-inverse);
           font-size: var(--text-base); line-height: 1; }
  /* Completed steps are clickable links back to that step (upcoming stay non-interactive). */
  .stepper__link { display: flex; flex-direction: column; align-items: center; gap: var(--space-2);
           color: inherit; text-decoration: none; }
  .stepper__link:hover { text-decoration: none; }
  .stepper__link:hover .stepper__label { color: var(--color-text); text-decoration: underline; }
  .stepper__link:hover .stepper__num { background: var(--color-accent-hover); border-color: var(--color-accent-hover); }

  /* Order layout — items on the left, a sticky summary/actions sidebar on the right; one column below 900px. */
  .order-layout { display: grid; grid-template-columns: 1fr; gap: var(--space-6); align-items: start; }
  @media (min-width: 900px) { .order-layout { grid-template-columns: 1fr minmax(18rem, 22rem); } }
  .order-aside { position: sticky; top: var(--space-6); display: flex; flex-direction: column; gap: var(--space-4); }

  /* Choice cards — selectable rows for product categories (link cards) and payment methods (radio cards).
     Whole card is the target; a checked radio card takes the brand highlight; a disabled one dims. */
  .choice-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-3); }
  /* Section-hub list of sub-area links — cap the width so rows don't stretch across a wide page. */
  .choice-list--hub { max-inline-size: 40rem; }
  .choice { display: flex; align-items: flex-start; gap: var(--space-3); padding: var(--space-4);
           border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-surface);
           color: inherit; cursor: pointer; }
  .choice:hover { border-color: var(--color-accent); background: var(--color-surface-hover); text-decoration: none; }
  .choice:has(input:checked) { border-color: var(--color-accent); background: var(--color-accent-subtle);
           box-shadow: inset 0 0 0 1px var(--color-accent); }
  .choice:has(input:disabled) { opacity: .55; cursor: not-allowed; }
  .choice:has(input:disabled):hover { border-color: var(--color-border); background: var(--color-surface); }
  .choice__control { flex: none; margin-block-start: 2px; }
  .choice__body { flex: 1 1 auto; min-inline-size: 0; }
  .choice__title { display: block; font-weight: var(--weight-medium); }
  .choice__desc { display: block; color: var(--color-text-muted); margin-block-start: 2px; }
  /* trailing affordance on a .choice that navigates rather than selects */
  .choice__chevron { flex: none; align-self: center; color: var(--color-text-subtle); }

  /* Attribute sub-list — the key: value detail under a choice card or an order line. */
  .attributes { list-style: none; margin: var(--space-2) 0 0; padding: 0; display: flex;
           flex-direction: column; gap: 2px; color: var(--color-text-muted); }

  /* Stacked button bar — full-width buttons in the order sidebar (legacy .buttons.stacked). */
  .button-bar--stacked { flex-direction: column; align-items: stretch; margin-block-start: 0; }
  .button-bar--stacked .btn { justify-content: center; }
}

/* ── Charts ──────────────────────────────────────────────────────────────────
   Server-rendered SVG charts (AMPS::Chart). Series colours arrive INLINE from the
   generator as token values, so charts re-theme with the rest of the UI; these
   rules own structure + interaction only. Hover tooltips reuse .tooltip--rich via
   the Floating-UI binder (data-tooltip-anchor / data-tooltip-html). */
@layer components {
  .chart-svg { inline-size: 100%; height: auto; display: block; }
  .chart-svg .grid line { stroke: var(--color-border-subtle); }
  /* 11px is intentionally below --text-sm — axis labels need to stay compact within the plot. */
  .chart-svg .axis text { fill: var(--color-text-muted); font-size: 11px; font-family: var(--font-sans); }
  .chart-svg .line { fill: none; stroke-width: 1.5; }
  .chart-svg .area { stroke: none; fill-opacity: .14; }

  /* Per-point hover targets carry the tooltip; a marker dot per series appears on hover and the
     tooltip anchors to the top series' dot, so it attaches to a point rather than the whole column. */
  .chart-svg .hit { fill: transparent; cursor: crosshair; }
  .chart-svg .pt { opacity: 0; r: 3; stroke: var(--color-surface); stroke-width: 1.5; pointer-events: none;
                   transition: opacity var(--transition), r var(--transition); }
  .chart-svg .col:hover .pt { opacity: 1; r: 6.5; }
  .chart-svg .anchor { fill: none; stroke: none; pointer-events: none; }

  /* Grouped bars — colour is inline; hovering a group darkens its bars like the client libraries. */
  .chart-svg .bar { transition: filter var(--transition); }
  .chart-svg .bargroup:hover .bar { filter: brightness(0.92); }

  /* Donut slices — pop out along their mid-angle (--dx/--dy) on hover; separated by a surface stroke. */
  .chart-svg .slice { stroke: var(--color-surface); stroke-width: 2; cursor: pointer;
                      transition: transform var(--transition); }
  .chart-svg .slice:hover { transform: translate(var(--dx), var(--dy)); }
  .chart-svg .donut-total { fill: var(--color-text); font-family: var(--font-sans);
                            font-size: 26px; font-weight: var(--weight-semibold); }  /* bespoke centre-total size */
  .chart-svg .donut-sub { fill: var(--color-text-muted); font-family: var(--font-sans); font-size: var(--text-sm); }
  /* Isolated data point (a defined value flanked by gaps) — a static visible dot; colour is inline. */
  .chart-svg .point { stroke: none; }

  /* Date picker — a text input (dd/mm/yyyy) upgraded by datepicker.js to a calendar popover. The calendar
     glyph sits in a differentiated segment on the right (grey fill + a divider), mirroring the file button;
     three background layers — icon over the divider over the segment fill. Colours are tokens (both themes). */
  .input--date { cursor: pointer; inline-size: 10rem; max-inline-size: 100%;
    padding-inline-end: 2.75rem; background-repeat: no-repeat;
    background-position: right 12px center, right 2.5rem center, right center;
    background-size: 16px 16px, 1px 100%, 2.5rem 100%;
    background-image:
      url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8 2v4M16 2v4M3 10h18'/%3E%3Crect width='18' height='18' x='3' y='4' rx='2'/%3E%3C/svg%3E"),
      linear-gradient(var(--color-field-border), var(--color-field-border)),
      linear-gradient(var(--color-surface-hover), var(--color-surface-hover)); }
  /* Dark: lighten the glyph so it stays legible; the divider/fill layers use tokens so they re-theme. */
  [data-theme="dark"] .input--date { background-image:
      url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8 2v4M16 2v4M3 10h18'/%3E%3Crect width='18' height='18' x='3' y='4' rx='2'/%3E%3C/svg%3E"),
      linear-gradient(var(--color-field-border), var(--color-field-border)),
      linear-gradient(var(--color-surface-hover), var(--color-surface-hover)); }
  @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) .input--date {
    background-image:
      url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8 2v4M16 2v4M3 10h18'/%3E%3Crect width='18' height='18' x='3' y='4' rx='2'/%3E%3C/svg%3E"),
      linear-gradient(var(--color-field-border), var(--color-field-border)),
      linear-gradient(var(--color-surface-hover), var(--color-surface-hover)); } }
  .input-daterange { display: flex; align-items: center; gap: var(--space-2); }
  .input-daterange__sep { color: var(--color-text-muted); }

  .datepicker { position: absolute; inset-block-start: 0; inset-inline-start: 0; z-index: 60; inline-size: 16rem;
    padding: var(--space-3); background: var(--color-surface); border: 1px solid var(--color-border);
    border-radius: var(--radius-md); box-shadow: var(--shadow-lg); }
  .datepicker__header { display: flex; align-items: center; justify-content: space-between; margin-block-end: var(--space-2); }
  .datepicker__month { font-weight: var(--weight-medium); }
  .datepicker__nav { inline-size: 1.75rem; block-size: 1.75rem; border: 0; background: none; cursor: pointer;
    color: var(--color-text-muted); font-size: 1.15rem; line-height: 1; border-radius: var(--radius-sm); }
  .datepicker__nav:hover { background: var(--color-surface-hover); color: var(--color-text); }
  .datepicker__grid { display: grid; grid-template-columns: repeat(7, 1fr); }
  .datepicker__dow { text-align: center; color: var(--color-text-subtle); padding-block: 2px; }
  .datepicker__days { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
  .datepicker__day { aspect-ratio: 1; border: 0; background: none; border-radius: var(--radius-sm); cursor: pointer;
    color: var(--color-text); font-variant-numeric: tabular-nums; }
  .datepicker__day:hover { background: var(--color-accent-subtle); }
  .datepicker__day.is-today { box-shadow: inset 0 0 0 1px var(--color-border-strong); }
  .datepicker__day.is-selected { background: var(--color-accent); color: var(--color-text-inverse); }
  .datepicker__day.is-disabled { color: var(--color-text-subtle); opacity: .4; cursor: default; }
  .datepicker__day.is-disabled:hover { background: none; }

  /* Editable data grid — input cells with a live-totalled footer (editable-grid.js + big.js). */
  .data-table--editable td { padding: var(--space-2); vertical-align: middle; }
  .data-table--editable td .input, .data-table--editable td .select { inline-size: 100%; }
  .data-table--editable .cell-num { text-align: end; font-variant-numeric: tabular-nums; }
  .data-table--editable tfoot th { text-align: end; font-weight: var(--weight-normal); color: var(--color-text-muted); }
  .data-table--editable tfoot td { text-align: end; font-variant-numeric: tabular-nums; }
  .data-table--editable tfoot .is-grand th, .data-table--editable tfoot .is-grand td {
    font-weight: var(--weight-semibold); color: var(--color-text); }

  /* Legend / stats table shown beneath a chart. */
  .chart-legend { inline-size: 100%; margin-block-start: var(--space-4); border-collapse: collapse; }
  /* At 14px the legend outgrows a phone-width card, so it scrolls sideways rather than spilling out. */
  .chart-legend-scroll { overflow-x: auto; }
  .chart-legend th, .chart-legend td { padding: 4px 8px; text-align: end; color: var(--color-text-muted); font-weight: var(--weight-normal); }
  .chart-legend thead th { color: var(--color-text-subtle); border-block-end: 1px solid var(--color-border-subtle); }
  .chart-legend__name { text-align: start; color: var(--color-text); display: flex; align-items: center; gap: 8px; }
  /* colour is set inline from the series, so a swatch always matches its line */
  .chart-legend__swatch { inline-size: 10px; block-size: 10px; border-radius: 2px; flex: none; display: inline-block; }
  .chart-legend__value { color: var(--color-text); font-variant-numeric: tabular-nums; }
  /* Key row above a chart: names each series by its swatch, wrapping onto more lines on a phone. */
  .chart-key { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px 18px; margin: 0 0 var(--space-3); padding: 0; list-style: none; }
  .chart-key__item { display: inline-flex; align-items: center; gap: 6px; color: var(--color-text); }
}

/* ============================================================================
   MEDIUM COMPONENTS (2026-07 template-audit backlog #9–#16)
   ========================================================================= */
@layer components {
  /* Full-page outcome states — terminal success / error / forbidden / not-found
     pages rendered through bare-layout.tpl. One .outcome block covers all four;
     variants differ only by copy, one icon and one colour modifier. Zero JS. */
  .outcome-layout { min-block-size: 100dvh; display: grid; place-items: center;
           padding: var(--space-10) var(--space-6); background: var(--color-canvas); }
  .outcome { max-inline-size: 34rem; text-align: center; }
  .outcome__icon { inline-size: 4.5rem; block-size: 4.5rem;
           margin: 0 auto var(--space-5); display: grid; place-items: center;
           border-radius: var(--radius-full); }
  .outcome__icon .icon { inline-size: 32px; block-size: 32px; }
  .outcome__icon--success { background: var(--color-success-bg); color: var(--color-success-text); border: 1px solid var(--color-success-border); }
  .outcome__icon--warning { background: var(--color-warning-bg); color: var(--color-warning-text); border: 1px solid var(--color-warning-border); }
  .outcome__icon--danger  { background: var(--color-danger-bg);  color: var(--color-danger-text);  border: 1px solid var(--color-danger-border); }
  .outcome__icon--neutral { background: var(--color-surface-sunken); color: var(--color-text-muted); border: 1px solid var(--color-border); }
  .outcome__title { font-size: var(--text-2xl); letter-spacing: var(--tracking-tight); }
  .outcome__message { color: var(--color-text-muted); margin-block-start: var(--space-3); }
  .outcome__ref { margin-block-start: var(--space-4); color: var(--color-text); }

  .button-bar--center { justify-content: center; }
}

@layer components {
  /* Error summary — the GOV.UK-style "There is a problem" list built (or server-rendered) then focused
     on submit-with-errors by field-validate.js. Reuses the .alert.alert--danger shell; announced by
     focus alone (tabindex=-1 + a real <h2>), never role=alert / aria-live (a focused live region is
     announced twice). */
  .error-summary__title { font-size: var(--text-base); font-weight: var(--weight-medium);
           margin: 0 0 var(--space-2); display: flex; align-items: center; gap: var(--space-2); }
  .error-summary__title .icon { color: var(--color-danger-solid); flex: none; }
  .error-summary__list { list-style: none; margin: 0; padding: 0;
           display: flex; flex-direction: column; gap: var(--space-1); }
  .error-summary__list a { color: var(--color-danger-text); font-weight: var(--weight-medium);
           text-decoration: underline; text-underline-offset: 2px; }
  .error-summary__list a:hover { color: var(--color-danger-solid); }
  /* Programmatic .focus() sets :focus (not the heuristic :focus-visible), so key the ring off :focus to
     guarantee the moved-to summary shows a visible indicator (GOV.UK pattern). */
  .error-summary:focus { outline: 2px solid var(--color-accent-text); outline-offset: 3px; }
}

@layer components {
  /* Add another — the MOJ repeatable "add another" editor: a stacked list of single-row mini-forms
     (a numbered "Item N" label · its input · a Remove button) you can grow/shrink. The item itself is
     the focus target on add/remove (tabindex=-1); the + Add button sits ABOVE the list. See add-another.js. */
  .add-another__items { display: block; }
  .add-another__item { border-inline-start: 3px solid transparent; padding-inline-start: var(--space-4); }
  /* Divider + spacing between consecutive items. */
  .add-another__item + .add-another__item { margin-block-start: var(--space-5);
           padding-block-start: var(--space-5); border-block-start: 1px solid var(--color-border-subtle); }
  /* Programmatic .focus() sets :focus (not :focus-visible), so key the ring off :focus. */
  .add-another__item:focus { outline: var(--focus-ring); outline-offset: 2px; border-radius: var(--radius-sm); }
  /* Errored item: red left border + faint danger wash (colour is never the only signal — also .field__error + aria-invalid). */
  .add-another__item--error { border-inline-start-color: var(--color-danger-solid);
           background: var(--color-danger-bg); border-radius: var(--radius-sm); }
  /* Single-row item: leading label · input (fills) · Remove. Wraps on narrow viewports. */
  .add-another__row { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
  .add-another__label { flex: none; inline-size: 9rem; font-weight: var(--weight-medium); color: var(--color-text); }
  .add-another__row .input { flex: 1; min-inline-size: 0; }
  .add-another__row .add-another__remove { flex: none; }
  .add-another__item .field__error { margin-block-start: var(--space-2); }
  /* + Add sits above the list; margin-below separates it from the first item, and from top hints. */
  .add-another__add { margin-block-end: var(--space-4); }
  .add-another > .field__hint { margin-block-end: var(--space-4); }
  /* Sticky save bar (a newly added bottom item is scrollIntoView'd so this can't hide it — 2.4.11). */
  .add-another__savebar { position: sticky; inset-block-end: 0; background: var(--color-canvas);
           padding-block: var(--space-3); margin-block-start: var(--space-5);
           border-block-start: 1px solid var(--color-border-subtle); }
}

@layer components {
  /* Filter strip (components/filter-strip.tpl) — the top of the card that holds its results. Each kind of
     field has one width on every page (an ID or short select 8.5rem, a longer select its longest option,
     a date the 10rem it has everywhere else) and only free text grows, so a row ends on the card's right
     edge whenever it holds free text. The last two fields and the buttons wrap as one tail, so the buttons
     always end a row of fields. */
  .card__body:has(> .filter-strip) { container-type: inline-size; }   /* the rules below need a container */
  .filter-strip { --unit: 8.5rem; --date: 10rem; display: flex; flex-wrap: wrap; align-items: flex-end;
           gap: var(--space-4) var(--space-3); margin-block-end: var(--space-6); }
  .filter-strip .field { flex: none; inline-size: var(--unit); }
  /* the floor is what the tail measures itself by, so a tail with free text wraps whole, never inside */
  .filter-strip .field--wide { flex: 1 1 12rem; min-inline-size: 12rem; }
  /* a choice is never clipped while the card can fit it, and never pushes past the card when it cannot */
  .filter-strip .field:has(> .select) { inline-size: max-content; min-inline-size: var(--unit); max-inline-size: 100%; }
  .filter-strip .field:has(> .input--date) { inline-size: var(--date); }
  .filter-strip .input--date { inline-size: 100%; }
  .filter-strip__range { flex: none; display: grid; grid-template-columns: repeat(2, var(--date)); gap: var(--space-3); }
  .filter-strip .filter-strip__range > .field { inline-size: auto; }
  .filter-strip__tail { flex: 1 1 auto; min-inline-size: 0; display: flex; flex-wrap: wrap; align-items: flex-end; gap: inherit; }
  /* without free text the tail keeps its own width, so its buttons sit beside its fields, not at the far edge */
  .filter-strip__tail:not(:has(> .field--wide)) { flex-grow: 0; }
  /* the margin only takes space when the buttons wrap onto a line of their own, right-aligning them there */
  .filter-strip__buttons { display: flex; gap: var(--space-2); flex: none; margin-inline-start: auto; }
  .filter-strip__buttons > .btn--primary { order: 1; }
  /* Clear Filters is the clear control, so the search box has no cancel button of its own */
  .filter-strip input[type="search"] { -webkit-appearance: none; appearance: none; }
  .filter-strip input[type="search"]::-webkit-search-cancel-button { -webkit-appearance: none; }
  /* icon-only: square at the control height, named by aria-label, labelled on hover and focus by the shared tooltip */
  .btn--icon { flex: none; inline-size: var(--control-h); padding: 0; justify-content: center; position: relative; }
  .btn__badge { position: absolute; inset-block-start: -7px; inset-inline-end: -7px; min-inline-size: 18px; block-size: 18px;
           padding-inline: 5px; border-radius: 9px; background: var(--color-accent); color: var(--color-text-inverse);
           font-size: var(--text-sm); font-weight: var(--weight-semibold); line-height: 18px; text-align: center; font-variant-numeric: tabular-nums; }
  .filter-strip__more[aria-expanded="true"] { border-color: var(--color-accent-text); color: var(--color-accent-text); }
  /* a live strip's results replace the Search button; it stays as the no-JS submit */
  html.js .filter-strip__submit { display: none; }
  /* the panel is the strip's last row, so opening it never moves the bar above it; it collapses by class, never
     [hidden], so without JS it is simply shown */
  .filter-strip__panel { flex: 1 1 100%; display: flex; flex-wrap: wrap; align-items: flex-end; gap: inherit; }
  html.js .filter-strip__panel:not(.is-open) { display: none; }
  html:not(.js) .filter-strip__more { display: none; }
  [data-live-results] { transition: opacity .15s; }
  [data-live-results].is-loading { opacity: .5; transition-delay: .15s; }
  /* phone: a field per row; buttons take half a row where they fit, so Clear pairs with Export and Search spans the last */
  @container (max-width: 30rem) {
    .filter-strip .field, .filter-strip__range, .filter-strip__tail { flex: 1 1 100%; min-inline-size: 0; }
    .filter-strip__range { grid-template-columns: 1fr; }
    .filter-strip__buttons { flex: 1 1 100%; flex-wrap: wrap; }
    .filter-strip__buttons > .btn { flex: 1 1 calc(50% - var(--space-2)); justify-content: center; }
    /* with JS a live strip's last select keeps its icons beside it, shrinking rather than pushing them past the card */
    html.js .filter-strip--live .filter-strip__tail { flex-wrap: nowrap; }
    html.js .filter-strip--live .filter-strip__tail > .field { flex: 1 1 auto; min-inline-size: 0; }
    html.js .filter-strip--live .filter-strip__buttons { flex: none; }
    .filter-strip--live .filter-strip__buttons > .btn--icon { flex: none; }
  }

  /* a chart that shares the filter strip's card (chart-panel.tpl with bare) */
  .chart-panel + .chart-panel { margin-block-start: var(--space-6); }
  .chart-panel > .card__title { margin-block-end: var(--space-3); }
}

@layer components {
  /* Card with two stacked bodies (e.g. a form body above a results body — the async lookup wizard):
     a hairline separates the second body from the first. */
  .card__body + .card__body { border-block-start: 1px solid var(--color-border-subtle); }
}

/* ============================================================================
   LOW COMPONENTS (2026-07 template-audit backlog #21–#30)
   Most of that tier needed NO new CSS at all — export clusters (#22), the
   external-link affordance (#29) and the last-login header (#30) are pure
   compositions of existing classes, and #27 was dropped outright. What follows
   is the entire remaining cost of the tier.
   ========================================================================= */
@layer components {
  /* BLF presence (#21) — extends .status; the dot never carries meaning on its own, its state name sits
     inline beside it (1.4.1), repainted live by blf-presence.js. Only Ringing blinks, at 1 Hz — a third
     of the 3 Hz seizure threshold (2.3.1) — animating opacity, never to 0 and never a colour flash, and
     suppressed under the OS reduced-motion preference. */
  .status--busy::before { background: var(--color-danger-solid); }
  /* stale/unknown is a hollow ring — distinguishable from --inactive by SHAPE, not just tone */
  .status--unknown::before { background: transparent; box-shadow: inset 0 0 0 2px var(--color-text-subtle); }
  .status--blink::before { animation: amps-blf-blink 1s steps(2, jump-none) infinite; }
  @keyframes amps-blf-blink { 50% { opacity: .3; } }
  @media (prefers-reduced-motion: reduce) { .status--blink::before { animation: none; } }

  /* Secret field (#23) — monospace so lookalike glyphs (0/O, 1/l, rn/m) stay distinguishable when a
     revealed credential is read aloud or transcribed. The field is type="text", never type="password":
     it holds a THIRD PARTY's credential that staff administer, and a non-password input is not a
     password-manager target — which is the whole of #27 solved without decoy fields. */
  .input--secret { font-family: var(--font-mono); }

  /* Quantity spinner in an editable grid — fills its --col-qty column and stays digit-aligned. */
  .input--qty { inline-size: 100%; text-align: end; font-variant-numeric: tabular-nums; }

  /* File upload (#25) — the NATIVE control, skinned; no dropzone, because the built-in button is
     keyboard/AT-operable and prints the chosen filename itself. The selector button is a full-height
     flush segment sized to the standard field height so file rows line up with every other field, and
     its leading corners are rounded to sit INSIDE the field's radius — squared corners there clip the
     rounded border and make the red invalid outline look broken. */
  .input--file { padding-block: 0; padding-inline-start: 0; cursor: pointer; line-height: calc(var(--control-h) - 2px); }
  .input--file::file-selector-button {
           margin-inline-end: var(--space-3); padding: 0 var(--space-4); block-size: calc(var(--control-h) - 2px);
           box-sizing: border-box;   /* the * reset does not reach ::file-selector-button */
           border: 0; border-inline-end: 1px solid var(--color-field-border);
           border-start-start-radius: calc(var(--radius-md) - 1px);
           border-end-start-radius: calc(var(--radius-md) - 1px);
           background: var(--color-surface-hover); color: var(--color-text);
           font: inherit; font-weight: var(--weight-medium); cursor: pointer; }
  .input--file::file-selector-button:hover { background: var(--color-border-subtle); }
  @media (forced-colors: active) { .input--file::file-selector-button { border: 1px solid ButtonBorder; } }

  /* Inline edit-in-place (#26) — no new class names; one .detail-list row swaps its read-only value
     for a compact field. 18rem matches .input--search so field widths stay on one scale. */
  [data-ie] .field { inline-size: min(18rem, 100%); }
  /* Keep the dt label optically level with the control in BOTH states: top-aligned plus the input's own
     inset, so a growing row never drags the label out of line the way align-items:center would. */
  .detail-list:has([data-ie]) > dt { align-self: start; padding-block-start: calc((var(--control-h) - 1lh) / 2); }
}

@layer utilities {
  /* Save/Cancel sit on the SAME line as the input they act on and STAY there when a validation error
     appears beneath it — .cluster centres by default, which would drift them down as the field grows.
     This MUST live in the utilities layer: .cluster is a utility, and layer order beats both specificity
     and source order, so the same rule in @layer components would silently lose to it. */
  [data-ie-form] { align-items: flex-start; }
}
