/* Copied from design/ui-kit, kit v1.3 */
/* 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;
  font-family: var(--font-figure, inherit);
}
/* Same digit width without the right alignment: dates and times in a column. */
.tnum { font-variant-numeric: tabular-nums; }
/* A figure with a meaning, for a ledger: .num-in (money or goods coming in), .num-out (going out, in --danger as .num-in is in --ok, so in and out read as a pair), .num-neg (a balance below zero, the same red: the sign and the column say which), .num-void (a voided entry: muted and struck through, as a
   voided table row is). They go on a td.num, or on a span in a list row or a .kpi-value. The sign is written in the text ("+ 1.250.000", "− 350.000", with the true minus U+2212) and the
   kind is a column title or a .visually-hidden word, so colour is never the only cue. Put a no-break space between the sign and the digits, and a figure never wraps. .num-unit is the unit after the figure ("đ", "kg"), a step smaller. --font-figure (a tenant's own cut
   for figures, say a narrower one) is used by every figure class when it is set; unset, the figure keeps the text font. */
.num-in, .num-out, .num-neg, .num-void { font-variant-numeric: tabular-nums; font-family: var(--font-figure, inherit); white-space: nowrap; }
.num-in { color: var(--ok); }
.num-out { color: var(--danger); }
.num-neg { color: var(--danger); }
.num-void { color: var(--muted); text-decoration: line-through; }
.num-unit { margin-inline-start: 0.25em; font-size: 0.75em; font-weight: 500; }

/* 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%; }
/* A button that adds a row ("+ Thêm vật tư"): the same .btn (every state, height and the coarse-pointer size) with a dashed edge, so it reads as an empty place. Write it
   button.btn.btn-dashed with .btn-block under a list of cards, a plus svg.icon, then the words. */
.btn-dashed { border-style: dashed; }
.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,
.panel-head h4 { 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; }

/* A panel that opens and closes: details.panel.panel-fold > summary.panel-head (the title, a .panel-sub line, and at the end svg.icon.panel-caret) then .panel-body. No script. Closed, the
   head has no line under it; open, the caret turns up. Put open on the details to start it open. */
.panel-fold > summary { list-style: none; cursor: pointer; border-radius: inherit; }
.panel-fold > summary::-webkit-details-marker { display: none; }
.panel-fold:not([open]) > summary { border-bottom: 0; }
.panel-fold > summary:hover .panel-caret { color: var(--text); }
.panel-caret { flex: none; margin-inline-start: auto; color: var(--muted); transition: transform var(--dur-1) var(--ease); }
.panel-fold[open] .panel-caret { transform: rotate(180deg); }

/* 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, and the action of a status card (below): 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; }
  .kpi-row:has(> .kpi > .kpi-action) > .kpi { grid-row: span 4; }
}
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-family: var(--font-figure, inherit); 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); }
/* A figure that keeps its whole amount: .kpi-value.kpi-value-fit, for a cash book where the card must say "549.500.000 đ" and not "549,5M". The figure stays on one line and its size follows the
   width of its own card (a container query on the card, no script): the full --fs-3xl while the card is wide enough, then smaller down to --fs-md, never below. Fourteen characters
   (digits, dots and the unit) are assumed; --fit-chars says another count when the tenant's amounts are longer or shorter ("--fit-chars: 12" for "549.500.000 đ", 16 for amounts
   in the hundred thousand millions). Set it on the .kpi-row, not on one card, so the cards of a row keep one size. A figure that still does not fit at --fs-md is written short (k, tr, tỷ: see COMPONENTS.md). The card is the container, so it needs a width of its own: a
   cell of .kpi-row, or a width set on it. The unit takes its size from the figure: --fs-md at full size, down to --fs-xs. The app look restates the largest size (look-app.css). */
.kpi:has(.kpi-value-fit) { container-type: inline-size; }
.kpi-value-fit {
  --kpi-fit-max: var(--fs-3xl);
  --kpi-fit-size: clamp(var(--fs-md), calc(100cqw / (var(--fit-chars, 14) * 0.62)), var(--kpi-fit-max));
  font-size: var(--kpi-fit-size);
}
.kpi-value-fit .kpi-unit { font-size: clamp(var(--fs-xs), 0.7em, var(--fs-md)); }
/* .kpi-row-pair on a row of exactly two cards keeps them side by side down to the narrowest phone, where a row of cards falls to one a line under 27rem. Use it with .kpi-value-fit for two
   whole amounts (a cash book: money owed to us, money we owe) that the reader compares at a glance; the figures shrink to fit (above). */
@container (max-width: 27rem) {
  .kpi-row.kpi-row-pair:where(:has(> :nth-child(2):last-child)) > .kpi { --kpi-span: 6; }
}
.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; }
/* A card with a button that changes what it shows, the status card: the clock-in card ("Đã vào ca 7:18" with Ra ca), the hours of today with Vào ca. .kpi-action goes after .kpi-foot as a
   direct child and holds one or two .btn, which share the line and wrap under 9rem each. The card is a div.kpi, never a.kpi or button.kpi: a button cannot sit inside a link or another
   button, and a figure that also opens a list takes its link in .kpi-label. The action is a fourth line after the three the cards of a .kpi-row share: when any card of a row has one, every card of
   that row shares four lines, so a card without one stays as tall as its neighbours and keeps its head, figure and foot in line with theirs, with the room left under its foot. */
.kpi-action { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-1); }
.kpi-action > .btn { flex: 1 1 9rem; }

/* 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, except in
   the scrolling strip below. 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); }

/* A strip of tabs that scrolls sideways, for more of them than the width holds (a cash book with seven funds, a shop with many stores): .kpi-tabs.kpi-tabs-scroll, the markup of .kpi-tabs
   with a short label, a figure and one foot line in each tab. A tab is as wide as its own content and no narrower than --tab-w (9rem); few tabs share the row, many scroll under a thin
   scrollbar, and the tabs snap to the start edge. kit.js scrolls the chosen tab into view when it is changed by any tab script (the strip scrolls, never the page). The figure is a fixed
   --fs-xl here (not .kpi-value-fit, whose size follows the tab's width and so cannot size the tab). A tab may take data-tone="ok|warn|bad" (danger is the same as bad, as on .kpi): a
   tinted tab and a figure in the tone's colour, for a fund below zero. A tone is never the only cue: the figure carries its sign (.num-neg, a true minus) and the foot says it in a word
   (a .tag.tag-bad "Âm"). The chosen tab is raised on the surface with its tone as the edge. */
.kpi-tabs-scroll {
  display: flex;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  scroll-padding-inline: var(--sp-1);
  scrollbar-width: thin;
  scrollbar-color: var(--field-border) transparent;
}
.kpi-tabs-scroll > .kpi { flex: 1 0 auto; min-width: var(--tab-w, 9rem); padding: var(--sp-3) var(--sp-4); scroll-snap-align: start; }
.kpi-tabs-scroll .kpi-label,
.kpi-tabs-scroll .kpi-foot { white-space: nowrap; }
.kpi-tabs-scroll .kpi-value { font-size: var(--fs-xl); }
.kpi-tabs-scroll > .kpi[data-tone="ok"] { --tab-tone: var(--ok); --tab-tone-bg: var(--ok-bg); --tab-tone-line: var(--ok-line); }
.kpi-tabs-scroll > .kpi[data-tone="warn"] { --tab-tone: var(--warn); --tab-tone-bg: var(--warn-bg); --tab-tone-line: var(--warn-line); }
.kpi-tabs-scroll > .kpi:is([data-tone="bad"], [data-tone="danger"]) { --tab-tone: var(--danger); --tab-tone-bg: var(--danger-bg); --tab-tone-line: var(--danger-line); }
.kpi-tabs-scroll > .kpi[data-tone] { background: var(--tab-tone-bg); border-color: var(--tab-tone-line); }
.kpi-tabs-scroll > .kpi[data-tone] .kpi-value { color: var(--tab-tone); }
.kpi-tabs-scroll > .kpi[data-tone][aria-selected="true"] { background: var(--surface); border-color: var(--tab-tone); }

/* 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, tone, new, 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); }
/* A whole row tinted by its state: tr[data-tone="ok|warn|bad|info"] (not yet synced, a rounded line the system made, an error row). The tint is the tone's -bg token, so the sticky cells
   follow; the row says the same in words in a tag or a cell, since colour is never alone; a .cell-fill keeps its own colour. A figure inside keeps its meaning (.num-in, .num-neg): every
   tone's colour is gated on every tone's tint. A tone beats the stripe and loses to hover and selection. .row-new: a row just added (the tenant removes the class after a moment, so there is
   no animation); it shares the selected row's tint, so a bar at the start edge tells them apart and a word ("Mới") says it. */
.tbl tbody tr[data-tone="ok"] { --row-bg: var(--ok-bg); }
.tbl tbody tr[data-tone="warn"] { --row-bg: var(--warn-bg); }
.tbl tbody tr[data-tone="bad"] { --row-bg: var(--danger-bg); }
.tbl tbody tr[data-tone="info"] { --row-bg: var(--info-bg); }
.tbl tbody tr.row-new { --row-bg: var(--notice-bg); }
.tbl tbody tr.row-new > :first-child { box-shadow: inset var(--sp-1) 0 0 var(--primary), var(--line-b, 0 0 transparent), var(--line-t, 0 0 transparent), var(--line-r, 0 0 transparent); }
/* The opening balance of a ledger: tr.row-open, the first row of the tbody ("Số dư đầu kỳ 01/07", "Tồn đầu kỳ"). Quieter than a data row: the --surface-2 fill, muted text at regular weight, a
   firmer line under it that separates it from the movements, and bold only on the balance (the last td.num). Not struck and not italic. The label is the row header, th scope="row" (colspan
   over the columns before the figures, or one cell with the other cells left empty; in a table with .tbl-sticky-col leave the cells, a wide first cell would stay over the scrolling ones); the figure sits
   in the balance column. Hover and selection work as on any row, since it only sets --row-bg. Under .tbl-cards it is a card of the same quiet look titled by the label, and its empty cells are left out. */
.tbl tbody tr.row-open { --row-bg: var(--surface-2); }
.tbl tbody tr.row-open th,
.tbl tbody tr.row-open td { color: var(--muted); border-bottom-color: var(--field-border); }
.tbl tbody tr.row-open th { font-weight: 400; }
.tbl tbody tr.row-open td.num:last-child { font-weight: 600; }
.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); }

/* Attendance and other grids of small marks (a month of days across, a worker down): td.cell-mark, the whole cell softly tinted and its mark centred, a symbol or a word ("Đ" full day, "½" half,
   "P" paid leave, "KP" unpaid leave, "-"). Tones: .cell-mark-ok (--ok-bg), -info (--info-bg), -bad (--danger-bg), -muted (--sunk); plain is the row's colour, for a blank day. The mark is in --text
   (gated on every one of these tints), never in the tone's colour, so the letter says the state and the tint only helps. -na is "not there yet" (before the worker joined): a diagonal hatch of
   --field-border on the row's colour and no mark. The cell is --tap square at least. A cell that opens something is td.cell-mark > a (the whole cell is the target, the focus ring inside it); give it
   an aria-label with the whole meaning. td[data-off] shades a day off (a Sunday, a holiday) down the column, halfway from --surface-2 to --sunk (--surface-2 alone hardly shows on a white panel): put it on every td of that day and on its th (the head takes --sunk), and say
   it in the head too ("CN"): shade is a help, the word is the cue. A mark cell keeps its tint over a day off. .cell-corner is a small figure in the cell's top-end corner (overtime hours): 0.75rem
   in --muted, inside the cell or its link, which is positioned for it. A figure that small is not read aloud in context, so the cell or link carries aria-label with the full meaning ("Ngày 5, đủ
   công, tăng ca 2 giờ") and the corner is aria-hidden. Under .tbl-cards a mark cell is a card field like any other (give it data-label). */
.tbl tbody td[data-off] { --cell-bg: color-mix(in srgb, var(--surface-2), var(--sunk) 55%); background: var(--cell-bg); }
.tbl thead th[data-off] { background: var(--sunk); }
.tbl tbody td.cell-mark {
  position: relative;
  min-width: var(--tap);
  height: var(--tap);
  padding: 0;
  font-weight: 600;
  color: var(--text);
  text-align: center;
  background: var(--cm-bg, var(--cell-bg, var(--row-bg)));
}
.cell-mark > a {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-width: var(--tap);
  min-height: var(--tap);
  color: inherit;
  text-decoration: none;
}
.cell-mark > a:hover { box-shadow: inset 0 0 0 2px var(--field-border-hover); }
.cell-mark > a:focus-visible { outline: 3px solid var(--focus); outline-offset: -3px; }
.cell-mark-ok { --cm-bg: var(--ok-bg); }
.cell-mark-info { --cm-bg: var(--info-bg); }
.cell-mark-bad { --cm-bg: var(--danger-bg); }
.cell-mark-muted { --cm-bg: var(--sunk); }
.tbl tbody td.cell-mark-na { background-image: repeating-linear-gradient(135deg, transparent 0 6px, var(--field-border) 6px 7px); }
.cell-corner { position: absolute; top: 2px; inset-inline-end: 3px; font-size: 0.75rem; font-weight: 600; line-height: 1; color: var(--muted); pointer-events: none; }
/* More marks in one cell: .cell-corner-start sits in the top-start corner, .cell-corner-bottom in the bottom-end one, so a cell can say three small things at once (computed, from the
   fingerprint machine, being saved). A corner may hold an svg.icon instead of a figure (star for computed, fingerprint, spinner); it is aria-hidden like any corner, so the cell's
   aria-label says it in words. */
.cell-corner-start { inset-inline: 3px auto; }
.cell-corner-bottom { top: auto; bottom: 2px; }
.cell-corner > .icon { display: block; width: 0.6875rem; height: 0.6875rem; }
/* A grid of hour figures (minhson's month sheet: "8", "8,5", "7,5P", "5K", "1.200") rather than letters: .tbl-marks on the table. Under a mouse a mark cell is --mark-w (2.25rem)
   wide and --mark-h (1.875rem) high instead of --tap square, since 31 days at 44px is 1364px; the figure is --fs-sm in tabular digits with 2px at each side, and a figure wider than
   the cell widens its column instead of spilling over. The other cells take compact padding; the day heads and the day totals of a <tfoot> are centred with 2px at each side (the
   cells that are neither [data-sticky] nor .num). Under a finger, or with data-tap="all", the cells are --tap again and the grid scrolls. The corners shrink to fit the smaller cell. */
.tbl-marks { --mark-w: 2.25rem; --mark-h: 1.875rem; font-variant-numeric: tabular-nums; }
.tbl-marks :is(th, td) { padding: var(--sp-2) var(--sp-3); }
.tbl-marks :is(thead, tfoot) :is(th, td):not([data-sticky], .num) { padding-inline: 2px; text-align: center; }
.tbl-marks tfoot td:not([data-sticky], .num) { font-size: var(--fs-xs); }
.tbl-marks tbody :is(th, td) { padding-block: var(--sp-1); }
.tbl-marks tbody td.cell-mark { min-width: var(--mark-w); height: var(--mark-h); padding: 0 2px; font-size: var(--fs-sm); white-space: nowrap; }
.tbl-marks .cell-mark > a { min-width: var(--mark-w); min-height: var(--mark-h); }
.tbl-marks .cell-corner { top: 1px; font-size: 0.625rem; }
.tbl-marks .cell-corner-bottom { top: auto; bottom: 1px; }
.tbl-marks .cell-corner > .icon { width: 0.5625rem; height: 0.5625rem; }
@media (pointer: coarse) { .tbl-marks { --mark-w: var(--tap); --mark-h: var(--tap); } }
:root[data-tap="all"] .tbl-marks { --mark-w: var(--tap); --mark-h: var(--tap); }
/* The cell being typed into: td.cell-mark > input.cell-input is drawn over the cell and its neighbours, at least 5rem wide, so "7,5P" fits while typing and the column keeps its width.
   It goes under the sticky columns, as a scrolled cell does. */
.tbl tbody td.cell-mark > .cell-input { position: absolute; inset-block: 0; inset-inline-start: 0; z-index: 2; width: max(100%, 5rem); min-width: 0; height: 100%; padding: 0 var(--sp-2); font-weight: 600; text-align: center; background: var(--surface); box-shadow: inset 0 0 0 2px var(--focus), var(--sh-2); }
/* Short day: .cell-mark-warn (--warn-bg), fewer hours than the shift ("6,5" of 8). */
.cell-mark-warn { --cm-bg: var(--warn-bg); }
/* Not recorded: td.cell-mark.cell-mark-missing, a past working day nobody has entered yet. The cell holds only a .visually-hidden "Chưa chấm" and draws an empty dashed ring in
   --muted at its centre: an empty slot, not a value. It is not "chưa chốt" (data-state="open" below), which is a value that is not final and keeps its value inside a dashed frame. */
.tbl tbody td.cell-mark-missing::after { content: ""; position: absolute; top: 50%; left: 50%; width: 0.75rem; height: 0.75rem; margin: -0.375rem 0 0 -0.375rem; border: 1.5px dashed var(--muted); border-radius: 50%; pointer-events: none; }
/* Computed but editable: td.cell-auto (a mark cell or a sheet cell) holds a value the software worked out (the overtime meal, hours from the fingerprint machine) that the user may still
   change. The value is italic at weight 500, put a star icon in a .cell-corner, and say "tự tính" in the aria-label. It is not .cell-ro, which is locked (muted on --sunk). .mark-auto is
   the same idea as a word: a small italic "tự" after a value or inside a chip ("Cơm TC tự"). */
.tbl tbody td.cell-auto { font-style: italic; font-weight: 500; }
.tbl tbody td.cell-auto .cell-input { font-style: italic; }
.mark-auto { margin-inline-start: var(--sp-1); font-size: var(--fs-xs); font-style: italic; font-weight: 600; }
/* Saving a cell: data-save on the td. "saving" draws a pulsing dot in the top-start corner (add aria-busy="true"); "error" (the save failed: "Chưa lưu · Thử lại") is the danger tint
   and a 2px ring, like an invalid cell; "conflict" (someone else changed it first: "vừa được sửa bởi …, Tải lại ô") is the warn tint and a dashed 2px frame, so the two differ by shape
   too. The cell keeps the value the user typed until a save succeeds. The words go in a .cell-pop inside the cell (below). */
.tbl tbody td[data-save] { position: relative; }
.tbl tbody td[data-save="saving"]::after { content: ""; position: absolute; top: 3px; inset-inline-start: 3px; width: 0.4375rem; height: 0.4375rem; background: var(--info); border-radius: 50%; animation: kit-pulse 1s ease-in-out infinite; pointer-events: none; }
.tbl tbody td[data-save="error"] { background: var(--danger-bg); box-shadow: inset 0 0 0 2px var(--danger); }
.tbl tbody td[data-save="conflict"] { background: var(--warn-bg); box-shadow: none; }
.tbl tbody td[data-save="conflict"]::after { content: ""; position: absolute; inset: 0; border: 2px dashed var(--warn); pointer-events: none; }
@media (prefers-reduced-motion: reduce) { .tbl tbody td[data-save="saving"]::after { animation: none; } }
/* The message of a cell's save: div.cell-pop[popover="manual"] as the last child of the td, with .cell-pop-msg (an icon and the sentence), .cell-pop-typed (the value kept:
   "Giá trị đã gõ: 8,5, vẫn giữ trên ô") and .cell-pop-foot (Thử lại or Tải lại ô, and a close button with data-cell-pop-close). It is in the top layer, so a scroll box never clips it;
   kit.js places it under the cell (over it when there is no room), keeps it inside the window, moves it with the cell when anything scrolls and hides it while the cell is out of view
   (under a sticky part or scrolled away). It opens when data-save turns "error" or "conflict" and when the cell takes the focus or a click; Esc or the close button shut it and give the
   focus back to the cell; one is open at a time. Being in the cell, its buttons come right after the cell in the tab order. Its edge and icon take the tone of the cell's state. */
.cell-pop {
  position: fixed;
  inset: auto;
  display: none;
  gap: var(--sp-2);
  width: min(20rem, calc(100vw - 2 * var(--sp-4)));
  margin: 0;
  padding: var(--sp-3) var(--sp-4);
  overflow: visible;
  font-size: var(--fs-sm);
  font-style: normal;
  font-weight: 400;
  line-height: var(--lh);
  color: var(--text);
  text-align: start;
  white-space: normal;
  background: var(--surface);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  box-shadow: var(--sh-2);
}
.cell-pop:popover-open { display: grid; }
.cell-pop::before { content: ""; position: absolute; top: -0.375rem; left: var(--pop-arrow, 1rem); width: 0.625rem; height: 0.625rem; margin-left: -0.3125rem; background: inherit; border-top: 1px solid; border-left: 1px solid; border-color: inherit; transform: rotate(45deg); }
.cell-pop[data-side="above"]::before { top: auto; bottom: -0.375rem; transform: rotate(225deg); }
.cell-pop-msg { display: flex; align-items: flex-start; gap: var(--sp-2); margin: 0; font-weight: 600; }
.cell-pop-msg .icon { flex: none; width: var(--icon-sm); height: var(--icon-sm); margin-top: 2px; color: var(--info); }
.cell-pop-typed { margin: 0; color: var(--muted); }
.cell-pop-typed b { color: var(--text); }
.cell-pop-foot { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: var(--sp-2); padding-top: var(--sp-2); border-top: 1px solid var(--panel-border); }
td[data-save="error"] > .cell-pop { border-color: var(--danger-line); }
td[data-save="error"] > .cell-pop .cell-pop-msg .icon { color: var(--danger); }
td[data-save="conflict"] > .cell-pop { border-color: var(--warn-line); }
td[data-save="conflict"] > .cell-pop .cell-pop-msg .icon { color: var(--warn); }
/* The columns of a day sheet. td/th[data-off="half"] is a half day off (a Saturday): a lighter shade than data-off; say "T7" in the head. The head of today is
   th[aria-current="date"] (bold, a 3px --focus bar under it) and every cell of that day carries data-today (a 1px --focus line at each side, so the column reads down the grid over any
   tint). data-future on the head and the cells of the days after today: the head is plain and muted, an unmarked cell is --surface-2, so the month ahead reads as not yet. A day
   off keeps its shade when it is in the future. Before the worker joined and after the worker left are both .cell-mark-na. */
.tbl tbody td[data-off="half"] { --cell-bg: color-mix(in srgb, var(--surface-2), var(--sunk) 25%); }
.tbl thead th[data-off="half"] { background: color-mix(in srgb, var(--surface-2), var(--sunk) 50%); }
.tbl thead th[aria-current="date"] { color: var(--text); box-shadow: inset 0 -3px 0 var(--focus); }
.tbl tbody td[data-today] { position: relative; }
.tbl :is(tbody, tfoot) td[data-today]::before { content: ""; position: absolute; inset: 0; border-inline: 1px solid var(--focus); pointer-events: none; }
.tbl thead th[data-future] { font-weight: 400; color: var(--muted); background: var(--surface); }
.tbl tbody td[data-future]:where(:not([data-off])) { --cell-bg: var(--surface-2); }
.tbl tbody td[data-future]:where(:not([data-off], .cell-mark)) { background: var(--cell-bg); }
/* A value that is not final yet: td.cell-mark[data-state="open"] (a month not closed, "Chưa chốt"): the value stays, italic, inside a dashed frame in the mark's own colour; the head of
   the column says "Chưa chốt" too. */
.tbl tbody td.cell-mark[data-state="open"] { font-style: italic; }
.tbl tbody td.cell-mark[data-state="open"]::after { content: ""; position: absolute; inset: 3px; border: 1.5px dashed currentColor; border-radius: var(--radius-sm); pointer-events: none; }
/* Two lines in a cell, for a fingerprint preview (person by day): td.cell-mark.cell-mark-2l > .cell-mark-sub (the swipes, "07:28–17:05", small and muted in the mono face) +
   .cell-mark-main (the result, "8 giờ", with a small muted note in a <small>: "tăng ca 1", "thiếu 0,5", "1 lần quẹt"). Start-aligned, padded, as high as its lines. A swipe that will
   not be used is written as s.cell-mark-sub (or del): struck through, with the reason in the main line ("chấm tay · giữ" keeps the value typed by hand, "bỏ qua" skips the day). */
.tbl tbody td.cell-mark-2l { height: auto; padding: var(--sp-1) var(--sp-2); text-align: start; line-height: var(--lh-tight); }
.cell-mark-sub { display: block; font-family: var(--font-mono, ui-monospace, "SF Mono", Menlo, Consolas, monospace); font-size: var(--fs-xs); font-weight: 400; color: var(--muted); }
:is(s, del).cell-mark-sub { text-decoration: line-through; }
.cell-mark-main { display: block; }
.cell-mark-main small { margin-inline-start: var(--sp-1); font-size: var(--fs-xs); font-weight: 600; color: var(--muted); }
td.cell-mark[data-grade] .cell-mark-main small { color: inherit; } /* on a grade fill the letter takes the gated --on-grade ink; --muted fails on the dark fills */
/* A grade per month (tranxuanchien's person by twelve months): td.cell-mark[data-grade="1".."5"], an ordered tone from 1 (A+, best) to 5 (D, worst). The fill is --grade-N and the
   mark --on-grade-N; until those tokens exist the fill falls back to --ok, --ok-line, --warn-line, --danger and --danger-hover, the mark to --surface, --text, --text, --on-danger and
   --on-danger. The letter is the signal ("A+", "A", "B", "C", "D") and the colour only helps. The same tones serve .mark-strip and .legend-swatch. Month states: imported from a file
   (the head says "Từ file"; no link, since no record stands behind it), not closed (data-state="open"), future (data-future, empty), not evaluated (.cell-mark-muted with "–", as its swatch .legend-swatch-muted; the aria-label says "không đánh giá"). */
