/* TechViet UI kit v1.3
   Needs the :root tokens from build-tokens.mjs (tokens.base.json + the tenant's tokens.json).
   Every colour, size, radius, shadow, z-index and duration is a var(--*); the only literals are
   1px lines and icon geometry. Class names follow what the queued tenants already converged on. */

/* Base ------------------------------------------------------------------- */

*,
*::before,
*::after { box-sizing: border-box; }

[hidden] { display: none !important; }

html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

body {
  margin: 0;
  min-height: 100dvh;
  font-family: var(--font-body);
  font-size: var(--fs-base);
  line-height: var(--lh);
  color: var(--text);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

h1,
h2,
h3 {
  margin: 0;
  font-family: var(--font-heading);
  font-weight: var(--heading-weight);
  line-height: var(--lh-tight);
  color: var(--heading);
  text-wrap: balance;
}
h1 { font-size: var(--fs-2xl); }
h2 { font-size: var(--fs-xl); }
h3 { font-size: var(--fs-lg); }

p { margin: 0; }

a {
  color: var(--link);
  text-underline-offset: 0.2em;
}

button { font: inherit; }

svg { display: block; }

:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}

.visually-hidden {
  position: absolute !important;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  top: var(--sp-3);
  left: var(--sp-3);
  z-index: var(--z-toast);
  padding: var(--sp-2) var(--sp-4);
  font-weight: 600;
  color: var(--text);
  background: var(--surface);
  border: 2px solid var(--focus);
  border-radius: var(--radius-sm);
  transform: translateY(-200%);
}
.skip-link:focus { transform: none; }

.container {
  width: min(100% - 2 * var(--sp-4), var(--container-w, 72rem));
  margin-inline: auto;
  padding-block: var(--sp-6);
}
/* The page uses the whole width the window gives it, with only the side gutters: a dashboard or a list beside a sidebar. */
.container-fluid { --container-w: 100%; }

.muted { color: var(--muted); }

/* A line of small text under a block (a card, a table, a form): the gap above keeps it from sticking to the block. */
.caption { margin-top: var(--sp-3); font-size: var(--fs-sm); color: var(--muted); }

/* Right-aligned figures that line up in columns (amounts, quantities). */
.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}
/* Same digit width without the right alignment: dates and times in a column. */
.tnum { font-variant-numeric: tabular-nums; }

/* Buttons ---------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  min-height: var(--tap);
  padding: 0 var(--sp-4);
  font-weight: 600;
  line-height: var(--lh-tight);
  text-align: center;
  text-decoration: none;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--field-border);
  border-radius: var(--radius);
  cursor: pointer;
  transition: background-color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.btn:hover { background: var(--surface-hover); border-color: var(--field-border-hover); }
.btn:active { background: var(--sunk); }

.btn-primary { color: var(--on-primary); background: var(--primary); border-color: var(--primary); }
.btn-primary:hover,
.btn-primary:active { background: var(--primary-hover); border-color: var(--primary-hover); }

.btn-danger { color: var(--danger); border-color: var(--danger-line); }
.btn-danger:hover { background: var(--danger-bg); border-color: var(--danger); }

.btn-danger-solid { color: var(--on-danger); background: var(--danger); border-color: var(--danger); }
.btn-danger-solid:hover,
.btn-danger-solid:active { background: var(--danger-hover); border-color: var(--danger-hover); }

.btn-ghost { color: var(--muted); background: transparent; border-color: transparent; }
.btn-ghost:hover { color: var(--text); background: var(--surface-hover); border-color: transparent; }

.btn-link {
  min-height: 0;
  padding: 0;
  color: var(--link);
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}
.btn-link:hover { background: none; }

.btn-sm { min-height: calc(var(--tap) - 8px); padding: 0 var(--sp-3); font-size: var(--fs-sm); }
.btn-lg { min-height: calc(var(--tap) + 4px); padding: 0 var(--sp-6); font-size: var(--fs-lg); }
.btn-block { width: 100%; }
.btn-icon { flex: none; width: var(--tap); padding: 0; }
.btn-icon.btn-sm { width: calc(var(--tap) - 8px); }
.btn-sm .icon { width: var(--icon-sm); height: var(--icon-sm); }

.btn-group { display: inline-flex; }
.btn-group .btn + .btn { margin-left: -1px; }
.btn-group .btn:not(:first-child) { border-top-left-radius: 0; border-bottom-left-radius: 0; }
.btn-group .btn:not(:last-child) { border-top-right-radius: 0; border-bottom-right-radius: 0; }

/* Loading: keep the label, add a spinner. The tenant also sets disabled or aria-disabled. */
.btn[aria-busy="true"] { cursor: progress; }
.btn[aria-busy="true"]::before {
  content: "";
  flex: none;
  width: 1em;
  height: 1em;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: kit-spin 0.75s linear infinite;
}
/* The spinner stands in for an icon button's icon, so the icon is hidden to avoid showing both. */
.btn[aria-busy="true"] > .icon { display: none; }
@keyframes kit-spin { to { transform: rotate(360deg); } }

/* Disabled comes last so it wins over every variant and hover. */
.btn:disabled,
.btn:disabled:hover,
.btn[aria-disabled="true"],
.btn[aria-disabled="true"]:hover {
  color: var(--muted);
  background: var(--sunk);
  border-color: var(--panel-border);
  cursor: not-allowed;
}
.btn-link:disabled,
.btn-link[aria-disabled="true"] { background: none; border: 0; }
.btn[aria-busy="true"]:disabled,
.btn[aria-busy="true"][aria-disabled="true"] { cursor: progress; }

/* Panel ------------------------------------------------------------------ */

.panel {
  background: var(--surface);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--sh-1);
}
.panel-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2) var(--sp-4);
  padding: var(--sp-4) var(--sp-5);
  border-bottom: 1px solid var(--panel-border);
}
.panel-head h2,
.panel-head h3 { font-size: var(--fs-lg); }
.panel-sub { margin-top: var(--sp-1); font-size: var(--fs-sm); color: var(--muted); }
/* What sits at the right of the head: a period menu, a button. */
.panel-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); }
.panel-body { padding: var(--sp-5); }
.panel-foot {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--sp-3);
  padding: var(--sp-4) var(--sp-5);
  background: var(--surface-2);
  border-top: 1px solid var(--panel-border);
  border-radius: 0 0 var(--radius-lg) var(--radius-lg);
}
/* A table or list that runs edge to edge inside the panel. */
.panel-flush > .panel-body { padding: 0; }

/* The numbered parts of one form: a .panel for each part, its head .panel-head.panel-head-part, which keeps to one line: .part-num (the number in a circle, or a check icon once the
   part is done: .part-num-ok), the title, and at the far end .panel-meta (how far the part is filled, such as "3/5"). */
.panel-head-part { flex-wrap: nowrap; justify-content: flex-start; gap: var(--sp-3); }
.part-num {
  display: inline-grid;
  flex: none;
  place-items: center;
  width: 1.75rem;
  height: 1.75rem;
  font-size: var(--fs-sm);
  font-weight: 700;
  line-height: 1;
  color: var(--on-primary);
  background: var(--primary);
  border-radius: 50%;
}
.part-num .icon { width: var(--icon-sm); height: var(--icon-sm); }
.part-num-ok { color: var(--ok); background: var(--ok-bg); box-shadow: inset 0 0 0 1px var(--ok-line); }
.panel-meta { margin-inline-start: auto; font-size: var(--fs-sm); color: var(--muted); white-space: nowrap; }

/* KPI card ---------------------------------------------------------------- */

/* One figure with its name, the change against the last period and a line of context, for the top of a dashboard. .kpi-row lays several out on a grid (13rem or more each). The card
   is an <a> when it opens the list behind the figure. data-tone="ok|warn|danger" puts a bar at the left edge, as thick as a notice's; the words in .kpi-foot say the same, so the colour
   is never alone. The figure stays on one line: from a million up write it short (128,5M, 34,2k) and give the whole number to a screen reader in .visually-hidden and to the mouse in title.
   A row never ends with a lone card: it is 12 columns and the card takes the share of the count (two: halves, three: thirds, four: quarters, six or more: thirds), and by the width of the row itself it
   falls to halves (four, under 55rem; six or more, under 41rem) or one a line (three, under 41rem; any, under 27rem), never three and one. Cards side by side line up: they are as tall as the tallest, and where subgrid is available the name, the figure and the foot of every card in a row share the same lines, so a name
   that wraps or a foot of two lines in one card does not push the figures of the others out of step. Keep to head, value and foot: a progress bar goes inside .kpi-foot. */
.kpi-row { gap: var(--sp-4); }
.kpi {
  display: grid;
  align-content: start;
  gap: var(--sp-2);
  min-width: 0;
  padding: var(--sp-4) var(--sp-5);
  color: var(--text);
  text-decoration: none;
  background: var(--surface);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--sh-1);
}
.kpi-row,
.kpi-tabs { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); container-type: inline-size; }
.kpi-row > .kpi,
.kpi-tabs > .kpi { grid-column: span var(--kpi-span, 4); }
:where(.kpi-row, .kpi-tabs):where(:has(> :nth-child(4):last-child)) > .kpi { --kpi-span: 3; }
:where(.kpi-row, .kpi-tabs):where(:has(> :nth-child(2):last-child)) > .kpi { --kpi-span: 6; }
:where(.kpi-row, .kpi-tabs):where(:has(> :only-child)) > .kpi { --kpi-span: 12; }
@container (max-width: 55rem) {
  :where(.kpi-row, .kpi-tabs):where(:has(> :nth-child(4):last-child)) > .kpi { --kpi-span: 6; }
}
@container (max-width: 41rem) {
  :where(.kpi-row, .kpi-tabs):where(:has(> :nth-child(3):last-child)) > .kpi { --kpi-span: 12; }
  :where(.kpi-row, .kpi-tabs):where(:has(> :nth-child(5))) > .kpi { --kpi-span: 6; }
}
@container (max-width: 27rem) {
  :where(.kpi-row, .kpi-tabs) > .kpi { --kpi-span: 12; }
}
@supports (grid-template-rows: subgrid) {
  .kpi-row > .kpi,
  .kpi-tabs > .kpi { grid-row: span 3; grid-template-rows: subgrid; }
}
a.kpi:hover { background: var(--surface-hover); border-color: var(--field-border-hover); }
.kpi[data-tone] { box-shadow: inset var(--sp-1) 0 0 var(--kpi-tone), var(--sh-1); }
.kpi[data-tone="ok"] { --kpi-tone: var(--ok); }
.kpi[data-tone="warn"] { --kpi-tone: var(--warn); }
.kpi[data-tone="danger"] { --kpi-tone: var(--danger); }
.kpi-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); }
.kpi-label { font-size: var(--fs-sm); color: var(--muted); }
.kpi-icon { display: inline-flex; flex: none; align-items: center; justify-content: center; width: 2rem; height: 2rem; color: var(--muted); background: var(--surface-2); border-radius: var(--radius); }
.kpi-icon .icon { width: 1.25rem; height: 1.25rem; }
.kpi-value { font-size: var(--fs-3xl); font-weight: 600; line-height: var(--lh-tight); font-variant-numeric: tabular-nums; white-space: nowrap; }
.kpi-unit { margin-inline-start: var(--sp-1); font-size: var(--fs-md); font-weight: 400; color: var(--muted); }
.kpi-foot { display: flex; flex-wrap: wrap; align-content: start; align-items: center; gap: var(--sp-1) var(--sp-2); font-size: var(--fs-sm); color: var(--muted); }
.kpi-foot:has(> .bar) { flex-direction: column; align-items: stretch; }
.kpi-foot > .bar { --bar-w: auto; }

/* KPI strip: the cards are the switch for what is shown below them (a chart, a table). .kpi-tabs[role="tablist"] > button.kpi[role="tab"] (aria-selected, aria-controls) with the same
   .kpi-head, .kpi-value and .kpi-foot written as <span>, since a button holds only phrasing content. The chosen card is raised on a sunk track, as in .seg; no data-tone here. Arrow
   keys, Home and End as for .tabs. */
.kpi-tabs {
  gap: var(--sp-1);
  padding: var(--sp-1);
  background: var(--sunk);
  border-radius: calc(var(--radius-lg) + var(--sp-1));
}
.kpi-tabs > .kpi {
  font: inherit;
  text-align: start;
  cursor: pointer;
  background: transparent;
  border-color: transparent;
  box-shadow: none;
}
.kpi-tabs > .kpi:hover { background: var(--surface-hover); }
.kpi-tabs > .kpi[aria-selected="true"] { background: var(--surface); box-shadow: var(--sh-2); }
.kpi-tabs > .kpi[aria-selected="true"] .kpi-label { color: var(--text); }

/* Table ------------------------------------------------------------------ */

.tbl-wrap {
  position: relative; /* contains the absolutely placed .visually-hidden header text, which overflow alone does not clip */
  overflow-x: auto;
  background: var(--surface);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius-lg);
}
.panel-flush .tbl-wrap { border: 0; border-radius: 0; }
/* The first and last blocks of a panel with no padding reach its corners (a bulk bar, a table header, a pager), so they take the panel's own corner radius instead of poking out of it. */
.panel-flush > :first-child { border-top-left-radius: inherit; border-top-right-radius: inherit; }
.panel-flush > :last-child { border-bottom-left-radius: inherit; border-bottom-right-radius: inherit; }
/* A body that is itself first or last (a panel with a table and no head or foot) clips what is in it to the rounded corners, so the header row and the last row do not poke out. */
.panel-flush > .panel-body:is(:first-child, :last-child) { overflow: clip; }
/* The same for any order of blocks: a bulk bar before the body, a pager after it, a hidden bar that is still the first child. Clipping the panel itself does not depend on which child comes first,
   and (unlike overflow: hidden) leaves position: sticky inside a scrolling table alone. */
.panel-flush { overflow: clip; }
/* Scrollbars that stay on screen. macOS hides overlay bars until the pointer scrolls, so a wide table gave no sign that it scrolls sideways. */
@supports selector(::-webkit-scrollbar) {
  .tbl-wrap::-webkit-scrollbar { width: 12px; height: 12px; }
  .tbl-wrap::-webkit-scrollbar-track,
  .tbl-wrap::-webkit-scrollbar-corner { background: var(--surface-2); }
  .tbl-wrap::-webkit-scrollbar-thumb {
    background: var(--field-border);
    border: 3px solid var(--surface-2);
    border-radius: var(--radius-pill);
  }
  .tbl-wrap::-webkit-scrollbar-thumb:hover { background: var(--field-border-hover); }
}
@supports not selector(::-webkit-scrollbar) {
  .tbl-wrap { scrollbar-color: var(--field-border) var(--surface-2); }
}

.tbl {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-md);
}
.tbl th,
.tbl td {
  padding: var(--sp-3) var(--sp-4);
  text-align: left;
  vertical-align: middle;
  border-bottom: 1px solid var(--panel-border);
}
.tbl th {
  font-size: var(--fs-sm);
  font-weight: 600;
  white-space: nowrap;
  color: var(--muted);
  background: var(--surface-2);
}
.tbl th.num,
.tbl td.num { text-align: right; }
/* A row's colour is --row-bg, painted on its cells, so a sticky cell can cover the cells that scroll under it. Later rules win: zebra, hover, selected. */
.tbl tbody tr { --row-bg: var(--surface); }
.tbl tbody td,
.tbl tbody th { background: var(--row-bg); }
/* A row header may hold a name with its code under it, so unlike a column title it wraps instead of widening the table. */
.tbl tbody th { font-size: inherit; color: var(--text); white-space: normal; }
.tbl-zebra tbody tr:nth-child(even) { --row-bg: var(--surface-2); }
.tbl tbody tr:hover { --row-bg: var(--surface-hover); }
.tbl tbody tr[aria-selected="true"] { --row-bg: var(--notice-bg); }
.tbl tbody tr:last-child td,
.tbl tbody tr:last-child th { border-bottom: 0; }
.tbl td.actions { text-align: right; white-space: nowrap; }
.tbl td.actions > .btn + .btn { margin-inline-start: var(--sp-2); }
.tbl-compact th,
.tbl-compact td { padding: var(--sp-2); }
.tbl-dense { font-size: var(--fs-sm); }
/* Column titles may wrap onto lines, so a table of long titles over short figures (a unit in each title) fits its column instead of scrolling sideways. Not for a table in .tbl-scroll,
   whose point is one line per cell. */
.tbl.tbl-th-wrap thead th { white-space: normal; }

/* Totals row: a <tfoot> of the same columns. */
.tbl tfoot th,
.tbl tfoot td {
  font-weight: 600;
  color: var(--text);
  background: var(--surface-2);
  border-top: 1px solid var(--field-border);
  border-bottom: 0;
}
.tbl tfoot th { font-size: inherit; }