:is(td.cell-mark, .mark-strip > li, .legend-swatch)[data-grade="1"] { --grade-bg: var(--grade-1, var(--ok)); --grade-fg: var(--on-grade-1, var(--surface)); }
:is(td.cell-mark, .mark-strip > li, .legend-swatch)[data-grade="2"] { --grade-bg: var(--grade-2, var(--ok-line)); --grade-fg: var(--on-grade-2, var(--text)); }
:is(td.cell-mark, .mark-strip > li, .legend-swatch)[data-grade="3"] { --grade-bg: var(--grade-3, var(--warn-line)); --grade-fg: var(--on-grade-3, var(--text)); }
:is(td.cell-mark, .mark-strip > li, .legend-swatch)[data-grade="4"] { --grade-bg: var(--grade-4, var(--danger)); --grade-fg: var(--on-grade-4, var(--on-danger)); }
:is(td.cell-mark, .mark-strip > li, .legend-swatch)[data-grade="5"] { --grade-bg: var(--grade-5, var(--danger-hover)); --grade-fg: var(--on-grade-5, var(--on-danger)); }
.tbl tbody td.cell-mark[data-grade] { --cm-bg: var(--grade-bg); color: var(--grade-fg); }
/* A person's year on a phone: ol.mark-strip > twelve li (data-grade, data-state="open", data-future) > span.mark-strip-month ("T1", aria-hidden) + span.mark-strip-mark ("A+",
   aria-hidden) + a .visually-hidden sentence ("Tháng 1: A+"). Twelve equal columns under the name, about 26px each at 390px; an empty month is --surface-2 with no mark. */
.mark-strip { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); gap: 2px; margin: 0; padding: 0; list-style: none; }
.mark-strip > li { display: grid; gap: 2px; min-width: 0; text-align: center; }
.mark-strip-month { font-size: var(--fs-xs); line-height: var(--lh-tight); color: var(--muted); }
.mark-strip-mark { position: relative; display: grid; place-items: center; height: 1.5rem; font-size: var(--fs-xs); font-weight: 700; color: var(--grade-fg, var(--text)); background: var(--grade-bg, var(--surface-2)); border-radius: var(--radius-sm); }
.mark-strip > li[data-state="open"] .mark-strip-mark { font-style: italic; outline: 1.5px dashed currentColor; outline-offset: -3px; }

/* A key to the marks of a grid or the lines of a chart: ul.legend (or div) > li.legend-item > span.legend-swatch + the words. The swatch repeats what it explains: the same tint as the cell
   (-ok -info -bad -muted, -off for a day off, -na hatched), holding the same symbol in --text ("Đ", "P"), or, for a chart, -solid (a block of --legend-color) and -line (a stroke of it);
   --legend-color is --series-1 unless a style sets another (--series-2 .. --series-4, --series-prev). Items wrap and are plain text, not controls. The words are always there: colour and hatch
   alone are not a key. Not the chart legend of .chart-cols (.cc-legend), which stays as it is. */
.legend { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4); margin: 0; padding: 0; list-style: none; font-size: var(--fs-sm); color: var(--text); }
.legend-item { display: inline-flex; align-items: center; gap: var(--sp-2); }
.legend-swatch {
  flex: none;
  display: inline-grid;
  place-items: center;
  width: 1.5rem;
  height: 1.5rem;
  font-size: var(--fs-xs);
  font-weight: 700;
  line-height: 1;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--field-border);
  border-radius: var(--radius-sm);
}
.legend-swatch-ok { background: var(--ok-bg); border-color: var(--ok-line); }
.legend-swatch-info { background: var(--info-bg); border-color: var(--info-line); }
.legend-swatch-bad { background: var(--danger-bg); border-color: var(--danger-line); }
.legend-swatch-muted { background: var(--sunk); }
.legend-swatch-off { background: color-mix(in srgb, var(--surface-2), var(--sunk) 55%); }
.legend-swatch-na { background: repeating-linear-gradient(135deg, var(--surface) 0 4px, var(--field-border) 4px 5px); }
.legend-swatch-solid { background: var(--legend-color, var(--series-1)); border-color: transparent; }
.legend-swatch-line { height: 0; border: 0; border-top: 2px solid var(--legend-color, var(--series-1)); border-radius: 0; background: none; }
/* Swatches for the day sheet's other states, each drawn as the cell draws it: -warn (a short day), -missing (the empty dashed ring of "Chưa chấm"), -half (a Saturday), -future,
   -today (the --focus lines at the sides), -open (the dashed frame of "Chưa chốt"), [data-grade="1".."5"] (a grade tone, the letter inside), and an svg.icon inside a plain swatch
   for a corner mark (star for computed, fingerprint, spinner). */
.legend-swatch-warn { background: var(--warn-bg); border-color: var(--warn-line); }
.legend-swatch-missing::after { content: ""; width: 0.625rem; height: 0.625rem; border: 1.5px dashed var(--muted); border-radius: 50%; }
.legend-swatch-half { background: color-mix(in srgb, var(--surface-2), var(--sunk) 25%); }
.legend-swatch-future { background: var(--surface-2); }
.legend-swatch-today { box-shadow: inset 2px 0 0 var(--focus), inset -2px 0 0 var(--focus); }
.legend-swatch-open { font-style: italic; border-style: dashed; border-color: var(--text); }
.legend-swatch[data-grade] { color: var(--grade-fg); background: var(--grade-bg); border-color: transparent; }
.legend-swatch > .icon { width: 0.875rem; height: 0.875rem; color: var(--muted); }

/* 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; }

/* An inactive row (an item no longer used, a locked account, a worker who left): .row-off on a tr, or on a work-list row. Muted text and no strike, because it was not cancelled;
   say the state in a tag ("Ngừng dùng") or the row would be read as merely quiet. A filled cell keeps its colour. Voided is another thing: tr.row-void above, and for a list row
   .row-void or .work-row[data-state="void"] (below, under the work list). */
.tbl tbody tr.row-off td,
.tbl tbody tr.row-off th { color: var(--muted); }
.tbl tbody tr.row-off td.cell-fill { color: var(--fill-fg, var(--text)); }

/* 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 th:first-child,
.tbl-sticky-col.tbl-sticky tfoot :is(th, td):first-child,
.tbl-sticky-col.tbl-sticky.tbl-select thead th:nth-child(2),
.tbl-sticky-col.tbl-sticky.tbl-select tfoot :is(th, td):nth-child(2) { z-index: calc(var(--z-sticky) + 1); }

/* Two-level head, for figures that come in groups (stock: Tồn đầu, Nhập, Xuất, Tồn cuối, each over SL and Giá trị): .tbl-head-group on the <thead> (not on a tr: the rules need to tell the
   group row from the row of titles, and a class on the thead does both) holding two rows. The first row is the groups: th[scope="colgroup"][colspan] cells, centred, each with a short line
   under its own span (inset, so neighbouring groups stay apart); a first column that names the row is one th[rowspan="2"] that reads as an ordinary column title. The second row is the usual th[scope="col"]
   (class num for figures). In .tbl-sticky both rows stick, the second under the first: the group row has the height --tbl-head-row1 and the second row sticks at that offset, so they stack
   with no gap and no overlap. The default is the group row's own height from the kit's cell padding (two --sp-3 and one line of --fs-sm; two --sp-2 under .tbl-compact), which holds while the group
   names stay on one line; set --tbl-head-row1 on the table or its wrapper when the head is taller. colspan on these cells is fine: they stick to the top only, so they cannot cover the scrolling cells
   (colspan stays out of cells that stick to the start edge, see COMPONENTS.md). Under .tbl-sticky-col the first cell of the group row must not span columns. Under .tbl-cards the whole head is hidden
   as before, so the groups go with it: each td names its group in data-label ("Nhập · SL"). */
.tbl-head-group { --head-row1: var(--tbl-head-row1, calc(2 * var(--sp-3) + var(--fs-sm) * var(--lh))); }
.tbl-compact .tbl-head-group { --head-row1: var(--tbl-head-row1, calc(2 * var(--sp-2) + var(--fs-sm) * var(--lh))); }
.tbl-head-group > tr:first-child > th {
  height: var(--head-row1);
  color: var(--text);
  text-align: center;
  border-bottom: 0;
  background-image: linear-gradient(var(--field-border), var(--field-border));
  background-repeat: no-repeat;
  background-position: center bottom;
  background-size: calc(100% - var(--sp-2)) 1px;
}
.tbl-head-group > tr:first-child > th[rowspan] { height: auto; color: var(--muted); text-align: start; background-image: none; border-bottom: 1px solid var(--panel-border); }
.tbl-sticky .tbl-head-group > tr:first-child > th:not([rowspan]) { --line-b: initial; }
.tbl-sticky .tbl-head-group > tr:first-child > th[rowspan] { border-bottom: 0; }
.tbl-sticky .tbl-head-group > tr:nth-child(2) > th { top: var(--head-row1); }
/* Between groups a hairline runs down the whole table, head to foot, so a figure reads with its group (kit.js puts .col-group-start on the cells where a group begins, see
   the part in kit.js). The group row's own line under each span stays inset. */
.tbl .col-group-start { border-inline-start: 1px solid var(--panel-border); }
/* A row of titles whose first cell is under the rowspan cell is not the table's first column: it must not stick to the start edge. */
.tbl-sticky-col .tbl-head-group:has(> tr:first-child > th[rowspan]) > tr:nth-child(2) > th:first-child { left: auto; z-index: var(--z-sticky); --line-r: initial; }

/* Several columns that stay at the start, and one at the end: .tbl-sticky-cols (in place of .tbl-sticky-col) in a .tbl-scroll, with data-sticky="1" to "6" on every cell of the
   start columns, counted from the start (th and td, in the head, the body and the foot), and data-sticky="end" on the cells of one last column (Thực lĩnh). The number is the
   column's place, not the cell's: on the second row of a block (below) the first cell is column 4 and carries data-sticky="4", so it stops in the right place although the cells
   before it belong to the row above (rowspan). Offsets: kit.js measures the head's sticky cells (a ResizeObserver, so a font that loads late or a name that grows moves them) and
   writes --sticky-x2 .. --sticky-x6 on the table. Without kit.js, set the widths on the table, --sticky-w1 .. --sticky-w6 (and --sticky-w-end): each sticky cell takes its width and
   the offsets are their sums. The last start column draws a 2px --field-border line at its end and the end column one at its start, so the frozen parts read as frozen. In the head
   and the foot the sticky cells sit above the scrolling ones. A colspan over exactly the start columns is fine (a foot label, a group row: data-sticky="1" on it), since it covers
   only what is frozen anyway. Where the scroll box is under 48rem wide the end column scrolls with the others, and under 40rem only column 1 stays (four frozen columns would fill a phone); on a phone show one person at a time (a card) instead of the sheet. */
.tbl-sticky-cols {
  --sticky-x1: 0px;
  --sticky-x2: var(--sticky-w1, 0px);
  --sticky-x3: calc(var(--sticky-w1, 0px) + var(--sticky-w2, 0px));
  --sticky-x4: calc(var(--sticky-w1, 0px) + var(--sticky-w2, 0px) + var(--sticky-w3, 0px));
  --sticky-x5: calc(var(--sticky-w1, 0px) + var(--sticky-w2, 0px) + var(--sticky-w3, 0px) + var(--sticky-w4, 0px));
  --sticky-x6: calc(var(--sticky-w1, 0px) + var(--sticky-w2, 0px) + var(--sticky-w3, 0px) + var(--sticky-w4, 0px) + var(--sticky-w5, 0px));
}
/* Borders are separate here, each cell drawing only its end and bottom edge: with collapsed borders the half of a border that falls between two sticky cells belongs to neither, and
   the cells scrolling underneath showed through that seam. */
.tbl-sticky-cols { border-collapse: separate; border-spacing: 0; }
.tbl-sticky-cols.tbl-grid :is(th, td) { border-top: 0; border-left: 0; }
.tbl-scroll:has(> .tbl-sticky-cols) { container-type: inline-size; }
.tbl-sticky-cols :is(th, td) { box-shadow: var(--line-b, 0 0 transparent), var(--line-t, 0 0 transparent), var(--line-r, 0 0 transparent), var(--line-l, 0 0 transparent); }
.tbl-sticky-cols [data-sticky] { position: sticky; z-index: calc(var(--z-sticky) - 1); }
.tbl-sticky-cols :is(thead, tfoot) [data-sticky] { z-index: calc(var(--z-sticky) + 1); }
.tbl-sticky-cols [data-sticky="1"] { left: var(--sticky-x1); }
.tbl-sticky-cols [data-sticky="2"] { left: var(--sticky-x2); }
.tbl-sticky-cols [data-sticky="3"] { left: var(--sticky-x3); }
.tbl-sticky-cols [data-sticky="4"] { left: var(--sticky-x4); }
.tbl-sticky-cols [data-sticky="5"] { left: var(--sticky-x5); }
.tbl-sticky-cols [data-sticky="6"] { left: var(--sticky-x6); }
.tbl-sticky-cols [data-sticky="end"] { right: 0; --line-l: inset 2px 0 0 var(--field-border); }
.tbl-sticky-cols [data-sticky]:not([data-sticky="end"]) { --line-r: inset -1px 0 0 var(--panel-border); }
.tbl-sticky-cols [data-sticky]:not([data-sticky="end"]):not(:has(+ [data-sticky]:not([data-sticky="end"]))) { --line-r: inset -2px 0 0 var(--field-border); }
:where(.tbl-sticky-cols) :where([data-sticky="1"]) { width: var(--sticky-w1); min-width: var(--sticky-w1); max-width: var(--sticky-w1); }
:where(.tbl-sticky-cols) :where([data-sticky="2"]) { width: var(--sticky-w2); min-width: var(--sticky-w2); max-width: var(--sticky-w2); }
:where(.tbl-sticky-cols) :where([data-sticky="3"]) { width: var(--sticky-w3); min-width: var(--sticky-w3); max-width: var(--sticky-w3); }
:where(.tbl-sticky-cols) :where([data-sticky="4"]) { width: var(--sticky-w4); min-width: var(--sticky-w4); max-width: var(--sticky-w4); }
:where(.tbl-sticky-cols) :where([data-sticky="5"]) { width: var(--sticky-w5); min-width: var(--sticky-w5); max-width: var(--sticky-w5); }
:where(.tbl-sticky-cols) :where([data-sticky="6"]) { width: var(--sticky-w6); min-width: var(--sticky-w6); max-width: var(--sticky-w6); }
:where(.tbl-sticky-cols) :where([data-sticky="end"]) { width: var(--sticky-w-end); min-width: var(--sticky-w-end); max-width: var(--sticky-w-end); }
:where(.tbl-sticky-cols) :where([data-sticky][colspan]) { width: auto; min-width: 0; max-width: none; }
@container (max-width: 48rem) {
  .tbl-sticky-cols [data-sticky="end"] { right: auto; --line-l: initial; }
}
@container (max-width: 40rem) {
  .tbl-sticky-cols [data-sticky]:not([data-sticky="1"], [data-sticky="end"]) { left: auto; }
}

/* Several totals rows that all stay at the foot: tfoot.tbl-foot-rows in a .tbl-sticky table (Tổng giờ, Tổng tăng ca, Tổng suất ăn). Every foot cell is --tbl-foot-row high (default:
   two --sp-2 and one line of --fs-sm; --mark-h under .tbl-marks) with no padding above or below, one line each, so the rows stack: the last at bottom 0, the one before it one row up,
   and so on for up to four rows. A cell with rowspan over the foot rows (the grand total under the end column) sticks at bottom 0 and fills them all. CSS only: set --tbl-foot-row on
   the table or its wrapper when a foot row must be taller. */
.tbl-foot-rows { --foot-row: var(--tbl-foot-row, calc(2 * var(--sp-2) + var(--fs-sm) * var(--lh))); }
.tbl-marks .tbl-foot-rows { --foot-row: var(--tbl-foot-row, var(--mark-h)); }
.tbl .tbl-foot-rows > tr > :is(th, td) { height: var(--foot-row); padding-block: 0; font-size: var(--fs-sm); line-height: var(--lh-tight); white-space: nowrap; }
.tbl .tbl-foot-rows > tr > td:not([data-sticky], .num) { font-size: var(--fs-xs); }
.tbl .tbl-foot-rows > tr > [rowspan] { height: auto; }
.tbl-sticky .tbl-foot-rows > tr:nth-last-child(2) > * { bottom: var(--foot-row); }
.tbl-sticky .tbl-foot-rows > tr:nth-last-child(3) > * { bottom: calc(2 * var(--foot-row)); }
.tbl-sticky .tbl-foot-rows > tr:nth-last-child(4) > * { bottom: calc(3 * var(--foot-row)); }
.tbl-sticky .tbl-foot-rows > tr > :is(th, td)[rowspan] { bottom: 0; }

/* The colours of a month sheet (a .tbl-marks with sticky columns), after the minhson sheet: the head on a light tint of --primary (--sheet-head-bg) with the day numbers
   in --text and a 2px line under it; the day-off heads keep --sunk; the total column at the end (data-sticky="end", Thực lĩnh) on a tint of --sheet-accent down its whole
   length (10 %, --sheet-total-bg), its head a step stronger (18 %, --sheet-total-head-bg), so the figure that matters is found at a glance. --sheet-accent is --primary;
   a tenant with a second brand colour sets it to that colour on the table (or on :root). The <tfoot> totals stand on --surface-2 as on any table. On paper the sheet prints
   in black and white as before. */
.tbl-marks.tbl-sticky-cols {
  --sheet-head-bg: color-mix(in srgb, var(--primary) 7%, var(--surface));
  --sheet-accent: var(--primary);
  --sheet-total-bg: color-mix(in srgb, var(--sheet-accent) 10%, var(--surface));
  --sheet-total-head-bg: color-mix(in srgb, var(--sheet-accent) 18%, var(--surface));
}
.tbl-marks.tbl-sticky-cols thead th { color: var(--text); background: var(--sheet-head-bg); border-bottom: 2px solid var(--field-border); }
.tbl-marks.tbl-sticky-cols thead th[data-off] { background: var(--sunk); }
.tbl-marks.tbl-sticky-cols thead th[data-sticky="end"] { background: var(--sheet-total-head-bg); }
.tbl-marks.tbl-sticky-cols :is(tbody, tfoot) [data-sticky="end"] { background: var(--sheet-total-bg); }
.paper .tbl-marks.tbl-sticky-cols :is(thead th, [data-sticky="end"]) { background: white; }

/* One block of rows per person: tbody.tbl-block, one tbody each, its first row holding the cells that name the person with rowspan over the block (STT, Mã, Họ tên, and Thực lĩnh at
   the end), the other rows starting at the first column of their own (Giờ làm, Tăng ca, Suất ăn). A 2px --field-border line separates one block from the next; hovering any row
   lights the whole block, so the rowspan cells light with it. Two kinds of block (six rows, twelve rows) mix freely. With .tbl-sticky-cols give every cell its column number in
   data-sticky (see above) and the rowspan cells stay where they belong on every row of the block. In print a block never splits across pages (see .paper-flow). */
.tbl tbody.tbl-block + tbody.tbl-block > tr:first-child > * { border-top: 2px solid var(--field-border); }
.tbl tbody.tbl-block:hover > tr:not([aria-selected="true"]) { --row-bg: var(--surface-hover); }
/* A detail row that opens under its main row: button.row-toggle[aria-expanded][aria-controls] in the main row (kit.js flips aria-expanded and the `hidden` of the row it controls) and
   tr.row-detail[hidden] > td[colspan] holding the detail (a .tbl-compact inside is drawn as a card). Open, it sits on a tinted band with a bar at its start edge. The toggle's icon is
   caret-right and turns down when open. In a tbody.tbl-block the detail is one more row of the block (tr.block-detail is the same thing). */
.row-toggle .icon { transition: transform var(--dur-1) var(--ease); }
.row-toggle[aria-expanded="true"] .icon { transform: rotate(90deg); }
.tbl tr:is(.row-detail, .block-detail):not([hidden]) > td { padding: var(--sp-3) var(--sp-4) var(--sp-4) var(--sp-5); background: var(--surface-2); border-inline-start: 3px solid var(--primary); }
.tbl tr:is(.row-detail, .block-detail) .tbl { background: var(--surface); border: 1px solid var(--panel-border); }

/* 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;
}

/* A group heading row in any table (a team in a day sheet: "Tổ hàn 1 · 6 người"): tr.grp > th[scope="rowgroup"] (the name) + empty cells, as the first row of its group's tbody, or
   a tbody of its own before the group's blocks. --surface-2, bold, no hover. In a .tbl-sticky-cols table the name cell spans the start columns (colspan, data-sticky="1"), one empty
   td spans the scrolling columns and an empty td[data-sticky="end"] closes the row, so the frozen lines run on unbroken. */
.tbl tbody tr.grp { --row-bg: var(--surface-2); }
.tbl tbody tr.grp:hover { --row-bg: var(--surface-2); }
.tbl tbody tr.grp > * { font-weight: 700; }
.tbl tbody + tbody > tr.grp:where(:first-child) > * { border-top: 1px solid var(--field-border); }

/* Permission matrix: roles across, permissions down, one box where they meet. .tbl.tbl-matrix.tbl-sticky.tbl-sticky-col in a .tbl-scroll, so the role names stay on top and the
   permission names stay at the start edge while the rest scrolls both ways. The first column is th[scope="row"] (the permission, wrapping at --matrix-name-w, 10rem); the head is a th[scope="col"] per
   role, centred. The boxes are td.cell-check > label > input[type="checkbox"] with an aria-label that names both ("Lập phiếu thu, Kế toán"): the label fills the cell, so the target is the whole
   cell and at least --tap square, and the checkbox is centred. A group of permissions is its own <tbody> opening with tr.grp > th[scope="rowgroup"] (the group's name, in the first cell only: a
   colspan there would stick at the start edge and cover the scrolling cells) and empty td: --surface-2, bold, and no hover. A locked box, a right nobody may take away (the administrator's):
   td.cell-check[data-locked][aria-disabled="true"][title="Quản trị luôn có quyền này"] holding the real input as disabled checked, an svg.icon (lock) and, for a screen reader, a .visually-hidden
   sentence named by the input's aria-describedby; the lock beside the box says it without colour. A box just changed and not yet saved is td.cell-dirty (the kit's changed-cell tint and
   bar at the start edge) with a dot in its corner too, and "đã đổi, chưa lưu" in the input's aria-label. Counting the changes ("3 thay đổi chưa lưu") and saving belong to the page. */
.tbl-matrix thead th:not(:first-child) { text-align: center; }
.tbl-matrix.tbl-sticky-col tbody th:first-child,
.tbl-matrix.tbl-sticky-col thead th:first-child { width: var(--matrix-name-w, 10rem); min-width: var(--matrix-name-w, 10rem); max-width: var(--matrix-name-w, 10rem); }
.tbl-matrix.tbl-sticky-col tbody th:first-child { white-space: normal; }
.tbl-matrix td.cell-check { position: relative; width: auto; min-width: var(--matrix-col-w, 6rem); padding: 0; text-align: center; }
.tbl-matrix td.cell-check > label { display: grid; place-items: center; min-width: var(--tap); min-height: var(--tap); cursor: pointer; }
.tbl-matrix tbody tr.grp { --row-bg: var(--surface-2); }
.tbl-matrix tbody tr.grp:hover { --row-bg: var(--surface-2); }
.tbl-matrix tbody tr.grp > * { font-weight: 700; }
.tbl-matrix tbody + tbody tr.grp > * { border-top: 1px solid var(--panel-border); }
.tbl-matrix td.cell-check[data-locked] > label { cursor: not-allowed; }
.tbl-matrix td.cell-check[data-locked] input { cursor: not-allowed; }
.tbl-matrix td.cell-check[data-locked] .icon { position: absolute; top: 50%; left: calc(50% + 0.875rem); width: var(--icon-sm); height: var(--icon-sm); color: var(--muted); transform: translateY(-50%); }
.tbl-matrix td.cell-check.cell-dirty::after { content: ""; position: absolute; top: var(--sp-2); inset-inline-end: var(--sp-2); width: 0.5rem; height: 0.5rem; background: var(--warn); border-radius: 50%; }

/* 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, a check cell goes to the corner and a <tfoot> of totals is the last card. 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. --cards-cols (default 2) is the number of columns the cells
   share under the title: a table of a few short figures (a date, a quantity, a price, an amount) is shorter as a card with --cards-cols: 3 on the .tbl-cards wrapper, about 90px a cell at 390px, so keep each
   figure to nine characters or write it short (k, tr). */
.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(var(--cards-cols, 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); }
  .tbl-cards .tbl tbody tr.row-new { box-shadow: inset var(--sp-1) 0 0 var(--primary); }
  .tbl-cards .tbl tbody tr.row-new > :first-child { box-shadow: none; }
  .tbl-cards .tbl tbody tr.row-open td:empty { display: none; }
  /* A <tfoot> of totals becomes the last card, in the stronger edge of a total, titled by its th; give its figures a data-label as the other cells have. */
  .tbl-cards .tbl tfoot { display: block; padding: 0 var(--sp-3) var(--sp-3); background: var(--surface-2); }
  .tbl-cards .tbl tfoot tr { display: grid; grid-template-columns: repeat(var(--cards-cols, 2), minmax(0, 1fr)); gap: var(--sp-2) var(--sp-4); padding: var(--sp-4); background: var(--surface); border: 1px solid var(--field-border); border-radius: var(--radius); }
  .tbl-cards .tbl tfoot th { grid-column: 1 / -1; }
  .tbl-cards .tbl tfoot td:empty { display: none; }
}

/* 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; }

/* The formula bar over a sheet: .tbl-formula (role="group" with an aria-label) > .tbl-formula-ref (where the current cell is, in words: "Trần Thị Lan · Giờ làm · ngày 12"),
   output.tbl-formula-value (its value, in a field-like box), p.tbl-formula-note (an svg.icon and a span: how the value came about, "Tự tính từ vân tay 07:28–17:05", cut with "…")
   and .tbl-formula-end (buttons). The page fills it when the current cell moves; it is not a live region, since the cell itself is read when it takes the focus. */
.tbl-formula { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2) var(--sp-3); min-height: calc(var(--tap) + var(--sp-1)); padding: var(--sp-2) var(--sp-4); font-size: var(--fs-sm); border-bottom: 1px solid var(--panel-border); }
.tbl-formula-ref { flex: none; min-width: 12rem; padding: var(--sp-1) var(--sp-2); font-weight: 600; color: var(--text); background: var(--sunk); border-radius: var(--radius-sm); }
.tbl-formula-value { flex: none; min-width: 4.5rem; padding: var(--sp-1) var(--sp-2); font-weight: 700; font-variant-numeric: tabular-nums; color: var(--text); background: var(--surface); border: 1px solid var(--field-border); border-radius: var(--radius-sm); }
.tbl-formula-note { display: flex; flex: 1 1 12rem; align-items: center; gap: var(--sp-1); min-width: 0; margin: 0; color: var(--muted); }
.tbl-formula-note > .icon { flex: none; width: var(--icon-sm); height: var(--icon-sm); }
.tbl-formula-note > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tbl-formula-end { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-inline-start: auto; }
/* Full screen for a wide sheet: a button[data-fullscreen="<id of the panel>"] with icons.svg#corners-out and its words, optional data-fullscreen-off="Thu nhỏ" (kit.js swaps icon and words) puts data-fullscreen-on on the panel, which then covers the window
   (fixed, the dialog layer, no rounding) with its scroll box filling what is left, so the page around it (sidebar, top bar, the cards above) is out of the way; the page behind does
   not scroll. The same button, Esc (when no popover or dialog is open) or leaving the page turns it off, and the focus stays on the button. Keep the button inside the panel. */
[data-fullscreen-on] { position: fixed; inset: 0; z-index: var(--z-dialog); display: flex; flex-direction: column; width: auto; max-width: none; margin: 0; border-radius: 0; }
[data-fullscreen-on] > .panel-body { display: flex; flex: 1 1 auto; flex-direction: column; min-height: 0; }
[data-fullscreen-on] .tbl-scroll { flex: 1 1 auto; max-height: none; min-height: 0; }
:root:has([data-fullscreen-on]) { overflow: hidden; }

/* 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); }
/* A range of cells being picked to paste into or fill: data-range on each cell of the range, its value the edges of the range the cell lies on ("top start", "bottom", "" inside).
   The range takes the selected tint (--notice-bg) over any tone and a 2px --focus line round its outside; the current cell keeps its own outline (aria-selected). */
.tbl tbody td[data-range] { background: var(--notice-bg); box-shadow: var(--range-t, 0 0 transparent), var(--range-b, 0 0 transparent), var(--range-s, 0 0 transparent), var(--range-e, 0 0 transparent); }
.tbl tbody td[data-range~="top"] { --range-t: inset 0 2px 0 var(--focus); }
.tbl tbody td[data-range~="bottom"] { --range-b: inset 0 -2px 0 var(--focus); }
.tbl tbody td[data-range~="start"] { --range-s: inset 2px 0 0 var(--focus); }
.tbl tbody td[data-range~="end"] { --range-e: inset -2px 0 0 var(--focus); }

/* 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; }

/* The number of a voucher or an order (PC-2026-0412, DH-0087): .doc-code is the code in the mono face, bold, tabular, on one line and with no box, so it reads as a code inside a
   heading, a table cell or a list row; --font-mono sets the face, as for .code-chip. .doc-code-solid is the same code on a dark plate, for the number at the top of a voucher:
   --heading as the fill and --surface as the text, the pair the palette gate already checks in every palette. It is not .code-chip, which is a boxed mark of two to four
   characters. Printing: a background is often not printed, so the plate becomes an outline in the text colour. Put the code in a span; read as text, it needs no label. */
.doc-code {
  font-family: var(--font-mono, ui-monospace, "SF Mono", Menlo, Consolas, monospace);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
  white-space: nowrap;
}
.doc-code-solid {
  display: inline-block;
  padding: var(--sp-1) var(--sp-3);
  line-height: var(--lh-tight);
  color: var(--surface);
  background: var(--heading);
  border-radius: var(--radius-sm);
}
@media print {
  .doc-code-solid { color: var(--heading); background: none; border: 2px solid currentColor; }
}

/* A rubber stamp for the state of a whole document: span.stamp ("TẠM TÍNH", "ĐÃ CHỐT", "ĐÃ HỦY"), upper case, spaced, in a double rule of its own colour: -warn for a provisional
   figure (tạm tính), -ok for a closed one (đã chốt), -bad for a cancelled one; plain is --text. -lg is the big one at the head of a sheet or a page, tilted two degrees. The words
   are the state; the colour only helps. On a .paper and in print it is black on white (the sheet's two colours), so a black-and-white copy keeps it. */
.stamp {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  padding: var(--sp-1) var(--sp-3);
  font-size: var(--fs-sm);
  font-weight: 700;
  line-height: var(--lh-tight);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--text);
  background: var(--surface);
  border: 2px solid currentColor;
  border-radius: var(--radius-sm);
  box-shadow: inset 0 0 0 2px var(--surface), inset 0 0 0 3px currentColor;
}
.stamp .icon { width: 1.1em; height: 1.1em; }
.stamp-warn { color: var(--warn); }
.stamp-ok { color: var(--ok); }
.stamp-bad { color: var(--danger); }
.stamp-lg { padding: var(--sp-2) var(--sp-4); font-size: var(--fs-lg); transform: rotate(-2deg); }
.paper .stamp { color: black; background: white; box-shadow: inset 0 0 0 2px white, inset 0 0 0 3px black; }
@media print {
  .stamp { color: black; background: white; box-shadow: inset 0 0 0 2px white, inset 0 0 0 3px black; }
}

/* 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" (a bar that is the only source of a state is a progressbar: see below). --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); }

/* Column chart --------------------------------------------------------------- */

/* Grouped columns drawn in HTML and CSS only (no svg, so the text never shrinks with a viewBox): one group per category (a month), one column per series, a tick across a column for the same
   period a year ago. The look app keeps its own single-series chart under .chart, .chart-plot, .chart-col; this one is .chart-cols and its parts are .cc-*, so neither reaches the other.
     figure.chart-cols > figcaption.cc-title (what it shows) + ul.cc-legend > li > span.cc-swatch[data-series="1"] + the series' word ("Thu"); the last item may be span.cc-swatch.cc-swatch-prev
     "Cùng kỳ năm trước"; then div.cc-scroll > div.cc-body > ol.cc-axis[aria-hidden="true"] > li[style="--t:.5"] (a tick label such as "200 tr", --t its place from 0 to 1 of the axis maximum) and
     div.cc-plot[role="img"][aria-label="…the numbers in words…"] > div.cc-group > div.cc-cols > .cc-col[data-series="1"][style="--v:.78"] (a span, or an a: the link covers the group's full height) holding
     span.visually-hidden ("Thu tháng 9: 312,5 triệu") and optionally span.cc-mark[aria-hidden="true"][style="--v:.62"] (the previous period, at 62% of the axis maximum), then div.cc-label ("T9").
   --v is the share of the axis maximum, 0 to 1. The axis and the plot must agree: --cc-rows (the gaps between tick lines, default 4) is set on the figure with the ticks at 0, 1/rows ... 1; the
   gridlines are drawn from --cc-rows, in --panel-border, with the baseline in --field-border. --cc-h sets the plot's height (13rem), --cc-group-w the least width of a group (3.25rem), --cc-col-max
   the widest a column grows (2.25rem), --cc-bg the card's ground (--surface; the stripes and the axis cover are drawn in it).
   Colour is never the only cue: series 1 is solid, 2 diagonal stripes, 3 dots, 4 vertical stripes (all from --series-1 .. --series-4, which every palette sets and check-palette.mjs holds to 3:1 on
   --surface and --bg), the legend repeats the pattern and the word, and the previous period is a tick in --series-prev edged in --cc-bg. The figure's words are on .cc-plot, not on the figure: a
   scroll box must be focusable (a keyboard user scrolls it), and nothing focusable may sit inside role="img". So .cc-scroll takes tabindex="0", role="region" and an aria-label whenever the chart
   may scroll (more groups than fit the narrowest screen), and .cc-plot is role="img" with the whole series in its aria-label; when the columns are links (a.cc-col), the plot is role="group" with an aria-label and each link is
   named by its .visually-hidden words. Either way the real content is the table: follow the figure with a .tbl (or a table.visually-hidden) of the same numbers. At least --cc-group-w per group, so
   twelve months on a phone scroll inside .cc-scroll with the axis kept at the start edge (position: sticky), never the page; under a finger a link column is --tap wide. An empty chart has no
   .cc-group: put p.cc-empty in the plot. */
.chart-cols { --cc-h: 13rem; --cc-label-h: 1.75rem; --cc-axis-w: 3.75rem; --cc-group-w: 3.25rem; --cc-rows: 4; --cc-bg: var(--surface); margin: 0; }
.cc-title { margin-bottom: var(--sp-2); font-size: var(--fs-md); font-weight: 700; }
.cc-legend { display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-4); margin: 0 0 var(--sp-3); padding: 0; list-style: none; font-size: var(--fs-sm); }
.cc-legend li { display: inline-flex; align-items: center; gap: var(--sp-2); }
.cc-swatch { flex: none; width: 0.875rem; height: 0.875rem; background: var(--cc-fill); border-radius: 3px; }
.cc-swatch-prev { width: 1.25rem; height: 3px; background: var(--series-prev); border-radius: 1px; }
.cc-col, .cc-swatch { --cc-fill: var(--series-1); }
.chart-cols [data-series="2"] { --cc-fill: repeating-linear-gradient(135deg, var(--series-2) 0 4px, var(--cc-bg) 4px 6px); }
.chart-cols [data-series="3"] { --cc-fill: radial-gradient(circle, var(--cc-bg) 1.5px, transparent 2px) 0 0 / 7px 7px, var(--series-3); }
.chart-cols [data-series="4"] { --cc-fill: repeating-linear-gradient(90deg, var(--series-4) 0 4px, var(--cc-bg) 4px 6px); }
.cc-scroll { overflow-x: auto; }
.cc-body { display: flex; width: max-content; min-width: 100%; padding-top: var(--sp-2); }
.cc-axis {
  position: sticky;
  left: 0;
  z-index: 2;
  flex: none;
  width: var(--cc-axis-w);
  height: calc(var(--cc-h) + var(--cc-label-h));
  margin: 0;
  padding: 0;
  list-style: none;
  background: var(--cc-bg);
}
.cc-axis li {
  position: absolute;
  right: var(--sp-2);
  bottom: calc(var(--cc-label-h) + var(--t, 0) * var(--cc-h));
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
  line-height: 1;
  color: var(--muted);
  white-space: nowrap;
  transform: translateY(50%);
}
.cc-plot { position: relative; display: flex; flex: 1 1 auto; min-height: calc(var(--cc-h) + var(--cc-label-h)); }
.cc-plot::before {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: var(--cc-h);
  box-sizing: border-box;
  background: linear-gradient(to bottom, var(--panel-border) 1px, transparent 1px) 0 0 / 100% calc(100% / var(--cc-rows)) repeat-y;
  border-bottom: 1px solid var(--field-border);
}
.cc-group { position: relative; z-index: 1; display: grid; grid-template-rows: var(--cc-h) var(--cc-label-h); flex: 1 0 var(--cc-group-w); padding-inline: var(--sp-1); }
.cc-cols { display: flex; align-items: stretch; justify-content: center; gap: 2px; min-height: 0; }
.cc-col { position: relative; display: flex; flex: 1 1 0; align-items: flex-end; min-width: 0; max-width: var(--cc-col-max, 2.25rem); color: inherit; text-decoration: none; }
.cc-col::before { content: ""; flex: none; width: 100%; height: calc(var(--v, 0) * 100%); background: var(--cc-fill); border-radius: var(--radius-sm) var(--radius-sm) 0 0; }
a.cc-col:hover { background: var(--surface-hover); }
a.cc-col:focus-visible { outline-offset: 1px; }
.cc-mark { position: absolute; inset-inline: 0; bottom: calc(var(--v, 0) * 100%); height: 3px; margin-bottom: -1px; background: var(--series-prev); border-radius: 1px; box-shadow: 0 0 0 1px var(--cc-bg); pointer-events: none; }
.cc-label { display: flex; align-items: center; justify-content: center; font-size: var(--fs-xs); color: var(--muted); white-space: nowrap; }
.cc-empty { position: absolute; inset: 0 0 var(--cc-label-h); z-index: 1; display: grid; place-items: center; padding: var(--sp-3); font-size: var(--fs-sm); text-align: center; color: var(--muted); }

/* Rank list ------------------------------------------------------------------ */

/* The biggest items first, as rows with a bar: top 8 expenses, top 10 materials, hours by order. ul.rank-list > li > a.rank-row (a div.rank-row when the row goes nowhere) > span.rank-no (optional: the place,
   muted, "1"; aria-hidden when the order is plain from the list, since an ol or the words already say it) + span.rank-name + span.rank-sub (optional, muted: "trả NCC · 42 phiếu") + span.bar.rank-bar
   [aria-hidden="true"][style="--v:62%"] (the kit's .bar, its width from --v: this row's share of the biggest row) + span.rank-val (the figure with .num-unit, never wraps, --font-figure). The whole row is the link:
   at least --tap high, hover tint --surface-hover, the kit's focus ring drawn inside the row; its text stays --text and --muted, the two colours gated on the hover ground. The bar is decoration and repeats the
   figure, so a state it shows in colour (data-tone="ok|warn|bad" on the row; the default is --primary) must also be said in words, in .rank-sub or a .visually-hidden span in the name.
   Wide: [no] name over sub, bar, value in one line. When the list is under 28rem wide (a container query on the list, not the window, so a narrow column of a wide page counts) the bar drops under the name and
   the value stays at the end of the name's line. The list is the container (container-type: inline-size), so give it a width, as every block in a column has; a shrink-to-fit parent would make it 0. */
.rank-list { container-type: inline-size; margin: 0; padding: 0; list-style: none; }
.rank-list > li { border-bottom: 1px solid var(--panel-border); }
.rank-list > li:last-child { border-bottom: 0; }
.rank-row {
  --rank-val-w: 6.5rem;
  display: grid;
  grid-template-columns: 1.75rem minmax(0, 1.4fr) minmax(5rem, 1fr) minmax(var(--rank-val-w), auto);
  grid-template-areas: "no name bar val" "no sub bar val";
  align-items: center;
  column-gap: var(--sp-3);
  min-height: var(--tap);
  padding: var(--sp-2) var(--sp-3);
  color: var(--text);
  text-decoration: none;
}
.rank-row:not(:has(.rank-no)) {
  grid-template-columns: minmax(0, 1.4fr) minmax(5rem, 1fr) minmax(var(--rank-val-w), auto);
  grid-template-areas: "name bar val" "sub bar val";
}
a.rank-row { transition: background-color var(--dur-1) var(--ease); }
a.rank-row:hover { background: var(--surface-hover); }
a.rank-row:focus-visible { outline-offset: -3px; }
.rank-no { grid-area: no; font-size: var(--fs-sm); font-variant-numeric: tabular-nums; color: var(--muted); }
.rank-name { grid-area: name; min-width: 0; font-weight: 600; overflow-wrap: anywhere; }
.rank-sub { grid-area: sub; min-width: 0; font-size: var(--fs-sm); color: var(--muted); overflow-wrap: anywhere; }
.rank-bar { grid-area: bar; --bar-w: 100%; --bar-h: 0.5rem; }
.rank-row[data-tone="ok"] .rank-bar { --bar-fill: var(--ok); }
.rank-row[data-tone="warn"] .rank-bar { --bar-fill: var(--warn); }
.rank-row[data-tone="bad"] .rank-bar { --bar-fill: var(--danger); }
.rank-val { grid-area: val; justify-self: end; font-family: var(--font-figure, inherit); font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
@container (max-width: 28rem) {
  .rank-row { grid-template-columns: 1.75rem minmax(0, 1fr) auto; grid-template-areas: "no name val" "no sub sub" "no bar bar"; }
  .rank-row:not(:has(.rank-no)) { grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "name val" "sub sub" "bar bar"; }
  .rank-bar { margin-top: var(--sp-2); }
  .rank-val { align-self: start; }
}

/* 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); }
/* More for a chip. .chip-count after the label is the number behind it (Tất cả 24, Đang làm 20): tabular digits and a quieter weight, in the chip's own colour so it stays readable on the
   chosen chip too. When the number needs a word for a screen reader put a .visually-hidden one in it ("20<span class="visually-hidden"> việc đang làm</span>").
   .chip-group-scroll keeps the group on one line that scrolls sideways, for ten or so chips on a phone: on the group (a .chip-group, role="radiogroup" or role="group" with an
   aria-label), the chips do not shrink or wrap. Let the last chip in view be cut by the edge, so the row plainly goes on. The padding keeps the focus ring of the first and last chip
   from being clipped by the scroll box, and the negative margin gives it back so the chips line up with the text around (the row reaches 8px past its container on each side, so keep it inside a page gutter or a card's padding). A row of chips that can be focused (radios, buttons,
   links) is already reachable by keyboard and scrolls to the chip in focus; add tabindex="0" to the row only when no chip in it can take the focus. Scroll the chosen chip into view when
   the choice is set from elsewhere (chip.scrollIntoView({ block: "nearest", inline: "nearest" })): that is the tenant's code. A row that has a .chip-add at its end is a
   role="group" holding a .chip-group[role="radiogroup"] (the radios) and then the button, because a button is not a radio.
   .chip-add is a chip that does something ("+ Thêm tổ") instead of being a value: a dashed edge, a plus icon and the link colour. It is a button (type="button") with no aria-checked or
   aria-pressed, and no role="radio"; .chip-dashed is a different thing, a value chip of another kind.
   a.chip is a chip that goes somewhere ("2 việc có vấn đề" opening the list filtered): the same look and the same --tap height under a finger, without the underline. */
.chip-count { font-weight: 500; font-variant-numeric: tabular-nums; }
a.chip { text-decoration: none; }
.chip-add { color: var(--link); border-style: dashed; }
.chip-group-scroll {
  flex-wrap: nowrap;
  margin: calc(var(--sp-2) * -1);
  padding: var(--sp-2);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-padding-inline: var(--sp-2);
  scrollbar-width: thin;
}
.chip-group-scroll > .chip-group { flex: none; flex-wrap: nowrap; }
.chip-group-scroll .chip { flex: none; white-space: nowrap; }

/* 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); }
/* Smaller choice cards for a pick among many short names, such as the fund of a voucher: .choice-cards.choice-cards-compact > label.choice-card > input[type="radio"] + svg.icon + .choice-title
   + .choice-sub (one muted line: the balance). Cards are 7.5rem or wider, so a phone fits two across. It is the same radiogroup as .choice-cards (role="radiogroup" with aria-labelledby,
   one name on the radios), so the keyboard is the radios' own: Tab enters the group, the arrows move and choose. A choice that is wrong (a withdrawal larger than the balance) puts
   aria-invalid="true" on the card's input (or on the label): the card takes a danger edge and tint, and the words go in a p.field-error under the group, named by the input's
   aria-describedby, so the colour is never alone. Clear it when the choice changes. */
.choice-cards-compact { grid-template-columns: repeat(auto-fill, minmax(min(100%, 6.5rem), 1fr)); gap: var(--sp-2); }
.choice-cards-compact .choice-card { gap: var(--sp-1); min-height: 0; padding: var(--sp-3); }
.choice-cards-compact .choice-card > .icon { color: var(--muted); }
.choice-cards-compact .choice-card:has(input:checked) > .icon { color: var(--text); }
.choice-cards-compact .choice-title { padding-inline-end: 0; }
.choice-cards-compact .choice-card:is(:has(input[aria-invalid="true"]), [aria-invalid="true"]) { background: var(--danger-bg); border-color: var(--danger); box-shadow: inset 0 0 0 1px var(--danger); }
.choice-cards-compact .choice-card:is(:has(input[aria-invalid="true"]), [aria-invalid="true"]) > .icon { color: var(--danger); }

/* Two fields that name a direction, "Từ" and "Đến" (a transfer between funds, a move between stores), with a button between them that swaps what they hold:
   .pair-pick[data-pair-swap] > .field + button.btn.btn-icon.pair-swap[aria-label="Đổi chiều"] > svg.icon (arrows-left-right) + .field. Each .field is a label and one picker: a plain select,
   a .combo, a radiogroup of choice cards or a text input (kit.js swaps what they hold and fires input and change on both: it needs the same options in both, else it does nothing).
   Side by side while the group is 28rem wide or more, the button between them on the line of the controls; under that the fields stack and the button sits between them with its arrows turned
   to point up and down. Keep a hint or an error out of the two fields (put them under the group), so the button stays level with the controls; beside two groups of cards it is centred instead. */
.pair-pick { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: end; gap: var(--sp-3); container-type: inline-size; }
@container (max-width: 28rem) {
  .pair-pick > * { grid-column: 1 / -1; }
  .pair-pick > .pair-swap { justify-self: center; }
  .pair-pick > .pair-swap .icon { rotate: 90deg; }
}
.pair-pick:has(.choice-cards) > .pair-swap { align-self: center; }

/* 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); }

/* A time of day typed on the 24-hour clock, as 14:00: input.input.input-time[data-time] (kit.js reads it). It is no wider than five digits need, and the digits are tabular so
   11:11 and 22:05 line up in a column of fields. */
.input-time { max-width: 8rem; font-variant-numeric: tabular-nums; }

/* A whole number in a short range, with a minus and a plus button: .number-stepper[data-stepper] > button.btn.btn-icon[data-step="-1"], input.input, button.btn.btn-icon[data-step="1"]
   (kit.js reads data-min, data-max, data-step-by). At the end of the range the button that would leave it gets aria-disabled="true", so it keeps the focus. */
.number-stepper { display: inline-flex; align-items: center; gap: var(--sp-1); }
.number-stepper .input { width: 4rem; text-align: center; font-variant-numeric: tabular-nums; }
.number-stepper .btn[aria-disabled="true"] { background: none; opacity: 0.4; cursor: not-allowed; }
/* A unit after the number ("g", "giờ"): .number-stepper > input.input + .affix, before the plus button. The unit joins the field's frame and shares its height. kit.js reads
   data-decimals on the .number-stepper (with it data-step-by, data-min and data-max may be 0,5) and then shows and parses a comma. */
.number-stepper > .input:has(+ .affix) { border-start-end-radius: 0; border-end-end-radius: 0; }
.number-stepper > .input + .affix { align-self: stretch; margin-inline-start: calc(var(--sp-1) * -1); border-inline-start: 0; border-start-end-radius: var(--radius); border-end-end-radius: var(--radius); }

/* 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); }

/* Amount typed on a phone: the large field. input.input.input-lg is a figure at about 2.25rem in a box 3rem high, right-aligned with tabular digits, for a field that holds one
   number the user keys with a thumb (an amount, a quantity). In a .input-affix its unit ("đ") grows with it. The coarse-pointer font rule is passed by writing the class pair. A
   figure longer than the box scrolls inside the field. */
.input.input-lg {
  min-height: 3rem;
  padding-block: 0;
  font-family: var(--font-figure, inherit);
  font-size: 2.25rem;
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
  text-align: end;
}
.input-affix:has(> .input-lg) > .affix { min-width: 3rem; justify-content: center; font-size: var(--fs-xl); font-weight: 600; }

/* Quick keys under an amount field (kit.js reads data-money-keys, or the sibling [data-money] in the same .field): .money-keys > button.btn[data-append="000"] and button.btn[data-clear],
   one row sharing the width, each key --tap plus 4px high and wide (48px). Each key's text says what it does and carries an aria-label ("Thêm 000", "Xóa số tiền"). */
.money-keys { display: flex; gap: var(--sp-2); }
.money-keys .btn { flex: 1 1 0; min-width: calc(var(--tap) + 4px); min-height: calc(var(--tap) + 4px); font-variant-numeric: tabular-nums; }

/* 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); }
/* A segmented choice that fills its row, for a few options picked with a thumb: .seg.seg-block. The group takes the full width of its container and the items share it equally, each
   --tap high and its label on one line, cut with "…" when it does not fit (keep each label to about ten characters: four items, "Không phép" the longest, fit a 358px card with 12px of padding at 390px, in the app look's 14px text too; in a card with 20px of padding they get 79px each and the longest label is cut). An icon before the label is
   an svg.icon in the item. Same roles as .seg: role="radiogroup" with an aria-label, role="radio" and aria-checked on each item, one tab stop, arrow keys. A disabled item is
   disabled (the arrow keys skip it) or aria-disabled="true" and the tenant's script ignores it; say why next to the group, since a dimmed item explains nothing.
   data-tone="ok|warn|bad|info" on an item gives the chosen state its own colour (Thu green, Chi amber, Chuyển blue; Có mặt, Nghỉ phép, Không phép): the tone's -bg as the fill, the tone
   as the text and a 2px edge all round, so the chosen item differs from the others by shape and weight as well as hue, and its words say what it is. No tone: the plain look above.
   Pairs gated by check-palette.mjs: each tone on its -bg, and the tone on --sunk for the edge. */
.seg-item .icon { display: inline-block; vertical-align: -0.2em; margin-inline-end: var(--sp-1); }
.seg-item[data-tone="ok"] { --seg-tone: var(--ok); --seg-tone-bg: var(--ok-bg); }
.seg-item[data-tone="warn"] { --seg-tone: var(--warn); --seg-tone-bg: var(--warn-bg); }
.seg-item[data-tone="bad"] { --seg-tone: var(--danger); --seg-tone-bg: var(--danger-bg); }
.seg-item[data-tone="info"] { --seg-tone: var(--info); --seg-tone-bg: var(--info-bg); }
.seg-item[data-tone][aria-checked="true"] { color: var(--seg-tone); background: var(--seg-tone-bg); box-shadow: inset 0 0 0 2px var(--seg-tone); }
.seg-block { display: flex; width: 100%; }
.seg-block .seg-item { flex: 1 1 0; min-width: 0; min-height: var(--tap); padding-inline: 0; overflow: hidden; text-align: center; text-overflow: ellipsis; }
.seg-item:disabled,
.seg-item[aria-disabled="true"],
.seg-item:disabled:hover,
.seg-item[aria-disabled="true"]:hover { color: var(--muted); opacity: 0.5; cursor: not-allowed; }

/* 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); }
/* -neutral: grey, with a lock icon (icons.svg#lock) in the icon slot, for something that is shut, blocked or read-only ("Kỳ đã khóa, không sửa được", "Tài khoản bị khóa"). It is a
   state of the page, not news: give it role="status" or no role, never role="alert" (nothing needs to be read out at once), and let the lock and the words say it, not the grey. The
   icon is markup like the other kinds', so a notice without svg.notice-icon is the same box without it. Colours: --surface-2 with --panel-border, --muted for the icon and bar,
   --text for the title (text and muted on --surface-2 are gated in every palette). */
.notice-neutral { --notice-accent: var(--muted); background: var(--surface-2); border-color: var(--panel-border); }
.notice-neutral .notice-title { color: var(--text); }
.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-real-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) + var(--action-bar-extra, 0px) + 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 ------------------------------------------------- */