/* A long cell keeps one line and ends in an ellipsis; the tenant puts the full text in title. */
.tbl td.trunc {
  max-width: 16rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A second, quieter line under the main text of a cell: a code, a handle, a unit. */
.tbl .cell-sub { display: block; font-size: var(--fs-sm); font-weight: 400; color: var(--muted); }
/* The clock of a date and time kept in one column: the date reads first at full strength, the time follows in the quieter colour. Keep one space between them. */
.tbl .cell-time { color: var(--muted); }

/* Codes, dates and short labels stay on one line instead of breaking at a hyphen or a space. */
.tbl .nowrap { white-space: nowrap; }

/* A state as a filled cell, as the status column of a board: the whole cell takes the colour of its state and the word sits centred in the surface colour, so a column of results is
   read by colour from afar. A line of the row's own colour round the cell keeps neighbours apart, and the fill holds on a hovered or selected row. Put .cell-fill on the td and
   .cell-fill-label round the icon and the word, which still say the state without colour. Tones: -ok -warn -bad -info; plain is grey, for no result. */
.tbl tbody td.cell-fill {
  color: var(--fill-fg, var(--text));
  text-align: center;
  background: var(--fill-bg, var(--sunk));
  box-shadow: inset 0 0 0 var(--sp-1) var(--row-bg);
}
.cell-fill-label { display: inline-flex; align-items: center; gap: var(--sp-2); font-weight: 600; white-space: nowrap; }
.cell-fill .icon { width: 1.1em; height: 1.1em; }
.cell-fill-ok { --fill-bg: var(--ok); --fill-fg: var(--surface); }
.cell-fill-warn { --fill-bg: var(--warn); --fill-fg: var(--surface); }
.cell-fill-bad { --fill-bg: var(--danger); --fill-fg: var(--on-danger); }
.cell-fill-info { --fill-bg: var(--info); --fill-fg: var(--surface); }

/* Voided row. The text is struck through as well as muted, so colour is not the only cue. */
.tbl tbody tr.row-void td,
.tbl tbody tr.row-void th { color: var(--muted); text-decoration: line-through; }
/* A voided row gives up a filled cell: muted struck-through text would not read on the colour, and a cancelled row has no result to show. */
.tbl tbody tr.row-void td.cell-fill { background: var(--row-bg); box-shadow: none; }

/* Cell lines on every side, for tables read like a sheet. The outer edge is the wrapper's border. */
.tbl-grid th,
.tbl-grid td { border: 1px solid var(--panel-border); }
.tbl-grid tr > :first-child { border-left: 0; }
.tbl-grid tr > :last-child { border-right: 0; }
.tbl-grid thead tr:first-child > * { border-top: 0; }
.tbl-grid tbody tr:last-child > * { border-bottom: 0; }

/* A tall or wide table scrolls inside its wrapper; --tbl-max-h sets the height. Sticky parts need this. */
.tbl-scroll { max-height: var(--tbl-max-h, 28rem); overflow: auto; }
/* Inside a scrolling table a cell keeps one line and the table gets wider, as in a sheet; td.wrap lets a text column wrap at a readable width. */
.tbl-scroll .tbl th,
.tbl-scroll .tbl td { white-space: nowrap; }
.tbl-scroll .tbl td.wrap { min-width: 16rem; white-space: normal; }
.tbl-sticky { --check-w: 2.75rem; }
/* The lines of a sticky cell are inset shadows, not borders: a collapsed border stays behind when the table scrolls, a shadow travels with the cell.
   A cell may need several (the first header cell has the line below it and the line to its right), so each rule only sets a variable and one rule paints them together. */
.tbl-sticky :is(th, td),
.tbl-sticky-col :is(th, td) { box-shadow: var(--line-b, 0 0 transparent), var(--line-t, 0 0 transparent), var(--line-r, 0 0 transparent); }
.tbl-sticky thead th {
  position: sticky;
  top: 0;
  z-index: var(--z-sticky);
  border-bottom: 0;
  --line-b: inset 0 -1px 0 var(--panel-border);
}
.tbl-sticky tfoot th,
.tbl-sticky tfoot td {
  position: sticky;
  bottom: 0;
  z-index: var(--z-sticky);
  border-top: 0;
  --line-t: inset 0 1px 0 var(--field-border);
}
/* First column stays while the rest scrolls sideways; with .tbl-select the check column and the one after it both stay. */
.tbl-sticky-col :is(th, td):first-child {
  position: sticky;
  left: 0;
  z-index: calc(var(--z-sticky) - 1);
  --line-r: inset -1px 0 0 var(--panel-border);
}
.tbl-sticky-col.tbl-select :is(th, td):nth-child(2) {
  position: sticky;
  left: var(--check-w);
  z-index: calc(var(--z-sticky) - 1);
  --line-r: inset -1px 0 0 var(--panel-border);
}
.tbl-sticky-col.tbl-select :is(th, td):first-child { --line-r: initial; }
.tbl-sticky-col.tbl-sticky thead :is(th):is(:first-child, :nth-child(2)),
.tbl-sticky-col.tbl-sticky tfoot :is(th, td):is(:first-child, :nth-child(2)) { z-index: calc(var(--z-sticky) + 1); }

/* Row selection: a check column, the select-all box in its header, and tr[aria-selected="true"] for a chosen row. */
.tbl .cell-check {
  width: var(--check-w, 2.75rem);
  min-width: var(--check-w, 2.75rem);
  padding-right: 0;
}
.tbl input[type="checkbox"]:not(.switch input) { /* a .switch in a cell keeps its own 2.5 x 1.5 rem size */
  width: 1.125rem;
  height: 1.125rem;
  margin: 0;
  vertical-align: middle;
  accent-color: var(--primary);
  cursor: pointer;
}

/* Sortable header: a button in the th; the th carries aria-sort, the tenant swaps the icon (caret-up-down, arrow-up, arrow-down). */
.th-sort {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  min-height: 1.5rem;
  padding: 0;
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.th-sort .icon { width: var(--icon-sm); height: var(--icon-sm); }
.th-sort:hover { color: var(--text); }
.tbl th[aria-sort="ascending"],
.tbl th[aria-sort="descending"] { color: var(--text); }

/* Table as cards: on a phone a table of many columns scrolls sideways; as cards each row is read from top to bottom. Put .tbl-cards on the .tbl-wrap and a data-label (the column's name)
   on each td. Where the wrapper is under 36rem wide the rows become cards: the first cell (th scope="row") is the title, the other cells sit in two columns under it with the label
   above the value, a filled cell (td.cell-fill) is a strip across the card, its name at the left and the state at the right, row buttons (td.actions) run along the foot and a check cell goes to the corner. Wider, it is the table it was. The wrapper is the container, so a table in a narrow
   column flips as well as one on a phone. For a plain table: not with -sticky, -select or .tbl-scroll. The head row stays for screen readers. */
.tbl-cards { container-type: inline-size; }
@container (max-width: 36rem) {
  .tbl-cards .tbl,
  .tbl-cards .tbl tbody { display: block; }
  .tbl-cards .tbl thead { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); }
  .tbl-cards .tbl tbody { display: grid; gap: var(--sp-3); padding: var(--sp-3); background: var(--surface-2); }
  .tbl-cards .tbl tbody tr {
    position: relative;
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--sp-2) var(--sp-4);
    padding: var(--sp-4);
    background: var(--row-bg);
    border: 1px solid var(--panel-border);
    border-radius: var(--radius);
  }
  .tbl-cards .tbl :is(th, td) { display: block; width: auto; padding: 0; text-align: left; white-space: normal; background: none; border: 0; }
  .tbl-cards .tbl tbody th { grid-column: 1 / -1; font-weight: 600; }
  .tbl-cards .tbl td[data-label]::before { content: attr(data-label); display: block; font-size: var(--fs-sm); font-weight: 400; color: var(--muted); }
  .tbl-cards .tbl td.num { text-align: left; }
  .tbl-cards .tbl tbody td.cell-fill { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); grid-column: 1 / -1; padding: var(--sp-2) var(--sp-3); border-radius: var(--radius-sm); box-shadow: none; }
  .tbl-cards .tbl td.cell-fill[data-label]::before { color: inherit; }
  .tbl-cards .tbl td.actions { display: flex; grid-column: 1 / -1; justify-content: flex-end; gap: var(--sp-2); padding-top: var(--sp-3); border-top: 1px solid var(--panel-border); }
  .tbl-cards .tbl td.actions > .btn + .btn { margin-inline-start: 0; }
  .tbl-cards .tbl td.cell-check { position: absolute; top: var(--sp-3); right: var(--sp-3); }
  .tbl-cards .tbl tbody tr:has(.cell-check) th { padding-right: var(--sp-8); }
}

/* Bar above the table: search, filters and actions. */
.tbl-tools {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--panel-border);
}
.tbl-tools .input { flex: 1 1 14rem; width: auto; max-width: 22rem; }
.tbl-tools-end { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-left: auto; }

/* Shown while rows are selected; the tenant sets hidden and the count. */
.bulk-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2) var(--sp-3);
  padding: var(--sp-2) var(--sp-4);
  color: var(--text);
  background: var(--notice-bg);
  border-bottom: 1px solid var(--notice-border);
}
.bulk-bar[hidden] { display: none; }
.bulk-count { margin-right: auto; font-weight: 600; }

/* Cells that take typing, for sheet-like entry. The tenant owns focus, keys and saving; these are the looks. */
.tbl td.cell-edit { padding: 0; }
.cell-input {
  display: block;
  width: 100%;
  min-width: 5.5rem; /* a narrow screen scrolls the table instead of squeezing the typed text */
  padding: var(--sp-3) var(--sp-4);
  font: inherit;
  color: inherit;
  text-align: inherit;
  background: transparent;
  border: 0;
  border-radius: 0;
}
.tbl-compact .cell-input { padding: var(--sp-2); }
.cell-input:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
.tbl td[aria-selected="true"] { outline: 2px solid var(--focus); outline-offset: -2px; }
.tbl tbody td.cell-ro { color: var(--muted); background: var(--sunk); }
.tbl tbody td.cell-dirty { background: var(--warn-bg); box-shadow: inset 3px 0 0 var(--warn); }
.tbl tbody td[aria-invalid="true"] { background: var(--danger-bg); box-shadow: inset 0 0 0 2px var(--danger); }

/* Placeholder bar while rows load; put aria-busy="true" on the table. */
.skel {
  display: inline-block;
  width: var(--skel-w, 6rem);
  height: 0.9em;
  vertical-align: middle;
  background: var(--sunk);
  border-radius: var(--radius-sm);
  animation: kit-pulse 1.2s ease-in-out infinite;
}
@keyframes kit-pulse { 50% { opacity: 0.45; } }

/* An empty table is one row holding the empty state. */
.tbl .tbl-empty td { padding: 0; border: 0; }
.tbl-empty .empty { background: transparent; border: 0; border-radius: 0; }

/* Page controls under a table. */
.pager {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2) var(--sp-4);
  width: 100%;
  font-size: var(--fs-sm);
  color: var(--muted);
}
.pager-nav,
.pager-size { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-1); }
.pager-size { gap: var(--sp-2); }
.pager-size .combo { width: 5rem; }
.pager .btn { min-width: calc(var(--tap) - 8px); padding-inline: var(--sp-2); }
.pager .select { width: auto; min-height: calc(var(--tap) - 8px); padding-block: 0; }
.pager .btn[aria-current="page"] { color: var(--on-primary); background: var(--primary); border-color: var(--primary); }
.pager .btn[aria-current="page"]:hover { background: var(--primary-hover); border-color: var(--primary-hover); }
/* Alone, outside a table: .pager-center centres it; .pager-pages holds the numbered buttons and .pager-gap the "…" between them.
   .pager-status ("Trang 3 / 12") takes the place of the numbers on a phone, or everywhere with .pager-compact. */
.pager-center { justify-content: center; }
.pager-pages { display: flex; align-items: center; gap: var(--sp-1); }
.pager-gap { padding-inline: var(--sp-1); }
.pager-range { padding-inline: var(--sp-2); font-size: var(--fs-sm); font-variant-numeric: tabular-nums; color: var(--muted); }
.pager-status { display: none; padding-inline: var(--sp-2); font-variant-numeric: tabular-nums; color: var(--text); }
.pager-compact .pager-pages { display: none; }
.pager-compact .pager-status { display: inline; }
@media (max-width: 40rem) {
  .pager .pager-pages { display: none; }
  .pager .pager-status { display: inline; }
}

/* Tag and chip ----------------------------------------------------------- */

.tag {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  padding: var(--sp-1) var(--sp-2);
  font-size: var(--fs-xs);
  font-weight: 600;
  line-height: var(--lh-tight);
  white-space: nowrap;
  color: var(--muted);
  background: var(--surface-2);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius-pill);
}
.tag-lg { padding: var(--sp-1) var(--sp-3); font-size: var(--fs-sm); }
.tag-ok { color: var(--ok); background: var(--ok-bg); border-color: var(--ok-line); }
.tag-warn { color: var(--warn); background: var(--warn-bg); border-color: var(--warn-line); }
.tag-bad { color: var(--danger); background: var(--danger-bg); border-color: var(--danger-line); }
.tag-info { color: var(--info); background: var(--info-bg); border-color: var(--info-line); }
/* A draft is not yet a record: no fill and a dashed outline, so it reads as unfinished even without colour. */
.tag-draft { background: transparent; border-style: dashed; border-color: var(--field-border); }
/* Two other looks of the same tag. -solid is filled, for the one state that must outweigh the others in a column (an issued document): alone it is the ink colour, with -ok -warn
   -bad -info it takes that colour. -bare has no fill and no outline, for a column where a tag on every row would crowd the table (a result beside a status): the icon and the
   word still carry it. It is the same tag at the same size, so a status and a result side by side match; in a table every tag is one step up (--fs-sm) to sit with the 15px text. */
.tag-solid { color: var(--surface); background: var(--text); border-color: var(--text); }
.tag-solid.tag-ok { background: var(--ok); border-color: var(--ok); }
.tag-solid.tag-warn { background: var(--warn); border-color: var(--warn); }
.tag-solid.tag-bad { color: var(--on-danger); background: var(--danger); border-color: var(--danger); }
.tag-solid.tag-info { background: var(--info); border-color: var(--info); }
.tag-bare { padding-inline: 0; background: none; border-color: transparent; }
.tbl .tag:not(.delta) { font-size: var(--fs-sm); }
.tag .icon { width: 1.1em; height: 1.1em; }

/* A short code in a box, to tell kinds apart down a column at a glance (a material: TH steel, CSL sand). The code is the tenant's and stays short, two to four characters; --font-mono
   sets its face. It goes first in a .cell-lead, before the name. Where the full name is in the row too, the chip is aria-hidden. */
.code-chip {
  display: inline-flex;
  flex: none;
  align-items: center;
  justify-content: center;
  min-width: 2.75rem;
  padding: var(--sp-1) var(--sp-2);
  font-family: var(--font-mono, ui-monospace, "SF Mono", Menlo, Consolas, monospace);
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.02em;
  line-height: var(--lh-tight);
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--field-border);
  border-radius: var(--radius-sm);
}
/* A leading mark (a .code-chip) beside one or two lines of text, in a table cell or a list row. */
.cell-lead { display: flex; align-items: center; gap: var(--sp-3); min-width: 0; }

/* Change against the last period: arrow and percentage in a tag, .tag-ok for up and .tag-bad for down. The arrow and the hidden word carry the meaning as well as the colour. */
.delta { min-width: 4.75rem; justify-content: center; font-variant-numeric: tabular-nums; }
/* A figure with its badge beside it, in a td.num: the fixed badge width keeps the figures in line. */
.cell-metric { display: inline-flex; align-items: center; gap: var(--sp-2); }
/* A small bar after a figure: --v is the share filled, 0% to 100%, worked out by the tenant (the figure over its column's top or over a limit).
   It repeats the figure, so keep the figure in the cell and mark the bar aria-hidden="true". --bar-h sets the thickness. */
.bar {
  --bar-fill: var(--primary);
  flex: none;
  width: var(--bar-w, 2.5rem);
  height: var(--bar-h, 0.375rem);
  overflow: hidden;
  background: var(--sunk);
  border-radius: var(--radius-pill);
}
.bar::before {
  content: "";
  display: block;
  width: var(--v, 0%);
  height: 100%;
  background: var(--bar-fill);
  border-radius: inherit;
}
.bar-ok { --bar-fill: var(--ok); }
.bar-warn { --bar-fill: var(--warn); }
.bar-bad { --bar-fill: var(--danger); }

/* A filter the user switches on and off. aria-pressed carries the state. */
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: calc(var(--tap) - 8px);
  padding: 0 var(--sp-3);
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--field-border);
  border-radius: var(--radius-pill);
  cursor: pointer;
  transition: background-color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease);
}
.chip:hover { background: var(--surface-hover); border-color: var(--field-border-hover); }
.chip[aria-pressed="true"],
.chip[aria-checked="true"] { color: var(--on-primary); background: var(--primary); border-color: var(--primary); }
.chip-group { display: flex; flex-wrap: wrap; gap: var(--sp-2); }

/* Form ------------------------------------------------------------------- */

/* align-content: start keeps label, control and hint at their natural height when a form-grid row is taller than this field. */
.field { display: grid; align-content: start; gap: var(--sp-2); min-width: 0; }
.label { font-size: var(--fs-sm); font-weight: 600; color: var(--text); }
/* The star of a required field, red. In a label put aria-hidden="true" on it and `required` on the control, so a screen reader says "required" once. */
.req { color: var(--danger); }
.field-hint { font-size: var(--fs-sm); color: var(--muted); }
.field-error { font-size: var(--fs-sm); color: var(--danger); }

.input,
.select,
.textarea {
  width: 100%;
  min-height: var(--tap);
  padding: var(--sp-2) var(--sp-3);
  font: inherit;
  font-size: var(--fs-base);
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--field-border);
  border-radius: var(--radius);
  transition: border-color var(--dur-1) var(--ease);
}
.textarea { min-height: calc(var(--tap) * 2); resize: vertical; }
.input::placeholder,
.textarea::placeholder { color: var(--muted); opacity: 1; }
.input:hover,
.select:hover,
.textarea:hover { border-color: var(--field-border-hover); }
.input:focus-visible,
.select:focus-visible,
.textarea:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 1px;
  border-color: var(--field-border-hover);
}
.input[aria-invalid="true"],
.select[aria-invalid="true"],
.textarea[aria-invalid="true"] {
  border-color: var(--danger);
  box-shadow: inset 0 0 0 1px var(--danger);
}
.input:disabled,
.select:disabled,
.textarea:disabled,
.input[readonly],
.textarea[readonly] { color: var(--muted); background: var(--sunk); }

.select {
  padding-right: var(--sp-8);
  appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--muted) 50%), linear-gradient(135deg, var(--muted) 50%, transparent 50%);
  background-position: calc(100% - 20px) 50%, calc(100% - 14px) 50%;
  background-size: 6px 6px;
  background-repeat: no-repeat;
}