/* --topbar-h is the bar's minimum height. The bar is as tall as its tallest control plus its padding and its 1px line, which is more than 56px (a 44px button gives 61px, the app look's 48px
   button 65px, its 52px touch button 69px), so what sticks below the bar (a toast, .page-side, the scroll margin of a heading) uses --topbar-real-h, the larger of the two. A bar that wraps to a
   second row is taller still: the tenant sets --topbar-h (or --sticky-top on the part that sticks) to what it measures. */
:root { --topbar-real-h: max(var(--topbar-h), calc(var(--tap) + 2 * var(--sp-2) + 1px)); }
/* 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-real-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; }
/* A menu for long sentences (a demo script, a help list): .menu-wide is up to 26rem wide and its items wrap instead of running on one line. */
.menu-wide { width: min(26rem, calc(100vw - 2 * var(--sp-4))); }
.menu-wide .menu-item { white-space: normal; }

/* 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; }

/* Two-line value, groups and an "add new" row of the dropdown with search.
   Two lines: put data-combo-two-line on .combo and a div.combo-value[aria-hidden="true"] (with .combo-value-main and .combo-value-sub, empty) after the input. The input stays the
   control (typing, aria, its value is the main text, the sub line is its aria-describedby); while the list is closed and an item is chosen, kit.js shows the two lines over it and the
   input's own text is transparent. The sub line is the .combo-sub of the chosen option. The field is as high as two lines, empty or not, so the form does not jump on a choice;
   each line ends in "…". In the list, the sub line of an option goes under its text.
   Groups: li.combo-group[role="presentation"] > .combo-group-title (+ .combo-group-desc) before the options of its group; it is not an option (arrows skip it), and kit.js hides it
   when the filter leaves none of its options. The title is visual only, so word an option so that it stands alone when the group is not read out.
   Add row: li.combo-option.combo-add[role="option"] last in the list, with svg.icon (plus) then the words. It is reached by the arrows, is never the value, stays in view at the foot
   of a scrolling list, and on Enter or click kit.js closes the list and fires a bubbling `combo-add` event on .combo with detail.query (what was typed). */
.combo-value { display: none; }
.combo[data-combo-two-line] .input { height: max(var(--tap), calc(2 * var(--sp-2) + 2 * var(--border-w, 1px) + (var(--fs-base) + var(--fs-sm)) * var(--lh-tight))); }
.combo[data-combo-two-line][data-has-value]:not(:has([aria-expanded="true"])) .input { color: transparent; caret-color: transparent; }
.combo[data-combo-two-line][data-has-value]:not(:has([aria-expanded="true"])) .combo-value {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-width: 0;
  padding-inline: calc(var(--sp-3) + var(--border-w, 1px)) calc(var(--sp-3) + var(--icon) + var(--sp-2) + var(--border-w, 1px));
  line-height: var(--lh-tight);
  pointer-events: none;
}
.combo-value-main,
.combo-value-sub { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.combo-value-main { font-size: var(--fs-base); color: var(--text); }
.combo-value-sub { font-size: var(--fs-sm); color: var(--muted); }
.combo:has(.input:disabled) .combo-value-main { color: var(--muted); }
.combo[data-combo-two-line] .combo-option { display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: var(--sp-3); padding-block: var(--sp-2); }
.combo[data-combo-two-line] .combo-option::after { grid-column: 2; grid-row: 1 / span 2; }
.combo[data-combo-two-line] .combo-sub { grid-column: 1; margin: 0; }
.combo-group { padding: var(--sp-2) var(--sp-3) var(--sp-1); }
.combo-group:not(:first-child) { margin-top: var(--sp-2); }
.combo-group[hidden] { display: none; }
.combo-group-title,
.combo-group-desc { display: block; font-size: var(--fs-xs); color: var(--muted); }
.combo-group-title { font-weight: 600; }
.combo-add {
  position: sticky;
  bottom: calc(-1 * var(--sp-1));
  gap: var(--sp-2);
  margin: var(--sp-1) calc(-1 * var(--sp-1)) calc(-1 * var(--sp-1));
  padding-inline: calc(var(--sp-3) + var(--sp-1));
  font-weight: 600;
  color: var(--primary);
  background: var(--surface);
  border-top: 1px solid var(--panel-border);
  border-radius: 0;
}
.combo[data-combo-two-line] .combo-add { display: flex; }
/* The add row keeps the surface under it (the primary colour is not gated on the hover ground): hover underlines the words, the key-selected row gets the focus ring inside. */
.combo-option.combo-add:hover:not([aria-disabled="true"]),
.combo-option.combo-add[data-active="true"] { background: var(--surface); text-decoration: underline; }
.combo-option.combo-add[data-active="true"] { box-shadow: inset 0 0 0 2px var(--focus); }

/* 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); }

/* Month picker: the date picker's field and popover with twelve months instead of a day grid and the year in the head (.datepicker[data-monthpicker], same .calendar shell). */
.month-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--sp-1); }
.month-btn {
  height: 2.5rem;
  padding: 0;
  font: inherit;
  font-size: var(--fs-sm);
  color: var(--text);
  background: none;
  border: 0;
  border-radius: var(--radius);
  cursor: pointer;
}
.month-btn:hover { background: var(--surface-hover); }
.month-btn[aria-current="date"] { font-weight: 700; box-shadow: inset 0 0 0 1px var(--field-border); }
.month-btn[aria-pressed="true"] { color: var(--on-primary); background: var(--primary); }
.month-btn[aria-pressed="true"]:hover { background: var(--primary-hover); }
.month-btn[aria-disabled="true"] { color: var(--muted); text-decoration: line-through; cursor: not-allowed; }
.month-btn[aria-disabled="true"]:hover { background: none; }

/* Quarter picker: the month picker's field and popover with four quarters in place of twelve months (.datepicker[data-quarterpicker], same .calendar shell, the year in the head). The grid is two
   by two, and each button is "Quý 3" with its months under it (.month-btn-sub), so it is a wider, taller target than a month; the months line is muted, and takes the button's own colour
   when the quarter is the chosen one. */
.month-grid-q { grid-template-columns: repeat(2, 1fr); }
.month-grid-q .month-btn { display: grid; align-content: center; justify-items: center; height: 3.25rem; line-height: var(--lh-tight); }
.month-btn-sub { font-size: var(--fs-xs); font-weight: 400; color: var(--muted); }
.month-btn[aria-pressed="true"] .month-btn-sub { color: inherit; }

/* 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;
}
/* The input fills the frame's height, so the whole height of the frame is its target (kit.js sends a press on the frame's padding or on the dash to the nearest input). */
.daterange-input { align-self: stretch; }
.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%; }
}

/* Period navigation ------------------------------------------------------ */

/* Move through days, months, quarters or years one at a time, with a limit at each end: <div class="period-nav" role="group" aria-label="Tháng báo cáo" data-period-nav data-unit="month" data-value="2026-09"
   data-min="2026-04" data-max="2026-09"> with button.btn.btn-icon[data-period="-1"] (caret-left, aria-label "Tháng trước"), span.period-nav-label[aria-live="polite"][aria-atomic="true"] and
   button.btn.btn-icon[data-period="1"] (caret-right, "Tháng sau"). In the label: span.period-nav-text (kit.js writes "Tháng 09/2026", "Quý 3/2026", "Năm 2026" or "Thứ Tư, 30/09/2026") and, for a
   day or any unit, span.tag.tag-info.period-nav-now[hidden] ("Hôm nay", shown while the period holds today). kit.js, the date picker's neighbour, keeps the label, the limits and the event; the
   markup is complete without it (write the first label yourself). The label is plain text in a live region and has no role: role="status" would name a label that may one day become a button,
   and this one is not one; a page that turns it into a picker moves aria-live to a .visually-hidden span beside it. At a limit the button that would pass it takes aria-disabled="true", not
   disabled, so the keys keep their place; it is greyed by the same rule as every .btn. The label has a minimum width per unit (--period-label-w) so the buttons do not move when the words change. */
.period-nav { display: flex; align-items: center; gap: var(--sp-1); width: fit-content; max-width: 100%; }
.period-nav[data-unit="day"] { --period-label-w: 12rem; }
.period-nav[data-unit="quarter"] { --period-label-w: 7.5rem; }
.period-nav[data-unit="year"] { --period-label-w: 6rem; }
.period-nav-label {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--sp-1) var(--sp-2);
  min-width: var(--period-label-w, 9rem);
  padding-inline: var(--sp-2);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  text-align: center;
}
.period-nav-text { white-space: nowrap; }
/* The day switcher kept at the top of a scrolling screen: the weekday and date large between the two buttons, a "Hôm nay" link button after them (button.btn.btn-link.btn-sm.period-nav-today
   [data-period-today], hidden while the day is today), on --surface with a line under it. --sticky-top is the height of a bar above it. */
.period-nav-sticky {
  position: sticky;
  top: var(--sticky-top, 0);
  z-index: var(--z-sticky);
  width: auto;
  max-width: none;
  padding: var(--sp-2) var(--sp-3);
  background: var(--surface);
  border-bottom: 1px solid var(--panel-border);
}
.period-nav-sticky .period-nav-label { flex: 1 1 auto; min-width: 0; font-size: var(--fs-md); }
.period-nav-sticky .period-nav-text { white-space: normal; } /* the weekday on one line and the date under it when the room is short */
.period-nav-today { flex: none; margin-inline-start: var(--sp-1); padding-inline: var(--sp-2); }

/* 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); }
}
/* Bottom sheet: <dialog class="modal modal-sheet"> opened with showModal(). It rises from the bottom edge at every width, centred and up to 32rem wide, and under 40rem it
   takes the whole width like any modal. The edge is square at the bottom (it is docked) and a small handle at the top marks it as a sheet: the handle is only a mark, so closing is
   by the close button, the scrim or Escape, not by dragging. Head, body and foot are the modal's own. The "app" look redraws it with a rounder top and a larger shadow. */
@keyframes sheet-in { from { transform: translateY(100%); } }
.modal-sheet {
  width: min(100%, 32rem);
  max-width: 100%;
  margin: auto auto 0;
  border-bottom: 0;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  box-shadow: var(--sh-3);
}
.modal-sheet[open] { animation: sheet-in var(--dur-2) var(--ease-enter, var(--ease)); }
.modal-sheet::before {
  content: "";
  flex: none;
  width: var(--sp-8);
  height: var(--sp-1);
  margin: var(--sp-3) auto 0;
  background: var(--field-border);
  border-radius: var(--radius-pill);
}
.modal-sheet .modal-head { padding-top: var(--sp-3); }
.modal-sheet .modal-foot { padding-bottom: calc(var(--sp-4) + env(safe-area-inset-bottom, 0px)); border-radius: 0; }

/* A list of choices in a sheet: <ul class="sheet-list"><li><button class="sheet-item" type="button"><span class="sheet-ic" aria-hidden="true"><svg class="icon"/></span>Label</button></li>.
   .sheet-item is a button or a link (a sheet of links: <a class="sheet-item" href> with the label and, as the last child, an svg.icon chevron that moves to the end). Each item is at
   least --tap high. .sheet-item-danger for the one that deletes: its words say so, the red is the second cue. Icons are decoration (aria-hidden), the label is the name. */
.sheet-list { display: grid; gap: var(--sp-1); margin: 0; padding: 0; list-style: none; }
/* Groups in a sheet: div.sheet-group > h3.sheet-heading (a small muted title) + ul.sheet-list, one group after the other. */
.sheet-group + .sheet-group { margin-top: var(--sp-3); }
.sheet-heading { margin: 0; padding: var(--sp-2) var(--sp-3) var(--sp-1); font-size: var(--fs-xs); font-weight: 600; color: var(--muted); }
.sheet-item {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  width: 100%;
  min-height: calc(var(--tap) + var(--sp-1));
  padding: var(--sp-2) var(--sp-3);
  font: inherit;
  font-weight: 600;
  color: var(--text);
  text-align: start;
  text-decoration: none;
  background: none;
  border: 0;
  border-radius: var(--radius);
  cursor: pointer;
  transition: background-color var(--dur-1) var(--ease);
}
.sheet-item:hover { background: var(--surface-hover); }
.sheet-item:active { background: var(--sunk); }
.sheet-item > svg.icon:last-child { flex: none; margin-inline-start: auto; color: var(--muted); }
.sheet-ic {
  display: inline-flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: calc(var(--tap) - var(--sp-1));
  height: calc(var(--tap) - var(--sp-1));
  color: var(--muted);
  background: var(--sunk);
  border-radius: 50%;
}
.sheet-ic .icon { width: var(--icon); height: var(--icon); }
.sheet-item-danger { color: var(--danger); }
.sheet-item-danger .sheet-ic { color: var(--danger); background: var(--danger-bg); }

/* Action tiles: a few big touch targets, each an icon on a disc above a short label (the "Lập mới" sheet: Phiếu chi, Phiếu thu, Chuyển quỹ, Nhập kho, Xuất kho, Kiểm kê; the quick row on
   a home page). <div class="action-tiles"> > <a class="action-tile" href> or <button class="action-tile" type="button"> > <span class="action-tile-ic" aria-hidden="true"><svg class="icon"/></span>
   + the label as text, so the name needs no aria-label. .action-tile-primary on one tile (the usual action) fills it with the brand colour. The grid is a container, so it counts the width it
   is given, in a sheet or on a page: three across from 18rem, two across below it; exactly two or four tiles are halves from 18rem too, and a lone tile takes the row. A tile is at least
   5.5rem high, its label wraps (two lines at the width of three across) and never pushes the tile wider. Keep it to six or fewer. The icon is decoration; the label says what it does. */
.action-tiles { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: var(--sp-3); container-type: inline-size; }
.action-tile {
  --tile-span: 3;
  grid-column: span var(--tile-span);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  min-width: 0;
  min-height: 5.5rem;
  padding: var(--sp-3) var(--sp-2);
  font: inherit;
  font-size: var(--fs-sm);
  font-weight: 600;
  line-height: var(--lh-tight);
  color: var(--text);
  text-align: center;
  text-decoration: none;
  overflow-wrap: anywhere;
  background: var(--surface);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--sh-1);
  cursor: pointer;
  transition: background-color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease);
}
.action-tile:hover { background: var(--surface-hover); border-color: var(--field-border-hover); }
.action-tile:active { background: var(--sunk); }
.action-tile-ic {
  display: inline-flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: calc(var(--tap) + var(--sp-1));
  height: calc(var(--tap) + var(--sp-1));
  color: var(--text);
  background: var(--sunk);
  border-radius: 50%;
}
.action-tile-ic .icon { width: var(--icon-lg); height: var(--icon-lg); }
.action-tile-primary { color: var(--on-primary); background: var(--primary); border-color: var(--primary); }
.action-tile-primary:hover { background: var(--primary-hover); border-color: var(--primary-hover); }
.action-tile-primary:active { background: var(--primary-hover); }
.action-tile-primary .action-tile-ic { color: var(--primary); background: var(--on-primary); }
@container (min-width: 18rem) {
  .action-tile { --tile-span: 2; }
  .action-tiles:where(:has(> :nth-child(2):last-child), :has(> :nth-child(4):last-child)) > .action-tile { --tile-span: 3; }
  .action-tiles:where(:has(> :only-child)) > .action-tile { --tile-span: 6; }
}
/* 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; }
/* A head that stays in view while the page scrolls: .page-head-sticky wraps the crumbs, the .page-head and whatever else belongs to the head (a stepper, a notice). kit.js sets data-stuck while it
   is stuck, and then it gets a line and a shadow. Under 48rem it is not sticky (the phone keeps its room). */
.page-head-sticky { position: sticky; top: -1px; z-index: var(--z-sticky); display: grid; gap: var(--sp-3); margin-inline: calc(-1 * var(--sp-4)); padding: var(--sp-2) var(--sp-4) var(--sp-3); background: var(--bg); border-bottom: 1px solid transparent; transition: box-shadow var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease); }
.page-head-sticky[data-stuck] { border-bottom-color: var(--panel-border); box-shadow: var(--sh-2); }
@media (max-width: 47.99rem) { .page-head-sticky { position: static; } }
.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-real-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; }
/* A field in the act (a count to key in): .input is width: 100% everywhere else, which in this shrink-to-fit column would take the browser's own default width and squeeze the text,
   so here it is a fixed narrow box with its figure at the right. Its label is a .visually-hidden label or aria-label naming the row. */
.work-act > .input { width: 6rem; text-align: end; font-variant-numeric: tabular-nums; }
.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; }

/* A day entered person by person (minhson's daily attendance: only the exceptions need a tap): ul.entry-list > li.entry-row > .entry-who (strong.entry-name + .entry-sub: code,
   pay, hours a day, the fingerprint swipes with an icon), .entry-quick (a .seg[role="radiogroup"] of .seg-item with data-tone: Đủ giờ plain, Nửa ngày warn, P info, K bad), two
   .entry-field (.entry-hours and .entry-ot, each a .visually-hidden label and an .input-affix: input.input[data-number] + .affix "giờ" / "giờ TC"; a .field-error under the box when it is wrong), .entry-toggles
   (chips with aria-pressed and a check or x icon: Trưa/đêm, Cơm TC with a .mark-auto "tự" when the software turned it on, Ca đêm) and p.entry-note (a line under the row: a hint, or
   with .entry-note-warn an icon and a warning in --warn). data-state="changed" on the row (edited, not saved): the changed-cell look, --warn-bg and a 3px --warn bar at the start, with
   "đã sửa, chưa lưu" in a .visually-hidden span of the name. The list is the container: 72rem or wider, one line (name, buttons, hours, overtime, toggles); from 36rem, two lines
   (name and buttons, then the fields and the toggles); under 36rem, the name, the buttons across the width, the two fields side by side, the toggles, each on a line. */
.entry-list { margin: 0; padding: 0; list-style: none; container-type: inline-size; }
.entry-row {
  display: grid;
  grid-template-columns: 7.5rem 8.5rem minmax(0, 1fr) auto;
  grid-template-areas: "who who who quick" "hours ot toggles toggles" "note note note note";
  align-items: start;
  gap: var(--sp-2) var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--panel-border);
}
.entry-list > .entry-row:last-child { border-bottom: 0; }
.entry-row[data-state="changed"] { background: var(--warn-bg); box-shadow: inset 3px 0 0 var(--warn); }
.entry-who { grid-area: who; align-self: center; min-width: 0; }
.entry-name { display: block; font-weight: 600; }
.entry-sub { display: flex; flex-wrap: wrap; align-items: center; gap: 0 var(--sp-1); font-size: var(--fs-sm); color: var(--muted); }
.entry-sub .icon { width: var(--icon-sm); height: var(--icon-sm); }
.entry-quick { grid-area: quick; align-self: center; justify-self: end; }
.entry-quick .seg-item { min-height: calc(var(--tap) - 8px); }
.entry-field { min-width: 0; }
.entry-hours { grid-area: hours; }
.entry-ot { grid-area: ot; }
.entry-field .input { text-align: end; font-variant-numeric: tabular-nums; }
.entry-field .field-error { display: flex; align-items: flex-start; gap: var(--sp-1); margin: var(--sp-1) 0 0; }
.entry-field .field-error .icon { flex: none; margin-top: 2px; }
.entry-toggles { grid-area: toggles; display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); min-width: 0; min-height: calc(var(--tap) - 4px); }
.entry-toggles .chip[aria-pressed="false"] { color: var(--muted); border-style: dashed; }
.entry-note { grid-area: note; display: flex; align-items: flex-start; gap: var(--sp-2); margin: 0; font-size: var(--fs-sm); color: var(--muted); }
.entry-note .icon { flex: none; width: var(--icon-sm); height: var(--icon-sm); margin-top: 2px; }
.entry-note-warn { color: var(--warn); }
.entry-row:not(:has(> .entry-note)) { grid-template-areas: "who who who quick" "hours ot toggles toggles"; }
@container (min-width: 72rem) {
  .entry-row { grid-template-columns: minmax(12rem, 1fr) 15rem 7.5rem 8.5rem 22rem; grid-template-areas: "who quick hours ot toggles" "note note note note note"; }
  .entry-row:not(:has(> .entry-note)) { grid-template-areas: "who quick hours ot toggles"; }
  .entry-quick { justify-self: start; }
}
@container (max-width: 36rem) {
  .entry-row { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); grid-template-areas: "who who" "quick quick" "hours ot" "toggles toggles" "note note"; }
  .entry-row:not(:has(> .entry-note)) { grid-template-areas: "who who" "quick quick" "hours ot" "toggles toggles"; }
  .entry-quick { display: flex; justify-self: stretch; width: 100%; }
  .entry-quick .seg-item { flex: 1 1 0; min-width: 0; min-height: var(--tap); padding-inline: 0; }
}
/* A heading for a group of rows in a list: h3.group-head (h4 one level down) > span.group-head-title (the group's name, strong; it wraps) + span.group-head-count (muted: a no-break
   "· 4" with a .visually-hidden unit, "· 4 phiếu") + span.group-head-sum (at the end, never wraps: a net figure with .num-in, .num-out or .num-neg inside, or words such as
   "4/5 đã chấm"). It is a real heading, so a screen reader can jump from group to group; put it above the group's own list (ul.work-list, ul, .menu, a plain div of rows), so each
   group is a heading and a list, not one long list with headings between its items. It stands on --surface-2 between two hairlines, so the group's total reads as a total row and
   the rows under it as its parts; it takes the side padding of the rows under it from --group-head-px.
   .group-head-sticky keeps it at the top of its scroll box (--sticky-top says how far down: the height of a bar above it) on --surface-2 with a line under it, so rows pass beneath;
   wrap each group (head and list) in a div, and the next head pushes the last one off instead of covering it. */