.form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--sp-4);
}
.form-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--sp-3);
}
/* A form whose fields need different widths: .form-cols is a grid of 12 columns and a field takes .c-3 .c-4 .c-6 or .c-8 of them (no class: the whole row). When the form itself is
   under 36rem wide, in a narrow modal as well as on a phone, every field takes the whole row. */
.form-cols { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); gap: var(--sp-4); container-type: inline-size; }
.form-cols > * { grid-column: span 12; }
.form-cols > .c-3 { grid-column: span 3; }
.form-cols > .c-4 { grid-column: span 4; }
.form-cols > .c-6 { grid-column: span 6; }
.form-cols > .c-8 { grid-column: span 8; }
@container (max-width: 36rem) {
  .form-cols > :is(.c-3, .c-4, .c-6, .c-8) { grid-column: span 12; }
}

/* Checkbox and radio use the native control, tinted with the brand colour. */
.check {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
  min-height: var(--tap);
  padding: var(--sp-2) 0;
  cursor: pointer;
}
.check input {
  flex: none;
  width: 1.25rem;
  height: 1.25rem;
  margin: 0.1rem 0 0;
  accent-color: var(--primary);
}

.switch {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3);
  min-height: var(--tap);
  cursor: pointer;
}
.switch input {
  position: relative;
  flex: none;
  width: 2.5rem;
  height: 1.5rem;
  margin: 0;
  appearance: none;
  background: var(--field-border);
  border-radius: var(--radius-pill);
  cursor: pointer;
  transition: background-color var(--dur-1) var(--ease);
}
.switch input::after {
  content: "";
  position: absolute;
  top: 0.125rem;
  left: 0.125rem;
  width: 1.25rem;
  height: 1.25rem;
  background: var(--surface);
  border-radius: 50%;
  transition: transform var(--dur-1) var(--ease);
}
.switch input:checked { background: var(--primary); }
.switch input:checked::after { transform: translateX(1rem); }
.switch input:disabled { opacity: 0.5; cursor: not-allowed; }

/* A box the user ticks to confirm what cannot be taken back, such as the figures being right before a sample goes for approval: label.confirm > input[type="checkbox"] + a span
   with .confirm-title and the line of detail. Amber until it is ticked, then green. */
.confirm {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  color: var(--text);
  background: var(--warn-bg);
  border: 1.5px solid var(--warn-line);
  border-radius: var(--radius);
  cursor: pointer;
  transition: background-color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease);
}
.confirm input { flex: none; width: 1.375rem; height: 1.375rem; margin: 0; accent-color: var(--primary); }
.confirm-title { display: block; font-weight: 600; }
.confirm:has(input:checked) { background: var(--ok-bg); border-color: var(--ok-line); }
.confirm:has(input:focus-visible) { outline: 3px solid var(--focus); outline-offset: 1px; }

/* A choice of one (or of several) made from cards, for options that need a name and a line of detail, such as the material of a sample: .choice-cards > label.choice-card >
   input[type="radio"] + an optional .code-chip + .choice-title + .choice-sub. The input stays in the card at its top right and is what the keyboard and a screen reader use;
   the card takes the chosen look from :checked. */
.choice-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 10rem), 1fr)); gap: var(--sp-3); }
.choice-card {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--sp-2);
  min-height: 6rem;
  padding: var(--sp-3) var(--sp-4);
  background: var(--surface);
  border: 1px solid var(--field-border);
  border-radius: var(--radius);
  cursor: pointer;
  transition: background-color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease);
}
.choice-card:hover { background: var(--surface-hover); border-color: var(--field-border-hover); }
.choice-card input {
  position: absolute;
  inset-block-start: var(--sp-3);
  inset-inline-end: var(--sp-3);
  width: 1.25rem;
  height: 1.25rem;
  margin: 0;
  accent-color: var(--primary);
  cursor: pointer;
}
.choice-card:has(input:checked) { background: var(--notice-bg); border-color: var(--primary); box-shadow: inset 0 0 0 1px var(--primary); }
.choice-card:has(input:focus-visible) { outline: 3px solid var(--focus); outline-offset: 1px; }
.choice-card:has(input:disabled) { opacity: 0.5; cursor: not-allowed; }
.choice-title { padding-inline-end: calc(1.25rem + var(--sp-2)); font-weight: 600; line-height: var(--lh-tight); }
.choice-sub { font-size: var(--fs-sm); line-height: var(--lh-tight); color: var(--muted); }

/* Filter bar ------------------------------------------------------------- */

/* Filters above a list. .list-filters stacks the three parts with a gap: a .panel holding the .filter-bar, the .filter-active row, and the .result-bar. */
.list-filters { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--sp-3); }

/* The fields in one wrapping row, each at its own width: --field-w is a field's preferred width (13rem when unset), .filter-grow lets one take the rest
   of the row (the search). A field never gets narrower than 10rem unless the row is. Put it in .panel-body. */
.filter-bar { display: flex; flex-wrap: wrap; gap: var(--sp-3) var(--sp-4); }
.filter-bar > .field { flex: 0 1 var(--field-w, 13rem); min-width: min(100%, 10rem); }
.filter-bar > .filter-grow { flex: 1 1 18rem; }
@media (max-width: 40rem) { .filter-bar > .field { flex-basis: 100%; } }

/* A search field with its icon inside; the label stays above, as on every field. */
.input-icon { position: relative; }
.input-icon > .icon { position: absolute; top: 50%; left: var(--sp-3); color: var(--muted); transform: translateY(-50%); pointer-events: none; }
.input-icon .input { padding-inline-start: calc(var(--sp-3) + var(--icon) + var(--sp-2)); }

/* A button inside the field at its end, for show or hide of a password: .input-action > .input + .btn.btn-ghost.btn-sm. The input leaves --action-w (5rem) for it. */
.input-action { position: relative; }
.input-action .input { padding-inline-end: var(--action-w, 5rem); }
.input-action > .btn { position: absolute; inset-block: var(--sp-1); inset-inline-end: var(--sp-1); }

/* Fixed text inside the field's frame, after what the user types (or before it, with .affix first), such as the "/LAS-XD 1222" that follows a certificate number:
   .input-affix > .input + .affix. The input keeps the frame's rounding on the side away from the affix. */
.input-affix { display: flex; align-items: stretch; }
.input-affix > .input { flex: 1 1 auto; min-width: 0; }
.input-affix > .input:not(:last-child) { border-start-end-radius: 0; border-end-end-radius: 0; }
.input-affix > .input:not(:first-child) { border-start-start-radius: 0; border-end-start-radius: 0; }
.affix {
  display: inline-flex;
  flex: none;
  align-items: center;
  padding-inline: var(--sp-3);
  color: var(--muted);
  white-space: nowrap;
  background: var(--surface-2);
  border: 1px solid var(--field-border);
}
.affix:last-child { border-inline-start: 0; border-start-end-radius: var(--radius); border-end-end-radius: var(--radius); }
.affix:first-child { border-inline-end: 0; border-start-start-radius: var(--radius); border-end-start-radius: var(--radius); }

/* File upload: a label that is the whole drop target, holding the real file input (.visually-hidden, so the keyboard and the browser's own picker keep working), an icon, a title with the
   .dropzone-link inside it, and a .dropzone-hint saying what is accepted. The tenant sets data-over on it while a file is dragged over (and removes it on leave and drop) and
   aria-invalid="true" on the input when a file is refused, with the reason in a .field-error beside. What was chosen is listed under it as .file-list. */
.dropzone {
  position: relative;
  display: grid;
  justify-items: center;
  gap: var(--sp-1);
  padding: var(--sp-6) var(--sp-5);
  text-align: center;
  cursor: pointer;
  background: var(--surface);
  border: 1px dashed var(--field-border);
  border-radius: var(--radius-lg);
  transition: background-color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease);
}
.dropzone:hover { background: var(--surface-hover); border-color: var(--field-border-hover); }
.dropzone[data-over] { background: var(--notice-bg); border-color: var(--primary); border-style: solid; }
.dropzone:has(input:focus-visible) { outline: 3px solid var(--focus); outline-offset: 2px; }
.dropzone:has(input[aria-invalid="true"]) { border-color: var(--danger); }
.dropzone:has(input:disabled) { color: var(--muted); cursor: not-allowed; background: var(--sunk); border-color: var(--field-border); }
.dropzone-icon { width: 2rem; height: 2rem; color: var(--muted); }
.dropzone-title { font-weight: 600; }
.dropzone-link { color: var(--link); text-decoration: underline; text-underline-offset: 2px; }
.dropzone-hint { font-size: var(--fs-sm); color: var(--muted); }
/* The files chosen: an icon, the name (cut with an ellipsis) over a quiet line (size, then the state in words), and the remove button; a .bar under the text while it uploads. */
.file-list { display: grid; gap: var(--sp-2); margin: 0; padding: 0; list-style: none; }
.file-item {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--sp-2) var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  background: var(--surface);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
}
.file-item > .bar { grid-column: 2 / 3; }
.file-icon { display: inline-flex; color: var(--muted); }
.file-icon .icon { width: 1.5rem; height: 1.5rem; }
.file-text { display: grid; min-width: 0; }
.file-name { overflow: hidden; font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }
.file-sub { font-size: var(--fs-sm); color: var(--muted); }
.file-item[data-state="done"] .file-icon { color: var(--ok); }
.file-item[data-state="error"] { border-color: var(--danger-line); }
.file-item[data-state="error"] .file-icon,
.file-item[data-state="error"] .file-sub { color: var(--danger); }

/* From and to on one line: two date (or number) inputs around a dash. The group takes a label through aria-labelledby; each input has its own aria-label. */
.range { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; gap: var(--sp-2); }
.range-sep { color: var(--muted); }

/* What is filtered now: "Đang lọc:", a .chip-value for each filter with its remove button, and the clear-all link. Hide the row (hidden) when nothing is on. */
.filter-active { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2) var(--sp-3); font-size: var(--fs-sm); }
.filter-active[hidden] { display: none; }
.filter-active-label { color: var(--muted); }
.chip-value { gap: var(--sp-1); padding-inline-end: var(--sp-1); cursor: default; }
.chip-value:hover { background: var(--surface); border-color: var(--field-border); }
.chip-x {
  display: inline-grid;
  place-items: center;
  width: 1.5rem;
  height: 1.5rem;
  padding: 0;
  color: var(--muted);
  background: none;
  border: 0;
  border-radius: var(--radius-pill);
  cursor: pointer;
}
.chip-x:hover { color: var(--text); background: var(--surface-hover); }
/* A value of another kind than the chips around it (a round bar among the sizes of a steel sample, say): the same chip with a dashed edge. */
.chip-dashed { border-style: dashed; }

/* The line between the filters and the list: the count and what makes it up on the left, a choice of one on the right. */
.result-bar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--sp-2) var(--sp-4); color: var(--muted); }
.result-bar strong { color: var(--text); }

/* A choice of one that changes how the list is shown (sort, view): a radiogroup of buttons. Not .tabs-pill, which is for tabs that swap panels. */
.seg { display: inline-flex; width: fit-content; max-width: 100%; padding: var(--sp-1); background: var(--sunk); border-radius: var(--radius-pill); }
.seg-item {
  min-height: calc(var(--tap) - 12px);
  padding: 0 var(--sp-3);
  font: inherit;
  font-size: var(--fs-sm);
  font-weight: 600;
  white-space: nowrap;
  color: var(--muted);
  background: none;
  border: 0;
  border-radius: var(--radius-pill);
  cursor: pointer;
}
.seg-item:hover { color: var(--text); }
.seg-item[aria-checked="true"] { color: var(--text); background: var(--surface); box-shadow: var(--sh-1); }

/* Notice and toast ------------------------------------------------------- */

/* A message that stays in the page. Each kind has its own icon, so colour is not the only cue, and a bar at the left edge in the kind's colour.
   .notice > svg.icon.notice-icon + .notice-body (.notice-title, text, .notice-actions) + button.notice-close. -compact is one line:
   the title runs into the text and .notice-actions is a direct child, at the right edge. */
.notice {
  --notice-accent: var(--link);
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  color: var(--text);
  background: var(--notice-bg);
  border: 1px solid var(--notice-border);
  border-radius: var(--radius);
  box-shadow: inset var(--sp-1) 0 0 var(--notice-accent);
}
.notice-icon { margin-top: 1px; color: var(--notice-accent); }
.notice-body { flex: 1; min-width: 0; }
.notice-title { font-weight: 600; }
.notice-actions { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-2); }
.notice-close { flex: none; margin: calc(-1 * var(--sp-2)) calc(-1 * var(--sp-2)) calc(-1 * var(--sp-2)) 0; }
.notice-ok { --notice-accent: var(--ok); background: var(--ok-bg); border-color: var(--ok-line); }
.notice-ok .notice-title { color: var(--ok); }
.notice-warn { --notice-accent: var(--warn); background: var(--warn-bg); border-color: var(--warn-line); }
.notice-warn .notice-title { color: var(--warn); }
.notice-danger { --notice-accent: var(--danger); background: var(--danger-bg); border-color: var(--danger-line); }
.notice-danger .notice-title { color: var(--danger); }
.notice-info { --notice-accent: var(--info); background: var(--info-bg); border-color: var(--info-line); }
.notice-info .notice-title { color: var(--info); }
.notice-compact { align-items: center; padding: var(--sp-2) var(--sp-3) var(--sp-2) var(--sp-4); }
.notice-compact .notice-icon { margin-top: 0; }
.notice-compact .notice-title { display: inline; margin-inline-end: var(--sp-1); }
.notice-compact > .notice-actions { flex: none; margin-top: 0; }

/* A short message that appears over the page and leaves by itself, in the top right corner under the top bar (on a phone across the top), so
   the pager and the buttons at the bottom stay free. A card with a tinted icon badge: .toast-ok .toast-warn .toast-bad (default: info).
   .toast > .toast-icon (svg.icon) + .toast-body (.toast-title, .toast-text) + .toast-action (a .btn-link.btn-sm, "Hoàn tác") + button.toast-close.
   The tenant puts the newest first, keeps three at most (never dropping an error to make room), and holds a toast while it is hovered or focused:
   ok and info leave after about 5 seconds, warn and bad stay until closed; set data-leaving to fade it out, then remove it. */
.toast-host {
  position: fixed;
  top: calc(var(--topbar-h) + var(--sp-3));
  right: var(--sp-5);
  z-index: var(--z-toast);
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  width: min(100vw - 2 * var(--sp-4), 24rem);
  pointer-events: none;
}
.toast {
  --toast-accent: var(--info);
  --toast-tint: var(--info-bg);
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-3) var(--sp-3) var(--sp-4);
  font-size: var(--fs-md);
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--sh-3);
  pointer-events: auto;
  animation: toast-in var(--dur-2) var(--ease-enter, var(--ease));
  transition: opacity var(--dur-1) var(--ease), transform var(--dur-1) var(--ease);
}
.toast[data-leaving] { opacity: 0; transform: translateX(var(--sp-6)); }
.toast-ok { --toast-accent: var(--ok); --toast-tint: var(--ok-bg); }
.toast-warn { --toast-accent: var(--warn); --toast-tint: var(--warn-bg); }
.toast-bad { --toast-accent: var(--danger); --toast-tint: var(--danger-bg); }
.toast-icon {
  display: inline-flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  color: var(--toast-accent);
  background: var(--toast-tint);
  border-radius: var(--radius-pill);
}
.toast-body { flex: 1; min-width: 0; padding-block: var(--sp-1); }
.toast-title { font-weight: 600; }
.toast-text { margin-top: var(--sp-1); font-size: var(--fs-sm); color: var(--muted); }
.toast-action { flex: none; align-self: center; }
.toast-close { flex: none; margin: calc(-1 * var(--sp-1)) calc(-1 * var(--sp-1)) 0 0; }
@keyframes toast-in { from { opacity: 0; transform: translateX(var(--sp-6)); } }
@media (max-width: 40rem) {
  .toast-host { right: var(--sp-4); }
}
/* .toast-host-bottom on the host puts the toasts at the bottom edge, for a phone where the top is the page title. The newest toast (first in the list) sits
   lowest, nearest the thumb, and the older ones stack above it. Above a .tabbar or .action-bar on the page the host lifts by itself; --toast-bottom sets any other offset. */
.toast-host-bottom {
  top: auto;
  bottom: var(--toast-bottom, calc(var(--sp-4) + env(safe-area-inset-bottom, 0px)));
  flex-direction: column-reverse;
}
.toast-host-bottom .toast { animation-name: toast-in-up; }
.toast-host-bottom .toast[data-leaving] { transform: translateY(var(--sp-4)); }
@keyframes toast-in-up { from { opacity: 0; transform: translateY(var(--sp-4)); } }
body:has(.tabbar) .toast-host-bottom { --toast-bottom: calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px) + var(--sp-3)); }
body:has(.action-bar) .toast-host-bottom { --toast-bottom: calc(var(--action-bar-h) + env(safe-area-inset-bottom, 0px) + var(--sp-3)); }

/* Supporting text -------------------------------------------------------- */

/* A screen shows one short line and keeps the longer explanation out of sight until the user asks for it. Two patterns:
   .subtext is a <details>: its <summary> is the one-line sub-heading (cut with "…" when too long) and .subtext-more opens on a click.
   .tip is an "i" button (.tip-btn) beside a label or heading; its text opens on hover, focus or a click (a tap on a phone) and closes with Esc.
   Whatever the user must read to finish the task (the format of a field, an error) is not supporting text: keep it visible as .field-hint or .field-error. */