.group-head {
  display: flex;
  align-items: baseline;
  gap: var(--sp-1) var(--sp-2);
  margin: 0;
  padding: var(--sp-2) var(--group-head-px, var(--sp-4));
  font-size: var(--fs-md);
  line-height: var(--lh-tight);
  background: var(--surface-2);
  border-block: 1px solid var(--panel-border);
}
/* The first group of a list or a panel already has an edge above it, so its head draws no second line. */
:first-child > .group-head:first-child { border-top-color: transparent; }
.group-head-title { flex: 0 1 auto; min-width: 0; font-weight: 700; overflow-wrap: anywhere; }
.group-head-count { flex: none; font-weight: 400; color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.group-head-sum { flex: none; margin-inline-start: auto; font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; }
.group-head-sticky {
  position: sticky;
  top: var(--sticky-top, 0);
  z-index: var(--z-sticky);
  padding-block: var(--sp-2);
  background: var(--surface-2);
  border-bottom: 1px solid var(--panel-border);
}

/* A voucher line on a phone: li.line-card (in ul.line-card-list, or a div of them) > .line-card-head (.line-card-name strong + .line-card-sub muted "VT-0101 · kg" in a div, and at
   the end a button.btn.btn-ghost.btn-icon named for its line, "Xóa dòng: ...") + .line-card-body (two .field side by side, each a label above a large input: "Số lượng" with
   input[data-number], "Đơn giá" with input[data-money]; the inputs are large with the figure at the right, at least the phone field size) + .line-card-total (.line-card-total-label
   "Thành tiền" and output.line-card-total-figure, a figure that never wraps, in --font-figure) + p.field-error[aria-live="polite"] kept in the card, empty until the line is wrong:
   when it has words the card takes the danger edge, and the words say what to fix. Write the unit in the label ("Số lượng (kg)"). The tenant calculates the total; <output> is a
   polite live region itself. After the list, button.btn.btn-dashed.btn-block adds a line. */
.line-card-list { display: grid; gap: var(--sp-3); margin: 0; padding: 0; list-style: none; }
.line-card {
  display: grid;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4) var(--sp-4);
  background: var(--surface);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius-lg);
}
.line-card:has(.field-error:not(:empty):not([hidden])) { border-color: var(--danger); box-shadow: inset 0 0 0 1px var(--danger); }
.line-card-head { display: flex; align-items: flex-start; gap: var(--sp-2); margin: calc(-1 * var(--sp-2)) calc(-1 * var(--sp-2)) 0 0; padding-top: var(--sp-2); }
.line-card-head > div { flex: 1; min-width: 0; padding-top: var(--sp-1); }
.line-card-name { display: block; font-weight: 700; line-height: var(--lh-tight); overflow-wrap: anywhere; }
.line-card-sub { display: block; margin-top: 2px; font-size: var(--fs-sm); color: var(--muted); overflow-wrap: anywhere; }
.line-card-body { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-3); }
.line-card-body .input {
  min-height: 3rem;
  font-size: var(--fs-lg);
  font-variant-numeric: tabular-nums;
  text-align: end;
}
.line-card-total { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-3); padding-top: var(--sp-3); border-top: 1px dashed var(--panel-border); }
.line-card-total-label { font-size: var(--fs-sm); color: var(--muted); }
.line-card-total-figure { font-family: var(--font-figure, inherit); font-size: var(--fs-xl); font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; }
.line-card .field-error { margin: 0; }

/* A row of a stocktake: ul.count-list > li.count-row > .count-row-main (.count-row-name strong, .count-row-sub muted "VT-0031 · cây") + .count-row-book (muted, "Sổ: 120,5", the
   quantity on the books) + .count-row-field (a label.visually-hidden naming the row, and input.input[data-number], narrow with its figure at the right) + .count-row-diff, under the
   field: what the count differs from the books by, in words and a sign ("Lệch − 2,5"), with .num-in (more than the books), .num-out (less) or nothing (muted, "Khớp"); empty until
   something was counted, and then it takes no room. Put the list in a .panel-flush. Needs no script of its own: the field is the decimal quantity field. */
.count-list { margin: 0; padding: 0; list-style: none; }
.count-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center;
  gap: var(--sp-1) var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--panel-border);
}
.count-list > .count-row:last-child { border-bottom: 0; }
.count-row-main { display: grid; gap: 2px; min-width: 0; }
.count-row-name { font-weight: 600; line-height: var(--lh-tight); overflow-wrap: anywhere; }
.count-row-sub { font-size: var(--fs-sm); color: var(--muted); overflow-wrap: anywhere; }
.count-row-book { font-size: var(--fs-sm); color: var(--muted); font-variant-numeric: tabular-nums; text-align: end; white-space: nowrap; }
.count-row-field .input { width: 6rem; min-height: 3rem; font-size: var(--fs-lg); font-variant-numeric: tabular-nums; text-align: end; }
.count-row-diff { grid-column: 2 / -1; justify-self: end; font-size: var(--fs-sm); font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
.count-row-diff:empty { display: none; }
.count-row-diff:not(.num-in):not(.num-out):not(.num-neg):not(.num-void) { color: var(--muted); }

 .row-off on the li (or the .work-item): muted, nothing struck; a tag says "Ngừng dùng". A voided row, .row-void or
   .work-row[data-state="void"] on the li (or the .work-item): muted, the name and code struck through and the figure as .num-void is, while a tag ("Đã hủy") stays whole and readable;
   a voided row gives up its tone bar and its reason line's warning colour. .row-new: a row just added, the notice tint and a bar at the start edge, for the tenant to remove after a moment. */
.work-list .row-off,
.work-list .row-off .work-item,
.work-list :is(.row-void, .work-row[data-state="void"]),
.work-list :is(.row-void, .work-row[data-state="void"]) .work-item { color: var(--muted); }
.work-list :is(.row-void, .work-row[data-state="void"]) :is(.work-name, .work-code) { text-decoration: line-through; }
.work-list :is(.row-void, .work-row[data-state="void"]) :is(.num-in, .num-out, .num-neg, .num-void) { color: var(--muted); text-decoration: line-through; }
.work-list :is(.row-void, .work-row[data-state="void"]) .work-reason { color: var(--muted); }
.work-row:is(.row-void, [data-state="void"]) { --work-tone: transparent; }
.work-list .row-new { background: var(--notice-bg); box-shadow: inset var(--sp-1) 0 0 var(--primary); }

/* 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); }

/* The tile that adds a photo, last in a .thumb-list: li.thumb-add > label > input.visually-hidden[type="file"][accept="image/*"][multiple] + svg.icon + the words ("Chụp ảnh").
   With capture="environment" a phone opens its camera; without it (add .thumb-add-pick to the li, icon image or plus, words "Thêm ảnh") it opens the photo library: same look.
   The whole tile is the label, so the whole tile is the target (it is as wide as a photo and at least --tap tall) and the input inside it takes the focus; a dashed edge says
   "empty place". The tile is the shape of a photo's picture (4:3) and sits at the top of its cell, so it lines up with the photos beside it, not with their captions. There is no
   script: showing a preview of the files that were picked is the page's job (listen to `change` on the input and add a li.thumb before this tile). Alone in an empty list it is the
   only thing there. */
.thumb-add { align-self: start; min-width: 0; }
.thumb-add > label {
  display: grid;
  place-items: center;
  align-content: center;
  gap: var(--sp-1);
  aspect-ratio: 4 / 3;
  min-height: var(--tap);
  padding: var(--sp-2);
  font-size: var(--fs-sm);
  font-weight: 600;
  text-align: center;
  color: var(--text);
  cursor: pointer;
  background: var(--surface);
  border: 1px dashed var(--field-border);
  border-radius: var(--radius);
  transition: background-color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease);
}
.thumb-add > label:hover { background: var(--surface-hover); border-color: var(--field-border-hover); }
.thumb-add > label:has(input:focus-visible) { outline: 3px solid var(--focus); outline-offset: 2px; }
.thumb-add > label:has(input:disabled) { color: var(--muted); cursor: not-allowed; background: var(--sunk); }
.thumb-add > label > .icon { width: var(--icon-lg); height: var(--icon-lg); color: var(--muted); }

/* A grid of files, each a tile: ul.file-grid > li > a.file-tile (or div.file-tile) > svg.icon.file-tile-icon + span.file-tile-type (the type in letters: "PDF", "XLS", "DOC", "JPG")
   + span.file-tile-name (at most two lines, then "…") + .file-tile-foot > span.file-tile-size (+ an optional remove button, button.btn.btn-ghost.btn-icon.btn-sm). The type is words, so
   the icon and any colour are not the only cue. Columns of at least 8rem fill the width (two on a phone, more in a wide container). A tile that is a link is one a.file-tile;
   a tile with a remove button cannot be an a (a button does not go inside a link), so make it a div.file-tile and put the link on the name, a.file-tile-name, which stretches over the
   tile. The .file-list (one file a row, with progress) stays for upload and review lists; this grid is for a record's attachments. */
.file-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr));
  gap: var(--sp-3);
  margin: 0;
  padding: 0;
  list-style: none;
}
.file-grid > li { display: flex; min-width: 0; }
.file-tile {
  position: relative;
  display: grid;
  flex: 1;
  grid-template-columns: auto minmax(0, 1fr);
  align-content: start;
  align-items: start;
  gap: var(--sp-1) var(--sp-2);
  min-width: 0;
  padding: var(--sp-3);
  color: var(--text);
  text-decoration: none;
  background: var(--surface);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
}
a.file-tile:hover,
.file-tile:has(a.file-tile-name:hover) { background: var(--surface-hover); border-color: var(--field-border-hover); }
a.file-tile:focus-visible,
.file-tile:has(a.file-tile-name:focus-visible) { outline: 3px solid var(--focus); outline-offset: 2px; }
.file-tile:has(a.file-tile-name:focus-visible) a.file-tile-name:focus-visible { outline: none; }
.file-tile-icon { width: var(--icon-lg); height: var(--icon-lg); color: var(--muted); }
.file-tile-type {
  justify-self: end;
  padding: 0 var(--sp-2);
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  line-height: 1.5;
  text-transform: uppercase;
  color: var(--text);
  background: var(--sunk);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius-sm);
}
.file-tile-name,
.file-tile-foot { grid-column: 1 / -1; }
.file-tile-name {
  display: -webkit-box;
  overflow: hidden;
  overflow-wrap: anywhere;
  font-size: var(--fs-md);
  font-weight: 600;
  line-height: var(--lh-tight);
  color: inherit;
  text-decoration: none;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
}
a.file-tile-name::after { content: ""; position: absolute; inset: 0; border-radius: inherit; }
.file-tile-foot { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); min-height: var(--sp-6); }
.file-tile-size { font-size: var(--fs-sm); color: var(--muted); font-variant-numeric: tabular-nums; }
.file-tile-foot:has(.btn) { margin-block: calc(-1 * var(--sp-1)); margin-inline-end: calc(-1 * var(--sp-2)); } /* the button's own padding lines its icon up with the badge above */

/* A row of large photos that the reviewer swipes through, with the next one showing at the edge so the row plainly goes on: div.media-strip[role="region"][aria-label][tabindex="0"]
   > figure.media > .thumb-img (the <img>, or an icon and a name) + figcaption.media-cap. One scroll-snap row: a finger swipes it, and because the region takes focus the arrow keys
   scroll it for a mouse or keyboard user. A photo is --media-w of the strip (86%) but at most --media-max, so on a phone the next photo shows at the edge and on a wide screen
   several fit. The caption says which photo it is and when it arrived ("Ảnh 2 trên 4 · nhận 14:32"): the picture alone never carries that. */
.media-strip {
  --media-w: 86%;
  --media-max: 26rem;
  display: flex;
  gap: var(--sp-3);
  overflow-x: auto;
  padding-bottom: var(--sp-2);
  scroll-snap-type: x mandatory;
  overscroll-behavior-x: contain;
  scrollbar-width: thin;
}
.media { display: grid; align-content: start; gap: var(--sp-1); flex: none; width: min(var(--media-w), var(--media-max)); margin: 0; scroll-snap-align: start; }
.media-cap { font-size: var(--fs-sm); color: var(--muted); font-variant-numeric: tabular-nums; }

/* Rows the user puts in order: ul.sort-list[data-sortable] > li.sort-item > span.sort-handle[aria-hidden="true"] + span.sort-text (the title, and a span.sort-sub line under it) +
   span.sort-moves > two button.btn.btn-ghost.btn-icon.btn-sm[data-sort-up | data-sort-down] with an arrow-up and an arrow-down icon, each named for its row in aria-label
   ("Đưa lên: Vệ sinh cửa"). The handle is what a mouse or a finger drags, so a screen reader skips it; the two buttons need no dragging at all, which is the way for a keyboard
   and for anyone who cannot drag. kit.js moves the row and says its new place in a live region. data-label on the li names the row there. A long list does not scroll while it
   is being dragged: the buttons are the way across a screen. */
.sort-list { display: grid; gap: var(--sp-2); margin: 0; padding: 0; list-style: none; }
.sort-item { display: flex; align-items: center; gap: var(--sp-1); padding: var(--sp-1) var(--sp-1) var(--sp-1) 0; background: var(--surface); border: 1px solid var(--panel-border); border-radius: var(--radius); }
.sort-handle { flex: none; align-self: stretch; display: grid; place-items: center; width: 2rem; color: var(--muted); cursor: grab; touch-action: none; }
.sort-handle::before { content: ""; width: 0.75rem; height: 1.125rem; background-image: radial-gradient(circle, currentColor 1.5px, transparent 1.6px); background-size: 0.375rem 0.375rem; }
.sort-text { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
.sort-sub { display: block; font-size: var(--fs-sm); color: var(--muted); }
.sort-moves { flex: none; display: flex; }
.sort-moves .btn[aria-disabled="true"] { background: none; opacity: 0.4; }
.sort-item[data-dragging] { background: var(--surface-hover); border-color: var(--field-border-hover); box-shadow: var(--sh-3); }
.sort-item[data-dragging] .sort-handle { cursor: grabbing; }
.sort-list[data-dragging] { cursor: grabbing; user-select: none; }

/* A tree in a table, for a list of units (centre, departments, stations, squads): table.tbl.tbl-tree, each body row tr[data-level="0".."3"] in tree order, its first cell
   th.tree-cell[scope="row"] > [button.tree-toggle] + svg.icon (the kind of unit: buildings, tree-structure, users) + the name. Each level is set in by --tree-step (1.75rem; 3rem when the table has toggles, so a toggle fits between the elbow and the icon). The cell does not wrap (a long name scrolls the table). A row
   under another draws a hairline in --tree-line (a step darker than the row lines) from its parent's icon down and, with a curved elbow as in the sidebar, across to its own (the last child's line stops at its row; kit.js marks data-tree-last and
   data-tree-parent). The lines run unbroken up to four levels (0 to 3). A row whose children fold carries button.tree-toggle
   (btn btn-ghost btn-icon btn-sm, caret-down, aria-expanded, aria-label "Các đơn vị của Trạm Hạ Long"), set just before the icon (1.5rem) with the elbow ending at it, so the names of one
   level stay in line with or without a toggle; aria-expanded="false" hides the rows under it and turns the caret. Order among siblings: data-row-sort on the table and td.row-moves > two
   button.btn.btn-icon.btn-sm[data-row-up | data-row-down] (caret-up, caret-down, aria-label "Đưa lên: Phòng Kế toán - Tài chính"); a row moves with the rows under it and never
   leaves its parent. The rows under a parent can still take any other row style (aria-selected, a .cell-sub line in another cell). */
/* The guide is a step darker than the row lines (--tree-line), so where it crosses one it still reads as the tree, not as part of the grid. */
.tbl-tree { --tree-step: 1.75rem; --tree-lead: 0rem; --tree-tog-w: 1.5rem; --tree-line: color-mix(in srgb, var(--field-border) 60%, var(--panel-border)); }
/* With toggles each step holds the parent's line, a short elbow and the toggle, and the root level keeps room for one. */
.tbl-tree:has(.tree-toggle) { --tree-step: 3rem; --tree-lead: calc(var(--tree-tog-w) + 2px); }
.tbl-tree tr:has(> .tree-cell > .tree-toggle) { --tree-tog: calc(var(--tree-tog-w) + 2px); }
.tbl-tree tr[data-level="1"] { --lvl: 1; }
.tbl-tree tr[data-level="2"] { --lvl: 2; }
.tbl-tree tr[data-level="3"] { --lvl: 3; }
.tbl-tree .tree-cell { --tree-x: calc(var(--sp-3) + var(--tree-lead) + var(--lvl, 0) * var(--tree-step)); position: relative; padding-inline-start: var(--tree-x); white-space: nowrap; }
.tbl-tree .tree-cell > .icon { display: inline-block; margin-inline-end: var(--sp-2); vertical-align: -0.25em; color: var(--muted); }
.tbl-tree .tree-toggle { position: absolute; inset-block-start: 50%; inset-inline-start: calc(var(--tree-x) - var(--tree-tog-w) - 2px); width: var(--tree-tog-w); height: var(--tree-tog-w); min-height: 0; padding: 0; translate: 0 -50%; }
.tree-toggle .icon { transition: transform var(--dur-1) var(--ease); }
.tree-toggle[aria-expanded="false"] .icon { transform: rotate(-90deg); }
/* The guide, drawn as the sidebar's tree line (Core 2.0): a 1.5px line in --tree-line down from the parent's icon centre (one step back) that turns into this row's icon
   with a curved elbow; the line carries on past a row that has a sibling below and ends in the elbow at the last one. Every part of it is one layer of the mask of a single
   ::before painted in --tree-line (straight parts are gradients, the elbow a small svg), so all parts are drawn the same way and come out the same width; borders would
   not, since a browser rounds a 1.5px border and a 1.5px box differently. The ::before starts 1px up and stands in front, so the line runs over the row lines unbroken.
   Layers: the row's own line (full height, or down to the elbow at the last child), the elbow, the arm to the icon, the line of each ancestor that has a sibling below
   (kit.js sets .tree-rail-1, .tree-rail-2), and on a parent the start of its children's line under its icon (folded, none). */
.tbl-tree { --tree-r: 8px; }
.tbl-tree tbody tr > .tree-cell::before {
  --tm-nil: linear-gradient(#0000 0 0); /* a row with no line layer (a top-level row with no children) must mask to nothing; with every layer `none` the whole ::before would paint */
  --tree-own-x: round(down, calc(var(--tree-x) - var(--tree-step) + var(--icon) / 2 - 0.75px), 0.5px);
  content: "";
  position: absolute;
  z-index: 1;
  inset: -1px 0 0;
  background: var(--tree-line);
  pointer-events: none;
  -webkit-mask-image: var(--tm-own, var(--tm-nil)), var(--tm-top, var(--tm-nil)), var(--tm-elbow, var(--tm-nil)), var(--tm-arm, var(--tm-nil)), var(--tm-r1, var(--tm-nil)), var(--tm-r2, var(--tm-nil)), var(--tm-stub, var(--tm-nil));
  mask-image: var(--tm-own, var(--tm-nil)), var(--tm-top, var(--tm-nil)), var(--tm-elbow, var(--tm-nil)), var(--tm-arm, var(--tm-nil)), var(--tm-r1, var(--tm-nil)), var(--tm-r2, var(--tm-nil)), var(--tm-stub, var(--tm-nil));
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-size: 1.5px 100%, 1.5px calc(50% - var(--tree-r)), 9.5px 16px, calc(var(--tree-step) - var(--icon) / 2 - var(--sp-1) - var(--tree-tog, 0px) - 9.5px) 1.5px, 1.5px 100%, 1.5px 100%, 1.5px calc(50% - var(--icon) / 2 - 2px);
  mask-size: 1.5px 100%, 1.5px calc(50% - var(--tree-r)), 9.5px 16px, calc(var(--tree-step) - var(--icon) / 2 - var(--sp-1) - var(--tree-tog, 0px) - 9.5px) 1.5px, 1.5px 100%, 1.5px 100%, 1.5px calc(50% - var(--icon) / 2 - 2px);
  -webkit-mask-position: var(--tree-own-x) 0, var(--tree-own-x) 0, var(--tree-own-x) 50%, calc(var(--tree-own-x) + 9.5px) 50%, round(down, calc(var(--sp-3) + var(--tree-lead) + var(--icon) / 2 - 0.75px), 0.5px) 0, round(down, calc(var(--sp-3) + var(--tree-lead) + var(--tree-step) + var(--icon) / 2 - 0.75px), 0.5px) 0, round(down, calc(var(--tree-x) + var(--icon) / 2 - 0.75px), 0.5px) 100%;
  mask-position: var(--tree-own-x) 0, var(--tree-own-x) 0, var(--tree-own-x) 50%, calc(var(--tree-own-x) + 9.5px) 50%, round(down, calc(var(--sp-3) + var(--tree-lead) + var(--icon) / 2 - 0.75px), 0.5px) 0, round(down, calc(var(--sp-3) + var(--tree-lead) + var(--tree-step) + var(--icon) / 2 - 0.75px), 0.5px) 0, round(down, calc(var(--tree-x) + var(--icon) / 2 - 0.75px), 0.5px) 100%;
}
.tbl-tree tbody tr:not([data-level="0"]) { --tm-top: linear-gradient(#000 0 0); --tm-elbow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='9.5' height='16'%3E%3Cpath d='M0.75 0A8 8 0 0 0 8.75 8H9.5' fill='none' stroke='%23000' stroke-width='1.5'/%3E%3C/svg%3E"); --tm-arm: linear-gradient(#000 0 0); }
.tbl-tree tbody tr:not([data-level="0"]):not([data-tree-last]) { --tm-own: linear-gradient(#000 0 0); }
.tbl-tree tr.tree-rail-1 { --tm-r1: linear-gradient(#000 0 0); }
.tbl-tree tr.tree-rail-2 { --tm-r2: linear-gradient(#000 0 0); }
.tbl-tree tr[data-tree-parent]:not(:has(.tree-toggle[aria-expanded="false"])) { --tm-stub: linear-gradient(#000 0 0); }
.row-moves { width: 1%; white-space: nowrap; }
.row-moves .btn + .btn { margin-inline-start: var(--sp-1); }
.row-moves .btn[aria-disabled="true"] { opacity: 0.4; }
@media print { .tree-toggle, .row-moves .btn { display: none; } }

/* 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; }
/* The customer's letterhead at the top of a sheet: .paper-head > .paper-org (img.paper-logo + div.paper-org-text > p.paper-org-name, bold capitals, and p.paper-org-line for the
   address, tax code, phone) + p.paper-doc at the end (the form code "Mẫu số 02-TT", the voucher number, one per line, right-aligned), and a thin black line under it all.
   The logo has a fixed height (--paper-logo-h, 14 mm; 11 mm on A5) and a width that follows the file up to --paper-logo-w (50 mm; 38 mm on A5); it is never cropped or stretched
   and prints in its own colours (a black-only printer turns them grey). A wide logo that holds the name, or a square mark beside the name in text: either way the legal name is
   written out in .paper-org-name, since a voucher needs it; alt="" on the logo then, so a screen reader does not read the name twice. With no logo the text stands alone. */
.paper-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 6mm; margin-bottom: 5mm; padding-bottom: 3mm; border-bottom: 0.3mm solid black; }
.paper-org { display: flex; align-items: center; gap: 4mm; min-width: 0; }
.paper-logo { flex: none; display: block; width: auto; height: var(--paper-logo-h, 14mm); max-width: var(--paper-logo-w, 50mm); object-fit: contain; object-position: left center; }
.paper-org-text { min-width: 0; font-size: 0.9em; line-height: 1.3; }
.paper-org-name { margin: 0; font-size: 1.1em; font-weight: 700; text-transform: uppercase; }
.paper-org-line { margin: 0; }
.paper-doc { flex: none; margin: 0; font-size: 0.9em; line-height: 1.4; text-align: end; }
.paper-a5 .paper-head, .paper-a5-land .paper-head { --paper-logo-h: 11mm; --paper-logo-w: 38mm; gap: 4mm; margin-bottom: 3.5mm; }
@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; }
}

/* Print pages for the sheets. The kit had no @page rule, so an A4 sheet took whatever page the printer (or the page's own CSS) gave it. Each sheet now names its page: .paper
   "a4", .paper-landscape "a4-land", .paper-a5 "a5", .paper-a5-land "a5-land", through the `page` property, and the @page rules below give each name its size with margin 0
   (the sheet's padding is the margin). A named page governs only the sheets that name it, so A4 and A5 sheets can share one document, each on its own paper size, with a page
   break between sizes. Chrome lets an unnamed `@page { size: ... }` that comes later in the cascade override the named ones, so a page that prints these sheets sets no
   unnamed @page size after kit.css (to change a size or margin, redefine the named rule: `@page a4 { margin: 5mm }`). Print at scale 100%; a script that prints to PDF passes
   preferCSSPageSize. */
.paper { page: a4; }
.paper-landscape { page: a4-land; }
@page a4 { size: A4 portrait; margin: 0; }
@page a4-land { size: A4 landscape; margin: 0; }
@page a5 { size: A5 portrait; margin: 0; }
@page a5-land { size: A5 landscape; margin: 0; }

/* A5 sheet, for a voucher the shop prints and keeps (a payment or goods voucher). .paper.paper-a5 is 148 x 210 mm, .paper.paper-a5-land is 210 x 148 mm; the margins are the A4 ones
   times 0.7 and the type is a point smaller. A paper is mono: a figure keeps its meaning in its sign and its words, so .num-in, .num-out, .num-neg and .num-void take the sheet's
   black (not the green of .num-in; the sheet is white in every palette, so a token such as --text would turn pale in a dark one). .num-void keeps its strike-through. */
.paper-a5, .paper-a5-land {
  padding: 8.5mm 9mm 10mm 12.5mm;
  font-size: 10pt;
  page: a5;
}
.paper-a5 { width: 148mm; min-height: 210mm; }
.paper-a5-land { width: 210mm; min-height: 148mm; page: a5-land; }
.paper-a5 .paper-no, .paper-a5-land .paper-no { inset-block-end: 5mm; inset-inline-end: 9mm; font-size: 8.5pt; }
.paper :is(.num-in, .num-out, .num-neg, .num-void) { color: inherit; }
@media print {
  .paper-a5 { min-height: 209mm; }      /* a hair under the page, so rounding never opens a second page */
  .paper-a5-land { min-height: 147mm; }
}

/* The signature row at the foot of a paper: .paper-sign (three equal columns; .paper-sign-2 for two) > .paper-sign-slot (one per signer) > .paper-sign-role (the role, bold:
   "Người lập phiếu") + .paper-sign-hint (italic: "(Ký, ghi rõ họ tên)"). Under the hint the slot keeps an empty space to sign in, 18 mm (--sign-space on the row or a slot
   changes it; keep it at 18 mm or more). The row never splits across two pages. Put it after the table and the total, with .paper-no outside it. */
.paper-sign {
  --sign-space: 18mm;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0 4mm;
  margin-top: 6mm;
  text-align: center;
  break-inside: avoid;
}
.paper-sign-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.paper-sign-slot { padding-bottom: var(--sign-space); }
.paper-sign-role { display: block; font-weight: 700; }
.paper-sign-hint { display: block; font-size: 0.9em; font-style: italic; }

/* A3 sheets, for a wide month sheet: .paper.paper-a3 (297 x 420 mm) and .paper.paper-a3-land (420 x 297 mm), pages "a3" and "a3-land". On screen the stage scrolls sideways. */
.paper-a3 { width: 297mm; min-height: 420mm; page: a3; }
.paper-a3-land { width: 420mm; min-height: 297mm; page: a3-land; }
@page a3 { size: A3 portrait; margin: 0; }
@page a3-land { size: A3 landscape; margin: 0; }
/* A sheet whose table runs over several pages: add .paper-flow to the sheet (A4, A4 landscape, A3, A3 landscape). On screen it is one long sheet with 10 mm margins. In print the
   margins move to the page (the named pages a4-flow, a4-land-flow, a3-flow, a3-land-flow, 10 mm each, so every page has them, not only the first and last), the head row of the table
   repeats on each page, each tbody (one person's block, .tbl-block) and each row stays whole, a <tfoot> prints once at the end instead of at the foot of every page, and the .paper-sign
   row is never left alone at the top of a page: it stays with the last rows. Use a table with .tbl-marks inside; the type comes from the sheet (7pt on A3, 5.6pt on A4 landscape; set
   it on the table to change it), the table is fixed in layout so give the day columns a width with <col> (8.1 mm on A3, 5.3 mm on A4 landscape). To change the margin, redefine both
   the sheet's padding and the named page. */
.paper-flow { min-height: 0; padding: 10mm; page: a4-flow; }
.paper-landscape.paper-flow { page: a4-land-flow; }
.paper-a3.paper-flow { page: a3-flow; }
.paper-a3-land.paper-flow { page: a3-land-flow; }
@page a4-flow { size: A4 portrait; margin: 10mm; }
@page a4-land-flow { size: A4 landscape; margin: 10mm; }
@page a3-flow { size: A3 portrait; margin: 10mm; }
@page a3-land-flow { size: A3 landscape; margin: 10mm; }
.paper-flow .paper-sign { break-before: avoid; }
@media print {
  .paper-flow { width: auto; min-height: 0; padding: 0; }
  .paper-flow table { break-inside: auto; }
  .paper-flow thead { display: table-header-group; }
  .paper-flow tfoot { display: table-row-group; }
  .paper-flow tbody, .paper-flow tr { break-inside: avoid; }
}
/* The day sheet on paper, black and white (a .tbl-marks inside a .paper, on screen and in print): thin black lines, no tint, black marks; the meaning moves into the shape of the mark.
   Leave (P, .cell-mark-info) and unpaid leave (K, .cell-mark-bad) are bold, a short day (.cell-mark-warn) is underlined, a day not recorded (.cell-mark-missing) is a grey hatch, a day
   off (data-off) a grey column with "CN" in its head, before joining or after leaving (.cell-mark-na) a light hatch, a computed value (.cell-auto) italic. The grey prints
   (print-color-adjust: exact). The sheet's two colours are named, as the rest of .paper does. */
.paper .tbl-marks { table-layout: fixed; width: 100%; font-size: 5.6pt; line-height: 1.15; color: black; border-collapse: collapse; }
.paper.paper-a3, .paper.paper-a3-land { font-size: 9pt; }
:is(.paper-a3, .paper-a3-land) .tbl-marks { font-size: 7pt; }
.paper .tbl.tbl-marks :is(th, td) { height: auto; min-width: 0; padding: 0.4mm 0.6mm; font-size: inherit; font-weight: 400; color: black; background: white; border: 0.5pt solid black; box-shadow: none; overflow-wrap: anywhere; white-space: normal; }
.paper .tbl.tbl-marks thead th { font-weight: 700; text-align: center; vertical-align: middle; }
.paper .tbl-marks thead th .cell-sub { font-size: 0.85em; font-weight: 400; color: black; }
.paper .tbl-marks tbody th { text-align: start; vertical-align: top; }
.paper .tbl-marks tbody th .cell-sub { font-size: 0.85em; color: black; }
.paper .tbl-marks tbody td.cell-mark { padding: 0.4mm 0; letter-spacing: -0.06em; text-align: center; white-space: nowrap; overflow-wrap: normal; }
.paper .tbl-marks tbody.tbl-block + tbody.tbl-block > tr:first-child > * { border-top: 1.2pt solid black; }
.paper .tbl.tbl-marks tfoot :is(th, td) { font-weight: 700; }
.paper .tbl-marks tfoot > tr:first-child > * { border-top: 1.2pt solid black; }
.paper .tbl-marks td:is(.cell-mark-info, .cell-mark-bad) { font-weight: 800; }
.paper .tbl-marks td.cell-mark-warn { text-decoration: underline; text-underline-offset: 1px; }
.paper .tbl-marks td.cell-auto { font-style: italic; }
.paper .tbl-marks :is(th, td)[data-off]:not([data-off="half"]) { background: color-mix(in srgb, black 15%, white); }
.paper .tbl-marks td.cell-mark-missing { background: repeating-linear-gradient(135deg, white 0 0.6mm, color-mix(in srgb, black 45%, white) 0.6mm 0.85mm); }
.paper .tbl-marks td.cell-mark-missing::after { content: none; }
.paper .tbl-marks td.cell-mark-na { background: repeating-linear-gradient(135deg, white 0 1.2mm, color-mix(in srgb, black 20%, white) 1.2mm 1.5mm); }
.paper .tbl-marks td[data-today]::before { content: none; }
.paper .tbl-marks :is(.cell-corner, .cell-pop) { display: none; }
.paper .tbl-marks :is(th, td) { -webkit-print-color-adjust: exact; print-color-adjust: exact; }

/* What surrounds a sheet on screen and is no use on paper (the print and back buttons, a hint): hidden when printing, and only then. */
@media print {
  .no-print { display: none !important; }
}

/* 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; }
/* The same drawing in the plain card: svg.login-art as the first child of .panel-body, above .login-brand. Here it is a mark, not a backdrop: it sits in the flow, --login-art-w wide
   (4rem), at the start edge like the brand and in the brand colour (the svg takes currentColor, so a tenant can colour it). Give it aria-hidden="true": it is decoration. */
.login-card .login-art { position: static; width: var(--login-art-w, 4rem); max-width: 100%; justify-self: start; color: var(--primary); }
.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);
}
/* Card for the open (or last closed) period, above .sidebar-foot: .side-card > .side-card-head (.side-card-icon, .side-card-text > .side-card-label, .side-card-name, .side-card-sub) + a block button.
   .sidebar-pinned on the sidebar keeps the card and the foot at the foot of the window and scrolls only .sidebar-nav, which fades out above the card. Collapsed to icons the card is hidden. */
.side-card { display: grid; gap: var(--sp-3); margin-inline: var(--sp-2); padding: var(--sp-3); background: var(--surface-2); border: 1px solid var(--panel-border); border-radius: var(--radius-lg); }
.side-card-head { display: flex; align-items: center; gap: var(--sp-3); }
.side-card-icon { display: grid; flex: none; place-items: center; width: var(--control-h); height: var(--control-h); color: var(--primary); background: var(--surface); border: 1px solid var(--panel-border); border-radius: var(--radius); }
.side-card-text { display: grid; min-width: 0; line-height: 1.35; }
.side-card-label { font-size: var(--fs-xs); color: var(--muted); }
.side-card-name { font-weight: 600; color: var(--heading); }
.side-card-sub { font-size: var(--fs-sm); color: var(--muted); }
.sidebar-pinned { overflow-y: hidden; }
.sidebar-pinned > .sidebar-nav { flex: 1 1 auto; min-height: 0; padding-bottom: var(--sp-4); overflow-y: auto; -webkit-mask-image: linear-gradient(to bottom, black calc(100% - var(--sp-5)), transparent); mask-image: linear-gradient(to bottom, black calc(100% - var(--sp-5)), transparent); }
.sidebar-pinned > :is(.side-card, .sidebar-foot) { flex: none; }
.sidebar[data-collapsed="true"] .side-card { display: none; }

/* 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) + var(--action-bar-extra, 0px) + 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; }

/* Floating action button: the one main action of a top-level screen, a round primary button above the tab bar at the end edge. <button class="fab" type="button" aria-label="Thêm việc">
   <svg class="icon" aria-hidden="true"/></button>; .fab-ext adds a word beside the icon and makes it a pill (then the word is the name, no aria-label); a.fab is a link that navigates.
   --fab-bottom moves it where there is no tab bar. Put .has-fab next to .has-tabbar on the page so the last line stays clear of the button as well as the bar. Not for a screen that has
   an .action-bar (its main button is already in reach). A bottom toast host lifts above the button as it does above the bar. The "app" look refines the shadow and the press. */
:root { --fab-size: calc(var(--tap) + var(--sp-3)); }
.fab {
  position: fixed;
  inset-inline-end: var(--sp-4);
  bottom: var(--fab-bottom, calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px) + var(--sp-4)));
  z-index: var(--z-topbar);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  min-width: var(--fab-size);
  height: var(--fab-size);
  padding: 0;
  font: inherit;
  font-weight: 600;
  white-space: nowrap;
  color: var(--on-primary);
  background: var(--primary);
  border: 0;
  border-radius: 50%;
  box-shadow: var(--sh-3);
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--dur-1) var(--ease), transform var(--dur-1) var(--ease);
}
.fab:hover { background: var(--primary-hover); }
.fab:active { transform: scale(0.96); }
.fab .icon { width: var(--icon-lg); height: var(--icon-lg); }
.fab-ext { padding-inline: var(--sp-5); border-radius: var(--radius-pill); }
.has-tabbar.has-fab { padding-bottom: calc(var(--tabbar-h) + var(--fab-size) + var(--sp-8) + env(safe-area-inset-bottom, 0px)); }
body:has(.fab) .toast-host-bottom { --toast-bottom: calc(var(--fab-bottom, calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px) + var(--sp-4))) + var(--fab-size) + var(--sp-3)); }

/* 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:is(:has(> .topbar-back), :has(> .brand-mark:first-child), :has(> svg.icon:first-child)) {
  flex-wrap: nowrap;
  justify-content: flex-start;
  gap: var(--sp-2);
  padding-inline: var(--sp-2) var(--sp-3);
}
.topbar:is(:has(> .topbar-back), :has(> .brand-mark:first-child), :has(> svg.icon:first-child)) .btn-ghost { border-color: transparent; }
.topbar:is(:has(> .topbar-back), :has(> .brand-mark:first-child), :has(> svg.icon:first-child)) > .topbar-actions { flex: none; gap: var(--sp-1); }
.topbar-back { flex: none; }
/* The header of a first-level phone page (home, a list, stock) has no back button: the first child is a .brand-mark (the tenant's mark) or an svg.icon (the page's icon, aria-hidden)
   instead, and the bar is laid out the same way, with the same row height and the same cut with "…" for .topbar-title-text (the h1 of the page, so every phone page has a heading) and
   .topbar-title-sub (the date, say). At the end .topbar-actions holds one or two .btn.btn-ghost.btn-icon (refresh, search, ⋯), each with an aria-label. The mark and the icon sit at the
   page's own 16px gutter, where the back button's 44px box would. Only a mark or an icon that is the FIRST child counts, so the desktop bar (.brand and .nav), the slim bar of a sidebar
   layout and the sub-page header are as they were. */
.topbar > .brand-mark:first-child { margin-inline-start: var(--sp-2); }
.topbar > svg.icon:first-child { width: var(--icon-lg); height: var(--icon-lg); margin-inline: var(--sp-2) var(--sp-1); color: var(--header-muted); }
.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); }
/* A figure in the bar: .action-bar-sum is a block before the buttons (after the note, when there is one) with .action-bar-sum-label ("Chi từ Tiền mặt", one line, cut with "…" when it
   does not fit, at 11rem) over .action-bar-sum-figure, which carries a .num-in, .num-out or .num-neg class and the sign in its text ("− 350.000" and a .num-unit "đ"). The figure never wraps and the
   sum is never narrower than it: the label gives way, the buttons share what is left. With the sum and ONE button the bar stays one row at 390px (a 12-digit amount, "− 123.456.789.012 đ",
   still leaves the primary button 150px). With the sum, a ⋯ button and the primary one the same 12 digits do not fit one row: the primary button wraps under. With the sum and TWO buttons the sum takes the whole first row and the buttons share the second, so the bar is two rows high; the page's
   --action-bar-extra (a row, set here on the root) keeps .has-action-bar and the toast above it. A ⋯ button (.btn-icon, a menu) does not count as one of the two and keeps its width.
   For a screen that needs three actions: the primary button and a ⋯ .menu with the others, never a third button. */