.subtext { font-size: var(--fs-sm); color: var(--muted); }
/* The summary is a grid whose text column may shrink to nothing, so the nowrap line never widens the grid or panel around it. */
.subtext > summary {
  display: grid;
  grid-template-columns: minmax(0, auto) auto;
  align-items: center;
  column-gap: var(--sp-1);
  width: fit-content;
  max-width: 100%;
  list-style: none;
  cursor: pointer;
  border-radius: var(--radius-sm);
}
.subtext > summary::-webkit-details-marker { display: none; }
.subtext > summary:hover { color: var(--text); }
.subtext-line { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.subtext-caret { transition: transform var(--dur-1) var(--ease); }
.subtext[open] .subtext-caret { transform: rotate(180deg); }
.subtext-more { max-width: 48rem; margin-top: var(--sp-2); line-height: var(--lh); }

/* A label or heading with its tip button after it: <div class="tip-row"><label class="label" for="x">…</label><button class="tip-btn" …>. The label stays outside
   the button, so the field keeps its name. The button points to its text with aria-controls and aria-describedby, so a screen reader reads it as the description. */
.tip-row { display: flex; align-items: center; gap: var(--sp-1); }
.tip-btn {
  display: inline-flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  padding: 0;
  color: var(--muted);
  cursor: help;
  background: none;
  border: 0;
  border-radius: var(--radius-pill);
}
.tip-btn:hover,
.tip-btn[aria-expanded="true"] { color: var(--text); background: var(--surface-hover); }
.tip {
  position: fixed;
  inset: auto;
  width: max-content;
  max-width: min(20rem, calc(100vw - 2 * var(--sp-4)));
  margin: 0;
  padding: var(--sp-2) var(--sp-3);
  font-size: var(--fs-sm);
  font-weight: 400;
  line-height: var(--lh);
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  box-shadow: var(--sh-2);
}

/* Tooltip for a button that has only an icon: its name (the aria-label) shown on hover after a short wait and on keyboard focus. The tenant keeps one .tooltip[popover="manual"]
   with aria-hidden="true" (the button already carries the name, so a screen reader must not hear it twice), shows it above the button or below when there is no room, and closes
   it on leave, blur, scroll and Esc. The pointer may move onto it. It is a hint, not a place for anything that must be read: nothing on a touch screen, where the icon needs
   its own label when its meaning is not plain. */
.tooltip {
  position: fixed;
  inset: auto;
  width: max-content;
  max-width: min(16rem, calc(100vw - 2 * var(--sp-4)));
  margin: 0;
  padding: var(--sp-1) var(--sp-2);
  overflow: visible;
  font-size: var(--fs-sm);
  font-weight: 400;
  line-height: var(--lh-tight);
  color: var(--surface);
  background: var(--text);
  border: 0;
  border-radius: var(--radius-sm);
  box-shadow: var(--sh-2);
}

/* Empty state ------------------------------------------------------------ */

.empty {
  display: grid;
  align-content: center;
  justify-items: center;
  gap: var(--sp-2);
  padding: var(--sp-9) var(--sp-5);
  text-align: center;
  color: var(--muted);
  background: var(--surface);
  border: 1px dashed var(--field-border);
  border-radius: var(--radius-lg);
}
/* A sprite icon (<svg class="empty-ic"><use href="icons.svg#tray"/></svg>, with or without .icon) draws in the muted colour at 3rem. A drawing that sets its own fill
   attribute, such as the stroke icon fill="none", keeps it. */
.empty-ic,
.icon.empty-ic { width: 3rem; height: 3rem; color: var(--muted); }
.empty-ic:not([fill]) { fill: currentColor; }
.empty-title { font-size: var(--fs-lg); font-weight: 600; color: var(--text); }
.empty .btn { margin-top: var(--sp-3); }

/* Top bar and navigation ------------------------------------------------- */

/* A link to a part of the page (nav.toc, a skip link) must not leave the heading under the sticky bar. */
:where(main, [role="main"]) [id] { scroll-margin-top: calc(var(--topbar-h) + var(--sp-4)); }
.topbar {
  position: sticky;
  top: 0;
  z-index: var(--z-topbar);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3) var(--sp-5);
  min-height: var(--topbar-h);
  padding: var(--sp-2) var(--sp-5);
  color: var(--header-fg);
  background: var(--header-bg);
  border-bottom: var(--header-border);
}
.topbar :focus-visible { outline-color: var(--header-focus); }
.topbar-actions { display: flex; align-items: center; gap: var(--sp-3); }
.topbar .btn {
  color: var(--header-fg);
  background: transparent;
  border-color: var(--header-button-border);
}
.topbar .btn:hover { background: var(--header-button-hover); }
/* The outline stays the bar's own, so the button keeps its shape where the primary colour is the bar's colour (green). */
.topbar .btn-primary { color: var(--on-primary); background: var(--primary); }
.topbar .btn-primary:hover { background: var(--primary-hover); }
/* The signed-in person: a .btn that holds .avatar-label (an avatar, the name and the role in spans). The words take the bar's colours. A count on a nav item is a .side-badge in it. */
.topbar .avatar-label { text-align: left; }
.topbar .avatar-label-name, .topbar .avatar-label-sub { display: block; }
.topbar .avatar-label-name { color: var(--header-fg); }
.topbar .avatar-label-sub { color: var(--header-muted); }
.nav-item .side-badge { margin-left: var(--sp-2); }
/* The dense bar: the brand's second line goes and the account keeps only its avatar (the words stay for screen readers), so six or so nav items fit in one row. It starts
   at 89rem (1424px), measured for six items with the two-line brand and the account words, which need about 88rem; .topbar-dense on the bar asks for it at any width, for a
   role with more items. Where even the dense bar is too wide the bar wraps to a second row, as before. */
.topbar-dense .brand-sub { display: none; }
.topbar-dense .avatar-label > :not(.avatar) { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
@media (max-width: 89rem) {
  .topbar .brand-sub { display: none; }
  .topbar .avatar-label > :not(.avatar) { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3);
  font-size: var(--fs-lg);
  font-weight: 700;
  color: inherit;
  text-decoration: none;
}
.brand-mark {
  display: inline-grid;
  place-items: center;
  flex: none;
  width: 2.25rem;
  height: 2.25rem;
  font-size: var(--fs-md);
  font-weight: 700;
  line-height: 1;
  color: var(--mark-fg);
  background: var(--mark-bg);
  border-radius: var(--mark-radius);
}
.brand-mark:empty { display: none; }
.topbar .brand-mark { color: var(--header-mark-fg); background: var(--header-mark-bg); }

/* A logo the customer sent. In .brand, either an <img class="brand-logo"> alone (a file with the name in it, a wide wordmark; give it the customer's name as alt), or an <img alt="">
   inside .brand-mark (a square or symbol-only file) with the name as text beside it. The height is fixed, the width follows the file up to a limit, and the logo is never cropped or
   stretched. --logo-h and --logo-w change the two sizes; -lg is for a login page. -plate puts a plate in the surface colour behind a logo drawn for a light background, for a dark bar.
   No file yet, or it failed to load: the tenant shows the .brand-mark with letters. */
.brand-logo { display: block; flex: none; width: auto; height: var(--logo-h, 2.25rem); max-width: var(--logo-w, 11rem); object-fit: contain; object-position: left center; }
.brand-logo-lg { --logo-h: 3.5rem; --logo-w: 16rem; }
.brand-logo-plate { box-sizing: content-box; padding: var(--sp-1) var(--sp-2); background: var(--surface); border-radius: var(--radius-sm); }
.brand-mark:has(> img), .topbar .brand-mark:has(> img) { overflow: hidden; background: none; }
.brand-mark > img { width: 100%; height: 100%; object-fit: contain; }
/* A brand with a second line (a place, a licence number): .brand > .brand-mark + .brand-text > .brand-name + .brand-sub. The second line is cut with an ellipsis past --brand-sub-w. */
.brand-text { display: grid; min-width: 0; line-height: 1.2; }
.brand-sub { max-width: var(--brand-sub-w, 16rem); overflow: hidden; font-size: var(--fs-xs); font-weight: 400; color: var(--muted); text-overflow: ellipsis; white-space: nowrap; }
.topbar .brand-sub { color: var(--header-muted); }

.nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-1);
  margin: 0;
  padding: 0;
  list-style: none;
}
.nav-item {
  display: inline-flex;
  align-items: center;
  min-height: calc(var(--tap) - 4px);
  padding: 0 var(--sp-3);
  font-weight: 600;
  color: var(--header-muted);
  text-decoration: none;
  background: none;
  border: 0;
  border-radius: var(--radius);
  cursor: pointer;
  transition: background-color var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.nav-item:hover { color: var(--header-fg); background: var(--header-button-hover); }
.nav-item[aria-current="page"] {
  color: var(--header-fg);
  background: var(--header-button-hover);
  box-shadow: inset 0 -2px 0 currentColor;
}

/* Icon ------------------------------------------------------------------- */

/* An inline svg that takes the text colour: <svg class="icon"><use href="icons.svg#house"/></svg> */
.icon {
  flex: none;
  width: var(--icon);
  height: var(--icon);
  fill: currentColor;
}
.icon-sm { width: var(--icon-sm); height: var(--icon-sm); }
.icon-lg { width: var(--icon-lg); height: var(--icon-lg); }
/* svg is a block in this kit, so an icon sits on its own line. -inline puts it in a line of text instead, level with the letters. */
.icon-inline { display: inline-block; vertical-align: -0.2em; }

/* Avatar ----------------------------------------------------------------- */

/* A person or an organisation as initials or a photo. --avatar-size sets the size and the initials follow it. Beside the name it is decoration
   (aria-hidden="true", alt=""); on its own it needs role="img" and aria-label. The tenant works out the initials (first letter of the first and last word). */
.avatar {
  --avatar-size: 2.5rem;
  position: relative;
  display: inline-flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: var(--avatar-size);
  height: var(--avatar-size);
  font-size: max(calc(var(--avatar-size) * 0.4), var(--fs-xs));
  font-weight: 600;
  line-height: 1;
  color: var(--text);
  text-transform: uppercase;
  background: var(--sunk);
  border-radius: var(--radius-pill);
  user-select: none;
}
.avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: inherit; }
.avatar-xs { --avatar-size: 1.5rem; }
.avatar-sm { --avatar-size: 2rem; }
.avatar-lg { --avatar-size: 3rem; }
.avatar-xl { --avatar-size: 4rem; }
.avatar-square { border-radius: min(var(--radius), calc(var(--avatar-size) / 4)); }
.avatar-primary { color: var(--on-primary); background: var(--primary); }
.avatar-ok { color: var(--ok); background: var(--ok-bg); }
.avatar-warn { color: var(--warn); background: var(--warn-bg); }
.avatar-bad { color: var(--danger); background: var(--danger-bg); }
.avatar-info { color: var(--info); background: var(--info-bg); }
/* Presence: a dot at the bottom right, grey by default. Put the state in a .visually-hidden span inside, since the colour alone says nothing. */
.avatar-dot {
  position: absolute;
  right: 0;
  bottom: 0;
  width: max(0.5rem, calc(var(--avatar-size) * 0.28));
  aspect-ratio: 1;
  background: var(--field-border);
  border-radius: var(--radius-pill);
  box-shadow: 0 0 0 2px var(--avatar-ring, var(--surface));
}
.avatar-dot-ok { background: var(--ok); }
.avatar-dot-warn { background: var(--warn); }
.avatar-dot-bad { background: var(--danger); }
/* An avatar with a name and a quieter line under it (an email, a role): in a table cell, a list, or the .menu-head of a menu. */
.avatar-label { display: flex; align-items: center; gap: var(--sp-3); min-width: 0; }
.avatar-label-name,
.avatar-label-sub { display: block; }
.avatar-label-name { font-weight: 600; color: var(--text); }
.avatar-label-sub { font-size: var(--fs-sm); color: var(--muted); }
/* Several people, overlapping; the last one may be a "+3" count. */
.avatar-group { display: inline-flex; align-items: center; }
.avatar-group > .avatar { box-shadow: 0 0 0 2px var(--avatar-ring, var(--surface)); }
.avatar-group > .avatar + .avatar { margin-inline-start: calc(var(--avatar-size) / -5); }

/* Menu ------------------------------------------------------------------- */

/* A list of actions opened from a button (a row's "⋮", a "More" button). It is a popover, so it sits in the top layer above a scrolling
   table instead of being clipped by it; Esc and a click elsewhere close it. The tenant places it next to the button (the gallery shows how). */
.menu {
  position: fixed;
  inset: auto;
  margin: 0;
  min-width: 11rem;
  max-width: calc(100vw - 2 * var(--sp-4));
  padding: var(--sp-1);
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  box-shadow: var(--sh-2);
}
.menu-item {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  width: 100%;
  min-height: calc(var(--tap) - 8px);
  padding: 0 var(--sp-3);
  font: inherit;
  font-size: var(--fs-md);
  color: inherit;
  text-align: left;
  text-decoration: none;
  white-space: nowrap;
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.menu-item:hover,
.menu-item:focus-visible { background: var(--surface-hover); }
.menu-item:focus-visible { outline-offset: -2px; }
.menu-item-danger { color: var(--danger); }
.menu-item-danger:hover,
.menu-item-danger:focus-visible { background: var(--danger-bg); }
.menu-item:disabled { color: var(--muted); background: none; cursor: not-allowed; }
.menu-sep { height: 1px; margin: var(--sp-1) calc(-1 * var(--sp-1)); background: var(--panel-border); border: 0; }
/* A heading for a group of items, a header with the signed-in person, and the tick of a menuitemradio or menuitemcheckbox (shown while aria-checked="true"). */
.menu-label { padding: var(--sp-2) var(--sp-3) var(--sp-1); font-size: var(--fs-xs); font-weight: 600; color: var(--muted); }
.menu-head { padding: var(--sp-2) var(--sp-3) var(--sp-3); }
.menu-check { visibility: hidden; }
.menu-item[aria-checked="true"] .menu-check { visibility: visible; }

/* Dropdown with search ---------------------------------------------------- */

/* A text field that filters a list and picks one item. The list is a popover, so a table, modal or drawer does not clip it; put the list inside the
   same dialog as the field, and the tenant places it under the field (the gallery shows how, with the keys). Typing ignores accents. */
.combo { position: relative; }
.combo .input { padding-inline-end: calc(var(--sp-3) + var(--icon) + var(--sp-2)); }
.combo-caret {
  position: absolute;
  top: 50%;
  right: var(--sp-3);
  color: var(--muted);
  pointer-events: none;
  transform: translateY(-50%);
  transition: transform var(--dur-1) var(--ease);
}
.combo:has([aria-expanded="true"]) .combo-caret { transform: translateY(-50%) rotate(180deg); }
/* Select-only: the same field and list for a short fixed list, with no typing (readonly on the input). It keeps the look of a field, not the greyed look of a read-only one. */
.combo-select .input { color: var(--text); background: var(--surface); cursor: pointer; caret-color: transparent; }
.combo-select .input::selection { background: transparent; }
.combo-list {
  position: fixed;
  inset: auto;
  max-height: 16rem;
  margin: 0;
  padding: var(--sp-1);
  overflow-y: auto;
  list-style: none;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  box-shadow: var(--sh-2);
}
.combo-option {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  min-height: calc(var(--tap) - 8px);
  padding: var(--sp-1) var(--sp-3);
  font-size: var(--fs-md);
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.combo-option[hidden],
.combo-empty[hidden] { display: none; }
.combo-option:hover:not([aria-disabled="true"]),
.combo-option[data-active="true"] { background: var(--surface-hover); }
.combo-option[aria-selected="true"] { font-weight: 600; }
.combo-option[aria-selected="true"]::after {
  content: "";
  flex: none;
  width: 0.3rem;
  height: 0.6rem;
  margin-inline-end: var(--sp-1);
  border: solid currentColor;
  border-width: 0 2px 2px 0;
  transform: rotate(45deg) translate(-1px, -1px);
}
.combo-option[aria-disabled="true"] { color: var(--muted); cursor: not-allowed; }
.combo-sub { margin-inline-start: auto; font-size: var(--fs-sm); font-weight: 400; color: var(--muted); }
.combo-empty { padding: var(--sp-3); font-size: var(--fs-sm); color: var(--muted); text-align: center; }

/* Date picker ------------------------------------------------------------ */

/* A text field that takes dd/mm/yyyy (or eight digits) with a button that opens a calendar, so the format does not depend on the browser's language. The calendar is
   a popover in the same box as the field, which the tenant places under it, so a table, modal or drawer does not clip it. The date is kept as yyyy-mm-dd in the
   input's data-value; data-min and data-max (same format) switch off the days outside them. The gallery has the script and the keys. */
.datepicker { position: relative; }
.datepicker .input { padding-inline-end: calc(var(--sp-2) + 2rem + var(--sp-1)); }
.datepicker-btn {
  position: absolute;
  top: 50%;
  right: var(--sp-2);
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  padding: 0;
  color: var(--muted);
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transform: translateY(-50%);
}
.datepicker-btn:hover:not(:disabled) { color: var(--text); background: var(--surface-hover); }
.datepicker-btn:disabled { cursor: not-allowed; }

.calendar {
  width: 20rem;
  max-width: calc(100vw - 2 * var(--sp-2));
  padding: var(--sp-3);
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--sh-2);
}
.calendar[popover] { position: fixed; inset: auto; margin: 0; }
.calendar-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); margin-bottom: var(--sp-2); }
/* Month and year are the kit's dropdown (.combo-select), made small to fit the head. */
.calendar-head [data-month] { width: 7.5rem; }
.calendar-head [data-year] { width: 5.75rem; }
.calendar-head .combo .input { min-height: 2rem; padding: 0 1.75rem 0 var(--sp-2); font-size: var(--fs-sm); font-weight: 600; }
.calendar-head .combo-caret { right: var(--sp-2); }
/* Six weeks, Monday first, always: the height stays the same from month to month. */
.calendar-grid { width: 100%; border-collapse: collapse; table-layout: fixed; }
.calendar-grid th { padding: var(--sp-1) 0; font-size: var(--fs-xs); font-weight: 600; color: var(--muted); text-align: center; }
.calendar-grid td { padding: 1px; text-align: center; }
.calendar-day {
  width: 100%;
  height: 2.5rem;
  padding: 0;
  font: inherit;
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
  color: var(--text);
  background: none;
  border: 0;
  border-radius: var(--radius);
  cursor: pointer;
}
.calendar-day:hover { background: var(--surface-hover); }
.calendar-day[data-outside] { color: var(--muted); }
.calendar-day[aria-current="date"] { font-weight: 700; box-shadow: inset 0 0 0 1px var(--field-border); }
td[aria-selected="true"] > .calendar-day { color: var(--on-primary); background: var(--primary); }
td[aria-selected="true"] > .calendar-day:hover { background: var(--primary-hover); }
/* A day outside data-min and data-max: struck through as well as muted, so colour is not the only cue. */
.calendar-day[aria-disabled="true"] { color: var(--muted); text-decoration: line-through; cursor: not-allowed; }
.calendar-day[aria-disabled="true"]:hover { background: none; }
.calendar-foot { display: flex; justify-content: space-between; margin-top: var(--sp-2); padding-top: var(--sp-2); border-top: 1px solid var(--panel-border); }

/* Date range ------------------------------------------------------------- */

/* One field for a from date and a to date, each typed as dd/mm/yyyy, with one calendar for both: click the first day, then the last, and the days between are filled.
   The presets at the left of the calendar (Hôm nay, 7 ngày qua, Tháng này ...) set both at once. data-min and data-max (yyyy-mm-dd) on the field switch off the days outside
   them. Both inputs keep their date as yyyy-mm-dd in data-value, like .datepicker, so a page reads them the same way. The gallery has the script and the keys. */
.daterange {
  position: relative;
  display: flex;
  width: fit-content; /* as wide as its two dates and the button, whatever the column it sits in */
  max-width: 100%;
  align-items: center;
  gap: var(--sp-2);
  min-height: var(--tap);
  padding: 0 var(--sp-2) 0 var(--sp-3);
  background: var(--surface);
  border: 1px solid var(--field-border);
  border-radius: var(--radius);
  transition: border-color var(--dur-1) var(--ease);
}
.daterange:hover { border-color: var(--field-border-hover); }
.daterange:has(> :focus-visible) { outline: 3px solid var(--focus); outline-offset: 1px; border-color: var(--field-border-hover); }
.daterange:has(> [aria-invalid="true"]) { border-color: var(--danger); box-shadow: inset 0 0 0 1px var(--danger); }
.daterange-input {
  flex: 0 1 auto;
  width: 9.5ch; /* a dd/mm/yyyy date (eight digits and two slashes, so nine digits at most) and a little air; it gives way when the room is short */
  min-width: 0;
  padding: 0;
  font: inherit;
  font-size: var(--fs-base);
  color: var(--text);
  text-align: center;
  background: none;
  border: 0;
}
.daterange-input::placeholder { color: var(--muted); opacity: 1; }
.daterange-input:focus-visible { outline: 0; }
.daterange > .datepicker-btn { position: static; flex: none; transform: none; }

/* display only while open: an author display on a popover would override the browser's display: none and leave a closed calendar on the page. */
.calendar-range:popover-open { display: flex; }
.calendar-range { gap: var(--sp-3); width: auto; }
.calendar-presets { display: flex; flex: none; flex-direction: column; gap: 2px; width: 9.5rem; padding-right: var(--sp-3); border-right: 1px solid var(--panel-border); }
.calendar-preset {
  display: flex;
  align-items: center;
  width: 100%;
  min-height: 2.25rem;
  padding: 0 var(--sp-3);
  font: inherit;
  font-size: var(--fs-sm);
  color: var(--text);
  text-align: left;
  background: none;
  border: 0;
  border-radius: var(--radius);
  cursor: pointer;
}
.calendar-preset:hover { background: var(--surface-hover); }
.calendar-preset[aria-pressed="true"] { font-weight: 700; background: var(--notice-bg); }
.calendar-preset[aria-disabled="true"] { color: var(--muted); text-decoration: line-through; cursor: not-allowed; }
.calendar-preset[aria-disabled="true"]:hover { background: none; }
.calendar-body { min-width: 0; }
.calendar-months { display: flex; gap: var(--sp-4); }
.calendar-month { width: 17.5rem; }
.calendar-title { flex: 1 1 auto; font-size: var(--fs-sm); font-weight: 600; text-align: center; }
.calendar-hint { align-self: center; font-size: var(--fs-sm); color: var(--muted); }
/* The days of the range are a band across the cells; its two ends are the filled days. A day of the next month shown in the grid is left empty. */
.calendar-range td[data-range] { background: var(--notice-bg); }
.calendar-range td[data-range="start"] { background: linear-gradient(to right, transparent 50%, var(--notice-bg) 50%); }
.calendar-range td[data-range="end"] { background: linear-gradient(to left, transparent 50%, var(--notice-bg) 50%); }
.calendar-range td[data-range="only"] { background: none; }
.calendar-range td[data-range="in"] > .calendar-day { color: var(--text); background: none; border-radius: 0; }
.calendar-range td[data-range="in"] > .calendar-day:hover { background: var(--surface-hover); }
.calendar-range td[data-range="start"] > .calendar-day,
.calendar-range td[data-range="end"] > .calendar-day,
.calendar-range td[data-range="only"] > .calendar-day { color: var(--on-primary); background: var(--primary); }
@media (min-width: 48.01rem) {
  .calendar-month:first-child [data-nav="1"],
  .calendar-month:last-child [data-nav="-1"] { visibility: hidden; }
}
@media (max-width: 48rem) {
  .calendar-range { flex-direction: column; width: min(22rem, calc(100vw - 2 * var(--sp-2))); }
  .calendar-presets { flex-direction: row; flex-wrap: wrap; width: auto; gap: var(--sp-1); padding: 0 0 var(--sp-3); border-right: 0; border-bottom: 1px solid var(--panel-border); }
  .calendar-preset { width: auto; border: 1px solid var(--panel-border); border-radius: var(--radius-pill); }
  .calendar-month + .calendar-month { display: none; }
  .calendar-month { width: 100%; }
}

/* Modal ------------------------------------------------------------------ */

/* A native <dialog>: showModal() gives the scrim, the focus trap and Escape. */
.modal {
  width: min(var(--modal-w), calc(100vw - 2 * var(--sp-4)));
  max-height: calc(100dvh - 2 * var(--sp-4));
  margin: auto;
  padding: 0;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--sh-3);
}
.modal[open] {
  display: flex;
  flex-direction: column;
  animation: modal-in var(--dur-2) var(--ease-enter, var(--ease));
}
.modal::backdrop { background: var(--scrim); }
.modal-sm { width: min(var(--modal-w-sm), calc(100vw - 2 * var(--sp-4))); }
.modal-lg { width: min(var(--modal-w-lg), calc(100vw - 2 * var(--sp-4))); }
html:has(.modal[open], .drawer[open]) { overflow: hidden; }

.modal-head,
.drawer-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-3);
  padding: var(--sp-5) var(--sp-5) var(--sp-3);
}
.modal-sub,
.drawer-sub { margin-top: var(--sp-1); font-size: var(--fs-sm); color: var(--muted); }
/* A form may wrap the head, body and foot. */
.modal > form,
.drawer > form { display: flex; flex: 1 1 auto; flex-direction: column; min-height: 0; }
.modal-body,
.drawer-body {
  flex: 1 1 auto;
  padding: var(--sp-3) var(--sp-5) var(--sp-5);
  overflow-y: auto;
}
.modal-foot,
.drawer-foot {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--sp-2);
  padding: var(--sp-4) var(--sp-5);
  background: var(--surface-2);
  border-top: 1px solid var(--panel-border);
  border-radius: 0 0 var(--radius-lg) var(--radius-lg);
}
.drawer-foot { border-radius: 0; }
@keyframes modal-in {
  from { opacity: 0; transform: translateY(var(--sp-2)) scale(0.98); }
}
/* On a phone the dialog is a sheet that rises from the bottom edge. */
@media (max-width: 40rem) {
  .modal,
  .modal-sm,
  .modal-lg {
    width: 100%;
    max-width: 100%;
    margin: auto 0 0;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  }
  .modal-foot { border-radius: 0; }
}

/* Drawer ----------------------------------------------------------------- */

/* A panel that slides in from the right edge (.drawer-left: the left) over a scrim, for a record's details or a filter form that should not leave the page.
   A native <dialog> like the modal: showModal() gives the scrim, the focus trap and Escape. It is 400 px wide, .drawer-lg 560 px, and on a phone it leaves
   a strip of scrim at the other edge. It shares .modal-sub's look and the head, body and foot of the modal under the names .drawer-head, .drawer-body and .drawer-foot. */
.drawer {
  position: fixed;
  inset: 0 0 0 auto;
  width: min(var(--modal-w-sm), calc(100vw - var(--sp-7)));
  max-width: none;
  height: 100dvh;
  max-height: none;
  margin: 0;
  padding: 0;
  color: var(--text);
  background: var(--surface);
  border: 0;
  border-left: 1px solid var(--panel-border);
  border-radius: 0;
  box-shadow: var(--sh-3);
}
.drawer[open] {
  display: flex;
  flex-direction: column;
  animation: drawer-in var(--dur-2) var(--ease-enter, var(--ease));
}
.drawer::backdrop { background: var(--scrim); }
.drawer-lg { width: min(var(--modal-w), calc(100vw - var(--sp-7))); }
.drawer-left { inset: 0 auto 0 0; border-right: 1px solid var(--panel-border); border-left: 0; }
.drawer-left[open] { animation-name: drawer-in-left; }
@keyframes drawer-in { from { transform: translateX(100%); } }
@keyframes drawer-in-left { from { transform: translateX(-100%); } }

/* Global search ---------------------------------------------------------- */

/* The key that opens something, as a hint on a button or in a footer: <kbd class="kbd" aria-hidden="true">Ctrl K</kbd>. */
.kbd {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.5rem;
  padding: 0 var(--sp-1);
  font-family: inherit;
  font-size: var(--fs-xs);
  font-weight: 600;
  line-height: 1.5;
  color: var(--muted);
  background: var(--surface-2);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius-sm);
}

/* The button that opens the search over every record. It looks like a field so people know to type; a click, or the key named in its hint, opens
   .modal-search. Under 40rem, and with -compact, only the icon stays: the text stays in the page for screen readers. In .topbar it takes the header colours. */
.search-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  width: min(100%, 20rem);
  min-height: var(--tap);
  padding: var(--sp-2) var(--sp-3);
  font: inherit;
  font-size: var(--fs-base);
  color: var(--muted);
  text-align: left;
  background: var(--surface);
  border: 1px solid var(--field-border);
  border-radius: var(--radius);
  cursor: pointer;
  transition: border-color var(--dur-1) var(--ease);
}
.search-btn:hover { border-color: var(--field-border-hover); }
/* On the top bar the search shrinks before the bar wraps: it asks for little (11rem) and grows up to 20rem when there is room. */
.topbar .search-btn:not(.search-btn-compact) { flex: 1 1 11rem; width: auto; min-width: 0; max-width: 20rem; }
.search-btn-text { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.search-btn-compact { width: var(--tap); justify-content: center; padding: 0; }
.search-btn-compact .search-btn-text,
.search-btn-compact .kbd {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}
.topbar .search-btn { color: var(--header-muted); background: transparent; border-color: var(--header-button-border); }
.topbar .search-btn:hover { color: var(--header-fg); background: var(--header-button-hover); }
.topbar .search-btn .kbd { color: inherit; background: transparent; border-color: var(--header-button-border); }

/* The dialog behind the button: <dialog class="modal modal-search"> holding .search-box (the field and a close button), .search-results (a listbox of
   .search-group headings and .search-item options, or .search-empty) and .search-foot (the keys). It sits near the top so the list can grow downward.
   The tenant fetches the results and moves .search-item[data-active="true"] with the arrow keys. */
.modal-search {
  width: min(var(--modal-w), calc(100vw - 2 * var(--sp-4)));
  max-height: calc(100dvh - 12vh - var(--sp-4));
  margin: 12vh auto auto;
}
.search-box {
  display: flex;
  flex: none;
  align-items: center;
  gap: var(--sp-3);
  padding: 0 var(--sp-3) 0 var(--sp-4);
  border-bottom: 1px solid var(--panel-border);
}
.search-box:focus-within { box-shadow: inset 0 calc(-1 * var(--sp-1) / 2) 0 var(--focus); }
.search-box > .icon { flex: none; color: var(--muted); }
.search-input {
  flex: 1 1 auto;
  min-width: 0;
  min-height: calc(var(--tap) + var(--sp-3));
  padding: 0;
  font: inherit;
  font-size: var(--fs-lg);
  color: var(--text);
  background: transparent;
  border: 0;
  outline: 0;
  appearance: none;
}
.search-input::placeholder { color: var(--muted); opacity: 1; }
.search-input::-webkit-search-cancel-button { display: none; }
.search-results { flex: 1 1 auto; min-height: 0; max-height: 24rem; padding: var(--sp-2); overflow-y: auto; }
.search-group { padding: var(--sp-2) var(--sp-3) var(--sp-1); font-size: var(--fs-xs); font-weight: 600; color: var(--muted); }
.search-item {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  min-height: calc(var(--tap) + var(--sp-2));
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.search-item:hover,
.search-item[data-active="true"] { background: var(--surface-hover); }
.search-item-icon {
  display: grid;
  flex: none;
  place-items: center;
  width: 2rem;
  height: 2rem;
  color: var(--muted);
  background: var(--surface-2);
  border-radius: var(--radius-sm);
}
.search-item-main { display: grid; min-width: 0; }
.search-item-title { overflow: hidden; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }
.search-item-sub { font-size: var(--fs-sm); color: var(--muted); }
.search-item-meta { flex: none; margin-inline-start: auto; font-size: var(--fs-sm); color: var(--muted); }
.search-item[hidden],
.search-results [role="group"][hidden],
.search-empty[hidden] { display: none; }
.search-empty { padding: var(--sp-6) var(--sp-3); font-size: var(--fs-sm); color: var(--muted); text-align: center; }
.search-foot {
  display: flex;
  flex: none;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
  font-size: var(--fs-xs);
  color: var(--muted);
  background: var(--surface-2);
  border-top: 1px solid var(--panel-border);
  border-radius: 0 0 var(--radius-lg) var(--radius-lg);
}
.search-foot .kbd { background: var(--surface); }
@media (max-width: 40rem) {
  .search-btn { width: var(--tap); justify-content: center; padding: 0; }
  .search-btn .search-btn-text,
  .search-btn .kbd {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }
  .modal-search { width: 100%; max-width: 100%; max-height: 100dvh; margin: 0 0 auto; border-radius: 0 0 var(--radius-lg) var(--radius-lg); }
  .search-foot { display: none; }
}

/* Progress list ---------------------------------------------------------- */

/* A set of criteria that each have a result, in a card (after .panel-head): ul.progress-list > li.progress-item > .progress-row (a span, or an a or button when the row
   opens that criterion) > .progress-no + .progress-label + a .tag with the result (the status tags of the table). aria-current="true" on the row in view tints it,
   as a selected table row is, and puts a bar at its left. */
.progress-list { margin: 0; padding: 0; overflow: hidden; list-style: none; }
.progress-list:last-child { border-radius: 0 0 var(--radius-lg) var(--radius-lg); }
.progress-item + .progress-item { border-top: 1px solid var(--panel-border); }
.progress-row {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  width: 100%;
  min-height: 3.25rem;
  padding: var(--sp-2) var(--sp-5);
  font: inherit;
  color: var(--text);
  text-align: left;
  text-decoration: none;
  background: none;
  border: 0;
  border-radius: 0;
}
a.progress-row, button.progress-row { cursor: pointer; }
a.progress-row:not([aria-current="true"]):hover, button.progress-row:not([aria-current="true"]):hover { background: var(--surface-hover); }
.progress-row:focus-visible { outline-offset: -3px; }
.progress-no { flex: none; min-width: 2.25rem; font-weight: 600; font-variant-numeric: tabular-nums; color: var(--muted); }
.progress-label { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
.progress-row .tag { flex: none; }
.progress-row[aria-current="true"] { background: var(--notice-bg); box-shadow: inset 3px 0 0 var(--primary); }
.progress-row[aria-current="true"] .progress-no { color: var(--text); }
.progress-row[aria-current="true"] .progress-label { font-weight: 700; }

/* Page contents ---------------------------------------------------------- */

/* The links to the parts of one long page (settings, a guide, a form in sections): nav.toc > ul > li > a, the part in view marked aria-current="location".
   A list after a link is its second level; .toc-label is a group heading (text, not a link) with its own list. The tenant keeps it in view (position: sticky on a wrapper) and moves the mark. */
.toc { font-size: var(--fs-sm); }
.toc ul { margin: 0; padding: 0; list-style: none; }
.toc-label {
  display: block;
  margin-top: var(--sp-3);
  padding: var(--sp-1) var(--sp-3);
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}
.toc > ul > li:first-child > .toc-label { margin-top: 0; }
.toc a {
  display: flex;
  align-items: center;
  min-height: calc(var(--tap) - 16px);
  padding: var(--sp-1) var(--sp-3);
  color: var(--muted);
  text-decoration: none;
  overflow-wrap: anywhere;
  border-left: 2px solid var(--panel-border);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
.toc a:hover { color: var(--text); background: var(--surface-hover); }
.toc a[aria-current="location"] { font-weight: 600; color: var(--text); border-left-color: var(--primary); }
.toc a + ul a { padding-left: var(--sp-6); }
/* The look of Vibe's "On this page" (vibe.monday.com): no rule at the left, and the part in view is a grey pill in bold, so the mark is not colour alone. Group headings lose the capitals.
   -card frames it as a card with a title row (.toc-title: the words, and an icon at the end). .toc-float folds the card behind an icon button that opens it below, for the corner of a page. */
.toc-pill a { border-left-color: transparent; border-radius: var(--radius-sm); }
.toc-pill a[aria-current="location"] { background: var(--sunk); border-left-color: transparent; }
.toc-pill .toc-label { font-size: var(--fs-sm); letter-spacing: 0; text-transform: none; }
.toc-card { padding: var(--sp-3); background: var(--surface); border: 1px solid var(--panel-border); border-radius: var(--radius-lg); box-shadow: var(--sh-2); }
.toc-title { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); margin: 0 0 var(--sp-2); padding: var(--sp-1) var(--sp-3); font-size: var(--fs-sm); font-weight: 600; color: var(--text); }
/* The icon at the end of the title may be a button that folds the list to its title and opens it again: .toc-title > button.btn.btn-ghost.btn-icon.btn-sm[aria-expanded]. The page puts
   .toc-folded on the nav (the gallery has the few lines of script); the list is not removed, so a page that draws it again keeps the fold. */
.toc-title > .btn { margin-block: calc(var(--sp-1) * -1); margin-inline-end: calc(var(--sp-2) * -1); }
.toc-folded > ul { display: none; }
.toc-folded > .toc-title { margin-bottom: 0; }
.toc-float { position: relative; display: inline-block; }
.toc-float > summary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--tap);
  height: var(--tap);
  color: var(--text);
  list-style: none;
  background: var(--surface);
  border: 1px solid var(--field-border);
  border-radius: var(--radius);
  cursor: pointer;
}
.toc-float > summary::-webkit-details-marker { display: none; }
.toc-float > summary:hover { background: var(--surface-hover); }
.toc-float > .toc { position: absolute; inset-inline-end: 0; top: calc(100% + var(--sp-2)); z-index: var(--z-dropdown); width: min(16rem, 80vw); }

/* Page frame ------------------------------------------------------------- */

/* The path to a page: <nav class="crumbs" aria-label="..."><ol><li><a>...</a></li><li aria-current="page">this page</li></ol></nav>, placed above .page-head. */
.crumbs ol { display: flex; flex-wrap: wrap; align-items: center; gap: 0 var(--sp-2); margin: 0 0 var(--sp-2); padding: 0; list-style: none; font-size: var(--fs-sm); color: var(--muted); }
.crumbs li + li::before { content: "/"; content: "/" / ""; margin-right: var(--sp-2); }
.crumbs a { color: inherit; }
.crumbs a:hover { color: var(--text); }
.crumbs [aria-current="page"] { color: var(--text); overflow-wrap: anywhere; }

/* The top of a page: the title block (h1, .page-lead under it, .page-meta for tags and facts in one line) and .page-actions at its right. The actions drop under the title
   when there is no room. */
.page-head { display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: space-between; gap: var(--sp-3) var(--sp-5); }
.page-head > :first-child { flex: 1 1 24rem; min-width: 0; }
.page-lead { margin-top: var(--sp-1); max-width: 65ch; color: var(--muted); }
/* Supporting text in the title block instead of a lead: one short line, the rest behind it (see Supporting text). */
.page-head .subtext { margin-top: var(--sp-1); }
.page-meta { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2) var(--sp-3); margin-top: var(--sp-2); font-size: var(--fs-sm); color: var(--muted); }
.page-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); }