.action-bar-sum { flex: 1 1 auto; display: grid; min-width: min-content; line-height: var(--lh-tight); }
.action-bar-sum-label { max-width: 11rem; overflow: hidden; font-size: var(--fs-sm); color: var(--muted); text-overflow: ellipsis; white-space: nowrap; }
.action-bar-sum-figure { margin: 0; font-size: var(--fs-xl); font-weight: 700; white-space: nowrap; font-variant-numeric: tabular-nums; }
.action-bar > .btn-icon { flex: none; }
.action-bar > .btn-icon.btn-lg { width: calc(var(--tap) + 4px); }
.action-bar:has(> .action-bar-sum ~ .btn:not(.btn-icon) ~ .btn:not(.btn-icon)) > .action-bar-sum { flex: 1 0 100%; }
:root:has(.action-bar > .action-bar-sum ~ .btn:not(.btn-icon) ~ .btn:not(.btn-icon)) { --action-bar-extra: calc(var(--tap) + var(--sp-2)); }
/* .action-bar-sticky (same markup, same content): from 64rem up, where there is no phone shell, the bar is not fixed to the window edge but a card at the end of its form column (put it
   last in the column) that sticks to the bottom of the window, --sp-4 above the edge, until the column ends. Under 64rem it is the fixed bar above. Give the page .has-action-bar as for
   the phone: from 64rem the room it reserves is dropped, since the card takes its place in the page. The column is a block or a flex column, not a grid: a sticky grid item can only move
   inside its own grid area. */
@media (min-width: 64rem) {
  .action-bar-sticky {
    position: sticky;
    inset-inline: auto;
    bottom: var(--sp-4);
    margin-top: var(--sp-4);
    padding: var(--sp-3) var(--sp-4);
    border: 1px solid var(--panel-border);
    border-radius: var(--radius-lg);
    box-shadow: var(--sh-2);
  }
  .has-action-bar:has(.action-bar-sticky) { padding-bottom: 0; }
  /* The card is wide enough for the sum and two buttons on one row. */
  .action-bar-sticky:has(> .action-bar-sum ~ .btn:not(.btn-icon) ~ .btn:not(.btn-icon)) > .action-bar-sum { flex: 1 1 auto; }
}

/* 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,
  .draft-banner .btn-link,
  .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); }
  .sort-handle { width: var(--tap); }
  .month-btn { height: var(--tap); }
  a.cc-col { min-width: var(--tap); }
  .panel-fold > summary { min-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); }
}

/* The same sizes under a mouse, as an opt-in: <html data-tap="all"> for a tenant whose people use a large target with a mouse (a touch-screen till, a laptop, or a design that wants 44px everywhere).
   Nothing changes where the attribute is not set. It repeats the touch block's sizes above (keep the two lists in step) and leaves out the field text size, which is there only so iOS Safari
   does not zoom. The app look has its own rules for this in look-app.css. */
:root[data-tap="all"] :is(.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); }
:root[data-tap="all"] :is(.btn-link.btn-sm, .draft-banner .btn-link, .panel-actions > .btn-link) { min-height: var(--tap); }
:root[data-tap="all"] :is(.btn-icon.btn-sm, .pager .btn) { min-width: var(--tap); }
:root[data-tap="all"] .btn-icon.btn-sm { width: var(--tap); }
:root[data-tap="all"] .chip-x { width: var(--tap); height: var(--tap); margin-block: -1px; }
:root[data-tap="all"] .datepicker-btn { width: var(--tap); height: var(--tap); }
:root[data-tap="all"] .sort-handle { width: var(--tap); }
:root[data-tap="all"] .month-btn { height: var(--tap); }
:root[data-tap="all"] a.cc-col { min-width: var(--tap); }
:root[data-tap="all"] .panel-fold > summary { min-height: var(--tap); }
:root[data-tap="all"] .datepicker .input { padding-inline-end: calc(var(--sp-2) + var(--tap) + var(--sp-1)); }

/* Offline draft (kit.js, form[data-draft]): .draft-banner is the notice kit.js puts at the top of a form that has a draft to bring back (.notice.notice-warn, "Khôi phục" and
   "Bỏ nháp"); the form adds a little room under it. .draft-offline is the line it shows while the browser is offline (a wifi-slash icon and one sentence, role="status"); in an
   .action-bar it is a full row above the buttons, centred, like .action-bar-note, so a fixed bar grows by a line (--action-bar-extra). The icon and the words carry it, not the colour. */
.draft-banner { margin-bottom: var(--sp-4); }
.draft-offline { display: flex; align-items: center; gap: var(--sp-2); margin: var(--sp-3) 0 0; font-size: var(--fs-sm); color: var(--text); }
.draft-offline .icon { flex: none; color: var(--warn); }
.action-bar > .draft-offline { flex: 1 0 100%; justify-content: center; margin: 0; }

/* Summary strip ------------------------------------------------------------ */

/* A flat strip of two to four totals inside a panel (income, spending, difference; opening, in and out, closing): dl.stat-strip > div.stat-strip-item > dt (the name, muted) + dd (the figure).
   No icon, no shadow, only a 1px line round the strip and between the cells. A dd is a figure: --font-figure, tabular, it never wraps (put .num-in, .num-out or .num-neg on a span inside, the
   sign in the text with a no-break space and a true minus, as on .kpi-value; a plain figure takes no class), and it may hold two (a "+29,6tr" and a "−16,5tr", with a space between the spans) that
   wrap between them. The strip keeps ONE row for two or three cells at every width down to 17.5rem: the cells shrink, a dt wraps to a second line, and the figure steps down with the width of the strip (a
   container query, no script) from --fs-lg to --fs-xs, never below, so "− 12.400.000 đ" fits a cell of about 100px; --fit-chars says another length than 14 characters. Four cells go two by two
   under 24rem. Under 17.5rem, where three figures no longer fit side by side, every cell is a full row (the dt at the start, the figure at the end). The cell of the figure that matters most takes
   .stat-strip-item-total (a bolder figure, its dt in --text). A dt and its dd stay together for a screen reader as a description list; the dt says what the figure is, with no icon. The strip is the
   container, so it needs a width of its own: a block in a panel, not a shrink-to-fit flex item. */
.stat-strip {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  margin: 0;
  container-type: inline-size;
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
}
.stat-strip-item {
  --stat-cells: 3;
  --stat-fit-cells: var(--stat-cells);
  --stat-pad: var(--sp-3);
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: var(--sp-1);
  grid-column: span calc(12 / var(--stat-cells));
  min-width: 0;
  padding: var(--sp-2) var(--stat-pad);
}
.stat-strip:has(> :nth-child(2):last-child) > .stat-strip-item { --stat-cells: 2; }
.stat-strip:has(> :nth-child(4):last-child) > .stat-strip-item { --stat-cells: 4; }
.stat-strip-item + .stat-strip-item { border-inline-start: 1px solid var(--panel-border); }
.stat-strip-item dt { font-size: var(--fs-sm); line-height: var(--lh-tight); color: var(--muted); overflow-wrap: anywhere; }
.stat-strip-item dd {
  margin: 0;
  font-family: var(--font-figure, inherit);
  font-size: clamp(var(--fs-xs), calc((100cqw / var(--stat-fit-cells) - 2 * var(--stat-pad)) / (var(--fit-chars, 14) * 0.52)), var(--fs-lg));
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  line-height: var(--lh-tight);
  white-space: nowrap;
}
.stat-strip-item dd > * { white-space: nowrap; }
.stat-strip-item dd:has(> * + *) { white-space: normal; }
.stat-strip-item-total dt { color: var(--text); font-weight: 600; }
.stat-strip-item-total dd { font-weight: 700; }
/* A cell with a tone: .stat-strip-item[data-tone="ok|warn|bad"] (danger is the same as bad) colours its figure, the way a .kpi's tone does for the card. The dt or a word in the dd still says what the figure
   means ("Lỗi", "Quá hạn"), so the colour is never alone. --ok, --warn and --danger on --surface are pairs the gate tests. */
.stat-strip-item[data-tone="ok"] dd { color: var(--ok); }
.stat-strip-item[data-tone="warn"] dd { color: var(--warn); }
.stat-strip-item:is([data-tone="bad"], [data-tone="danger"]) dd { color: var(--danger); }
@container (max-width: 24rem) {
  .stat-strip-item { --stat-pad: var(--sp-1); }
  .stat-strip:has(> :nth-child(4):last-child) > .stat-strip-item { --stat-cells: 2; }
  .stat-strip:has(> :nth-child(4):last-child) > .stat-strip-item:nth-child(3) { border-inline-start: 0; }
  .stat-strip:has(> :nth-child(4):last-child) > .stat-strip-item:nth-child(n+3) { border-top: 1px solid var(--panel-border); }
}
@container (max-width: 17.5rem) {
  .stat-strip-item {
    --stat-fit-cells: 1.7; /* the name takes the rest of the row */
    --stat-pad: var(--sp-3);
    flex-direction: row;
    align-items: baseline;
    grid-column: 1 / -1;
  }
  .stat-strip-item + .stat-strip-item { border-inline-start: 0; border-top: 1px solid var(--panel-border); }
  .stat-strip-item dt { overflow-wrap: normal; }
  .stat-strip-item dd { flex: 1 0 auto; text-align: end; }
}

/* Panel tone and state, and a row that folds ------------------------------------ */

/* A panel can say how things stand with it. data-tone="ok|warn|bad|info" paints a 4px bar at its inline-start edge, as a .work-row[data-tone] does (the bar is the panel's own border, so
   nothing inside can cover it, and in the app look too). The bar is never alone: put the word in the head, a .tag with the same tone (.tag-ok, .tag-warn, .tag-bad, .tag-info: "Có vấn đề", "Đã
   duyệt") in .panel-state. data-state="unsaved" draws the edge dashed in --field-border, no shadow, as a provisional thing, with a "Chưa lưu" .tag.tag-warn in .panel-state; data-state="readonly"
   fills the panel with --surface-2 and mutes its text, with a "Chỉ đọc" .tag (a lock icon) in .panel-state. Neither state disables anything: the inputs inside stay readable, so the markup still
   sets readonly or disabled on them. The two can sit together (an unsaved card with a problem keeps the bar on a dashed edge). .panel-state is the head's slot for that word: pushed to the end of
   the head, before .panel-actions. Text colours are the same pairs the kit gates on --surface and --surface-2. */
.panel[data-tone] { --panel-tone: var(--panel-border); border-inline-start: var(--sp-1) solid var(--panel-tone); }
.panel[data-tone="ok"] { --panel-tone: var(--ok); }
.panel[data-tone="warn"] { --panel-tone: var(--warn); }
.panel[data-tone="bad"] { --panel-tone: var(--danger); }
.panel[data-tone="info"] { --panel-tone: var(--info); }
.panel[data-state="unsaved"] { border-style: dashed; border-color: var(--field-border); box-shadow: none; }
.panel[data-state="unsaved"][data-tone] { border-inline-start-style: solid; border-inline-start-color: var(--panel-tone); }
.panel[data-state="readonly"] { color: var(--muted); background: var(--surface-2); box-shadow: none; }
.panel-head > .panel-state { flex: none; margin-inline-start: auto; }
.panel-head > .panel-state ~ .panel-actions { margin-inline-start: 0; }

/* A list row that folds into its content, light enough for twenty in a list: details.row-fold > summary (the row) + .row-fold-body (what it opens). The summary is a row at least --tap high:
   inside it put the same parts as a work row (.work-main with .work-title, .work-sub, .work-tags, .work-reason; .work-end; an .avatar before them) or, for plain text, a span.row-fold-main;
   the last part is svg.icon.row-fold-caret (aria-hidden), at the end, which turns over when the row is open. No panel chrome: a 1px line between rows (siblings, or li in .row-fold-list) and
   a light line under the summary when open. .row-fold-body is indented (--row-fold-indent, default 2.25rem: set it on the details to the width of the avatar and its gap, 2.75rem for .avatar-sm, to line the content up with the title).
   data-tone="ok|warn|bad|info" on the details paints the same 4px bar as a .work-row on the summary and the body, and the word for it goes in the summary (a .tag). The native element does the work: Enter and Space
   open it, no script, the marker is removed and the summary has the kit's focus ring drawn inside the row. Needs a page that draws a closed details' content hidden, which every browser does. */