/* Blocks in a column with a gap (--stack-gap changes it). A page is usually .stack > .crumbs + .page-head + .page-cols. */
.stack { display: grid; grid-template-columns: minmax(0, 1fr); align-content: start; gap: var(--stack-gap, var(--sp-4)); }

/* A page of two columns: .page-main and, beside it, .page-side (give it .stack for several panels). They sit side by side while the main keeps --main-min (36rem) and
   the side its width --side-w (20rem), measured in the room the page has, so it works with or without a sidebar; otherwise the side follows the main at full width.
   On a window tall enough the side stays in view while the page scrolls, below --sticky-top (the top bar's height and a gap). */
.page-cols { display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--sp-4) var(--sp-5); }
.page-main { flex: 999 1 var(--main-min, 36rem); min-width: 0; }
.page-side { flex: 1 1 var(--side-w, 20rem); min-width: 0; }
/* -even: columns of equal width (queues side by side) instead of a main and a side. Each child, a .stack for several panels, takes --col-min (28rem) and shares what is left;
   nothing sticks. Three columns work the same way. */
.page-cols-even > * { flex: 1 1 var(--col-min, 28rem); min-width: 0; }
@media (min-height: 40rem) {
  .page-side { position: sticky; top: var(--sticky-top, calc(var(--topbar-h) + var(--sp-4))); }
}

/* Description list ------------------------------------------------------- */

/* Labels and values: <dl class="dl"><div><dt>label</dt><dd>value</dd></div>...</dl>. A row puts the two side by side when there is room and one under the other when not.
   -lines draws a rule between rows; -tight is the small cut for a narrow .page-side. --dl-key and --dl-val set the label width and the least width of a value. */
.dl { margin: 0; }
.dl > div { display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-4); padding: var(--sp-2) 0; }
.dl > div:first-child { padding-top: 0; }
.dl > div:last-child { padding-bottom: 0; }
.dl dt { flex: 0 1 var(--dl-key, 11rem); color: var(--muted); }
.dl dd { flex: 1 1 var(--dl-val, 12rem); min-width: 0; margin: 0; font-weight: 500; overflow-wrap: anywhere; }
.dl-lines > div + div { border-top: 1px solid var(--panel-border); }
.dl-tight { --dl-key: 6.5rem; --dl-val: 8rem; font-size: var(--fs-sm); }
.dl-tight > div { padding: var(--sp-1) 0; }

/* Work list -------------------------------------------------------------- */

/* A short list of things to do as rows, not a table: ul.work-list > li > a.work-item (a div when the row is not a link) > a leading mark (a .code-chip, an icon), .work-main and .work-end.
   .work-main holds .work-title (.work-code in bold, then .work-name), .work-sub (one muted line, cut with an ellipsis) and .work-reason (an icon and a sentence in the warning colour:
   why the row needs a look). .work-end holds a status (a .tag, a figure) and a .work-meta line (a date, a time). Put it in a .panel-flush, so the rows run to the panel's edges with a
   rule between them. Under 26rem of its own width the end moves under the text. A table is still the right choice when the reader compares columns across rows. */
.work-list { margin: 0; padding: 0; list-style: none; container-type: inline-size; }
.work-item {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--sp-4);
  min-height: 4rem;
  padding: var(--sp-3) var(--sp-4);
  color: var(--text);
  text-decoration: none;
  border-bottom: 1px solid var(--panel-border);
}
.work-list > li:last-child > .work-item { border-bottom: 0; }
a.work-item { transition: background-color var(--dur-1) var(--ease); }
a.work-item:hover { background: var(--surface-hover); }
a.work-item:focus-visible { outline-offset: -3px; }
.work-main { min-width: 0; }
.work-title { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-1) var(--sp-2); }
.work-code { font-weight: 700; }
.work-name { font-weight: 600; }
.work-sub { margin-top: 2px; overflow: hidden; font-size: var(--fs-sm); color: var(--muted); text-overflow: ellipsis; white-space: nowrap; }
.work-reason { display: flex; align-items: flex-start; gap: var(--sp-2); margin-top: var(--sp-2); font-size: var(--fs-sm); color: var(--warn); }
.work-reason .icon { flex: none; margin-top: 2px; }
.work-end { display: flex; flex-direction: column; align-items: flex-end; gap: var(--sp-1); text-align: right; }
.work-meta { font-size: var(--fs-sm); color: var(--muted); }
@container (max-width: 26rem) {
  .work-item { grid-template-columns: auto minmax(0, 1fr); }
  .work-end { grid-column: 2; flex-direction: row; flex-wrap: wrap; align-items: center; gap: var(--sp-1) var(--sp-3); text-align: left; }
}

/* A row with a button of its own, for a list the worker ticks off: ul.work-list > li.work-row > a.work-item (the text; the link opens the row) + .work-act (a .tick or a .btn,
   and a .work-meta line under it, such as the time it was done). The act stays at the right edge at every width, where .work-end drops under the text below 26rem. Inside
   .work-main put the time as .work-meta, the title, .work-sub (add .work-sub-wrap to let it run to two lines) and .work-tags: the status .tag and short facts such as "2/2 ảnh".
   data-tone="ok|warn|bad|info" on the row paints a bar at its left edge; the tag says the state in words, so colour never carries it alone. */
.work-row {
  --work-tone: transparent;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: stretch;
  border-bottom: 1px solid var(--panel-border);
  box-shadow: inset var(--sp-1) 0 0 var(--work-tone);
}
.work-row[data-tone="ok"] { --work-tone: var(--ok); }
.work-row[data-tone="warn"] { --work-tone: var(--warn); }
.work-row[data-tone="bad"] { --work-tone: var(--danger); }
.work-row[data-tone="info"] { --work-tone: var(--info); }
.work-list > .work-row:last-child { border-bottom: 0; }
.work-row > .work-item { display: flex; gap: var(--sp-3); min-width: 0; border-bottom: 0; }
.work-row > .work-item > .work-main { flex: 1 1 auto; }
.work-act { display: grid; align-self: center; justify-items: center; gap: var(--sp-1); padding: var(--sp-3) var(--sp-4) var(--sp-3) 0; }
.work-tags { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-1) var(--sp-2); margin-top: var(--sp-2); font-size: var(--fs-sm); color: var(--muted); }
.work-sub-wrap { display: -webkit-box; white-space: normal; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }

/* Checklist controls ------------------------------------------------------ */

/* .tick is the round one-touch control for work done with wet or gloved hands: 56px (--tap + --sp-3), larger than .btn-lg. button.tick[aria-pressed] toggles done, and the
   pressed state fills it --ok with its check icon. a.tick.tick-primary (or a label around a file input) holds the camera icon for a line that needs photos before it can be
   ticked, so the worker is never shown a control that does nothing. aria-disabled="true" locks it. Name the task in aria-label: "Tích: Vệ sinh cửa hàng đầu ca". */
.tick {
  display: inline-grid;
  flex: none;
  place-items: center;
  width: calc(var(--tap) + var(--sp-3));
  height: calc(var(--tap) + var(--sp-3));
  padding: 0;
  color: var(--muted);
  text-decoration: none;
  background: var(--surface);
  border: 2px solid var(--field-border);
  border-radius: var(--radius-pill);
  cursor: pointer;
  transition: background-color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.tick .icon { width: var(--icon-lg); height: var(--icon-lg); }
.tick:hover { color: var(--text); background: var(--surface-hover); border-color: var(--field-border-hover); }
.tick[aria-pressed="true"] { color: var(--surface); background: var(--ok); border-color: var(--ok); }
.tick-primary { color: var(--on-primary); background: var(--primary); border-color: var(--primary); }
.tick-primary:hover { color: var(--on-primary); background: var(--primary-hover); border-color: var(--primary-hover); }
.tick:disabled,
.tick:disabled:hover,
.tick[aria-disabled="true"],
.tick[aria-disabled="true"]:hover { color: var(--muted); background: var(--sunk); border-color: var(--panel-border); cursor: not-allowed; }

/* Thumbnails of the photos of one attempt, each with its state in words: ul.thumb-list > li.thumb[data-state="done|uploading|error"] > .thumb-img + .thumb-foot (.thumb-cap, and a
   button: remove, or Thử lại when the state is error). .thumb-img holds the <img> (object-fit: cover); without one it shows an icon and a name. While uploading, a .bar inside
   .thumb-img shows the progress. The grid fills the width with columns of at least 8rem; .thumb-list-sm makes them 5rem for a row of four on a phone. */
.thumb-list {
  --thumb-min: 8rem;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--thumb-min), 1fr));
  gap: var(--sp-3);
  margin: 0;
  padding: 0;
  list-style: none;
}
.thumb-list-sm { --thumb-min: 5rem; gap: var(--sp-2); }
.thumb { display: grid; align-content: start; gap: var(--sp-1); min-width: 0; }
.thumb-img {
  position: relative;
  display: grid;
  place-items: center;
  align-content: center;
  gap: var(--sp-1);
  aspect-ratio: 4 / 3;
  overflow: hidden;
  padding: var(--sp-2);
  font-size: var(--fs-sm);
  text-align: center;
  color: var(--muted);
  background: var(--sunk);
  border-radius: var(--radius);
}
.thumb-img > img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.thumb-img > .icon { width: calc(var(--icon-lg) * 1.5); height: calc(var(--icon-lg) * 1.5); }
.thumb-img > .bar { position: absolute; inset-inline: var(--sp-3); bottom: var(--sp-3); width: auto; background: var(--surface); }
.thumb-img::after { content: ""; position: absolute; inset: 0; border-radius: inherit; box-shadow: inset 0 0 0 2px var(--thumb-ring, transparent); pointer-events: none; }
.thumb[data-state="uploading"] { --thumb-ring: var(--field-border); }
.thumb[data-state="error"] { --thumb-ring: var(--danger); }
.thumb-foot { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--sp-1) var(--sp-2); }
.thumb-cap { font-size: var(--fs-sm); color: var(--muted); font-variant-numeric: tabular-nums; }
.thumb[data-state="error"] .thumb-cap { color: var(--danger); }

/* A sheet of A4 as it prints, for a record the customer also fills on paper (a test sheet, a certificate). The sheet is white with black text whatever the palette, as the
   printout is, so it names the two colours instead of taking a token. .paper-stage (the grey table the sheets lie on; it scrolls sideways when the screen is narrower) > .paper
   (.paper-landscape turns it) > the content, with .paper-no for the page number at the foot. The tenant sets the document font and the margins it needs on .paper.
   Printing drops the stage and the shadow and breaks the page after each sheet. */
.paper-stage {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--sp-6);
  padding: var(--sp-6);
  overflow: auto;
  background: var(--sunk);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius-lg);
}
.paper {
  position: relative;
  flex: none;
  width: 210mm;
  min-height: 297mm;
  margin-inline: auto;
  padding: 12mm 13mm 14mm 18mm;
  font-size: 11pt;
  line-height: 1.32;
  font-variant-numeric: lining-nums tabular-nums;
  color: black;
  background: white;
  box-shadow: var(--sh-3);
}
.paper-landscape { width: 297mm; min-height: 210mm; }
.paper-no { position: absolute; inset-block-end: 7mm; inset-inline-end: 13mm; font-size: 9.5pt; }
@media print {
  .paper-stage { display: block; padding: 0; overflow: visible; background: none; border: 0; border-radius: 0; }
  .paper { margin: 0; box-shadow: none; break-after: page; }
}

/* Timeline --------------------------------------------------------------- */

/* A history, one entry per change, as a version log: ol.timeline > li.timeline-item > an optional .timeline-mark (the version, "v9", in a pill; an entry without one gets a dot) and
   .timeline-body > .timeline-title (what changed) + .timeline-meta (who and when as span and time, a dot between them; a .tag may come first) + .timeline-note (why). Not progress
   (that is .stepper-vertical). The latest entry is aria-current="true": its mark or dot is filled with the primary colour. data-tone="warn|danger|ok|info" colours the mark or dot and
   tints the note; the entry still says in words what changed (a .tag in the meta), because the colour is not the only sign. */
.timeline { --tl-w: 3.25rem; --tl-gap: var(--sp-4); --tl-mark: 1.625rem; --tl-pad: var(--sp-4); --tl-y: calc(var(--tl-pad) + 0.7rem); margin: 0; padding: 0; list-style: none; }
.timeline-item {
  position: relative;
  display: grid;
  grid-template-columns: var(--tl-w) minmax(0, 1fr);
  column-gap: var(--tl-gap);
  padding-block: var(--tl-pad);
}
/* The rail runs behind the marks, from the first mark to the last. */
.timeline-item::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc(var(--tl-w) / 2 - 1px);
  width: 2px;
  background: var(--panel-border);
}
.timeline-item:first-child::before { top: var(--tl-y); }
.timeline-item:last-child::before { bottom: auto; height: var(--tl-y); }
.timeline-item:only-child::before { display: none; }
.timeline-item:not(:has(> .timeline-mark))::after {
  content: "";
  position: absolute;
  top: calc(var(--tl-y) - 0.3125rem);
  left: calc(var(--tl-w) / 2 - 0.3125rem);
  width: 0.625rem;
  height: 0.625rem;
  background: var(--tone, var(--field-border));
  border-radius: 50%;
}
.timeline-mark {
  position: relative;
  z-index: 1;
  box-sizing: border-box;
  justify-self: center;
  align-self: start;
  min-width: 2.5rem;
  height: var(--tl-mark);
  margin-top: calc(var(--tl-y) - var(--tl-pad) - var(--tl-mark) / 2);
  padding-inline: var(--sp-2);
  font-size: var(--fs-sm);
  font-weight: 500;
  line-height: var(--tl-mark);
  font-variant-numeric: tabular-nums;
  text-align: center;
  white-space: nowrap;
  color: var(--tone, var(--muted));
  background: var(--tone-bg, var(--surface-hover));
  border-radius: var(--radius-pill);
}
.timeline-item[aria-current="true"]::after { background: var(--primary); }
.timeline-item[aria-current="true"] .timeline-mark { color: var(--on-primary); background: var(--primary); }
.timeline-item[data-tone="warn"] { --tone: var(--warn); --tone-bg: var(--warn-bg); }
.timeline-item[data-tone="danger"] { --tone: var(--danger); --tone-bg: var(--danger-bg); }
.timeline-item[data-tone="ok"] { --tone: var(--ok); --tone-bg: var(--ok-bg); }
.timeline-item[data-tone="info"] { --tone: var(--info); --tone-bg: var(--info-bg); }
.timeline-body { grid-column: 2; min-width: 0; }
.timeline-title { font-size: var(--fs-base); font-weight: 500; line-height: var(--lh); overflow-wrap: anywhere; }
.timeline-meta { display: flex; flex-wrap: wrap; align-items: center; margin-top: var(--sp-1); font-size: var(--fs-sm); color: var(--muted); }
.timeline-meta > :is(span, time) + :is(span, time)::before { content: "·"; margin-inline: var(--sp-2); }
.timeline-meta time { font-variant-numeric: tabular-nums; }
.timeline-note { margin-top: var(--sp-2); padding: var(--sp-2) var(--sp-3); font-size: var(--fs-sm); overflow-wrap: anywhere; background: var(--surface-2); border-radius: var(--radius-sm); }
.timeline-item[data-tone] .timeline-note { background: var(--tone-bg); }

/* Activity feed, the same list for what happened around a record (not a log of its edits): .timeline-feed. Each entry is a date (time.timeline-date) with a dot on the rail and, under
   it, a rounded row .timeline-card: an icon badge (.timeline-ic), one sentence (.timeline-text, names in strong, a .tag where a state is named) and, at the end, a link or button
   (.btn-icon). A line of facts under it is a .timeline-detail > div > dt + dd (+ a button). data-tone tints the row and colours the dot and the icon; the sentence says it too. */
.timeline-feed { --tl-w: 1.25rem; --tl-gap: var(--sp-3); --tl-pad: var(--sp-2); --tl-y: calc(var(--tl-pad) + 0.55rem); }
.timeline-feed .timeline-body { display: grid; gap: var(--sp-1); }
.timeline-date { font-size: var(--fs-sm); line-height: 1.1rem; color: var(--muted); font-variant-numeric: tabular-nums; }
.timeline-card { display: flex; align-items: center; gap: var(--sp-3); min-width: 0; padding: var(--sp-2) var(--sp-3); background: var(--tone-bg, var(--surface-2)); border-radius: var(--radius); }
.timeline-ic { display: inline-flex; flex: none; align-items: center; justify-content: center; width: 1.75rem; height: 1.75rem; color: var(--tone, var(--muted)); background: var(--surface); border-radius: var(--radius-sm); }
.timeline-ic .icon { width: 1rem; height: 1rem; }
.timeline-text { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
.timeline-text strong { font-weight: 600; }
.timeline-card > .btn { flex: none; margin-inline-start: auto; }
.timeline-detail { margin: 0; }
.timeline-detail > div { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); padding: var(--sp-2) var(--sp-3); font-size: var(--fs-sm); background: var(--surface); border: 1px solid var(--panel-border); border-radius: var(--radius); }
.timeline-detail dt { color: var(--muted); }
.timeline-detail dd { display: flex; align-items: center; gap: var(--sp-2); margin: 0; font-weight: 500; }

/* Sign-in ---------------------------------------------------------------- */

/* A narrow card in the middle of the page: <main class="login"> > .panel.login-card (--login-w) + a quiet .caption under it. In the card, .panel-body holds .login-brand (a .brand),
   a div with the h1 and a .page-lead, and the form as a .stack ending in a .btn-block; .panel-foot holds the help text. --login-h is the page's height (a full window by default). */
.login { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--sp-4); min-height: var(--login-h, 100dvh); padding: var(--sp-4); }
.login-card { width: min(100%, var(--login-w, 25rem)); }
.login-card .panel-body { display: grid; gap: var(--sp-4); }
.login-card .panel-foot { justify-content: flex-start; }
.login-brand { justify-self: start; }
.login > .caption { max-width: var(--login-w, 25rem); margin-top: 0; text-align: center; }

/* Sign-in with a panel beside the form: .login-split > .login-aside + .login (the form as above, now flat on the page's surface). The panel holds the brand, an h1 and a
   p, a drawing in .login-art (an svg that takes currentColor) and .login-facts (a row of strong + text). It is dark by default: --aside-bg, --aside-fg and --aside-muted
   change that. Under 60rem the panel becomes a band above the form, without the drawing and the facts. */
.login-split { display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr); min-height: var(--login-h, 100dvh); background: var(--surface); }
.login-split > .login { min-height: 0; }
.login-split .login-card { background: none; border-color: transparent; box-shadow: none; }
.login-split .login-card .panel-body, .login-split .login-card .panel-foot { padding-inline: 0; }
.login-split .login-card .panel-foot { background: none; border-radius: 0; border-top-color: var(--panel-border); }
.login-aside {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: var(--sp-6);
  padding: var(--sp-9);
  overflow: hidden;
  color: var(--aside-fg, var(--surface));
  background: var(--aside-bg, var(--heading));
}
.login-aside > :not(.login-art) { position: relative; z-index: 1; }
.login-aside .brand-sub { color: var(--aside-muted, var(--sunk)); }
.login-aside h1 { max-width: 22ch; font-size: var(--fs-3xl); line-height: 1.2; color: inherit; }
.login-aside p { max-width: 46ch; margin-top: var(--sp-3); color: var(--aside-muted, var(--sunk)); }
.login-art { position: absolute; right: -2.5rem; bottom: 7.5rem; width: 38.75rem; max-width: none; height: auto; pointer-events: none; }
.login-facts { display: flex; flex-wrap: wrap; gap: var(--sp-3) var(--sp-6); font-size: var(--fs-sm); color: var(--aside-muted, var(--sunk)); }
.login-facts strong { display: block; color: var(--aside-fg, var(--surface)); }
@media (max-width: 60rem) {
  .login-split { grid-template-columns: minmax(0, 1fr); }
  .login-aside { padding: var(--sp-5); }
  .login-aside h1 { font-size: var(--fs-xl); }
  .login-aside .login-art, .login-aside .login-facts { display: none; }
}

/* Tabs ------------------------------------------------------------------- */

.tabs {
  display: flex;
  gap: var(--sp-1);
  overflow-x: auto;
  border-bottom: 1px solid var(--panel-border);
  scrollbar-width: thin;
}
.tab {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  flex: none;
  min-height: var(--tap);
  margin-bottom: -1px;
  padding: 0 var(--sp-4);
  font-size: var(--fs-md);
  font-weight: 600;
  white-space: nowrap;
  color: var(--muted);
  text-decoration: none;
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  cursor: pointer;
  transition: color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease);
}
.tab:hover { color: var(--text); }
.tab:focus-visible { outline-offset: -3px; }
.tab[aria-selected="true"],
.tab[aria-current="page"] { color: var(--text); border-bottom-color: var(--primary); }
.tab:disabled,
.tab[aria-disabled="true"] { opacity: 0.5; cursor: not-allowed; }
.tab-count {
  padding: 0 var(--sp-2);
  font-size: var(--fs-xs);
  line-height: 1.5;
  color: var(--muted);
  background: var(--sunk);
  border-radius: var(--radius-pill);
}
.tab[aria-selected="true"] .tab-count,
.tab[aria-current="page"] .tab-count { color: var(--on-primary); background: var(--primary); }
.tab-panel { padding-top: var(--sp-4); }

/* Pills: a segmented control, for switching a view rather than a page. */
.tabs-pill {
  width: fit-content;
  max-width: 100%;
  padding: var(--sp-1);
  background: var(--sunk);
  border-bottom: 0;
  border-radius: var(--radius-pill);
}
.tabs-pill .tab {
  min-height: calc(var(--tap) - 8px);
  margin: 0;
  border: 0;
  border-radius: var(--radius-pill);
}
.tabs-pill .tab[aria-selected="true"],
.tabs-pill .tab[aria-current="page"] { background: var(--surface); box-shadow: var(--sh-1); }

/* Stepper ---------------------------------------------------------------- */

/* The steps of a long entry, in order: li.step[data-state="done|current|error|todo"] > .step-btn (a button when the user may go to that step, a span when not;
   aria-current="step" on the current one) > .step-mark (a number, a check when done, "!" on error) + .step-label. Three layouts of the same list:
   horizontal, the steps as cells on a grey track like .tabs-pill with the current one the white cell; .stepper-circles, numbered circles joined by a hairline (Vibe's look), which
   is also what .stepper-vertical draws in a column; .stepper-bars, segments with the label under each. A step may carry a .step-sub under its label (inside a .step-text with the
   label) and, in a column, a .step-meta at the right (a date, a tag).
   Done is the success colour, an error the danger colour; the text is the size of the page's text.
   A horizontal list that is narrow (a small modal, a phone) keeps the label of the current step only; the others keep it for screen readers. */
.stepper {
  display: flex;
  gap: var(--sp-1);
  margin: 0;
  padding: var(--sp-1);
  list-style: none;
  container-type: inline-size;
  background: var(--sunk);
  border-radius: var(--radius-lg);
}
.step { display: flex; flex: 1 1 auto; min-width: 0; }
.step-btn {
  display: flex;
  flex: 1 1 auto;
  align-items: center;
  gap: var(--sp-3);
  min-width: 0;
  min-height: 3.25rem;
  padding: var(--sp-2) var(--sp-4);
  font: inherit;
  font-weight: 600;
  line-height: var(--lh-tight);
  color: var(--muted);
  text-align: left;
  background: none;
  border: 0;
  border-radius: var(--radius);
}
button.step-btn { cursor: pointer; }
button.step-btn:not([aria-current="step"]):hover { background: var(--surface-hover); }
.step-btn[aria-current="step"] { background: var(--surface); box-shadow: var(--sh-1); }
.step-mark {
  display: grid;
  flex: none;
  place-items: center;
  width: 2rem;
  height: 2rem;
  font-size: var(--fs-md);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  background: var(--surface);
  border: 2px solid var(--field-border);
  border-radius: var(--radius-pill);
}
.step-mark .icon { width: 1.125rem; height: 1.125rem; }
.step-label { min-width: 0; overflow-wrap: anywhere; text-wrap: balance; }
.step-text { display: flex; flex: 1 1 auto; flex-direction: column; justify-content: center; gap: 2px; min-width: 0; min-height: 2rem; }
.step-sub, .step-meta { font-size: var(--fs-sm); font-weight: 400; color: var(--muted); }
.step-meta { flex: none; margin-left: auto; font-variant-numeric: tabular-nums; text-align: right; }
.step[data-state="current"] .step-btn { color: var(--text); }
.step[data-state="current"] .step-mark { color: var(--primary); border-color: var(--primary); }
.step[data-state="done"] .step-btn { color: var(--ok); }
.step[data-state="done"] .step-mark { color: var(--surface); background: var(--ok); border-color: var(--ok); } /* white on the success colour: the ratio of ok on surface, which the gate tests */
.step[data-state="error"] .step-btn { color: var(--danger); }
.step[data-state="error"] .step-mark { color: var(--on-danger); background: var(--danger); border-color: var(--danger); }
@container (max-width: 40rem) {
  .stepper:not(.stepper-vertical, .stepper-bars) .step:not(:has([aria-current="step"])) { flex: 0 0 auto; }
  .stepper:not(.stepper-vertical, .stepper-bars) .step:not(:has([aria-current="step"])) .step-btn { padding-inline: var(--sp-3); }
  .stepper:not(.stepper-vertical, .stepper-bars) .step:not(:has([aria-current="step"])) .step-label {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
  }
}

/* Bars: the progress shown as segments, no marks; the state is the segment's colour, the label's colour and the hidden words. */
.stepper-bars { gap: var(--sp-2); padding: 0; background: none; border-radius: 0; }
.stepper-bars .step { flex: 1 1 0; }
.stepper-bars .step-btn { flex-direction: column; align-items: stretch; gap: var(--sp-2); min-height: 0; padding: 0; background: none; box-shadow: none; border-radius: var(--radius-sm); }
.stepper-bars .step-btn::before { content: ""; height: 0.5rem; background: var(--sunk); border-radius: var(--radius-pill); }
.stepper-bars .step-mark { display: none; }
.stepper-bars button.step-btn:hover { color: var(--text); background: none; }
.stepper-bars .step-btn[aria-current="step"]::before { background: var(--primary); }
.stepper-bars .step[data-state="done"] .step-btn::before { background: var(--ok); }
.stepper-bars .step[data-state="error"] .step-btn::before { background: var(--danger); }

/* Circles joined by lines, after Vibe's MultiStepIndicator: .stepper-circles on the same list; a .step-text with .step-label and .step-sub gives a step a second line. No grey
   track: a numbered circle with the text beside it and a hairline to the next step. The current step is a filled circle with a ring round it, a done step a filled circle with a
   check, one still to come an outline circle; an error keeps the danger colour. A step you can go back to darkens when pointed at. The ring is cut out in the surface colour, so
   the list belongs on a panel, a modal or the page's own surface. .stepper-compact makes the circles smaller and keeps the label only. */
:is(.stepper-circles, .stepper-vertical) { align-items: center; gap: 0; padding: var(--sp-2); background: none; border-radius: 0; }
:is(.stepper-circles, .stepper-vertical) .step { flex: 0 1 auto; align-items: center; }
:is(.stepper-circles, .stepper-vertical) .step:not(:last-child)::after {
  content: "";
  flex: 0 20 auto; /* the line gives way first, down to --sp-4, before a label is cut; it takes a width, not a flex-basis, so the step's own width counts it */
  width: 3rem;
  min-width: var(--sp-4);
  height: 1px;
  margin-inline: var(--sp-3);
  background: var(--panel-border);
}
:is(.stepper-circles, .stepper-vertical) .step-btn { flex: 0 1 auto; gap: var(--sp-4); min-height: var(--tap); padding: 0; font-weight: 500; background: none; box-shadow: none; border-radius: var(--radius-sm); }
:is(.stepper-circles, .stepper-vertical) button.step-btn:not([aria-current="step"]):hover { background: none; }
:is(.stepper-circles, .stepper-vertical) .step[data-state] .step-btn { color: var(--text); }
:is(.stepper-circles, .stepper-vertical) .step[data-state="error"] .step-btn { color: var(--danger); }
:is(.stepper-circles, .stepper-vertical) .step-sub { color: var(--text); }
:is(.stepper-circles, .stepper-vertical) .step-mark { width: 2.25rem; height: 2.25rem; font-weight: 500; color: var(--text); border-width: 1px; }
:is(.stepper-circles, .stepper-vertical) .step-mark .icon { width: 1.25rem; height: 1.25rem; }
:is(.stepper-circles, .stepper-vertical) .step[data-state="done"] .step-mark,
:is(.stepper-circles, .stepper-vertical) .step[data-state="current"] .step-mark { color: var(--on-primary); background: var(--primary); border-color: var(--primary); }
:is(.stepper-circles, .stepper-vertical) .step[data-state="current"] .step-mark { box-shadow: 0 0 0 var(--sp-1) var(--surface), 0 0 0 calc(var(--sp-1) + 2px) var(--primary); }
:is(.stepper-circles, .stepper-vertical) button.step-btn:hover .step-mark { color: var(--on-primary); background: var(--primary-hover); border-color: var(--primary-hover); }
:is(.stepper-circles, .stepper-vertical) .step[data-state="error"] button.step-btn:hover .step-mark { background: var(--danger-hover); border-color: var(--danger-hover); }
.stepper-circles.stepper-compact .step-mark { width: 1.75rem; height: 1.75rem; font-size: var(--fs-sm); }
.stepper-circles.stepper-compact .step-mark .icon { width: 1rem; height: 1rem; }
.stepper-circles.stepper-compact .step-btn { gap: var(--sp-2); }
.stepper-circles.stepper-compact .step:not(:last-child)::after { width: 2.5rem; margin-inline: var(--sp-2); }
.stepper-circles.stepper-compact .step-sub { display: none; }
.stepper-circles.stepper-compact .step-label { max-width: 9.375rem; overflow: hidden; font-weight: 400; text-overflow: ellipsis; white-space: nowrap; }
/* Vertical: the same list in a column, for a rail beside a form or the progress of one order. .stepper-vertical on its own takes the look of .stepper-circles (the same circles,
   colours, ring and text, no tinted row), and the hairline runs down from the middle of each circle to the next. A step may carry a .step-meta at the right (a date, a tag). */
.stepper-vertical { flex-direction: column; align-items: stretch; }
.stepper-vertical .step { flex-direction: column; align-items: flex-start; }
.stepper-vertical .step-btn { align-self: stretch; }
.stepper-vertical .step:not(:last-child)::after {
  flex: none;
  width: 1px;
  min-width: 0;
  height: var(--sp-6);
  margin: var(--sp-2) 0 var(--sp-2) calc(1.125rem - 0.5px);
}
@container (max-width: 40rem) {
  .stepper.stepper-circles:not(.stepper-vertical) .step:not(:has([aria-current="step"])) .step-btn { padding-inline: 0; }
  .stepper.stepper-circles:not(.stepper-vertical) .step:not(:has([aria-current="step"])) .step-text {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
  }
  .stepper.stepper-circles:not(.stepper-vertical) .step:not(:last-child)::after { width: var(--sp-6); margin-inline: var(--sp-2); }
}
/* Under 22rem (a phone) four circles and the lines between them leave a few pixels for the title of the step in view: the lines shorten, the circle and its title sit closer, and the
   second line goes. */
@container (max-width: 22rem) {
  .stepper.stepper-circles:not(.stepper-vertical) .step:not(:last-child)::after { width: var(--sp-3); margin-inline: var(--sp-1); }
  .stepper.stepper-circles:not(.stepper-vertical) .step-btn { gap: var(--sp-2); }
  .stepper.stepper-circles:not(.stepper-vertical) .step-sub { display: none; }
}

/* A form in steps. In a modal: the stepper under the head (.wizard-steps), one .wizard-panel per step in the body with only the current one shown, and in the foot the
   Cancel button at the start (.modal-foot-start), Back, and Next, which becomes the finish on the last step. On a page: .wizard-page, a .panel with a .stepper-vertical
   at the left (a column of .panel-body) and a form .panel at the right with .wizard-progress in its head, the panels in its body and Back and Next in its foot; under 48rem
   the steps go above it in a row. .wizard-progress is the overall progress: a .bar with "Bước 2/4" before it and the percentage after. */