.row-fold { --row-fold-tone: transparent; }
.row-fold[data-tone="ok"] { --row-fold-tone: var(--ok); }
.row-fold[data-tone="warn"] { --row-fold-tone: var(--warn); }
.row-fold[data-tone="bad"] { --row-fold-tone: var(--danger); }
.row-fold[data-tone="info"] { --row-fold-tone: var(--info); }
.row-fold + .row-fold,
.row-fold-list > li + li { border-top: 1px solid var(--panel-border); }
.row-fold-list { margin: 0; padding: 0; list-style: none; }
.row-fold > summary {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  min-height: var(--tap);
  padding: var(--sp-2) var(--sp-4);
  list-style: none;
  cursor: pointer;
  box-shadow: inset var(--sp-1) 0 0 var(--row-fold-tone);
  transition: background-color var(--dur-1) var(--ease);
}
.row-fold > summary::-webkit-details-marker { display: none; }
.row-fold > summary::marker { content: ""; }
.row-fold > summary:hover { background: var(--surface-hover); }
.row-fold > summary:focus-visible { outline-offset: -3px; }
.row-fold > summary > :where(.work-main, .row-fold-main) { flex: 1 1 auto; min-width: 0; }
.row-fold-caret { flex: none; margin-inline-start: auto; color: var(--muted); transition: transform var(--dur-1) var(--ease); }
.row-fold[open] > summary > .row-fold-caret { transform: rotate(180deg); }
.row-fold-body {
  padding: var(--sp-3) var(--sp-4) var(--sp-4) calc(var(--sp-4) + var(--row-fold-indent, 2.25rem));
  border-top: 1px solid var(--panel-border);
  box-shadow: inset var(--sp-1) 0 0 var(--row-fold-tone);
}

/* KPI extras ---------------------------------------------------------------- */

/* A change that is neither good nor bad (spending up 9,6%: the reader judges): .tag.delta.delta-neutral, the trend badge in --muted on --sunk, no ok or bad hue. The arrow still says up or
   down and the .visually-hidden "Tăng " / "Giảm " is still there; a plain .tag.delta says the same on --surface-2, this one reads as a different thing beside a .tag-ok or .tag-bad badge. */
.delta-neutral { color: var(--muted); background: var(--sunk); }
/* A card the page is about: .kpi[data-emphasis] (the attribute alone) takes a 4px --primary bar across its top and a figure 1.25 times larger (in the app look too, from its larger base figure), on the
   same --surface. It is weight, not a state, so it needs no word; keep to one emphasised card in a row. With data-tone the bar at the start edge stays beside it. Colours: --text and --primary
   on --surface, both gated. A .kpi-value-fit card keeps its fit (a bigger ceiling, never wider than the card). Not for .kpi-tabs. */
.kpi[data-emphasis] { box-shadow: inset 0 var(--sp-1) 0 var(--primary), var(--sh-1); }
.kpi[data-emphasis][data-tone] { box-shadow: inset var(--sp-1) 0 0 var(--kpi-tone), inset 0 var(--sp-1) 0 var(--primary), var(--sh-1); }
.kpi[data-emphasis] .kpi-value { font-size: calc(var(--fs-3xl) * 1.25); }
.kpi[data-emphasis] .kpi-value-fit { --kpi-fit-max: calc(var(--fs-3xl) * 1.25); font-size: var(--kpi-fit-size); }
/* The block counterpart of .skel, for a card or a stack of rows that is loading: span.skel-block (a block, --skel-w across, default 100%, --skel-h tall, default 3rem, the kit's pulse and
   --sunk). The blocks are decoration: aria-hidden="true" on each, aria-busy="true" on what holds them, and one .visually-hidden[role="status"] line says what is loading ("Đang tải số liệu").
   The pulse stops under prefers-reduced-motion, as .skel's does. How a card's skeleton is laid out (a .kpi's three parts, a table's rows) is the page's: put blocks in the real container. */
.skel-block {
  display: block;
  width: var(--skel-w, 100%);
  max-width: 100%;
  height: var(--skel-h, 3rem);
  background: var(--sunk);
  border-radius: var(--radius);
  animation: kit-pulse 1.2s ease-in-out infinite;
}

/* Before and after ------------------------------------------------------------ */

/* What an edit changed, field by field (a diary entry, the history of a voucher): dl.diff (or div.diff) > .diff-row (a div) > dt.diff-field (the field's name, muted; a span or div in a
   div.diff) + dd.diff-values holding del.diff-old, svg.icon.diff-arrow[aria-hidden="true"] and ins.diff-new. The old value is struck through in --muted; the new one is --text, weight 600, with
   no underline (the browser's default for ins), on --sunk. Browsers and screen readers expose del and ins unevenly, so the words are in the text: inside each, a
   span.visually-hidden "Trước: " and "Sau: ", and a screen reader hears "Trước: 45.000.000, Sau: 54.000.000"; the strike and the arrow are the second cue. An empty old value is
   del.diff-old.diff-empty reading "(trống)" and an empty new value ins.diff-new.diff-empty reading "(đã xóa)": muted, not struck, not bold (write the words in the markup, in the tenant's language).
   A number is wrapped in span.num inside, so its digits are tabular in --font-figure. Rows are separated by a thin line. A long value wraps (overflow-wrap: anywhere), never widens the box. Under
   28rem of the list's own width (a container query, no script) the name goes above its values and the old and new stack, old above new, the arrow turning down. The list is a container, so it
   needs a width of its own. Text colours are --text and --muted on --surface and on --sunk, all gated. */
.diff { margin: 0; container-type: inline-size; }
.diff-row {
  display: grid;
  grid-template-columns: minmax(6rem, 9rem) minmax(0, 1fr);
  gap: var(--sp-1) var(--sp-4);
  padding: var(--sp-2) 0;
  font-size: var(--fs-sm);
}
.diff-row:first-child { padding-top: 0; }
.diff-row:last-child { padding-bottom: 0; }
.diff-row + .diff-row { border-top: 1px solid var(--panel-border); }
.diff-field { margin: 0; font-weight: 600; color: var(--muted); overflow-wrap: anywhere; }
.diff-values { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-1) var(--sp-2); min-width: 0; margin: 0; }
.diff-old,
.diff-new { min-width: 0; overflow-wrap: anywhere; }
.diff-old { color: var(--muted); text-decoration: line-through; text-decoration-thickness: 1.5px; }
.diff-new { padding: 0 var(--sp-1); font-weight: 600; color: var(--text); text-decoration: none; background: var(--sunk); border-radius: var(--radius-sm); }
.diff-empty { padding: 0; font-weight: 400; color: var(--muted); text-decoration: none; background: none; }
.diff-arrow { flex: none; align-self: center; color: var(--muted); }
/* .diff-inline: one line per field in a narrow column (a side panel): the field at the start, the old value crossed out, the arrow and the new value at the end, even under 28rem. */
.diff-inline .diff-row { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-3); }
.diff-inline .diff-field { flex: 1 1 auto; font-weight: 500; color: var(--text); }
.diff-inline .diff-values { flex: none; flex-wrap: nowrap; }
@container (max-width: 28rem) {
  .diff-inline .diff-row { display: flex; }
  .diff-inline .diff-values { flex-direction: row; align-items: baseline; }
  .diff-inline .diff-arrow { align-self: center; transform: none; }
}
@container (max-width: 28rem) {
  .diff-row { grid-template-columns: minmax(0, 1fr); }
  .diff-values { flex-direction: column; align-items: flex-start; }
  .diff-arrow { align-self: flex-start; transform: rotate(90deg); }
}

/* Progress bar that reports ------------------------------------------------------ */

/* The .bar above is decoration beside a figure (aria-hidden). When the bar is the only source of a state (rows written, a file sent), it is a progressbar: div.bar[role="progressbar"] with
   aria-valuenow, aria-valuemin, aria-valuemax, aria-valuetext ("Ghi 120/300 dòng": the sentence a screen reader says instead of "40%") and a name, aria-label or aria-labelledby ("Ghi sổ"); the
   fill is --v (a share, 0% to 100%, which the page keeps in step with aria-valuenow), as for the small bar. It spans the width of its box (--bar-w: 100%; a rule that sets its own width, as the photo thumbnail's bar does, wins) and under a .bar-label it is 0.5rem thick (--bar-h says another).
   data-tone="ok|warn|bad" colours the fill like .bar-ok, .bar-warn and .bar-bad (one set of colours); a tone is never the only cue, so say it in the .bar-label or beside the bar. .bar-label is the
   line of figures that goes with it, above or beside, muted, --font-figure, tabular ("Ghi 120/300 dòng · 40%"); give it two spans and they sit at the two ends. The label repeats aria-valuetext, so mark it
   aria-hidden="true" when it is static; when the page changes it as work goes on, make it role="status" (polite) instead and leave it readable, since a progressbar is not announced when its value
   changes. .bar[data-indeterminate] is for work of unknown length: no aria-valuenow (keep aria-valuetext, "Đang ghi…"), the fill is moving diagonal stripes across the whole bar; under
   prefers-reduced-motion the stripes stand still. The fill's width eases (and does not, under reduced motion). */
.bar[role="progressbar"] { --bar-w: 100%; }
.bar[role="progressbar"]::before { transition: width var(--dur-2) var(--ease); }
.bar[data-tone="ok"] { --bar-fill: var(--ok); }
.bar[data-tone="warn"] { --bar-fill: var(--warn); }
.bar[data-tone="bad"] { --bar-fill: var(--danger); }
.bar-label {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 0 var(--sp-3);
  margin: 0;
  font-family: var(--font-figure, inherit);
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}
.bar-label + .bar { --bar-h: 0.5rem; margin-top: var(--sp-1); }
.bar[data-indeterminate]::before {
  width: 100%;
  background: linear-gradient(135deg, var(--bar-fill) 25%, transparent 25% 50%, var(--bar-fill) 50% 75%, transparent 75%) 0 0 / 1rem 1rem;
  border-radius: 0;
  animation: kit-bar-stripes 0.8s linear infinite;
  transition: none;
}
@keyframes kit-bar-stripes { to { background-position: 1rem 0; } }
@media (prefers-reduced-motion: reduce) {
  .bar[data-indeterminate]::before { animation: none; }
}

/* Contents strip and section head --------------------------------------------- */

/* The links to the parts of one page as a strip across the page, at every width: nav.toc.toc-strip[aria-label] > ul > li > a (one level; no .toc-label). The links sit in one row that scrolls
   sideways inside the strip, never the page, and snap to the start edge; each link is at least --tap high and keeps its words on one line. The part in view is marked aria-current="true"
   (aria-current="location" does the same): bold, in --text, with a 2px --primary bar under it, so the mark is not colour alone. The tenant moves the mark as the page scrolls. A link hidden for
   a role is hidden with the hidden attribute on its li (or the a): nothing to do in the CSS, and the others close up. The strip is a row of links in a scroll box; every link is focusable, so a
   keyboard user reaches the ones off screen (the browser scrolls the focused link into view). */
.toc-strip > ul {
  display: flex;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-padding-inline: var(--sp-2);
  scroll-snap-type: x proximity;
  scrollbar-width: thin;
  border-bottom: 1px solid var(--panel-border);
}
.toc-strip > ul > li { flex: none; scroll-snap-align: start; }
.toc-strip a {
  min-height: var(--tap);
  padding: var(--sp-2) var(--sp-4);
  white-space: nowrap;
  border-width: 0 0 2px;
  border-color: transparent;
  border-style: solid;
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
}
.toc-strip a:is([aria-current="true"], [aria-current="location"]) { font-weight: 600; color: var(--text); border-bottom-color: var(--primary); }
.toc a[hidden] { display: none; }
/* The head of a part of a page: div.section-head (or header) > h2 (or h3) + .section-head-actions (optional: a button such as "Excel", a link), with a 1px rule under it. It needs no panel. The
   title takes the room it can and the actions sit at the end; when the head is narrower than about 28rem the actions wrap under the title (the title keeps at least 20rem before they do). */
.section-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2) var(--sp-4);
  padding-bottom: var(--sp-2);
  border-bottom: 1px solid var(--panel-border);
}
.section-head > :where(h2, h3) { flex: 1 1 20rem; min-width: 0; margin: 0; font-size: var(--fs-xl); overflow-wrap: anywhere; }
.section-head-actions { display: flex; flex: none; flex-wrap: wrap; align-items: center; gap: var(--sp-2); }

/* 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,
  .fab,
  .toast-host { display: none; }
  body { background: none; }
  .panel,
  .toast { box-shadow: none; }
}

/* Charts --------------------------------------------------------------------- */

/* The charts of the kit: the share bar (100 % stacked, horizontal) and the target bar, the line and area chart, the opt-in interactive and stacked columns of .chart-cols, the
   sparkline, the donut, and the stat strip's links and deltas. No library: the bar family is HTML and CSS (the text never scales with a viewBox), the line, area, sparkline and donut are
   svg that kit.js draws from a table in real pixel coordinates. One layer in kit.js (TvKit.chart) gives them all the same tooltip, keyboard, touch, legend toggles, selection and motion.
   Text never wears a series colour: labels, values and legends are --text and --muted, and what tells the series apart is the mark beside the words; a label inside a filled segment takes
   the paired --on-* token. Every value drawn is also in a table (visible or .visually-hidden), so the tooltip adds and never gates. */

/* One pair of variables for every mark: --mark (the fill or stroke) and --mark-ink (a label on it), picked by data-series="1..4|prev" (the chart colours), data-grade="1..5" (the ordered
   grade scale, best to worst: --grade-1 A+ deep green with a white letter, 2 A light green, 3 B amber line tint, 4 C danger red, 5 D darker red) or data-tone="ok|warn|bad|info|muted|draft|na"
   (the tone lines; draft is --sunk hatched in --field-border, na is --surface-2 hatched). --grade-2, --grade-3 and the tone lines are light fills below 3:1 on white as shapes: allowed because
   a segment carries its letter or the table its value, the 2px gap separates neighbours, and the legend names them. check-palette.mjs holds each label ink on its fill to 4.5:1. */
:is(.share-seg, .chart-key, .cl-series, .cd-slice) { --mark: var(--series-1); --mark-ink: var(--on-series); }
:is(.share-seg, .chart-key, .cl-series, .cd-slice)[data-series="2"] { --mark: var(--series-2); }
:is(.share-seg, .chart-key, .cl-series, .cd-slice)[data-series="3"] { --mark: var(--series-3); }
:is(.share-seg, .chart-key, .cl-series, .cd-slice)[data-series="4"] { --mark: var(--series-4); }
:is(.share-seg, .chart-key, .cl-series, .cd-slice)[data-series="prev"] { --mark: var(--series-prev); }
:is(.share-seg, .chart-key, .cl-series, .cd-slice)[data-grade="1"] { --mark: var(--grade-1); --mark-ink: var(--on-grade-1); }
:is(.share-seg, .chart-key, .cl-series, .cd-slice)[data-grade="2"] { --mark: var(--grade-2); --mark-ink: var(--on-grade-2); }
:is(.share-seg, .chart-key, .cl-series, .cd-slice)[data-grade="3"] { --mark: var(--grade-3); --mark-ink: var(--on-grade-3); }
:is(.share-seg, .chart-key, .cl-series, .cd-slice)[data-grade="4"] { --mark: var(--grade-4); --mark-ink: var(--on-grade-4); }
:is(.share-seg, .chart-key, .cl-series, .cd-slice)[data-grade="5"] { --mark: var(--grade-5); --mark-ink: var(--on-grade-5); }
:is(.share-seg, .chart-key, .cl-series, .cd-slice)[data-tone="ok"] { --mark: var(--ok-line); --mark-ink: var(--text); }
:is(.share-seg, .chart-key, .cl-series, .cd-slice)[data-tone="warn"] { --mark: var(--warn-line); --mark-ink: var(--text); }
:is(.share-seg, .chart-key, .cl-series, .cd-slice)[data-tone="bad"] { --mark: var(--danger-line); --mark-ink: var(--text); }
:is(.share-seg, .chart-key, .cl-series, .cd-slice)[data-tone="info"] { --mark: var(--info-line); --mark-ink: var(--text); }
:is(.share-seg, .chart-key, .cl-series, .cd-slice)[data-tone="muted"] { --mark: var(--sunk); --mark-ink: var(--text); }
:is(.share-seg, .chart-key, .cl-series, .cd-slice)[data-tone="draft"] { --mark: var(--sunk); --mark-ink: var(--text); --mark-pattern: repeating-linear-gradient(135deg, transparent 0 4px, var(--field-border) 4px 5px); }
:is(.share-seg, .chart-key, .cl-series, .cd-slice)[data-tone="na"] { --mark: var(--surface-2); --mark-ink: var(--text); --mark-pattern: repeating-linear-gradient(135deg, transparent 0 4px, var(--field-border) 4px 5px); }

/* The key of a series: span.chart-key (a square of --mark, with its hatch) or svg.chart-key.chart-key-line > line (a stroke with the series' dash, drawn by kit.js), in the tooltip, in a
   legend toggle and in the donut's legend. */
.chart-key { flex: none; display: inline-block; width: 0.75rem; height: 0.75rem; background-color: var(--mark); background-image: var(--mark-pattern, none); border-radius: 3px; }
.chart-key.chart-key-line { width: auto; height: 0.75rem; overflow: visible; background: none; border-radius: 0; }
.chart-key-line line { stroke: var(--mark); stroke-linecap: round; }

/* The tooltip: one div.chart-tip[popover="manual"][role="tooltip"] for the page, made by kit.js on first use and filled with textContent only. It sits in the top layer, so no scroll box
   clips it, beside the pointer or the focused mark (12px away, flipped near a viewport edge, kept inside the viewport). A header (the category, small, --tip-muted), then one row per series:
   the key, the value bold in --font-figure (values lead), the series' name in --tip-muted; a missing value reads "Chưa có". It fades in over --dur-1 and never slides; under reduced motion it is
   there at once (the kit's global rule). It never takes the pointer, so it cannot sit between the pointer and the plot. */
.chart-tip {
  position: fixed;
  inset: auto;
  top: 0;
  left: 0;
  z-index: var(--z-toast);
  width: max-content;
  max-width: min(16rem, calc(100vw - 2 * var(--sp-2)));
  margin: 0;
  padding: var(--sp-2) 10px;
  overflow: visible;
  font-size: var(--fs-sm);
  font-weight: 400;
  line-height: var(--lh-tight);
  color: var(--tip-fg);
  background: var(--tip-bg);
  border: 0;
  border-radius: var(--radius-sm);
  box-shadow: var(--sh-2);
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--dur-1) var(--ease);
}
.chart-tip.is-on { opacity: 1; }
.chart-tip-head { margin-bottom: var(--sp-1); font-size: var(--fs-xs); color: var(--tip-muted); }
.chart-tip-row { display: flex; align-items: center; gap: var(--sp-2); }
.chart-tip-row + .chart-tip-row { margin-top: 3px; }
.chart-tip-val { font-family: var(--font-figure, inherit); font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; }
.chart-tip-name { min-width: 0; color: var(--tip-muted); overflow-wrap: anywhere; }
.chart-tip-text { min-width: 0; overflow-wrap: anywhere; }

/* A legend whose items switch series on and off (data-legend-toggle on the figure; kit.js builds it for a line chart): ul.legend.legend-toggle > li > button.legend-item[type="button"]
   [aria-pressed="true"] > the key + the words. Hover or focus on an item dims the other series to 25 %; a click hides or shows its series (one always stays on) and the y scale stays as it was,
   so what is left compares honestly and nothing jumps. An item that is off keeps its words, in --muted and struck through, and its key at a third. Not the "Legend" of a grid above: that one is
   plain text and stays so. */
.legend-toggle { gap: var(--sp-1) var(--sp-2); }
.legend-toggle .legend-item {
  min-height: 2rem;
  padding: var(--sp-1) var(--sp-2);
  font: inherit;
  color: var(--text);
  text-align: start;
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.legend-toggle .legend-item:hover { background: var(--surface-hover); }
.legend-toggle .legend-item[aria-pressed="false"] { color: var(--muted); text-decoration: line-through; }
.legend-toggle .legend-item[aria-pressed="false"] .chart-key { opacity: 0.35; }
@media (pointer: coarse) {
  .legend-toggle .legend-item { min-height: var(--tap); }
}

/* Share bar: parts of one whole side by side (the grades of a unit, the states of the forms of a period).
     div.share-bar[role="img"][aria-label="Trạm Hòa Lạc: A+ 2 người (3,4 %), …"][data-tip-title="Trạm Hòa Lạc"] > span.share-seg[data-grade="1"][style="--v:.034"][data-tip="A+ · 2 người · 3,4 %"]
       (> span.share-lab "A+" in a .share-bar-lg)
   or, when each part opens its list, div.share-bar[role="group"][aria-label] > a.share-seg[href][aria-label="Chờ đánh giá: 24 phiếu, 12,1 %"][data-tip]. The colour comes from data-grade,
   data-tone or data-series (above; none is series 1). --v is the raw share, not the rounded label (6,7 + 86,7 + 6,7 printed is 100,1 and the widths are still right): it is a flex weight, so the
   parts fill the bar whatever they add up to. A part above 0 is at least 3px wide, so 0,51 % still shows; leave out a part of 0. A 2px gap between parts, a 4px radius only at the two outer ends.
   12px tall with no labels by default; .share-bar-lg is 24px with .share-lab inside each part, which kit.js hides when it does not fit with 4px each side (.is-tight on the part; the tooltip and
   the table carry it then). Nothing is cropped with overflow. In a table put the bar in a td: bars in one column line up by themselves; a tfoot total row takes .share-bar-lg. No evaluations
   at all: .share-bar.share-bar-empty > span.share-empty "Chưa có đánh giá" (a dashed track, never a NaN or an empty bar). The tooltip shows data-tip-title and the part's data-tip. kit.js
   grows the bar from the inline start, once, when it comes into view; a changed --v eases (not under reduced motion). */
.share-bar {
  --share-h: 12px;
  --share-r: 4px;
  position: relative;
  display: flex;
  gap: 2px;
  min-width: var(--share-min, 6rem);
  height: var(--share-h);
  contain: inline-size; /* in a table the labels inside must not widen the column: the bar takes the cell's width, kit.js hides labels that do not fit */
}
.share-bar-lg { --share-h: 24px; }
.share-seg {
  display: flex;
  flex: calc(var(--v, 0) * 1000) 1 0;
  align-items: center;
  justify-content: center;
  min-width: clamp(0px, var(--v, 0) * 100000px, 3px);
  color: var(--mark-ink);
  text-decoration: none;
  background-color: var(--mark);
  background-image: var(--mark-pattern, none);
  transition: flex-grow var(--dur-2) var(--ease), width var(--dur-2) var(--ease);
}
.share-seg.is-zero { display: none; }
.share-seg:nth-child(1 of .share-seg:not(.is-zero)) { border-start-start-radius: var(--share-r); border-end-start-radius: var(--share-r); }
.share-seg:nth-last-child(1 of .share-seg:not(.is-zero)) { border-start-end-radius: var(--share-r); border-end-end-radius: var(--share-r); }
a.share-seg:hover { filter: brightness(0.9); }
a.share-seg:focus-visible { position: relative; z-index: 1; outline: 2px solid var(--focus); outline-offset: 2px; }
.share-lab { padding-inline: 4px; font-size: var(--fs-xs); font-weight: 700; line-height: 1; white-space: nowrap; pointer-events: none; }
.share-bar:not(.share-bar-lg) .share-lab,
.share-seg.is-tight .share-lab { visibility: hidden; }
.share-bar:not(.share-bar-lg) .share-seg,
.share-seg.is-tight { overflow: clip; } /* only a label already hidden is clipped, so it cannot widen the page */
.share-bar-empty { align-items: center; justify-content: center; height: auto; min-height: var(--share-h); padding: 1px var(--sp-2); border: 1px dashed var(--field-border); border-radius: var(--share-r); }
.share-empty { font-size: var(--fs-xs); line-height: var(--lh-tight); color: var(--muted); }
@media (pointer: coarse) {
  .share-bar:has(> a.share-seg) { --share-h: 24px; }
}

/* Target bar: .share-bar.share-bar-abs, parts on an absolute scale the author picks (weights out of 105 when they add up to 105: --v is each weight / 105, so 100 sits at --at: .952). A --sunk
   track shows the rest of the scale; the parts keep exact places (the 2px gap is an edge in --share-gap, default --surface, so set it to the row's ground in a tinted row). span.share-target
   [style="--at:.952"] > span.share-target-lab "100": a 2px --text line that stands 4px above and below the bar with its label over it (the bar takes room above for it). The part beyond the
   target is a.share-seg.share-over or span: --danger with a 135° hatch of --danger-line, no label (the target and the words beside the bar say "thừa 5"). Change --v and --at as the weights are
   typed: the widths and the line ease over --dur-2 (not under reduced motion). */
.share-bar-abs { gap: 0; background: var(--sunk); border-radius: var(--share-r); }
.share-bar-abs > .share-seg { box-sizing: border-box; flex: none; width: calc(var(--v, 0) * 100%); border-inline-end: 2px solid var(--share-gap, var(--surface)); }
:is(.share-seg, .chart-key).share-over { --mark: var(--danger); --mark-pattern: repeating-linear-gradient(135deg, transparent 0 4px, var(--danger-line) 4px 7px); }
.share-bar:has(.share-target-lab) { margin-top: calc(var(--fs-xs) + var(--sp-3)); }
.share-target {
  position: absolute;
  top: -4px;
  bottom: -4px;
  inset-inline-start: calc(var(--at, 1) * 100%);
  width: 2px;
  margin-inline-start: -1px;
  background: var(--text);
  border-radius: 1px;
  pointer-events: none;
  transition: inset-inline-start var(--dur-2) var(--ease);
}
.share-target-lab { position: absolute; bottom: calc(100% + 2px); left: 50%; font-size: var(--fs-xs); font-weight: 700; line-height: 1; color: var(--text); white-space: nowrap; transform: translateX(-50%); }

/* The entry of the bar family (kit.js adds these only when the part comes into view, never without script or under reduced motion): a share bar opens from the inline start in 600ms;
   (the bar waits at opacity 0, not clipped, because an observer does not see a clipped box come into view); columns grow from the baseline in 600ms, one group after the other (--cc-i, the group's place, times --cc-stagger: min(40ms, 480ms / groups)). */
.share-bar.is-pre { opacity: 0; }
.share-bar.is-entering { clip-path: inset(-2rem 0 -2rem 0); transition: clip-path 600ms var(--ease-enter); }
.share-bar.is-start { clip-path: inset(-2rem 100% -2rem 0); }
.chart-cols[data-chart] .cc-col::before,
.chart-cols[data-chart].cc-stacked .cc-cols { transform-origin: bottom; }
.chart-cols[data-chart].is-pre .cc-col::before,
.chart-cols[data-chart].cc-stacked.is-pre .cc-cols { transform: scaleY(0); }
.chart-cols[data-chart].cc-stacked.is-pre .cc-col::before { transform: none; }
.chart-cols[data-chart].is-pre .cc-mark { opacity: 0; }
.chart-cols[data-chart].is-entering .cc-col::before,
.chart-cols[data-chart].cc-stacked.is-entering .cc-cols { transition: transform 600ms var(--ease-enter) calc(var(--cc-i, 0) * var(--cc-stagger, 40ms)); }
.chart-cols[data-chart].is-entering .cc-mark { transition: opacity var(--dur-2) var(--ease) calc(600ms + var(--cc-i, 0) * var(--cc-stagger, 40ms)); }

/* Interactive columns (data-chart on figure.chart-cols; nothing about .chart-cols changes without it). kit.js gives each group a tooltip (the values from data-value on each .cc-col, formatted
   with the figure's data-decimals and data-unit, or else from the column's hidden words after the colon; data-value on .cc-mark adds the previous period), a band of --surface-hover behind the
   group's full height while it is hovered or focused, the entry above, and with data-select a selection: the chosen group keeps its columns darker (brightness .85) and its label bold, and
   carries aria-pressed="true". A plot of span columns becomes one tab stop (role="group", ← → between groups, Home, End, Enter, Esc); link columns stay their own tab stops and their focus
   shows the same tooltip. A changed --v eases over 350ms. */
.chart-cols[data-chart] .cc-group::before {
  content: "";
  position: absolute;
  inset: 0 0 var(--cc-label-h);
  z-index: -1;
  background: var(--surface-hover);
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
  opacity: 0;
  transition: opacity var(--dur-1) var(--ease);
}
.chart-cols[data-chart] .cc-group.is-hot::before { opacity: 1; }
.chart-cols[data-chart] .cc-col::before,
.chart-cols[data-chart].cc-stacked .cc-col { transition: height 350ms var(--ease); }
.chart-cols[data-chart] .cc-group[aria-pressed="true"] .cc-col::before { filter: brightness(0.85); }
.chart-cols[data-chart] .cc-group[aria-pressed="true"] .cc-label { font-weight: 700; color: var(--text); }
.chart-cols[data-chart] .cc-plot:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }

/* Stacked columns: .chart-cols.cc-stacked stacks the columns of a group upwards (the first column of the group at the bottom), each --v a share of the axis maximum, so a group stands as
   high as its sum. A 2px gap between pieces (cut from the piece above, so the top stays exact), a 4px radius on the top piece only, and the patterns of each series as in the grouped chart. */
.cc-stacked .cc-cols { flex-direction: column-reverse; align-items: center; justify-content: flex-start; gap: 0; }
.cc-stacked .cc-col { box-sizing: border-box; flex: none; width: 100%; height: calc(var(--v, 0) * 100%); }
.cc-stacked .cc-col + .cc-col { padding-bottom: 2px; }
.cc-stacked .cc-col::before { height: 100%; border-radius: 0; }
.cc-stacked .cc-col:last-child::before { border-radius: var(--radius-sm) var(--radius-sm) 0 0; }

/* A reference line across the columns: div.cc-ref[style="--t:.8"] > span.cc-ref-lab "B" inside .cc-plot (--t is its place, 0 to 1 of the axis maximum), drawn like the line chart's:
   1px --muted dashes (4 on, 3 off), the label at the end edge in --fs-xs --muted on the card's ground; it is not a gridline and takes no pointer. */
.cc-ref {
  position: absolute;
  inset-inline: 0;
  bottom: calc(var(--cc-label-h) + var(--t, 0) * var(--cc-h));
  z-index: 2;
  height: 1px;
  background: repeating-linear-gradient(to right, var(--muted) 0 4px, transparent 4px 7px);
  pointer-events: none;
}
.cc-ref-lab { position: absolute; inset-inline-end: 0; bottom: 2px; padding-inline: 2px; font-size: var(--fs-xs); line-height: 1; color: var(--muted); background: var(--cc-bg); }

/* Line and area chart:
     figure.chart-line[data-chart-table="t-diem"] (data-y-min, data-y-max, data-y-step, data-refs="80:B;90:A;105:A+", data-unit=" điểm", data-decimals="1", data-curve="linear", data-area,
     data-legend-toggle, data-select, data-series-in="cols") > figcaption.chart-title
   followed by the table it draws (table#t-diem.tbl, visible or .visually-hidden: header cells after the first are the categories, each tbody tr a series named by its th; data-value on a cell
   wins over its text; an empty cell is a gap, never 0). kit.js adds the legend, div.cl-plot (one tab stop) > svg.cl-svg, and the live region. Series 1 solid, 2 long dashes, 3 dots, 4 dash
   and dot, the previous period short dashes in --series-prev, so colour is never alone; the legend draws the same dashes. Lines are --chart-line-w (2.5px; 3px in the app look), round joins
   and caps; the curve is monotone (it never overshoots the data). Gridlines 1px --panel-border, no axis lines or ticks; labels --fs-xs --muted, x labels thinned when they would touch (the
   first and last always show). Reference lines 1px --muted dashed, the label beyond the end edge. At rest only the last point of each series has a dot, and any point with no neighbour (so
   an isolated value stays visible); on hover or focus a 1px --field-border crosshair snaps to the nearest category with a dot on every series. The plot is --chart-h tall (15rem, 12.5rem
   when the chart is under 40rem wide), as wide as its container, and redraws when that changes. The plot fades to half while aria-busy="true" is on the figure or a parent. */
.chart-line,
.chart-donut { --chart-bg: var(--surface); container-type: inline-size; min-width: 0; margin: 0; }
.chart-line { --chart-h: 15rem; --chart-line-w: 2.5px; }
.chart-title { margin: 0 0 var(--sp-2); font-size: var(--fs-md); font-weight: 700; }
.chart-line > .legend,
.chart-donut > .legend { margin-bottom: var(--sp-3); }
.cl-plot { position: relative; height: var(--chart-h); touch-action: pan-y; border-radius: var(--radius-sm); }
@container (max-width: 40rem) {
  .cl-plot { --chart-h: 12.5rem; }
}
.cl-plot:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.cl-svg { display: block; width: 100%; height: 100%; overflow: visible; }
.cl-grid { stroke: var(--panel-border); stroke-width: 1px; shape-rendering: crispEdges; }
.cl-tick,
.cl-xlab,
.cl-ref-lab { font-size: var(--fs-xs); font-variant-numeric: tabular-nums; fill: var(--muted); }
.cl-xlab[aria-pressed="true"] { font-weight: 700; fill: var(--text); }
.cl-ref { stroke: var(--muted); stroke-width: 1px; stroke-dasharray: 4 3; shape-rendering: crispEdges; }
.cl-ref-lab { paint-order: stroke; stroke: var(--chart-bg); stroke-width: 3px; stroke-linejoin: round; }
/* data-refs-x="03/11=Hạn 03/11": a vertical line at the category whose label is 03/11, in --danger; the words beside it and the category's own label say the same, so the colour is never alone. */
.cl-refx { stroke: var(--danger); stroke-width: 2px; shape-rendering: crispEdges; }
.cl-refx-lab { font-size: var(--fs-xs); font-weight: 700; fill: var(--danger); paint-order: stroke; stroke: var(--chart-bg); stroke-width: 4px; stroke-linejoin: round; }
.cl-xlab.cl-refx-tick { font-weight: 700; fill: var(--danger); }
.cl-series { transition: opacity var(--dur-1) var(--ease); }
.cl-line { fill: none; stroke: var(--mark); stroke-width: var(--chart-line-w); stroke-linecap: round; stroke-linejoin: round; }
.cl-area { fill: var(--mark); fill-opacity: 0.1; stroke: none; }
.cl-dot { fill: var(--mark); stroke: var(--chart-bg); stroke-width: 2px; transform-box: fill-box; transform-origin: center; }
.cl-cross { stroke: var(--field-border); stroke-width: 1px; shape-rendering: crispEdges; }
.cl-cross-sel { stroke: var(--muted); }
:is(.cl-series, .cd-slice).is-dim { opacity: 0.25; }
:is(.cl-plot, .cd-body, .share-bar) { transition: opacity var(--dur-2) var(--ease); }
.chart-cols[data-chart] .cc-scroll { transition: opacity var(--dur-2) var(--ease); }
[aria-busy="true"] :is(.cl-plot, .cd-body, .share-bar),
.share-bar[aria-busy="true"],
[aria-busy="true"].chart-cols[data-chart] .cc-scroll,
[aria-busy="true"] .chart-cols[data-chart] .cc-scroll { opacity: 0.5; }
/* No data at all: kit.js puts p.chart-empty in the plot ("Chưa có số liệu", or the figure's data-empty) and draws no axes. */
.chart-empty { display: grid; place-items: center; box-sizing: border-box; height: 100%; margin: 0; padding: var(--sp-3); font-size: var(--fs-sm); text-align: center; color: var(--muted); border: 1px dashed var(--panel-border); border-radius: var(--radius); }

/* Donut: figure.chart-donut[data-chart-table] (data-unit, data-decimals, data-total-label "phiếu", data-select) > figcaption.chart-title, followed by a table of one series whose categories
   are the slices (a row of values under the header, or a column of values beside the row heads). Part-to-whole for 2 to 5 parts; when parts are compared across rows, use .share-bar. kit.js
   draws div.cd-body (one tab stop, ← → between slices) > svg.cd-svg + div.cd-centre (the total, --fs-2xl in --font-figure, and the word under it), then ul.cd-legend, the two inside div.cd-wrap. Up to four
   slices the legend is one column per slice (key, name, share large), four going 2 x 2 when they do not fit, so a slice never sits alone on a second line. Five or more make a list
   (.cd-legend-list): one row per slice with its key, name, value and share, beside the ring when the figure is 34rem wide or more, under it on a phone; the "Khác" row is a
   details that opens to the parts it holds, each with its own value and share. The ring is 22 % of the radius thick, with 2px gaps of --chart-bg; slices keep the table's order (--series-1..4, or data-series, data-grade or
   data-tone on the header cell of the slice) and more than five fold into "Khác" (--series-prev). A hovered or focused slice pulls out 4px and shows the tooltip; a selected one stays out.
   The ring sweeps in clockwise in 800ms, once. --donut-size sets the ring (12rem). */
.chart-donut { --donut-size: 12rem; }
.cd-body { position: relative; width: var(--donut-size); max-width: 100%; aspect-ratio: 1; margin-inline: auto; touch-action: pan-y; border-radius: 50%; }
.cd-body:focus-visible { outline: 2px solid var(--focus); outline-offset: 4px; }
.cd-svg { display: block; width: 100%; height: 100%; overflow: visible; }
.cd-slice { fill: var(--mark); stroke: var(--chart-bg); stroke-width: 2px; stroke-linejoin: round; transition: transform var(--dur-1) var(--ease), opacity var(--dur-1) var(--ease); }
.cd-track { fill: var(--sunk); }
.cd-centre { position: absolute; inset: 0; display: grid; place-content: center; padding: 22%; text-align: center; pointer-events: none; }
.cd-total { font-family: var(--font-figure, inherit); font-size: var(--fs-2xl); font-weight: 700; line-height: 1.1; font-variant-numeric: tabular-nums; }
.cd-word { font-size: var(--fs-xs); color: var(--muted); }
.cd-centre .chart-empty { height: auto; padding: 0; border: 0; }
.cd-legend { display: grid; grid-template-columns: repeat(auto-fit, minmax(6.5rem, 1fr)); gap: var(--sp-3) var(--sp-4); margin: var(--sp-4) 0 0; padding: 0; list-style: none; }
.cd-legend li { display: grid; align-content: start; gap: 2px; min-width: 0; }
.cd-legend-name { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-sm); line-height: var(--lh-tight); color: var(--muted); }
.cd-legend-pct { font-family: var(--font-figure, inherit); font-size: var(--fs-lg); font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; }
.cd-legend-n2, .cd-legend-n3 { grid-template-columns: repeat(var(--n), minmax(0, 1fr)); }
.cd-legend-n2 { --n: 2; }
.cd-legend-n3 { --n: 3; }
.cd-legend-n4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@container (min-width: 30rem) { .cd-legend-n4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.cd-legend-list { --cd-val: 3.5rem; --cd-pct: 4rem; grid-template-columns: minmax(0, 1fr) var(--cd-val) var(--cd-pct); gap: 0 var(--sp-3); }
.cd-legend-list > li, .cd-legend-list summary { display: grid; grid-column: 1 / -1; grid-template-columns: subgrid; align-items: center; gap: 0 var(--sp-3); }
.cd-legend-list > li { padding-block: var(--sp-2); border-top: 1px solid var(--panel-border); }
.cd-legend-list > li:first-child { border-top: 0; }
.cd-legend-list .cd-legend-name { color: var(--text); }
.cd-legend-list :is(.cd-legend-val, .cd-legend-pct) { font-family: var(--font-figure, inherit); font-size: var(--fs-sm); font-variant-numeric: tabular-nums; text-align: end; white-space: nowrap; }
.cd-legend-val { color: var(--muted); }
.cd-legend-list .cd-legend-pct { font-weight: 600; }
.cd-legend-more details { grid-column: 1 / -1; }
.cd-legend-more summary { grid-template-columns: minmax(0, 1fr) var(--cd-val) var(--cd-pct); }
.cd-legend-more summary { list-style: none; cursor: pointer; border-radius: var(--radius-sm); }
.cd-legend-more summary::-webkit-details-marker { display: none; }
.cd-legend-more summary:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.cd-legend-caret { flex: none; width: 0.875rem; height: 0.875rem; color: var(--muted); transform: rotate(-90deg); transition: transform var(--dur-1) var(--ease); }
.cd-legend-more details[open] .cd-legend-caret { transform: none; }
/* The Khác row's summary and the parts it opens to sit inside the details, outside the list's grid, so their rows take the list's column widths themselves. */
.cd-legend-sub { margin: var(--sp-1) 0 0; padding: 0; list-style: none; }
.cd-legend-sub > li { display: grid; grid-template-columns: minmax(0, 1fr) var(--cd-val) var(--cd-pct); align-items: center; gap: 0 var(--sp-3); padding-block: 2px; font-size: var(--fs-xs); }
.cd-legend-sub .cd-legend-name { padding-inline-start: calc(0.75rem + var(--sp-2)); font-size: var(--fs-xs); color: var(--muted); }
.cd-legend-sub :is(.cd-legend-val, .cd-legend-pct) { font-size: var(--fs-xs); font-weight: 400; color: var(--muted); }
@container (min-width: 34rem) {
  .cd-wrap-list { display: grid; grid-template-columns: var(--donut-size) minmax(0, 1fr); align-items: center; gap: var(--sp-6); }
  .cd-wrap-list .cd-legend { margin-top: 0; }
}

/* Sparkline: span.spark[data-values="72,5 81 79 96,3"][aria-hidden="true"] (space-separated, comma decimals, "-" for a missing value) in .kpi-foot, straight after .kpi-value, or in a table
   cell. kit.js draws an svg 100 % wide and --spark-h (2rem) tall: the line 2px in --muted, the last segment and the last point (r 3) in --primary; no axes, no tooltip. It is decoration:
   the figure's meaning is in the KPI's own text and in a .visually-hidden sentence the author writes ("Xu hướng 12 tháng: từ 72,5 lên 96,3"). A card with a sparkline after its value takes
   a fourth line, so the cards of its row keep their parts in line. */
.spark { --spark-h: 2rem; display: block; width: 100%; min-width: 4rem; height: var(--spark-h); }
.spark svg { display: block; width: 100%; height: 100%; overflow: visible; }
/* The line takes the card's tone (--spark-c: --primary, or the .kpi's data-tone colour); the months before the last are a lighter step of it, the last segment and point full.
   A missing month is bridged by a faint dotted line, so the trend never looks broken. Under a mouse or a finger a dot follows the nearest month and the chart tooltip says its
   label (data-labels, space-separated, same count as data-values) and value; it is a help only, the KPI text carries the meaning, so the sparkline stays out of the tab order. */
.spark { --spark-c: var(--primary); position: relative; cursor: crosshair; touch-action: pan-y; }
.kpi[data-tone="ok"] .spark { --spark-c: var(--ok); }
.kpi[data-tone="warn"] .spark { --spark-c: var(--warn); }
.kpi[data-tone="danger"] .spark { --spark-c: var(--danger); }
.spark-line { fill: none; stroke: var(--spark-c); stroke-opacity: .55; stroke-width: 2px; stroke-linecap: round; stroke-linejoin: round; }
.spark-last { stroke-opacity: 1; }
.spark-gap { fill: none; stroke: var(--spark-c); stroke-opacity: .4; stroke-width: 1.5px; stroke-dasharray: 1 4; stroke-linecap: round; }
.spark-dot { fill: var(--spark-c); stroke: var(--surface); stroke-width: 2px; }
.spark-hot { fill: var(--spark-c); stroke: var(--surface); stroke-width: 2px; opacity: 0; transition: opacity var(--dur-1) var(--ease); }
.spark.is-hot .spark-hot { opacity: 1; }
.spark.is-hot .spark-dot { opacity: .35; }
.kpi-foot > .spark { flex: 1 0 100%; order: -1; }
@media (prefers-reduced-motion: reduce) { .spark-hot { transition: none; } }

/* Stat strip with links or deltas: the cells share two rows (subgrid), so every figure sits on one line and every delta under it, even when a name wraps to two lines. */
.stat-strip:has(.stat-delta, .stat-strip-link) > .stat-strip-item { display: grid; grid-row: span 2; grid-template-rows: subgrid; row-gap: var(--sp-1); align-content: start; }
.stat-strip:has(.stat-delta, .stat-strip-link) > .stat-strip-item > dd { align-self: start; }

/* Stacked columns have no previous-period tick: a tick per series would sit at the wrong height inside the stack. Compare totals with a reference line (.cc-ref) instead. */
.chart-cols.cc-stacked .cc-mark { display: none; }
@supports (grid-template-rows: subgrid) {
  .kpi-row:has(> .kpi > .spark) > .kpi { grid-row: span 4; }
  .kpi-row:has(> .kpi > .spark) > .kpi:not(:has(> .spark)) > .kpi-foot { grid-row: 4; }
}

/* Stat strip with links and deltas. A cell that opens its list: div.stat-strip-item > dt > a.stat-strip-link[href] (the name, and svg.icon.icon-sm.stat-strip-caret "caret-right") + dd.
   The link stretches over the whole cell (hover --surface-hover, the focus ring inside the cell, the caret at the end in --muted) and its text stays --text and --muted: a dl may only hold
   div > dt + dd, so the cell itself cannot be the a. A change: dd > span.stat-delta[data-dir="up|down|flat"][data-good="up|down"] > an arrow icon + the signed figure + the period
   ("+2,1 so với T9"), on its own line under the figure (it wraps in a narrow cell); ok when the direction is the good one, danger when not, --muted when flat. The sign and the words carry it, the colour helps.
   Five and six cells keep one row while there is room and fall to three a row (six: 3 + 3, five: 3 + 2) under 40rem of strip. */
.stat-strip:has(.stat-strip-link) { overflow: hidden; }
.stat-strip-item:has(> dt > .stat-strip-link) { position: relative; padding-inline-end: calc(var(--stat-pad) + var(--icon-sm)); }
.stat-strip-link { color: inherit; text-decoration: none; }
.stat-strip-link::after { content: ""; position: absolute; inset: 0; }
.stat-strip-link:focus-visible { outline: 0; }
.stat-strip-item:has(> dt > .stat-strip-link:hover) { background: var(--surface-hover); }
.stat-strip-item:has(> dt > .stat-strip-link:focus-visible) { box-shadow: inset 0 0 0 2px var(--focus); }
.stat-strip-caret { position: absolute; inset-inline-end: var(--sp-2); top: 50%; color: var(--muted); transform: translateY(-50%); }
.stat-delta,
.stat-strip-item dd > .stat-delta { display: flex; align-items: flex-start; gap: 2px; margin-top: 2px; font-size: var(--fs-xs); font-weight: 500; line-height: var(--lh-tight); color: var(--muted); white-space: normal; overflow-wrap: anywhere; }
.stat-delta .icon { flex: none; width: 0.875rem; height: 0.875rem; margin-top: 1px; }
.stat-delta:is([data-dir="up"][data-good="up"], [data-dir="down"][data-good="down"]) { color: var(--ok); }
.stat-delta:is([data-dir="up"][data-good="down"], [data-dir="down"][data-good="up"]) { color: var(--danger); }
.stat-strip:has(> :nth-child(6):last-child) > .stat-strip-item { --stat-cells: 6; }
.stat-strip:has(> :nth-child(5):last-child) { grid-template-columns: repeat(30, minmax(0, 1fr)); }
.stat-strip:has(> :nth-child(5):last-child) > .stat-strip-item { --stat-cells: 5; grid-column: span 6; }
@container (max-width: 40rem) {
  .stat-strip:has(> :nth-child(6):last-child) > .stat-strip-item { --stat-cells: 3; }
  .stat-strip:has(> :nth-child(5):last-child) > .stat-strip-item { --stat-cells: 3; grid-column: span 10; }
  .stat-strip:has(> :nth-child(5):last-child) > .stat-strip-item:nth-child(n+4) { grid-column: span 15; }
  .stat-strip:is(:has(> :nth-child(5):last-child), :has(> :nth-child(6):last-child)) > .stat-strip-item:nth-child(4) { border-inline-start: 0; }
  .stat-strip:is(:has(> :nth-child(5):last-child), :has(> :nth-child(6):last-child)) > .stat-strip-item:nth-child(n+4) { border-top: 1px solid var(--panel-border); }
}
@container (max-width: 17.5rem) {
  .stat-strip:has(> :nth-child(5):last-child) > .stat-strip-item:nth-child(n) { grid-column: 1 / -1; }
}

/* Heat table: table.tbl[data-heat] tints each number cell of tbody from --surface (the smallest) to --heat-strong (the largest), one hue, so a big table reads at a glance.
   kit.js reads td.num (or any td with data-value) and sets --heat (0 to 1) on it: data-heat="" or "col" scales each column on its own (a year of income next to a year of spending),
   "row" each row, "table" the whole body. Cells without a number, tfoot totals and a column whose th has data-heat="off" stay plain. --heat-strong is a 45 % step of --series-1
   toward the surface, so body text stays above 4.5:1 on the darkest cell (gated; small muted text inside a tinted cell is drawn in --text); a tenant may set --heat-strong on the table for another hue. The numbers carry the
   data, the tint only helps: never a colour without its figure. .heat-key is the key (smallest, a ramp, largest), written by the author. */
.tbl[data-heat] tbody td.has-heat { background-color: color-mix(in srgb, var(--heat-strong) calc(var(--heat, 0) * 100%), var(--surface)); -webkit-print-color-adjust: exact; print-color-adjust: exact; }
.heat-key { display: inline-flex; align-items: center; gap: var(--sp-2); margin-top: var(--sp-2); font-size: var(--fs-xs); color: var(--muted); font-variant-numeric: tabular-nums; }
.heat-key-bar { width: 6rem; height: 0.625rem; border: 1px solid var(--panel-border); border-radius: var(--radius-sm); background: linear-gradient(to right, var(--surface), var(--heat-strong)); }
.tbl[data-heat] tbody td.has-heat :is(.muted, .cell-sub, .num-unit) { color: var(--text); }
/* Negative and positive in one scale (profit and loss, a change against last year): 0 is the white middle, positives deepen toward --heat-strong, negatives toward --heat-neg
   (a red step, as .num-neg is red), on one length for both sides so -50 and +50 are equally deep. kit.js switches to this by itself when a scale holds both signs;
   data-heat-mid="off" on the table keeps the plain light-to-dark scale. The minus sign stays in the figure, so the side never rests on colour. */
.tbl[data-heat] tbody td.has-heat.heat-neg { background-color: color-mix(in srgb, var(--heat-neg) calc(var(--heat, 0) * 100%), var(--surface)); }
.tbl[data-heat] tbody td.has-heat.heat-neg .num-neg { color: var(--text); }
.heat-key-bar-mid { width: 9rem; background: linear-gradient(to right, var(--heat-neg), var(--surface) 50%, var(--heat-strong)); }