.wizard-steps { padding: 0 var(--sp-5) var(--sp-4); }
.wizard-panel { display: grid; gap: var(--sp-4); align-content: start; }
.wizard-panel[hidden] { display: none; }
.wizard-panel > h3 { font-size: var(--fs-lg); }
.wizard-panel > h3:focus { outline: 0; }
/* A part that takes the focus when its step opens (its title, tabindex="-1") needs no ring: the whole step is what changed. */
.panel-head h2[tabindex="-1"]:focus { outline: 0; }
.modal-foot-start { margin-right: auto; }
.wizard-progress { display: flex; flex: 1 1 100%; align-items: center; gap: var(--sp-3); font-size: var(--fs-sm); color: var(--muted); font-variant-numeric: tabular-nums; }
.wizard-progress .bar { --bar-h: 0.5rem; flex: 1 1 auto; width: auto; }
.wizard-progress .bar::before { transition: width var(--dur-2) var(--ease); }
.wizard-page { display: grid; grid-template-columns: 16rem minmax(0, 1fr); gap: var(--sp-5); align-items: start; }
@media (max-width: 48rem) {
  .wizard-page { grid-template-columns: minmax(0, 1fr); }
  /* the rail turns to a row of circles on a phone, and only the step in view keeps its text */
  .wizard-page .stepper-vertical { flex-direction: row; align-items: center; }
  .wizard-page .stepper-vertical .step { flex-direction: row; align-items: center; }
  .wizard-page .stepper-vertical .step-btn { align-self: auto; }
  .wizard-page .stepper-vertical .step:not(:last-child)::after { flex: 0 20 auto; width: var(--sp-6); min-width: var(--sp-4); height: 1px; margin: 0 var(--sp-2); }
  .wizard-page .stepper-vertical .step:not(:has([aria-current="step"])) .step-btn { padding-inline: 0; }
  .wizard-page .stepper-vertical .step:not(:has([aria-current="step"])) :is(.step-text, .step-label) {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
  }
  /* the row has the width of a phone, so it shortens its lines and closes up the title, as the horizontal circles do */
  @container (max-width: 22rem) {
    .wizard-page .stepper-vertical .step:not(:last-child)::after { width: var(--sp-3); min-width: 0; margin: 0 var(--sp-1); }
    .wizard-page .stepper-vertical .step-btn { gap: var(--sp-2); }
    .wizard-page .stepper-vertical .step-sub { display: none; }
  }
}

/* Sidebar ---------------------------------------------------------------- */

/* The page is .app-shell > .sidebar + .app-main. Under 48rem the sidebar slides in over the page. */
.app-shell { display: flex; min-height: 100dvh; }
.app-main { flex: 1 1 auto; min-width: 0; }

.sidebar {
  position: sticky;
  top: 0;
  display: flex;
  flex: none;
  flex-direction: column;
  gap: var(--sp-4);
  width: var(--sidebar-w);
  height: 100dvh;
  padding: var(--sp-4) var(--sp-3);
  overflow-y: auto;
  background: var(--surface);
  border-right: 1px solid var(--panel-border);
  transition: width var(--dur-2) var(--ease);
}
.sidebar .brand { padding: var(--sp-1) var(--sp-3); color: var(--heading); }
.sidebar-nav { display: flex; flex-direction: column; gap: var(--sp-4); }
.side-heading {
  margin-bottom: var(--sp-1);
  padding: 0 var(--sp-3);
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}
.side-item {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  min-height: var(--tap);
  padding: 0 var(--sp-3);
  font-size: var(--fs-md);
  font-weight: 600;
  color: var(--muted);
  text-align: left;
  text-decoration: none;
  background: none;
  border: 0;
  border-radius: var(--radius);
  cursor: pointer;
  transition: background-color var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.side-item:hover { color: var(--text); background: var(--surface-hover); }
.side-item[aria-current="page"] { color: var(--text); background: var(--sunk); }
.side-label {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The signed-in person in .sidebar-foot: a .side-item (a button that opens a .menu) with an avatar and a .side-label of two lines, the name and the role. */
.side-label .avatar-label-name, .side-label .avatar-label-sub { display: block; overflow: hidden; text-overflow: ellipsis; }
.side-badge {
  display: inline-block;
  flex: none;
  min-width: 1.5rem;
  padding: 0 var(--sp-2);
  font-size: var(--fs-xs);
  line-height: 1.5;
  text-align: center;
  color: var(--badge-fg, var(--muted));
  background: var(--badge-bg, var(--sunk));
  border-radius: var(--radius-pill);
  box-shadow: inset 0 0 0 1px var(--badge-line, transparent);
}
.side-item[aria-current="page"] .side-badge { background: var(--badge-bg, var(--surface)); }
/* A count that means something takes a colour by case: -ok done or scheduled, -warn waiting for someone, -bad overdue or failed, -info new,
   -primary to draw the eye. Same pairs as .tag. When the colour matters, say it in words too: <span class="visually-hidden"> quá hạn</span>. */
.side-badge-ok { --badge-fg: var(--ok); --badge-bg: var(--ok-bg); --badge-line: var(--ok-line); }
.side-badge-warn { --badge-fg: var(--warn); --badge-bg: var(--warn-bg); --badge-line: var(--warn-line); }
.side-badge-bad { --badge-fg: var(--danger); --badge-bg: var(--danger-bg); --badge-line: var(--danger-line); }
.side-badge-info { --badge-fg: var(--info); --badge-bg: var(--info-bg); --badge-line: var(--info-line); }
.side-badge-primary { --badge-fg: var(--on-primary); --badge-bg: var(--primary); }
.sidebar-foot {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  margin-top: auto;
  padding-top: var(--sp-3);
  border-top: 1px solid var(--panel-border);
}

/* A group that folds, as in Vibe's navigation tree: details.side-group > summary.side-item (icon, .side-label, .side-group-caret that turns when open) and .side-sub, the items of the
   group, text only and set in under the label. The tenant puts `open` on the group that holds the current page. Collapsed to icons the group shows its icon alone and its items are
   hidden; the tenant opens the sidebar when the icon is pressed. A group with the current page is marked on its icon. */
.side-group > summary { list-style: none; }
.side-group > summary::-webkit-details-marker { display: none; }
.side-group-caret { transition: transform var(--dur-1) var(--ease); }
.side-group[open] > summary .side-group-caret { transform: rotate(90deg); }
.side-sub { display: flex; flex-direction: column; margin-block: var(--sp-1); --tree-x: calc(var(--sp-3) + var(--icon) / 2); --tree-w: calc(var(--sp-3) - var(--sp-2) + var(--icon) / 2); }
/* The item starts where the elbow ends, so the hover and current background never covers the line; its text still lines up with the group's label. */
.side-sub .side-item { min-height: calc(var(--tap) - var(--sp-2)); margin-inline-start: calc(var(--tree-x) + var(--tree-w)); padding-inline-start: calc(var(--sp-3) * 2 + var(--icon) - var(--tree-x) - var(--tree-w)); font-size: var(--fs-sm); font-weight: 500; }
.side-sub .side-item[aria-current="page"] { font-weight: 600; }
/* The tree line of Core 2.0: one hairline under the group's icon, running to where the last item turns, and a curved elbow into each item. */
.side-sub { position: relative; }
.side-sub::before {
  content: "";
  position: absolute;
  inset-block: 0 calc((var(--tap) - var(--sp-2)) / 2 + var(--sp-2));
  inset-inline-start: calc(var(--sp-3) + var(--icon) / 2);
  border-inline-start: 1px solid var(--panel-border);
}
.side-sub .side-item { position: relative; }
.side-sub .side-item::before {
  content: "";
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: calc(var(--tree-w) * -1);
  width: var(--tree-w);
  height: 50%;
  pointer-events: none;
  border-inline-start: 1px solid var(--panel-border);
  border-block-end: 1px solid var(--panel-border);
  border-end-start-radius: var(--sp-2);
}

/* Collapsed to icons: the labels stay in the page for screen readers, so give each item a title too. */
.sidebar[data-collapsed="true"] { width: var(--sidebar-w-collapsed); }
.sidebar[data-collapsed="true"] .side-label,
.sidebar[data-collapsed="true"] .side-heading,
.sidebar[data-collapsed="true"] .side-badge,
.sidebar[data-collapsed="true"] .side-group-caret,
.sidebar[data-collapsed="true"] .brand-text {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}
.sidebar[data-collapsed="true"] .side-item { justify-content: center; padding: 0; }
.sidebar[data-collapsed="true"] .side-sub { display: none; }
.sidebar[data-collapsed="true"] .side-group:has([aria-current="page"]) > summary { color: var(--text); background: var(--sunk); }
.sidebar[data-collapsed="true"] .brand { justify-content: center; padding-inline: 0; }
/* A wide logo is shown through a square window onto its left end, where the symbol usually is; a square one fills it. */
.sidebar[data-collapsed="true"] .brand-logo { width: var(--logo-h, 2.25rem); object-fit: cover; }
/* On a computer, the icon an item ends with (the up-down arrows of the account button) would sit tight against the avatar, so it goes; the label stays for a screen reader. */
@media (min-width: 48.0625rem) { .sidebar[data-collapsed="true"] .side-label ~ .icon { display: none; } }

.sidebar-open { display: none; }
/* Beside a sidebar the top bar is slim: the sidebar buttons at the left, the actions at the right edge, and the search centred between them. It sticks to the top of
   .app-main. Under 48rem the sidebar slides over the page and room is short, so the search sits at the right edge with the actions after it. */
.app-main > .topbar { justify-content: flex-start; column-gap: var(--sp-3); }
.app-main > .topbar .search-btn, .app-main > .topbar .topbar-actions { margin-left: auto; }
.app-main > .topbar .search-btn + .topbar-actions { margin-left: 0; }
/* Three columns, the flanks equal, so the search is centred on the bar and not on the room left between the other two. A flank grows past its half only when its
   content needs it, so a wide action pushes the centre a little instead of overlapping it. */
@media (min-width: 48.0625rem) {
  .app-main > .topbar { display: grid; grid-template-columns: minmax(auto, 1fr) minmax(11rem, 20rem) minmax(auto, 1fr); }
  .app-main > .topbar > .sidebar-collapse { grid-column: 1; justify-self: start; }
  .app-main > .topbar > .search-btn { grid-column: 2; margin-left: 0; }
  .app-main > .topbar > .topbar-actions { grid-column: 3; justify-self: end; margin-left: 0; }
}
/* The button that collapses the sidebar: aria-pressed="true" while collapsed, and its arrows turn to point out. */
.sidebar-collapse[aria-pressed="true"] .icon { transform: scaleX(-1); }
.sidebar-scrim { display: none; }
@media (max-width: 48rem) {
  .sidebar-open { display: inline-flex; }
  .sidebar,
  .sidebar[data-collapsed="true"] {
    position: fixed;
    inset: 0 auto 0 0;
    z-index: var(--z-drawer);
    width: var(--sidebar-w);
    max-width: 85vw;
    visibility: hidden;
    transform: translateX(-100%);
    box-shadow: var(--sh-3);
    transition: transform var(--dur-2) var(--ease), visibility 0s linear var(--dur-2);
  }
  .sidebar[data-open="true"] {
    visibility: visible;
    transform: none;
    transition: transform var(--dur-2) var(--ease);
  }
  .sidebar[data-collapsed="true"] .side-label,
  .sidebar[data-collapsed="true"] .side-heading,
  .sidebar[data-collapsed="true"] .side-badge,
  .sidebar[data-collapsed="true"] .brand-text { position: static; width: auto; height: auto; margin: 0; overflow: visible; clip: auto; }
  .sidebar[data-collapsed="true"] .side-item { justify-content: flex-start; padding: 0 var(--sp-3); }
  .sidebar[data-collapsed="true"] .side-group-caret { position: static; width: var(--icon-sm); height: var(--icon-sm); margin: 0; overflow: visible; clip: auto; }
  .sidebar[data-collapsed="true"] .side-sub { display: flex; }
  .sidebar[data-collapsed="true"] .side-sub .side-item { padding-inline-start: calc(var(--sp-3) * 2 + var(--icon) - var(--tree-x) - var(--tree-w)); }
  .sidebar[data-collapsed="true"] .side-group:has([aria-current="page"]) > summary { background: none; }
  .sidebar[data-collapsed="true"] .brand-logo { width: auto; object-fit: contain; }
  .sidebar-scrim {
    position: fixed;
    inset: 0;
    z-index: var(--z-scrim);
    display: block;
    background: var(--scrim);
  }
}

/* Phone shell ------------------------------------------------------------- */

/* Three pieces for a phone app that replaces the sidebar: a bottom tab bar, a sub-page header with a back button, and a bottom action bar. They are fixed or sticky to the
   screen, and the safe-area inset keeps them clear of the home indicator. Render the .tabbar only in the phone layout (the links also live in the sidebar or top bar of a wider
   one). The tab bar is for the top-level pages and the action bar for a sub-page, so they do not share a screen. The page gets .has-tabbar or .has-action-bar so its last
   line stays above the bar. */
:root {
  --tabbar-h: calc(var(--tap) + var(--sp-3));
  /* Room for one row of .btn-lg and a one-line note. */
  --action-bar-h: calc(var(--tap) + var(--sp-9) + var(--sp-3));
}
.has-tabbar { padding-bottom: calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px)); }
.has-action-bar { padding-bottom: calc(var(--action-bar-h) + env(safe-area-inset-bottom, 0px)); }

/* .tabbar (a nav) > .tabbar-item (a link, or a button that opens a .menu) > .tabbar-ic (svg.icon, and a .side-badge for a count) + .tabbar-label.
   The current item has aria-current="page", the bar over it and, by convention, the -fill version of its icon. */
.tabbar {
  position: fixed;
  inset-inline: 0;
  bottom: 0;
  z-index: var(--z-topbar);
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  padding-bottom: env(safe-area-inset-bottom, 0px);
  background: var(--surface);
  border-top: 1px solid var(--panel-border);
}
.tabbar-item {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-1);
  min-height: var(--tabbar-h);
  padding: var(--sp-1);
  font: inherit;
  font-size: var(--fs-xs);
  font-weight: 600;
  line-height: var(--lh-tight);
  color: var(--muted);
  text-decoration: none;
  background: none;
  border: 0;
  cursor: pointer;
  transition: background-color var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.tabbar-item:hover { color: var(--text); background: var(--surface-hover); }
.tabbar-item:active { background: var(--sunk); }
.tabbar-item:focus-visible { outline-offset: -3px; }
.tabbar-item[aria-current="page"] { color: var(--text); }
.tabbar-item[aria-current="page"]::before {
  content: "";
  position: absolute;
  top: 0;
  inset-inline: 25%;
  height: var(--sp-1);
  background: var(--primary);
  border-radius: 0 0 var(--radius-sm) var(--radius-sm);
}
.tabbar-ic { position: relative; display: inline-flex; }
.tabbar-ic .icon { width: var(--icon-lg); height: var(--icon-lg); }
.tabbar-ic > .side-badge { position: absolute; top: calc(var(--sp-1) * -1); inset-inline-start: 55%; }
.tabbar-label { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* A sub-page header: a .topbar whose first child is .topbar-back (a .btn.btn-ghost.btn-icon, an <a> to the parent page, with an aria-label) is laid out as one row: the back
   button, .topbar-title (.topbar-title-text, and .topbar-title-sub for a second line, each cut with "…") and, at the end, .topbar-actions for a ⋯ button. */
.topbar:has(> .topbar-back) {
  flex-wrap: nowrap;
  justify-content: flex-start;
  gap: var(--sp-2);
  padding-inline: var(--sp-2) var(--sp-3);
}
.topbar:has(> .topbar-back) .btn-ghost { border-color: transparent; }
.topbar:has(> .topbar-back) > .topbar-actions { flex: none; gap: var(--sp-1); }
.topbar-back { flex: none; }
.topbar-title { display: flex; flex: 1 1 auto; flex-direction: column; min-width: 0; }
.topbar-title-text,
.topbar-title-sub { margin: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.topbar-title-text { font-size: var(--fs-lg); font-weight: 600; line-height: var(--lh-tight); color: var(--header-fg); }
.topbar-title-sub { font-size: var(--fs-sm); color: var(--header-muted); }

/* .action-bar holds the screen's main buttons in thumb reach: .btn.btn-lg in a row (the .btn-primary one is twice as wide), and above them an optional
   .action-bar-note that says why a button is locked ("Gửi đủ 2 ảnh để bấm Xong"). */
.action-bar {
  position: fixed;
  inset-inline: 0;
  bottom: 0;
  z-index: var(--z-topbar);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2) var(--sp-3);
  padding: var(--sp-3) var(--sp-4) calc(var(--sp-3) + env(safe-area-inset-bottom, 0px));
  background: var(--surface);
  border-top: 1px solid var(--panel-border);
}
/* Each button starts from the width of its label, so a short secondary button never has to wrap it, and the spare width is shared 1 to 2. */
.action-bar > .btn { flex: 1 1 auto; }
.action-bar > .btn-primary { flex-grow: 2; }
.action-bar-note { flex: 1 0 100%; margin: 0; font-size: var(--fs-sm); text-align: center; color: var(--muted); }

/* Touch ------------------------------------------------------------------- */

/* Under a finger (a coarse pointer) every control is at least --tap high and wide, where a mouse keeps the compact sizes, and a field's text is at least 1rem, so iOS Safari
   does not zoom the page when the field takes focus. A tenant that sets --tap to 52px for a phone gets 52px here too. */
@media (pointer: coarse) {
  .btn-sm:not(.btn-link),
  .chip,
  .seg-item,
  .nav-item,
  .menu-item,
  .combo-option,
  .tabs-pill .tab,
  .toc a,
  .pager .btn,
  .pager .select,
  .side-sub .side-item { min-height: var(--tap); }
  .btn-link.btn-sm,
  .panel-actions > .btn-link { min-height: var(--tap); }
  .btn-icon.btn-sm,
  .pager .btn { min-width: var(--tap); }
  .btn-icon.btn-sm { width: var(--tap); }
  /* The remove button covers the chip's 1px edges too, so it is a full --tap target without making the chip taller. */
  .chip-x { width: var(--tap); height: var(--tap); margin-block: -1px; }
  .datepicker-btn { width: var(--tap); height: var(--tap); }
  .datepicker .input { padding-inline-end: calc(var(--sp-2) + var(--tap) + var(--sp-1)); }
  .input,
  .select,
  .textarea { font-size: max(var(--fs-base), 1rem); }
}

/* Motion and print ------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

@media print {
  .topbar,
  .sidebar,
  .sidebar-scrim,
  .tabbar,
  .action-bar,
  .toast-host { display: none; }
  body { background: none; }
  .panel,
  .toast { box-shadow: none; }
}
