/* The height here is what makes a full-height page possible at all: MudLayout is height:100%, and
   a percentage height resolves against nothing unless every ancestor up to <html> has one, so
   without this the whole chain collapses back to auto and the table cannot fill the screen. */
html, body {
    margin: 0;
    padding: 0;
    height: 100%;
}

/* Scrolling belongs to the region that owns the content — the data table's row container, say —
   not to the window, which would otherwise scroll the app bar and tab bar off the top. */
body {
    overflow: hidden;
}

/* Full-height page chain.

   These three rules have to be global. The classes sit on MudLayout / MudMainContent /
   MudContainer, and MudBlazor renders those elements itself, so they never carry a component's
   CSS-isolation scope attribute — a scoped rule compiles to "[scope] .class", a descendant
   selector, and matches nothing. (Same reason the app-bar shadow above lives here.)

   MudLayout ships height:100% but display:block, so a flex child of it would not be laid out by a
   flex container and would collapse. Making it the flex column is what feeds height downward. */
.mud-layout {
    display: flex;
    flex-direction: column;
}

/* Height comes through flex rather than a literal 100vh: MudBlazor puts
   padding-top:var(--mud-appbar-height) on the main content and ships no border-box reset, so
   100vh plus that padding would overflow the viewport by exactly the app bar's height. */
.app-main-content {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
    overflow: hidden;
}

/* min-height:0 is the load-bearing part: a flex item defaults to min-height:auto and refuses to
   shrink below its content, which would let a long table push the page instead of scrolling
   inside itself.

   overflow-y:auto keeps ordinary pages working now that the window no longer scrolls — a page
   taller than the screen scrolls here. A full-height page never triggers it, since its child is
   bounded to exactly the space available. */
.app-page-container {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
}

/* A page with a save bar pinned to its foot (InspectionNewPage) needs the browser to stop short of
   that bar when it scrolls a focused field into view — otherwise the on-screen keyboard opens, the
   field is scrolled to the very bottom edge of this region, and lands underneath the bar instead.
   Global for the same reason as the rule above: MudBlazor renders this container itself. */
.app-page-container:has(.iqc-create-ticket__actions) {
    scroll-padding-bottom: 80px;
}

/* Room for the on-screen keyboard where the browser slides it over the page instead of shrinking the
   layout (the PDA's MAUI WebView, Safari) — measured by js/keyboard-inset.js, 0 everywhere else. Without
   it the last field of a short form has nowhere to scroll to and stays under the keyboard.
   Restates MainLayout's pb-4 (16px) on top of the inset, and needs !important to do it, because
   MudBlazor's spacing utilities are themselves !important. */
.app-page-container {
    padding-bottom: calc(16px + var(--keyboard-inset, 0px)) !important;
}

/* While the operator is typing on a touch device, the pinned save bar (InspectionNewPage) drops back
   into the flow at the end of the form. Pinned, it sits on top of the keyboard and eats a fifth of the
   little height left — exactly where the field being typed into is — so "Số xe" ended up behind it. It
   pins again the moment the keyboard closes, which is when Lưu is pressed anyway. Global rather than
   scoped CSS: the focused <input> is rendered by MudBlazor and carries no scope attribute. */
@media (pointer: coarse) {
    .app-page-container:has(input:not([readonly]):focus, textarea:focus) .iqc-create-ticket__actions {
        position: static;
    }
}

/* The launcher's scrolling region.

   It does not use the .app-main-content / .app-page-container pair above: with no app bar and no tab
   strip there is nothing to hold still above the content, so the whole main area scrolls as one and a
   second nested box would only add padding around tiles that already centre themselves.

   It does need *a* region, though, and that is the bug this fixes: body is overflow:hidden by design
   (see the top of this file), so a page owning no scrolling region cannot be scrolled at all. The
   launcher owned none. On a desktop its tiles fit the screen and nothing looked wrong; stacked one per
   row on a phone they run off the bottom with no way to reach them. */
.app-main-content--scroll {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
}

/* App-bar shadow.

   This lives here rather than in MainLayout.razor.css because MudBlazor renders the <header>
   element itself, so it carries no CSS-isolation scope attribute and a scoped rule can never
   match it.

   The shadow is cast downward only — the theme's own elevation spreads on all four sides, which
   reads as a halo on a bar pinned to the top edge. The selector is doubled up because MudBlazor's
   `.mud-elevation-0 { box-shadow: none }` ties on specificity and would otherwise cancel it out. */
.mud-appbar.app-appbar {
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.06);
}

/* Search-term highlight inside table cells (HighlightCell).

   Here rather than in a scoped sheet for the same reason as the rule above: MudHighlighter emits
   its <mark> as a MarkupString, and Blazor never stamps the CSS-isolation attribute onto markup
   strings, so a scoped selector cannot match it.

   The browser default for <mark> is a fixed yellow that stays yellow on the dark theme, where it
   burns against the surface and drags the text to near-black. Theme variables keep the contrast
   in both themes. */
.data-table-highlight {
    background-color: var(--mud-palette-warning);
    color: var(--mud-palette-warning-text);
    border-radius: 2px;
    padding: 0 1px;
}

/* QR code image (QrCodeImage).

   Also global, and also because of a MarkupString: QRCoder's SVG is injected as markup, which never
   receives the CSS-isolation attribute. The SVG is sized in raw modules, so without this it renders
   far wider than the card holding it. */
.qr-code svg {
    display: block;
    width: 100%;
    height: auto;
}

/* Code 128 bars (Barcode128). Global for the same MarkupString reason as the QR above.

   Unlike the QR, the height comes from the box rather than from the aspect ratio: the component draws
   with preserveAspectRatio="none" so the caller can set bar height and symbol width independently,
   which a barcode may do and a QR may not. */
.barcode-128 {
    display: block;
    line-height: 0;
}

.barcode-128 svg {
    display: block;
    width: 100%;
    height: 100%;
}

/* "Rows per page" select in a MudDataGrid pager.

   Global rather than scoped to DataTable: MudDataGridPager renders through a cascading popover
   provider, so its markup carries no CSS-isolation attribute and a ::deep rule from the component
   never reaches it — the same reason the QR SVG above is handled here.

   MudBlazor already tries to cap this itself, with
   `.mud-table-pagination-select .mud-input .mud-input-root { max-width: 80px }`, but 9.8.0 stopped
   nesting .mud-input-root under .mud-input in the pager, so that selector matches nothing. The
   select then stretched to ~590px and dragged the 10/25/50/100 popover — which sizes to its
   anchor — out to the same width. Confirmed against a clean checkout: the width is present with
   every local rule reverted, so it arrived with the 9.7 -> 9.8 upgrade in commit e79da8a.

   Drop this block if a later MudBlazor restores its own cap. */
.mud-table-pagination .mud-select,
.mud-table-pagination-select {
    flex: 0 0 auto !important;
    width: auto !important;
    min-width: 72px;
    max-width: 120px !important;
}

.mud-table-pagination-select .mud-input-control-input-container,
.mud-table-pagination-select .mud-input-root {
    max-width: 120px !important;
}

/* ── Column width (DataTable's ColumnWidth) ─────────────────────────────────────────────────────
   MudDataGrid sizes a column to its content, so a spec table's limit columns — two or three digits —
   are laid out about that wide, and a heading like "Đạm CP tối thiểu (%)" wraps to four lines in the
   space left. Every header cell in a row is as tall as the tallest, so one squeezed column sets the
   height for all of them.

   Applied to header and body together. Setting it on the header alone does widen the column, since a
   table's columns size to the widest cell in them — but the body cells then keep their own smaller
   intrinsic width, and a `left` offset measured from the header (see the frozen-column block below)
   no longer lines up with the cells underneath it.

   `min-width`, not `width`, and that distinction is the whole rule.

   A table lays out at `table-layout: auto` unless told otherwise, and MudBlazor never sets it. Under
   auto layout `width` on a cell is only a *suggestion* — the algorithm is free to discard it in
   favour of the content's own measurement, and for a column holding "13" under a five-word heading
   that is exactly what happens. min-width is a genuine constraint the algorithm has to satisfy, so
   it is the only one of the two that does anything here. `width` was tried and had no visible effect
   whatsoever.

   The catch is that min-width is also a *hard* floor: used width is max(min-width, width), so it
   beats the inline width the resizer writes. Left unqualified it pinned every column at the floor
   and dragging one narrower did nothing, which read as resizing being broken.

   Hence the second rule: a cell carrying its own inline width has been resized by the user, and it
   drops the floor entirely so the drag governs. [style*="width"] is the test because that inline
   style is precisely what the resizer adds and nothing else here sets — the frozen-column script
   writes `left`, never `width`, so pinned columns keep their floor until they too are dragged.

   The variable has no fallback in the `var()`, deliberately: a table that never sets ColumnWidth
   leaves --dt-column-width undefined, the declaration is invalid at computed-value time, and the
   column falls back to MudBlazor's own sizing — which is exactly the opt-in behaviour wanted. Giving
   it a default here would silently impose a width on all thirteen tables instead.

   That opt-in design is also why this failed silently when the variable was first published through
   an @attributes splat: MudComponentBase declares Style as a real [Parameter], so a `style` key in
   the splat bound to that property rather than reaching the DOM. The variable was never defined, the
   var() stayed permanently invalid, and the rule simply did nothing — no error, no warning. It is
   passed through MudDataGrid's own Style parameter now (DataTable.GridStyle). Any future variable
   published to these cells has to go the same way.

   Global rather than scoped to DataTable, for the usual reason: MudDataGrid renders these cells
   itself, so they carry no CSS-isolation attribute and a ::deep rule never reaches them. */
.mud-data-grid thead .mud-table-cell,
.mud-data-grid tbody .mud-table-cell {
    min-width: var(--dt-column-width);
}

/* A resized column governs itself — see the note above. */
.mud-data-grid thead .mud-table-cell[style*="width"],
.mud-data-grid tbody .mud-table-cell[style*="width"] {
    min-width: 0;
}

/* ── Frozen columns (DataTable) ─────────────────────────────────────────────────────────────────
   Built on MudBlazor's own StickyLeft: setting it on a Column emits `.sticky-left`, and MudBlazor
   already ships `position: sticky`, a background and a z-index for it. This block adds only the two
   things it does not do.

   1. The offset for each pinned column after the first. MudBlazor's rule is `left: 0` for every
      sticky-left cell, so they all stack on top of each other — which is exactly what the earlier
      hand-rolled version got wrong in the other direction, guessing 120px/200px offsets that did
      not match the real column widths and let a scrolled value bleed through underneath.
      data-table.js measures the rendered columns and assigns each one its running-total offset, so
      they are always the true widths whatever the content or the user's resizing — and it works for
      any FrozenColumns, not the two this was originally written around.

   2. An opaque background that follows the row. MudBlazor uses --mud-palette-background-gray for
      every sticky cell, which reads as a grey stripe down a white table and hides the striping,
      hover and selection states underneath it.

   Global rather than scoped to DataTable: MudDataGrid renders these cells itself, so they carry no
   CSS-isolation attribute and a ::deep rule never reaches them. */

/* MudDataGrid puts .sticky-left on the *header* cell only — the body cells for the same column never
   get it, so setting StickyLeft freezes the header while the data scrolls out from under it. That is
   the misalignment this had before: the header said "Tên nguyên liệu" while values from three columns
   over slid past beneath it.

   The body cells are therefore marked by data-table.js, which also assigns every pinned cell its
   `left` inline. Both jobs live there rather than here because the offset for a pinned column is the
   running total of the widths before it — a value only the laid-out DOM knows, and one that needs a
   different number per column. Expressing that in CSS means one rule per column index, which is what
   this file used to do, and it silently stopped working past the last index anyone had written a
   rule for. */

/* Backgrounds, replacing MudBlazor's flat grey so the row's own state shows through.

   Every fill here is !important, and it is load-bearing rather than lazy: a pinned cell that loses
   its background to any other rule becomes transparent, and the columns scrolling underneath show
   straight through it. MudBlazor's striping and hover rules are written against the <tr> and beat a
   plain cell selector, so without this the frozen cells display values from other columns. */
.mud-data-grid thead .sticky-left {
    background-color: var(--mud-palette-surface) !important;
    /* Above the body cells: with FixedHeader the header is sticky on both axes, and the two sticky
       contexts overlap at the top-left corner. */
    z-index: 3 !important;
}

/* z-index 2, not 1, and it is the whole reason the bleed-through happened at all.

   A z-index only orders *positioned* elements. The scrolling cells are position:static, so they are
   not in the z-order — they paint in document order, in the same layer as everything else, which
   puts the later ones on top of an earlier sticky cell no matter what z-index it carries. Painting
   above them needs a value high enough that the sticky cell forms its own stacking level above the
   static flow, and the row's own background does not sit between the two.

   Symptom when this is wrong: hovering a row while scrolled sideways shows fragments of the
   scrolled-past columns through the frozen ones ("ts", "14" over the material code). */
.mud-data-grid tbody tr > td.dt-frozen-cell {
    background-color: var(--mud-palette-surface) !important;
    /* !important on the position too, not only the fill. A pinned cell that loses `sticky` to any
       other rule stops being pinned at all: it scrolls away with the rest of the row and uncovers
       whatever was passing underneath, which looks identical to a transparency bug. */
    position: sticky !important;
    z-index: 2 !important;
}

/* The stripe, and its parity has to match MudBlazor's exactly.

   MudBlazor stripes `tbody tr:nth-of-type(odd)`. This rule said `even`, so the frozen cells were
   tinted on precisely the rows the scrolling half left white and vice versa — the two halves of the
   table disagreed about every single row, and the seam between them looked like a rendering fault.

   Only non-virtualized rows, matching the `:not(.mud-table-body-virtualized)` qualifier MudBlazor
   puts on its own striping rule. With Virtualize on, the rows in the DOM are just the visible window
   and their ordinal position no longer tracks their position in the data, so nth-of-type would stripe
   a row that scrolling had just renumbered. The class-based rule below covers that mode instead. */
.mud-data-grid tbody:not(.mud-table-body-virtualized) tr:nth-of-type(odd) > td.dt-frozen-cell {
    background-color: var(--mud-palette-table-striped) !important;
}

/* Virtualized: MudBlazor marks the striped rows itself, so follow the marker rather than counting. */
.mud-data-grid tbody.mud-table-body-virtualized tr.mud-table-row-stripe > td.dt-frozen-cell {
    background-color: var(--mud-palette-table-striped) !important;
}

/* Hover has to be repeated on the cell. MudBlazor paints it on the <tr>, and a row background sits
   *behind* its cells — so a pinned cell keeps its own opaque fill and reads as unhighlighted while
   the rest of the row lights up.

   The bigger problem is the reverse case: if the cell background is left transparent so the row
   colour shows through, the scrolled columns show through it too, and a hovered row displays values
   from underneath the frozen cells. Repeating --mud-palette-table-hover keeps the cell opaque *and*
   the same colour as the row — which only works because AppTheme sets TableHover to an opaque
   colour. MudBlazor's stock default for it is translucent, and using that here is precisely the bug
   this rule looks like it should have fixed but did not: the fill applied, the row tinted correctly,
   and the scrolled-past columns still showed through the 4% gap.

   Rather than enumerate the class MudBlazor happens to use for a hovered row — which changed between
   versions — the attribute selector covers any state the row can be in.

   !important because MudBlazor's hover rules live inside @media (hover: hover) and would otherwise
   win on specificity over a plain cell selector. */
.mud-data-grid tbody tr:hover > td.dt-frozen-cell,
.mud-data-grid tbody tr[class*="hover"] > td.dt-frozen-cell {
    background-color: var(--mud-palette-table-hover) !important;
    /* Position and stacking restated alongside the fill: these selectors are more specific than the
       base rule above, so anything they do not repeat falls back to whatever MudBlazor set. */
    position: sticky !important;
    z-index: 2 !important;
}

/* Selection, same treatment as hover. --mud-palette-action-default-hover is translucent, so it
   cannot be used on a pinned cell for the reason above; the selected row keeps it (DataTable's own
   stylesheet paints the <tr>), while the frozen cells get the opaque table-hover fill layered over
   the surface so the tint reads the same without letting the scrolled columns through. */
.mud-data-grid tbody tr.data-table__row--selected > td.dt-frozen-cell {
    background-color: var(--mud-palette-table-hover) !important;
    position: sticky !important;
    z-index: 2 !important;
}

/* ── Footer frozen columns ────────────────────────────────────────────────────────────────────────
   Footer cells (in <tfoot>) need the same styling as body cells to stay fixed while scrolling.
   They include the frozen-cell marker applied by data-table.js. */
.mud-data-grid tfoot tr > td.dt-frozen-cell {
    background-color: var(--mud-palette-surface) !important;
    position: sticky !important;
    z-index: 2 !important;
}

/* ── Sticky sum-total row ─────────────────────────────────────────────────────────────────────────
   A page adds a FooterTemplate to a column (BaggingDailyRecordPage's per-column sums, for one) and
   MudDataGrid renders a <tfoot> for it — unlike MudTable it has no FixedFooter parameter to pin that
   row, so it is done here by hand, on the same selector shape QM-RP-01..03's own sticky total row
   already uses (.rp-table tfoot td, further down this file): position:sticky is set on the *cell*,
   not the <tfoot>/<tr>, which is the form that actually sticks across browsers.

   Global rather than scoped to DataTable.razor.css, for the same reason every other rule in this
   block is: MudDataGrid renders these cells itself, so they carry no CSS-isolation attribute and a
   ::deep rule never reaches them — the first version of this rule lived there, targeted
   position:sticky at the <tfoot> element instead of its cells, and silently pinned nothing.

   z-index 1, one below the frozen-footer-cell rule above (2): a frozen column in the footer row has
   to paint over a scrolling one in the same row, the same ordering the body rows use. */
.mud-data-grid tfoot td {
    position: sticky;
    bottom: 0;
    z-index: 1;
    background-color: var(--mud-palette-surface);
    font-weight: 600;
    border-top: 2px solid var(--mud-palette-table-lines);
}

/* ── Row highlight (DataTable's RowClass) ───────────────────────────────────────────────────────
   E.g. SparePartPage flags a row when OnHandStock < SafetyStock. RowClassFunc lands the class
   straight on MudDataGrid's own <tr> — the same mechanism MudBlazor uses for its own striping and
   hover — but styling it has to live here rather than as a ::deep rule in DataTable.razor.css, for
   the reason repeated throughout this file: MudDataGrid renders these elements itself, so they carry
   no CSS-isolation attribute and a ::deep rule never reaches them. (This is also why the
   data-table__row--selected rule in DataTable.razor.css has never actually painted anything itself —
   any tint a selected row shows comes from MudBlazor's own built-in selection styling, not from that
   rule; nobody noticed because the two happened to look the same.)

   The <tr> rule alone is enough for ordinary cells: they carry no background of their own, so the
   row's colour shows straight through them, and !important is enough to beat MudBlazor's un-!important
   striping/hover rules. Frozen cells are the exception — they always carry their own opaque fill (the
   block above) — so they need the same explicit override the striped/hover/selected states already
   get above. Matched to the same specificity as those (mud-data-grid + one state qualifier + the
   frozen-cell class, three elements) and declared after all of them, so a tie between e.g. a striped
   row that is also flagged is settled in the flagged row's favour by being later in the file. */
.mud-data-grid tbody tr.data-table__row--danger {
    background-color: color-mix(in srgb, var(--mud-palette-error) 12%, transparent) !important;
}

.mud-data-grid tbody tr.data-table__row--danger:hover {
    background-color: color-mix(in srgb, var(--mud-palette-error) 22%, transparent) !important;
}

.mud-data-grid tbody:not(.mud-table-body-virtualized) tr.data-table__row--danger > td.dt-frozen-cell,
.mud-data-grid tbody.mud-table-body-virtualized tr.data-table__row--danger > td.dt-frozen-cell {
    background-color: color-mix(in srgb, var(--mud-palette-error) 12%, var(--mud-palette-surface)) !important;
    position: sticky !important;
    z-index: 2 !important;
}

.mud-data-grid tbody tr.data-table__row--danger:hover > td.dt-frozen-cell {
    background-color: color-mix(in srgb, var(--mud-palette-error) 22%, var(--mud-palette-surface)) !important;
    position: sticky !important;
    z-index: 2 !important;
}

/* The seam: without it the freeze reads as a rendering glitch rather than a deliberate boundary.

   On the last pinned column only, in both the header and the body. The header keeps the structural
   selector — :not(:has(+ .sticky-left)) is "the next header is not pinned" — while the body cells
   use the marker data-table.js puts on them, since nothing in MudDataGrid's own body markup says
   which column a cell belongs to. */
.mud-data-grid thead .sticky-left:not(:has(+ .sticky-left)),
.mud-data-grid tbody td.dt-frozen-cell--last {
    border-right: 2px solid var(--mud-palette-lines-default) !important;
}

/* ── Grouped rows (DataTable's Groupable) ───────────────────────────────────────────────────────
   MudBlazor fills a group header with --mud-palette-background-gray. On a table whose rows are all
   collapsed that is every visible row, so the card reads as a grey block rather than a table — the
   fill was meant to mark the occasional heading among ordinary rows, not to become the surface.

   Painted with the card's own background instead, and separated by the same line the table already
   uses between rows, so the grouping is read from the expand chevron and the divider rather than
   from a wash of colour. Follows the frozen-column rules above in living here rather than in
   DataTable.razor.css: MudDataGrid renders these cells outside the component's isolation scope, so
   a ::deep rule never reaches them. */
.mud-data-grid .mud-table-cell.mud-datagrid-group {
    background-color: var(--mud-palette-surface);
    border-bottom: 1px solid var(--mud-palette-table-lines);
}

/* The heading is double-clickable along its whole width (js/data-table.js binds it), so it takes the
   pointer cursor to read as something that opens — the chevron alone is a small target on a row this
   wide.

   user-select:none because double-clicking text selects the word under the pointer, which would
   otherwise leave a highlighted word behind every time a group is opened. */
.mud-data-grid td.mud-datagrid-group {
    cursor: pointer;
    user-select: none;
}

/* A group heading is a heading, so it carries the weight — without this it is set in the same
   regular text as the rows under it and the two read as one flat list. The rule targets the cell
   rather than the template inside it so the chevron lines up with the text it labels. */
.mud-data-grid td.mud-datagrid-group {
    font-weight: 500;
}

/* The rows belonging to a group are stepped in past their heading, so the eye can see where one
   block ends and the next begins without a rule between them. Applied to the first cell only —
   indenting every cell would shift the whole grid out of line with its own header.

   Scoped to a tbody that actually has group headings in it: :has() on the body keeps the indent off
   every ungrouped table in the app, which would otherwise gain a stray gap down its left edge. */
.mud-data-grid tbody:has(td.mud-datagrid-group) tr:not(:has(> td.mud-datagrid-group)) > td:first-child {
    padding-left: 2.5rem;
}

/* ── Loading indicator (DataTable's Loading / MudTable's Loading) ───────────────────────────────
   LoadingDots is the app's one loading indicator, so MudBlazor's indeterminate bar has to go — but
   it cannot simply be swapped at the call site. MudDataGrid renders this strip whenever Loading is
   true and offers no way to replace it: its LoadingContent parameter fills the *empty-rows* slot in
   the body instead, and the bar is emitted alongside it regardless. Hiding it here is the only hook.

   Both the cell and the bar inside it: the cell is sticky with an opaque surface fill, so leaving it
   in place with an empty child would park a blank strip under the header while a table reloads. The
   second selector still matters on its own for MudTable, which drops the .mud-table-loading class
   from the cell in one of its states but always renders the bar.

   Here rather than in DataTable.razor.css for the reason repeated above: MudDataGrid renders these
   elements outside the component's isolation scope, so a ::deep rule never reaches them.

   What shows instead — DataTable puts LoadingDots in the body while the table is empty (its
   <LoadingContent>) and in the toolbar while rows are already on screen; RolePage, the one list
   screen still on a bare MudTable, supplies its own. */
.mud-table-loading,
.mud-table-loading-progress {
    display: none !important;
}

/* ══ QM design tokens (SSTP mockup) ═════════════════════════════════════════════════════════════
   The QM/IQC screens are drawn from the approved SSTP mockup, which uses a warm sand + slate
   palette rather than the sunjin.vn brand red/green that AppTheme.razor sets for the rest of the
   app. Those brand colours are deliberate (see AppTheme.razor) and stay untouched: the tokens
   below are scoped to `.qm-scope`, which only the QM pages put on their root element, so PM,
   CMMS, admin and the dashboard keep the brand palette exactly as it is.

   Global rather than a component's scoped sheet for the reason repeated throughout this file: a
   CSS-isolation sheet compiles every selector with a per-component attribute, so custom
   properties declared there cannot be inherited by child components. Tokens have to be defined
   somewhere every QM component can see them, and these are read by InspectionNewPage,
   InspectionTicketPage, QcProgressSteps, QcReadingPanel, QcTicketIdentityStrip and the shared
   QmBadge component.

   Naming is --qm-* so nothing here can collide with MudBlazor's own --mud-palette-* variables. */
.qm-scope {
    --qm-surface: #fbfaf8;
    /* Xám trung tính, không phải kem. #f1efea là be ngả vàng, mà trên lưới nhập nó nằm ngay cạnh hổ
       phách của dấu "phải nhập" và của ô cảnh báo — hai hàng tiêu đề vì thế đọc ra như đang được tô
       màu cảnh báo nhạt chứ không phải như nền. Xám không mang nghĩa nào trên màn này nên nó là màu
       duy nhất dùng được cho một dải chỉ để phân tầng. */
    --qm-surface-2: #eef0f3;
    --qm-border: #dcd7ce;
    --qm-text: #1e1c19;
    --qm-muted: #6b655c;
    --qm-faint: #948d82;

    --qm-brand: #1f4d6b;
    --qm-brand-soft: #e4ecf1;
    --qm-brand-ink: #17394f;

    /* Dải tiêu đề của bảng báo cáo ma trận (QM-RP-01/02/03). Lấy đúng bộ màu của mockup —
       --sj-navy / --sj-navy-050 trong file mockup QM-RP-01 — chứ không phải --qm-brand: dải này là
       nền đặc chạy suốt bề ngang chín mươi cột, mà #1f4d6b (xanh ngả lục của QM) ở diện tích ấy đọc
       ra một màu khác hẳn lúc nó chỉ là chữ hay viền. Vạch ngăn giữa các đoạn là màu nền của thẻ,
       nên khe hở giữa hai khối trông như bảng bị cắt rời chứ không phải bị kẻ thêm một đường. */
    --qm-report-band: #16325c;
    --qm-report-band-ink: #ffffff;
    --qm-report-subhead: #f1f5fa;
    --qm-report-subhead-ink: #16325c;
    --qm-report-band-gap: #ffffff;

    /* Đạt/Không đạt both come from MudBlazor's own palette, so a verdict badge, a tinted reading and
       the Đạt/Không đạt toggle beside them are the same two colours — và từ 11/09/2026 palette ấy
       chính là bộ màu của ô trạng thái (AppTheme.razor lấy đúng --qm-status-*), nên "cùng một màu"
       nay còn nghĩa là cùng màu với ô trên bảng. Derived rather than hard-coded so the app theme
       (and the dark override below) stays the one source. */
    --qm-pass: var(--mud-palette-success);
    --qm-pass-soft: color-mix(in srgb, var(--mud-palette-success) 12%, var(--mud-palette-surface));
    /* The "Không đạt" red is MudBlazor's own error colour, not a QM-palette brick, so the verdict
       badges and the tinted input read as the same red as the Đạt/Không đạt toggle beside them —
       that toggle is a MudButton and takes Color.Error, which no --qm-* value can match. Derived
       rather than hard-coded so the app theme (and the dark override below) stays the one source. */
    --qm-fail: var(--mud-palette-error);
    --qm-fail-soft: color-mix(in srgb, var(--mud-palette-error) 12%, var(--mud-palette-surface));
    --qm-warn: #8a6115;
    --qm-warn-soft: #f7efdd;
    --qm-info-soft: #eeece7;

    /* Màu của "chỗ đang chọn" trên lưới nhập, và cố tình không lấy từ palette.

       Primary của app này là đỏ thương hiệu (#FE1B24), mà đỏ trên một màn QC đã có nghĩa "Không đạt".
       Ô đang chọn viền đỏ và vùng quét tô hồng nhạt vì thế đang phát biểu một phán quyết mà không ai
       đưa ra — đúng lúc người dùng chỉ vừa bấm vào ô. Xanh là màu trung tính duy nhất còn lại trên
       lưới, nên nó nằm được lên trên cả một cột không đạt mà không cãi nhau với màu của cột ấy. Cùng
       một lập luận và cùng một mã màu với SpreadsheetGrid (Sunjin.Web/Components/Shared/Spreadsheet). */
    --qm-select: #1565c0;
    --qm-select-ink: #ffffff;
    --qm-select-soft: color-mix(in srgb, #1565c0 12%, var(--mud-palette-surface));

    /* Đường kẻ của lưới nhập, và cố tình không lấy --mud-palette-lines-default: mặc định của MudBlazor
       là rgba(0,0,0,.12), chọn cho một đường ngăn giữa hai vùng chứ không cho ba mươi cột ô vuông cạnh
       nhau — nhạt đến mức lưới không còn ra hình ô. Hai bậc: nét kẻ ô, và nét ranh giới (hết phần tiêu
       đề, hết cột nhãn ghim) đậm hơn một bậc để mắt tách được hai thứ. */
    --qm-grid-line: #c5cad3;
    --qm-grid-line-strong: #97a0ae;

    /* Square-ish, deliberately. The conclusion bar at the foot of a ticket is a flat strip with a top
       border and no rounding at all, and it reads as part of the page rather than as a floating card —
       so the panels above it are pulled down to nearly the same. 8px made a table of numbers look like a
       set of pills. Everything QM rounds off this token; MudBlazor's own components round off
       AppTheme's DefaultBorderRadius, which is set to match. */
    --qm-radius: 2px;
    --qm-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

    /* Numbers in a QC table are compared down the column, so they have to line up. */
    font-variant-numeric: tabular-nums;
}


/* The marker AppTheme.razor renders so a stylesheet can see the current theme — see the note there
   for why MudBlazor's own theme state is not readable from CSS. It paints nothing and occupies no
   space; only its data attribute matters. */
.app-theme-marker {
    display: none;
}

/* Dark mode for the QM screens.

   Gated on that marker rather than a `.mud-theme-dark` class: MudThemeProvider never stamps a
   dark-mode class on any element, and the `mud-theme-dark` class that does exist in MudBlazor's
   stylesheet is an unrelated Color.Dark utility (the sibling of .mud-theme-primary), not a page
   marker. A rule gated on that class matches nothing, and would silently pin these screens to the
   light palette inside a dark shell.

   The literal sand values above stay the light-mode design: they are the mockup's own palette, and
   deriving them from --mud-palette-surface would flatten the warm surface to plain white.

   `:has()` on <body> because the marker is rendered by a layout component, so it is a descendant of
   <body> but not an ancestor of the QM pages — a plain descendant selector could not reach across.
   Both the body-scoped and the root-scoped form are written out so the rule holds wherever the
   layout happens to mount the marker.

   Only the surfaces, text and soft fills move, plus the accent hues: a #1f4d6b brand or a #2f6b46
   pass colour is near-black against a dark card and reads as a hole in the panel, so these take the
   mockup's own dark-theme values.

   Non-QM screens are untouched: every declaration stays scoped to .qm-scope. */
body:has(.app-theme-marker[data-app-theme="dark"]) .qm-scope,
:root:has(.app-theme-marker[data-app-theme="dark"]) .qm-scope {
    --qm-surface: #1e1d17;
    --qm-surface-2: #24262a;
    --qm-border: #3a382d;
    --qm-text: #eae7df;
    --qm-muted: #a49d90;
    --qm-faint: #807a6e;

    --qm-brand: #7fb3d0;
    --qm-brand-soft: #1c2b34;
    --qm-brand-ink: #a6cde3;

    /* Nền tối: giữ nguyên sắc navy nhưng kéo xuống dưới màu thẻ, vì #16325c trên nền tối là một mảng
       sáng chói ngang tiêu đề. Khe ngăn đổi theo màu bề mặt, nếu để trắng thì thành một vệt sáng. */
    --qm-report-band: #16283f;
    --qm-report-band-ink: #e8eef7;
    --qm-report-subhead: #1c2b34;
    --qm-report-subhead-ink: #a6cde3;
    --qm-report-band-gap: var(--qm-surface);

    --qm-pass: var(--mud-palette-success);
    --qm-pass-soft: color-mix(in srgb, var(--mud-palette-success) 18%, var(--mud-palette-surface));
    --qm-fail: var(--mud-palette-error);
    --qm-fail-soft: color-mix(in srgb, var(--mud-palette-error) 18%, var(--mud-palette-surface));
    --qm-warn: #d3a75c;
    --qm-warn-soft: #2c2415;
    --qm-info-soft: #26251d;

    /* Sáng hơn hẳn bản nền trắng: #1565c0 đặt trên nền tối tụt xuống gần đen, ô địa chỉ cột đang chọn
       nhìn ra thành một ô trống. Chữ trên nền ấy phải đen lại, không còn trắng được. */
    --qm-select: #5b9bd5;
    --qm-select-ink: #10202e;
    --qm-select-soft: color-mix(in srgb, #5b9bd5 16%, var(--mud-palette-surface));

    --qm-grid-line: #3b4048;
    --qm-grid-line-strong: #5a6270;
}

/* ══ QM reading tables ══════════════════════════════════════════════════════════════════════════
   The IQC ticket's block panels and the NIR/Lab matrix, global for the same reason the tokens above
   are: these rules have to reach MudBlazor's own DOM (a MudNumericField's <input>, a MudToggleGroup's
   segments), and a CSS-isolation sheet cannot. `::deep` does not rescue it either — the scope
   attribute is only ever stamped on plain HTML elements, never on a component tag, so a `::deep`
   written off a MudPaper or a MudNumericField compiles cleanly and matches nothing. Scoped to
   .qm-scope, which only the QM pages carry. */
.qm-scope .iqc-panel {
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: var(--qm-radius);
    background-color: var(--mud-palette-surface);
    overflow: hidden;
}

.qm-scope .iqc-panel__head {
    padding: 8px 12px;
    background-color: var(--mud-palette-surface);
    border-bottom: 1px solid var(--mud-palette-lines-default);
}

/* The block's own code (NIR, LAB, DENSITY) as a monospace tag — the operator reads it as "this is
   the system's name for the thing". */
.qm-scope .iqc-panel__code {
    font-family: var(--qm-mono);
    font-size: 10.5px;
    font-weight: 700;
    line-height: 1;
    letter-spacing: 0.04em;
    white-space: nowrap;
    padding: 4px 7px;
    border-radius: var(--qm-radius);
    background-color: var(--mud-palette-background-gray);
    border: 1px solid var(--mud-palette-lines-default);
    color: var(--mud-palette-text-secondary);
}

.qm-scope .iqc-panel__title {
    font-size: 13.5px;
    font-weight: 600;
    color: var(--mud-palette-text-primary);
}

.qm-scope .iqc-panel__hint {
    font-size: 11px;
    color: var(--mud-palette-text-secondary);
}

/* Why a side is read-only right now, in the mockup's amber lock strip rather than a MudAlert — an
   alert's icon and elevation announce a problem, and a column waiting its turn is not one.

   Hổ phách chứ không phải đỏ: một ô chưa ai gõ số vào là việc còn phải làm, không phải lỗi. Đỏ trong
   màn này đã có nghĩa "Không đạt" rồi, tiêu nó vào chuyện "chưa đo" thì hai thứ ấy nhìn thoáng qua
   thành một — đúng cái mà một màn QC không được phép. Dấu * trên tên cột ở lưới bên dưới dùng chung
   đúng màu này vì nói cùng một điều. */
.qm-scope .iqc-panel__lock {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 12px;
    font-size: 12.5px;
    color: var(--qm-warn);
    background-color: var(--qm-warn-soft);
    border-bottom: 1px solid var(--mud-palette-lines-default);
}

.qm-scope .iqc-panel__body {
    overflow-x: auto;
}

/* Hand-rolled rather than MudSimpleTable: a row carries up to three numeric fields, a toggle and a
   verdict badge, and the row height has to stay tight enough that a dozen characteristics fit
   without the page scrolling. MudSimpleTable's cell padding, sized for text, makes that impossible.

   table-layout is fixed and every trailing column states its width, so the characteristic name takes
   what is left and every field and badge lands on the same x down the whole table. Auto layout could
   not do this: asked to shrink, it gave the result column less than the field inside it, and a field
   in a justify-end row then overflows to the LEFT, printing across the threshold beside it. */
.qm-scope .iqc-table {
    width: 100%;
    table-layout: fixed;
    border-collapse: collapse;
    font-size: 13px;
}

.qm-scope .iqc-table th {
    text-align: left;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    color: var(--mud-palette-text-secondary);
    background-color: var(--mud-palette-surface);
    padding: 6px 10px;
    border-bottom: 1px solid var(--mud-palette-lines-default);
    white-space: nowrap;
}

.qm-scope .iqc-table td {
    padding: 3px 10px;
    border-bottom: 1px solid var(--mud-palette-lines-default);
    vertical-align: middle;
    color: var(--mud-palette-text-primary);
}

.qm-scope .iqc-table tbody tr:last-child td {
    border-bottom: none;
}

/* Wide enough for a sentence, because not every cell in this column is a bound. The Auto-role blocks
   put their "why there is no number yet" note here (QcScreenLabels.ThresholdText), and at 88px that
   note wrapped to six lines of two or three words, stretching a one-row panel to the height of a
   six-row one with its input marooned in the middle. `break-word` rather than `anywhere` for the same
   reason: `anywhere` was splitting ordinary Vietnamese words mid-syllable to fit the old width. */
.qm-scope .iqc-table__threshold {
    width: 160px;
    font-size: 12px;
    line-height: 1.35;
    color: var(--mud-palette-text-secondary);
    overflow-wrap: break-word;
}

.qm-scope .iqc-table__result {
    width: var(--iqc-result-width, 112px);
    text-align: right;
}

.qm-scope .iqc-table__verdict {
    width: 112px;
    padding-left: 6px;
}

/* The QcAndLab block's table (QcReadingMatrix): two result columns instead of one, each sized
   independently. A left rule separates the two so the eye does not read a number in the wrong
   column — the only internal border in the table, because it is the only place two same-shaped
   columns adjoin. */
.qm-scope .iqc-table--dual .iqc-table__result--measured {
    width: var(--iqc-measured-width, 112px);
}

.qm-scope .iqc-table--dual .iqc-table__result--lab {
    width: var(--iqc-lab-width, 216px);
    border-left: 1px solid var(--mud-palette-lines-default);
}

/* Which column a header badge is about — "Đo nhanh" / "Lab" before the pill. Two badges in one header
   is only legible if each says what it counts; a bare pair reads as one verdict rendered twice. */
.qm-scope .iqc-panel__badge-label {
    font-size: 10.5px;
    font-weight: 600;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    white-space: nowrap;
    color: var(--mud-palette-text-secondary);
}

.qm-scope .iqc-table__row:hover {
    background-color: var(--mud-palette-action-default-hover);
}

/* A characteristic this material's spec does not constrain: dimmed, but still present. Removing the
   row would leave the operator unable to tell "not required for this material" from "the screen
   forgot it". */
/* A row whose spec sets no bound. It is dimmed on the threshold side only: the field beside it is
   editable now (QcReadingCells.IsLocked), and fading the whole row to 45% both said "you cannot use
   this" and dropped the input's contrast below what is readable. The "không dùng" tag next to the
   name carries the meaning instead. */
.qm-scope .iqc-table__row--na .iqc-table__threshold {
    opacity: 0.55;
}

.qm-scope .iqc-table__na {
    font-size: 11px;
    color: var(--mud-palette-text-secondary);
    white-space: nowrap;
}

/* One characteristic's fields on one source. */
.qm-scope .iqc-cells__cell {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 1px;
    flex: 1 1 0;
    min-width: 0;
}

/* This source does not measure the characteristic. A dash, not a disabled box: an empty box says
   "not filled in yet", which would be a different and wrong statement. */
.qm-scope .iqc-cells__none {
    color: var(--mud-palette-text-secondary);
}

.qm-scope .iqc-cells__input {
    width: 100%;
    min-width: 0;
}

/* MudBlazor's dense outlined field is 10.5px of padding above and below plus 4px of control margin —
   ~48px, which set the row height of every characteristic and is why a full ticket did not fit on one
   screen. Trimmed to ~30px here, the only place it can be done, since these elements are MudBlazor's
   own. */
.qm-scope .iqc-cells__input.mud-input-control.mud-input-control-margin-dense {
    margin: 0;
}

.qm-scope .iqc-cells__input input.mud-input-root-outlined.mud-input-root-margin-dense {
    padding-top: 4px;
    padding-bottom: 4px;
    font-size: 13px;
}

/* Result tinting: the field itself carries the verdict, so a number landing out of spec is visible
   where it was typed. The badge in the next column stays — this is a second signal, not a
   replacement, and tinting alone would leave the state invisible to a colour-blind operator. */
.qm-scope .iqc-cells__input--pass .mud-input-outlined-border {
    border-color: var(--qm-pass);
}

/* Nhạt hơn hẳn nền của ô không đạt, cùng lập luận với .iqc-sheet__cell--pass: phiếu bình thường là
   phiếu mọi số đều đạt, tô đậm cả loạt thì ô hỏng không còn nổi lên nữa. */
.qm-scope .iqc-cells__input--pass .mud-input-slot {
    background-color: color-mix(in srgb, var(--qm-pass) 7%, var(--mud-palette-surface));
}

.qm-scope .iqc-cells__input--fail .mud-input-outlined-border {
    border-color: var(--qm-fail);
}

.qm-scope .iqc-cells__input--fail .mud-input-slot {
    background-color: var(--qm-fail-soft);
}

/* ══ QM entry sheet (QcSheetTable) ═════════════════════════════════════════════════════════════
   The Excel-shaped grid: characteristics across the top, one row per sample. Global for the same
   reason the block panels above are — these rules reach the shared <input> the grid renders into a
   cell, and a CSS-isolation sheet cannot stamp its scope onto markup a parent controls. */
/* Scrolls sideways only: the sheet is one row deep, so there is no vertical overflow to cap. */
.qm-scope .iqc-sheet {
    overflow-x: auto;
    overflow-y: hidden;
    outline: none;
}

/* The focus ring belongs on the selected cell, not on the scroll box: a 33-column outline drawn round
   the whole pane says "this grid has focus" where the operator needs "this cell does". */
.qm-scope .iqc-sheet:focus-visible {
    outline: none;
}

/* Auto layout, not fixed. Fixed gave every column the same width whatever it was called, so "Ca" and
   "P" sat in the same width as "Kim loại nặng (As/Pb/Hg/Cd/F)" — the short names wasted the width and
   the long ones still could not fit it. Auto lets each column ask for what its own heading needs.

   Thử lại fixed 160px một lần nữa (11/09/2026) cho cột đều nhịp như trang tính, rồi trả về auto ngay
   trong ngày: cột đều thì đẹp hơn thật, nhưng nó mua bằng việc tên dài phải xuống dòng, và cái giá ấy
   đắt hơn. Chiều cao mới là chỗ chật, và chiều cao sửa được mà không đụng gì tới bề ngang.

   min-width: 100% so a stage with only a handful of characteristics still draws its header band across
   the pane rather than as a narrow table against empty space. */
.qm-scope .iqc-sheet__table {
    border-collapse: separate;
    border-spacing: 0;
    table-layout: auto;
    min-width: 100%;
    font-size: 13px;
    white-space: nowrap;
}

/* Cả lưới căn giữa theo cả hai trục — bốn hàng, mọi ô, không trừ hàng nào (11/09/2026).

   Trước đó mỗi hàng căn một kiểu: tiêu đề và ngưỡng căn trái, số đo căn phải, còn địa chỉ cột và Kết
   luận căn giữa. Từng luật một đều có lý của nó, nhưng xếp chồng lên nhau thì cùng một cột có ba mốc
   canh khác nhau, và mắt đọc dọc theo cột — từ tên chỉ tiêu xuống ngưỡng, xuống số vừa gõ, xuống kết
   luận — phải nhảy ngang ba lần cho một cột. Một mốc canh duy nhất ở giữa ô là thứ giữ cột thành cột.

   Riêng số đo: căn phải là lệ thường của một cột số, và nó đổi lấy việc số không thẳng hàng với chính
   cái tên và cái ngưỡng nó đang phải so. Ở đây mỗi cột chỉ có đúng một số, không phải một cột số dài
   để mà dóng hàng đơn vị, nên cái lệ ấy không mua được gì. font-variant-numeric: tabular-nums vẫn giữ,
   để bề ngang số không nhảy theo từng phím gõ. */

.qm-scope .iqc-sheet__table th,
.qm-scope .iqc-sheet__table td {
    border-right: 1px solid var(--qm-grid-line);
    border-bottom: 1px solid var(--qm-grid-line);
    padding: 2px 8px;
    overflow: hidden;
}

/* Ba đường này đậm hơn cả lưới: chúng là ranh giới (hết phần tiêu đề, hết cột nhãn) chứ không phải
   gạch kẻ ô.

   Lưới ở trên từng là hairline 45% cho đỡ nặng. Nặng thật, nhưng nhạt đến mức không còn ra hình ô, mà
   người dùng đọc bảng này đúng như đọc Excel — ô nào đến đâu là do đường kẻ nói. Nay lưới về một màu
   xám thấy rõ (--qm-grid-line) và ba đường ranh giới vẫn đậm hơn nó một bậc, nên tầng bậc cũ còn
   nguyên, chỉ là cả hai tầng cùng nhìn ra được. */
.qm-scope .iqc-sheet__table thead th,
.qm-scope .iqc-sheet__corner,
.qm-scope .iqc-sheet__rowhead {
    border-right-color: var(--qm-grid-line-strong);
    border-bottom-color: var(--qm-grid-line-strong);
}

/* Hai hàng tiêu đề là thông tin có sẵn, không phải chỗ nhập: nền xám của --qm-surface-2, tách hẳn khỏi
   nền trắng của hàng Kết quả bên dưới. Trước đây cả bốn hàng cùng nền surface nên bảng ba mươi cột nhìn
   vào chỉ thấy một mảng ô giống hệt nhau. */
.qm-scope .iqc-sheet__table thead th {
    background-color: var(--qm-surface-2);
    vertical-align: middle;
    text-align: center;
    font-weight: 600;
}

/* Floors, not caps. A table cell treats `height` as a minimum, so these keep the three header rows on
   a common baseline when every name is short, and simply give way to the tallest name when one is not.

   Hàng 1 là dải địa chỉ cột (A, B, C…), hàng 2 là tên chỉ tiêu, hàng 3 là ngưỡng. Ba con số này bám
   theo đúng thứ tự ấy trong markup — thêm một hàng tiêu đề mà quên sửa đây thì chiều cao lệch một
   hàng.

   20/32/18 → 24/40/26 (11/09/2026). Bốn hàng chồng lên nhau trong 104px là một dải chữ liền, không ra
   hình ô: chữ chạm mép trên rồi chạm mép dưới, và đường kẻ — thứ vừa được tô đậm lên để lưới đọc ra
   như Excel — lại không có chỗ trống nào hai bên để mà thành cạnh của ô. Cao thêm là cách duy nhất mua
   được khoảng thở ấy mà không đụng tới bề ngang cột. */
.qm-scope .iqc-sheet__table thead tr:nth-child(1) th { height: 24px; }
.qm-scope .iqc-sheet__table thead tr:nth-child(2) th { height: 40px; }
.qm-scope .iqc-sheet__table thead tr:nth-child(3) th { height: 26px; }

/* Dải địa chỉ cột kiểu bảng tính. Xám, chữ nhỏ, căn giữa — nó là thước đo chứ không phải dữ liệu, nên
   không được nặng hơn tên chỉ tiêu ngay dưới nó. */
.qm-scope .iqc-sheet__table .iqc-sheet__addr {
    padding: 0 6px;
    text-align: center;
    font-size: 11px;
    font-weight: 500;
    line-height: 1;
    letter-spacing: 0.04em;
    color: var(--mud-palette-text-secondary);
    background-color: var(--mud-palette-background-gray);
}

/* Ô địa chỉ của cột đang chọn — chỗ duy nhất trên cả lưới dùng nền đặc, nên nó là thứ mắt bắt được
   trước tiên khi quét ngang một bảng ba mươi cột.

   Xám đậm chứ không phải xanh: xanh trên lưới này chỉ còn nghĩa "ô đang trỏ tới", và một ô địa chỉ tô
   xanh đặc cách ô ấy ba hàng làm cái nghĩa đó loãng ra — nhìn vào không rõ chỗ gõ là ô nào. Đậm nhạt
   là đủ để đánh dấu một cột, vì dải địa chỉ vốn chỉ có một mức xám. */
.qm-scope .iqc-sheet__table .iqc-sheet__addr--active {
    background-color: var(--mud-palette-text-secondary);
    color: var(--mud-palette-surface);
    font-weight: 700;
}

/* Wide enough for words. The column carried a merged "Mẫu" and a row number in 44px; it now spells
   out Chỉ tiêu / Ngưỡng / Kết quả / Kết luận, and a label that wraps mid-word is worse than the
   number it replaced.

   Ghim theo trục ngang (position: sticky; left: 0). Lần ghim trước ghim cả hàng tiêu đề lẫn cột nhãn,
   tức là mỗi <th> nằm trên hai trục sticky cùng lúc trong một khung cuộn, và cái góc giao nhau phải
   xếp trên cả hai — thang z-index/top/left làm việc đó đã sai, dải tiêu đề đè lên hàng dữ liệu phía
   sau. Ở đây chỉ có một trục: lưới không cuộn dọc (nó cao đúng bốn hàng), nên không có góc nào để mà
   giao. Đổi lại, cuộn hết ba mươi cột sang phải vẫn đọc được mình đang ở hàng nào — chính là điều kiện
   để dám nới ô to ra ở dưới. */
.qm-scope .iqc-sheet__table .iqc-sheet__corner,
.qm-scope .iqc-sheet__table .iqc-sheet__rowhead {
    position: sticky;
    left: 0;
    z-index: 2;
    width: 84px;
    white-space: nowrap;
    background-color: var(--mud-palette-background-gray);
    text-align: center;
    font-weight: 600;
    color: var(--mud-palette-text-secondary);
}

.qm-scope .iqc-sheet__table .iqc-sheet__corner {
    font-size: 11px;
    vertical-align: middle;
    text-transform: uppercase;
    letter-spacing: 0.03em;
}

/* A floor, not a width. Numbers are short — a column called "Ca" would collapse to the width of the
   word and leave nowhere to read "12.34" — so this reserves enough for the value, and anything the
   heading needs beyond it the heading takes.

   96px → 128px (09/09/2026). Ít cột hiện ra trong một màn hơn, và đó là chủ ý: lưới này cuộn ngang từ
   ngày có ba mươi ba cột, nên vấn đề chưa bao giờ là "cho vừa màn" mà là ô nhập bé quá không đọc ra
   số vừa gõ. Cột nhãn đã ghim ở trên nên cuộn sang phải không mất chỗ dựa. */
.qm-scope .iqc-sheet__colhead {
    min-width: 128px;
}

/* The heading reads on one line, at its full width, and the column widens to hold it.

   This sheet used to do the opposite: a fixed 104px column with the name wrapped and clipped at two
   lines inside it. That kept the grid narrower, and it cost the one thing the header band is for —
   "Tạp chất không hòa tan" broke across three ragged lines, "Kim loại nặng (As/Pb/Hg/Cd/F)" lost its
   tail entirely, and the operator had to guess which characteristic they were typing a number into.
   The grid already scrolls horizontally at 33 columns; it scrolls a little further now, which is the
   cheaper of the two costs. */
.qm-scope .iqc-sheet__name {
    display: block;
    white-space: nowrap;
    line-height: 1.15;
    font-size: 12.5px;
}

/* Dấu "phải nhập" trên tên cột, và là dấu duy nhất nói điều đó.

   Trước 09/09/2026 còn hai thứ nữa nói y hệt: cả cột tiêu đề tô hổ phách, và ô còn trống của cột ấy
   cũng tô hổ phách. Một câu nói ba lần bằng hai phần ba diện tích bảng thì không còn là nhấn mạnh, và
   nền màu ở hàng nhập lại chính là chỗ màu đang phải để dành cho Đạt/Không đạt. Giữ màu hổ phách cho
   riêng dấu sao: nó vẫn không lẫn với xanh/đỏ của kết luận. */
.qm-scope .iqc-sheet__required {
    margin-left: 2px;
    font-weight: 700;
    color: var(--qm-warn);
}

.qm-scope .iqc-sheet__table .iqc-sheet__unit,
.qm-scope .iqc-sheet__table .iqc-sheet__threshold {
    font-weight: 500;
    font-size: 10.5px;
    line-height: 1;
    color: var(--mud-palette-text-secondary);
}

/* Which column the selection is in, carried up to the header — with the selected cell 20 rows down a
   long sheet, the highlighted heading is what says which characteristic is being typed into. Cùng sắc
   xám với ô địa chỉ ngay trên nó, nhạt đi, nên hai thứ đọc ra là một dải chọn liền mạch chứ không phải
   hai đánh dấu rời nhau. */
.qm-scope .iqc-sheet__table .iqc-sheet__colhead--active {
    background-color: color-mix(in srgb, var(--mud-palette-text-secondary) 14%, var(--qm-surface-2));
    color: var(--mud-palette-text-primary);
}

/* Hàng nhập — hàng duy nhất người dùng gõ vào, nên nó phải nhìn khác ba hàng kia.

   Ba tín hiệu, và cần cả ba vì mỗi cái hỏng theo một kiểu khác: nền trắng (hai hàng tiêu đề và hàng
   Kết luận đều nền xám), ô cao 44px với chữ 14px (22px/13px trước đây là cỡ của một ô đọc, không phải
   một ô gõ; 34px vẫn còn sát chữ), và vạch màu ở nhãn đầu hàng bên trái. Riêng màu nền thì không đủ: ô bắt buộc còn trống
   đang tô hổ phách và ô đã có kết luận đang tô xanh/đỏ, tức là đúng những ô cần nổi lên nhất lại là
   những ô mất nền trắng. */
.qm-scope .iqc-sheet__cell {
    height: 44px;
    background-color: var(--mud-palette-surface);
    font-size: 14px;
    text-align: center;
    font-variant-numeric: tabular-nums;
    cursor: cell;
}

/* Nhãn "Kết quả" của hàng nhập: nền trắng như hàng của nó và một vạch màu chủ đạo ở mép trái, để cái
   hàng ấy đọc ra được ngay cả khi lưới đã cuộn sang cột thứ ba mươi. */
.qm-scope .iqc-sheet__table .iqc-sheet__rowhead--entry {
    background-color: var(--mud-palette-surface);
    color: var(--mud-palette-text-primary);
    box-shadow: inset 3px 0 0 var(--qm-select);
}

/* Đạt thì gần như không tô, không đạt thì tô hẳn và đổi cả màu chữ.

   Một phiếu bình thường là phiếu mọi số đều đạt, nên tô đậm ô đạt biến cả bảng thành một mảng xanh mà
   trong đó ô hỏng — thứ duy nhất đáng nhìn — lại phải đi tìm. Cách chia này lấy đúng của
   SpreadsheetGrid (màn sheet-entry) và của Tekako: nền trắng là mặc định, màu chỉ dành cho ngoại lệ. */
.qm-scope .iqc-sheet__cell--pass {
    background-color: color-mix(in srgb, var(--qm-pass) 7%, var(--mud-palette-surface));
}

.qm-scope .iqc-sheet__cell--fail {
    background-color: var(--qm-fail-soft);
    color: var(--qm-fail);
    font-weight: 700;
}

/* Màu không bao giờ là tín hiệu duy nhất: ô không đạt còn mang một góc gấp, nên bảng vẫn đọc được khi
   in đen trắng hoặc khi người đọc không phân biệt được đỏ với xanh. Cùng một dấu SpreadsheetGrid dùng. */
.qm-scope .iqc-sheet__cell--fail {
    position: relative;
}

.qm-scope .iqc-sheet__cell--fail::after {
    content: "";
    position: absolute;
    top: 0;
    right: 0;
    border-width: 0 6px 6px 0;
    border-style: solid;
    border-color: transparent var(--qm-fail) transparent transparent;
}

/* The selected cell, drawn as an inset ring so it does not shift the row by a pixel the way a real
   border would. Dimmed when focus has left the grid, so a stale highlight never reads as "typing
   here goes into this cell". */
/* A swept block. Tinted rather than outlined: an outline per cell would draw thirty extra rectangles,
   and what the operator needs to see is the extent of the block, which a fill states in one shape. The
   lead cell keeps its ring on top so "where the next keystroke lands" stays unambiguous. */
/* Xanh chứ không phải primary. Primary của app là đỏ thương hiệu, nên vùng quét trước đây tô hồng và ô
   đang chọn viền đỏ — trên một lưới mà đỏ nghĩa là "Không đạt", cái đó đọc ra là mười hai ô vừa hỏng.
   Xem ghi chú ở --qm-select. */
.qm-scope .iqc-sheet__cell--range {
    background-color: var(--qm-select-soft);
}

/* Viền xanh chạy theo con trỏ chuột, và chỉ trên hàng nhập: xanh trên lưới này có đúng một nghĩa —
   "gõ vào đây" — nên nó chỉ được nằm trên một ô tại một thời điểm, chứ không trải ra cả cột như dải
   tiêu đề trước đây. Ba hàng còn lại (Chỉ tiêu, Ngưỡng, Kết luận) không nhận được phím nào nên không
   sáng lên, và phiếu đã khoá cũng vậy — một viền mời gõ trên phiếu không gõ được là một lời hứa suông.

   Đặt ngay trước hai luật --active bên dưới cho đúng thứ tự: ô đang chọn giữ viền của nó kể cả khi
   chuột đang ở ô khác. */
.qm-scope .iqc-sheet:not(.iqc-sheet--locked) .iqc-sheet__entryrow .iqc-sheet__cell:hover {
    box-shadow: inset 0 0 0 2px var(--qm-select);
}

.qm-scope .iqc-sheet__cell--active {
    box-shadow: inset 0 0 0 2px var(--qm-select);
}

.qm-scope .iqc-sheet__cell--active-blur {
    box-shadow: inset 0 0 0 2px var(--mud-palette-lines-default);
}

/* A bare input, not a MudNumericField: this one element is re-created into whichever cell is being
   edited, so it has to inherit the cell's metrics exactly or the row jumps on every move. */
.qm-scope .iqc-sheet__editor {
    display: block;
    box-sizing: border-box;
    width: 100%;
    height: 38px;
    border: none;
    outline: none;
    padding: 0;
    margin: 0;
    background: transparent;
    color: var(--mud-palette-text-primary);
    font: inherit;
    line-height: 38px;
    text-align: center;
}

/* Kết luận is the last row, under the readings it judges — QC asked for it there, and a verdict
   printed above the row it judges answers a question nobody has asked yet. The rule that separated it
   from the data therefore moves to the top edge, and the footer needs the header's own background:
   <tfoot> inherits none of `thead th`. */
.qm-scope .iqc-sheet__table tfoot th {
    background-color: var(--qm-surface-2);
    font-weight: 600;
    border-top: 2px solid var(--mud-palette-lines-default);
}

.qm-scope .iqc-sheet__verdictcell {
    height: 30px;
    text-align: center;
}

/* "Tùy chọn" — chữ nhạt, nghiêng, không tô nền. Đây là trạng thái thông lệ của một cột không có
   ngưỡng, mà tô đặc thì nó đứng ngang hàng với Đạt/Không đạt/Cần nhập, tức là ba việc phải làm và một
   việc không phải làm cùng kêu to như nhau. Chữ nghiêng để đọc ra ngay là một ghi chú chứ không phải
   một phán quyết. */
.qm-scope .iqc-sheet__table tfoot th.iqc-sheet__verdictcell--optional {
    color: var(--mud-palette-text-disabled);
    font-style: italic;
    font-weight: 500;
}

.qm-scope .iqc-sheet__table .iqc-sheet__verdictrow .iqc-sheet__rowhead {
    background-color: var(--mud-palette-background-gray);
    color: var(--mud-palette-text-secondary);
}

.qm-scope .iqc-panel__action.mud-button-root {
    text-transform: none;
    letter-spacing: 0;
    font-size: 12px;
    font-weight: 500;
    min-width: 0;
    padding: 2px 8px;
}

/* MudToggleGroup renders each side as a MudButton, so a toggle arrives on the button type scale:
   0.875rem, uppercase, wide letter-spacing. Two segments per criterion across a row of five reads
   louder than the readings themselves. */
.qm-scope .mud-toggle-item,
.qm-scope .mud-toggle-item .mud-button-label {
    font-size: 11.5px;
    line-height: 1.3;
    text-transform: none;
    letter-spacing: 0;
    white-space: nowrap;
}

.qm-scope .mud-toggle-item {
    min-width: 0;
}

/* Only the answered side carries the verdict colour. MudToggleGroup paints the unselected item in the
   group's colour too, which left "Không đạt" sitting in green next to a green "Đạt" — two lit answers
   where the operator gave one. */
.qm-scope .mud-toggle-item:not(.mud-button-filled) {
    color: var(--mud-palette-text-secondary);
}

/* MudChip ships a full pill radius. At the size StatusChip uses it that reads as a lozenge rather than
   a label, and it sits badly next to the square-cornered inputs and toggles it shares a row with —
   so StatusChip's own class softens it to the app's ordinary corner.

   Global rather than StatusChip's scoped sheet because MudChip is a component tag: CSS isolation only
   stamps plain HTML elements, so a `::deep` written from there compiles cleanly and matches nothing.
   Scoped to .status-chip so no other MudChip in the app is touched. */
.status-chip.mud-chip {
    border-radius: var(--qm-radius);
}

/* ══ Ô trạng thái QM ════════════════════════════════════════════════════════════════════════════

   Cột trạng thái trên màn QM tô nền cả ô, không đeo chip nữa. Một con chip nhỏ giữa ô để trắng đặt
   màu vào đúng phần mắt không quét: đọc dọc một cột trạng thái là đọc dải màu, còn chữ chỉ xác nhận
   lại. Bỏ chip cũng bỏ luôn viền và bo góc — chrome mà một ô đã tô nền không cần.

   Nền phải nằm trên chính thẻ <td>, nên lớp này đi qua CellClassFunc của cột (QmStatusCell) chứ
   không nằm trong CellTemplate: một thẻ bọc bên trong ô không bao giờ phủ hết phần đệm của ô, và
   phần hở đó lộ ra ngay khi hàng cao hơn một dòng. MudBlazor tô sọc và hover trên <tr>, mà nền ô vẽ
   đè lên nền hàng, nên không cần !important như khối cột đóng băng ở trên.

   Toàn cục chứ không phải scoped css của một màn: cùng bộ lớp này dùng cho DataTable của mười mấy
   màn master data lẫn hai bảng nhập NIR–Lab tự dựng, mà CSS isolation không với sang nhau được. */
/* Năm màu tô, khai báo ở :root chứ không trong khối .qm-scope ở đầu file, và cũng không lấy thẳng
   --mud-palette-*.

   Không dùng .qm-scope vì quá nửa số màn mang lớp này không nằm trong nó — danh sách master data,
   hàng đợi phiếu, báo cáo NCR, hai hộp thoại nhập — mà một --qm-* không thấy được ở đó thì
   background-color hỏng, ô thành trong suốt, và chữ trắng nằm trên nền trắng.

   Năm màu chọn cho tươi và cho tách bạch — mỗi trạng thái một sắc riêng hẳn. Chúng từng khác hẳn
   --mud-palette-* (bảng màu chrome của app khi ấy chọn cho nút và viền: Success là xanh ngả ôliu
   #65A714 trông xỉn khi ăn cả ô, Info lại là một sắc xanh lá khác nữa #397108, nên "Đã nhập" và
   "Chưa nhập" hoá ra hai ô xanh cạnh nhau không phân biệt được). Từ 11/09/2026 thì ngược lại:
   AppTheme.razor lấy đúng bốn trong năm màu này làm Success/Info/Warning/Error của cả app, nên hai
   bảng màu nay là một. Vẫn khai ở đây và vẫn khai bằng hex, vì đây là bộ gốc — theme chép theo, và
   ô trạng thái không thể phụ thuộc vào một biến mà chính nó là nguồn. */
:root {
    --qm-status-pass: #2fa84f;
    --qm-status-fail: #e53935;
    --qm-status-warn: #f0a11a;
    --qm-status-prog: #2c7be5;
    --qm-status-wait: #6b7280;
    --qm-status-ink: #fff;
}

/* Nền tối nuốt màu: cùng năm sắc ấy nâng sáng lên một bậc để ô không chìm vào mặt thẻ. Bám đúng
   cách nhận biết theme mà khối .qm-scope tối ở trên dùng. */
body:has(.app-theme-marker[data-app-theme="dark"]),
:root:has(.app-theme-marker[data-app-theme="dark"]) {
    --qm-status-pass: #35bc5c;
    --qm-status-fail: #f0534e;
    --qm-status-warn: #f5ae2e;
    --qm-status-prog: #3d8bf2;
    --qm-status-wait: #7a8391;
}

/* Tô đặc, chữ trắng. Bản đầu tô nhạt 12% cho đỡ nặng; nhạt thế thì cả cột chỉ hơn nền trắng một
   chút và lại thành đúng cái chip cũ phóng to. Cả ô ăn màu thật mới đọc được từ xa, đúng kiểu bảng
   theo dõi treo ở xưởng.

   Màu đi qua --qm-cell-bg/--qm-cell-ink chứ không đặt thẳng: hai bảng tự dựng ở cuối khối này phải
   tô lại đúng màu ấy với chọn lọc cao hơn, và một biến là cách duy nhất để chúng không phải chép
   lại cả năm nhánh. */
.qm-status-cell {
    --qm-cell-bg: var(--qm-status-wait);
    --qm-cell-ink: var(--qm-status-ink);
    background-color: var(--qm-cell-bg);
    color: var(--qm-cell-ink);
    font-weight: 600;
    text-align: center;
    white-space: nowrap;
}

.qm-status-cell--pass {
    --qm-cell-bg: var(--qm-status-pass);
}

.qm-status-cell--fail {
    --qm-cell-bg: var(--qm-status-fail);
}

.qm-status-cell--warn {
    --qm-cell-bg: var(--qm-status-warn);
}

.qm-status-cell--prog {
    --qm-cell-bg: var(--qm-status-prog);
}

/* Trạng thái trung tính vẫn tô đặc như bốn màu kia — một ô để trắng giữa cột đọc ra là "thiếu dữ
   liệu", chứ không phải "chưa tới lượt". */
.qm-status-cell--wait {
    --qm-cell-bg: var(--qm-status-wait);
}

/* Tiêu đề của cột trạng thái căn giữa theo ô. MudBlazor dựng đầu cột bằng flex nên text-align không
   với tới, phải nắn bằng justify-content.

   Đầu cột giữ nguyên nền trắng chữ đen (yêu cầu 11/09/2026, sau một lần thử tô xám đặc như ô ở dưới):
   nó chỉ đặt tên cho cột chứ không nói trạng thái nào, nên một mảng màu ở đó là màu không mang tin. */
.mud-data-grid th.qm-status-head .column-header {
    justify-content: center;
}

/* ── Chữ trong ô trạng thái phải trắng, và đây là chỗ nó bị mất ──────────────────────────────────
   MudBlazor tô chữ cho mọi ô thân bảng bằng `.mud-table-root .mud-table-body .mud-table-cell`: ba
   lớp, thắng `.qm-status-cell` một lớp. `color` trắng và `font-weight: 600` của khối trên vì thế
   chưa bao giờ tới được một ô nào trên DataTable — chữ vẫn là xám đen của body2 nằm trên nền xanh
   hoặc cam đặc, mờ hẳn so với chính con chữ trắng trên thẻ KPI ngay trên bảng (MudPaper không có
   luật nào tô chữ cho nội dung bên trong nó, nên bên ấy nhận được ngay).

   Nhắc lại ở đây với đúng một lớp nhiều hơn, chứ không đôn cả khối `.qm-status-cell` lên: lớp ấy còn
   dùng cho hai bảng HTML tự dựng (`.iqc-table`, `.iqc-sheet__table`) nằm ngoài `.mud-table-root`, và
   mỗi lần đôn chọn lọc là một lần một bảng khác phải đôn theo.

   Lớp thêm vào là `.mud-table-cell` chứ không phải `.mud-table-body`: cả hai đều có trên DOM, nhưng
   lớp của <tbody> là thứ MudBlazor có thể đổi giữa MudTable và MudDataGrid, còn lớp của chính ô thì
   không — mà chọn lọc thì chỉ cần đủ ba lớp cộng một thẻ.

   12px/600 là đúng cỡ và đúng nét của nhãn thẻ KPI, để hai chỗ nói cùng một trạng thái thì đọc ra
   cùng một giọng. */
.mud-table-root td.mud-table-cell.qm-status-cell {
    color: var(--qm-cell-ink);
    font-size: 12px;
    font-weight: 600;
}

/* Nhãn thẻ KPI lên 600 cho khớp: cùng cỡ 12px rồi, chỉ còn nét là khác. */
.metric-card__label {
    font-weight: 600;
}

/* ── Ngoài ô bảng: chip trạng thái và thẻ KPI cũng tô đặc, chữ trắng ─────────────────────────────
   Cùng một quy tắc mà cột trạng thái trong bảng đã theo từ 09/09/2026, và lý do cũng y hệt: màu chỉ
   chiếm một mẩu viền quanh mấy chữ thì phần lớn diện tích ô không mang tin gì, đọc từ xa không ra.

   Toàn app, không riêng QC (yêu cầu 11/09/2026: "làm màu ô và màu chữ như trong phiếu inspection cho
   toàn bộ trang web"). Bản trước bó trong .qm-scope, và đó chính là chỗ hỏng: cùng một trạng thái vẽ
   ra hai kiểu tuỳ người dùng đang đứng ở module nào. Tên biến còn mang tiền tố --qm-* là dấu vết của
   lần ấy, không phải phạm vi của nó.

   Mọi con chip mang màu có nghĩa, không riêng StatusChip: chip viền và chip nền mờ là hai biến thể
   MudBlazor vẽ sẵn cho cùng một ý, cả hai đều để màu ở đường bao hoặc ở chữ. Chip không khai Color
   (mã khối, số đếm, tên nhóm) thì không đụng tới — nó là nhãn trung tính, tô đặc thì thành một dãy
   khối xám không nói gì. */
.mud-chip.mud-chip-outlined.mud-chip-color-success,
.mud-chip.mud-chip-text.mud-chip-color-success { background-color: var(--qm-status-pass); }

.mud-chip.mud-chip-outlined.mud-chip-color-info,
.mud-chip.mud-chip-text.mud-chip-color-info { background-color: var(--qm-status-prog); }

.mud-chip.mud-chip-outlined.mud-chip-color-warning,
.mud-chip.mud-chip-text.mud-chip-color-warning { background-color: var(--qm-status-warn); }

.mud-chip.mud-chip-outlined.mud-chip-color-error,
.mud-chip.mud-chip-text.mud-chip-color-error { background-color: var(--qm-status-fail); }

/* Viền và màu chữ chung cho cả bốn, viết ra từng cái chứ không bắt bằng [class*="mud-chip-color-"]:
   Color.Default cũng có thể sinh ra một lớp khớp cái mẫu ấy, và khi đó chip không màu sẽ nhận chữ
   trắng mà không nhận nền — trắng trên gần-trong-suốt là chữ tàng hình. */
.mud-chip.mud-chip-outlined.mud-chip-color-success,
.mud-chip.mud-chip-text.mud-chip-color-success,
.mud-chip.mud-chip-outlined.mud-chip-color-info,
.mud-chip.mud-chip-text.mud-chip-color-info,
.mud-chip.mud-chip-outlined.mud-chip-color-warning,
.mud-chip.mud-chip-text.mud-chip-color-warning,
.mud-chip.mud-chip-outlined.mud-chip-color-error,
.mud-chip.mud-chip-text.mud-chip-color-error {
    border-color: transparent;
    color: var(--qm-status-ink);
}

/* Nền xám cho StatusChip không màu: Color.Default không sinh ra lớp màu nào cả nên bốn luật trên
   không bắt được nó, mà một chip trạng thái để trắng giữa một màn tô đặc đọc ra là "thiếu dữ liệu",
   đúng lập luận của .qm-status-cell--wait. Chỉ StatusChip, vì chỉ nó mới chắc chắn đang nói một
   trạng thái. */
.status-chip.mud-chip-outlined {
    border-color: transparent;
    background-color: var(--qm-status-wait);
    color: var(--qm-status-ink);
}

/* Thẻ KPI: cả thẻ ăn màu, không còn là vạch 3px ở mép trái.

   Vạch ấy là thứ khiến đổi bảng màu của QC sang màu tươi gần như không nhìn thấy gì trên mấy màn danh
   sách — ba pixel thì đổi sắc nào cũng thế. Trạng thái trung tính vẫn tô đặc (xám) chứ không để trắng,
   đúng lập luận của .qm-status-cell--wait: một thẻ trắng đứng giữa ba thẻ có màu đọc ra là "chưa tính
   được", không phải "bình thường". */
.metric-card {
    background-color: var(--qm-status-wait);
    border-color: transparent;
    color: var(--qm-status-ink);
}

.metric-card--running { background-color: var(--qm-status-pass); }
.metric-card--idle { background-color: var(--qm-status-prog); }
.metric-card--attention { background-color: var(--qm-status-warn); }
.metric-card--fault { background-color: var(--qm-status-fail); }

/* Nhãn và dòng ghi chú trong thẻ là .mud-text-secondary — màu chữ phụ chọn cho nền trắng, trên nền đặc
   thì gần như biến mất. Trắng mờ chứ không trắng hẳn, để con số vẫn là thứ nổi nhất trong thẻ. */
.metric-card .mud-text-secondary {
    color: color-mix(in srgb, var(--qm-status-ink) 78%, transparent);
}

/* Trừ cái nhãn: nó trắng hẳn như chữ trong ô trạng thái ngay dưới bảng, vì nó là chỗ duy nhất trong
   thẻ nói đúng cái điều mấy ô ấy nói. Dòng ghi chú dưới con số vẫn mờ — đó là ngữ cảnh, không phải
   trạng thái. Đặt sau luật trên vì hai bộ chọn ngang điểm chọn lọc, thứ tự là thứ phân định. */
.metric-card .metric-card__label {
    color: var(--qm-status-ink);
}

/* Vạch màu mép trái không còn việc gì khi cả thẻ đã ăn màu. Chọn lọc phải cao hơn luật trong
   MetricCard.razor.css: sheet ấy biên dịch thành .metric-card[b-xxx]::before, ngang điểm với một luật
   hai lớp ở đây, và hoà thì thắng thua tuỳ thứ tự nạp file. */
.mud-paper.metric-card::before {
    display: none;
}

/* Hai bảng nhập tự dựng đã có màu chữ và nền riêng cho ô của chúng, viết với chọn lọc cao hơn khối
   trên, nên chúng phải nhường lại ở đây. Nhường ở phía chúng chứ không đôn chọn lọc của
   .qm-status-cell lên: lớp ấy còn dùng cho hàng chục bảng khác, và mỗi lần đôn là một lần bảng nào đó
   khác lại phải đôn theo. */
.qm-scope .iqc-table td.qm-status-cell,
.qm-scope .iqc-sheet__table tfoot th.qm-status-cell {
    background-color: var(--qm-cell-bg);
    color: var(--qm-cell-ink);
}

/* Nút hành động trên bảng QM ăn màu primary, không còn xám như chữ trong ô.

   Hai chỗ, và chỉ hai chỗ: cụm nút đầu thanh công cụ của DataTable (lọc, chọn cột, xuất file, thêm)
   và nút mở chi tiết ở cuối mỗi dòng. Trên một bảng mà mỗi ô trạng thái đã là một mảng màu đặc, thứ
   duy nhất bấm được lại đang là thứ chìm nhất — người dùng quét cả hàng đợi mà không thấy đường vào
   phiếu.

   :not(.mud-button-text-*) để chừa lại những nút đã tự chọn màu: nút Xoá trong ô là Color.Error, mà
   luật này chọn lọc cao hơn lớp màu của MudBlazor nên không loại trừ thì nó nhuộm đỏ-thương-hiệu đè
   lên đỏ-lỗi. Cặp Sửa/Xoá trong ô cũng cố ý không đụng tới: primary của app là đỏ, đặt cạnh nút Xoá
   vốn đã đỏ thì thành hai nút đỏ liền nhau không phân biệt được.

   Chỉ trong .qm-scope: cùng DataTable ấy đang chạy trên PM, CMMS và admin, và yêu cầu này là của
   riêng QC. */
.qm-scope .data-table__toolbar .mud-icon-button:not([class*="mud-button-text-"]),
.qm-scope .data-table__row-action .mud-icon-button:not([class*="mud-button-text-"]) {
    color: var(--mud-palette-primary);
}

/* Nút biểu tượng trên thanh công cụ — nới vùng bấm, giữ nguyên cỡ hình.
   MudBlazor để nút cỡ Small là 30×30 (hình 20px + đệm 5px). Đó là dưới mức 40px một con trỏ cần để
   bấm trúng ngay lần đầu, và riêng nút lọc còn bị huy hiệu số đè lên một góc, nên phần thật sự nhắm
   được còn nhỏ hơn 30. Nút là flex căn giữa, nên min-width/height chỉ nới phần trống quanh hình chứ
   không phóng to hình.

   Đặt toàn cục và theo .mud-toolbar chứ không sửa một màn: cùng một nút ấy nằm trên mọi màn danh
   sách (DataTable), trên dải công cụ của báo cáo QM và trên hai bảng nhập NIR–Lab. CSS isolation
   không với sang nhau được, mà nới mỗi một chỗ thì đúng bằng việc dựng lại chính cái lệch mà mấy
   thanh công cụ kia đã phải đi đồng bộ theo DataTable để bỏ đi. Thanh công cụ thấp nhất trong app là
   44px nên 40px vẫn nằm gọn. */
.mud-toolbar .mud-icon-button {
    min-width: 40px;
    min-height: 40px;
}

/* ══ QM report tables ═══════════════════════════════════════════════════════════════════════════
   Global under .qm-scope rather than a component-scoped sheet: three report screens render the same
   table (QM-RP-01/02/03) and a scoped sheet only reaches the component that declares it. Same reason
   the iqc-* rules above are here.
   ─────────────────────────────────────────────────────────────────────────────────────────────── */
﻿/* The report grid: ~90 columns behind a horizontal scroll, with the two identity columns and the three
   header rows pinned so a number in the middle of the sheet can still be traced to its material and its
   characteristic. That is the whole reason this is not a DataTable — see the component's header. */
/* The card is the same shape DataTable's is — an outlined MudPaper with the grid scrolling inside
   it — so the two read as one control used twice. overflow:hidden is what keeps the scrolling grid's
   corners inside the card's radius. */
.qm-scope .rp-card {
    overflow: hidden;
}

/* The toolbar above the sheet, matching DataTable's strip — same 44px through MudBlazor's own
   variable (a plain `height` ties on specificity and loses to whichever sheet loads last), same
   narrowed gutters, same divider the grid would otherwise sit flush against.
   Duplicated from DataTable.razor.css rather than shared: that file is CSS-isolated to its own
   component and a ::deep rule in it cannot reach this one. */
.qm-scope .rp-toolbar {
    --mud-internal-toolbar-height: 44px;
    padding-left: 8px;
    padding-right: 8px;
    border-bottom: 1px solid var(--mud-palette-table-lines);
}

/* Fixed width, not flex-grow: a growing field would absorb the spacer's free space and drag itself
   back to the left edge, away from the cluster it belongs to. */
.qm-scope .rp-search {
    flex: 0 0 260px;
}

/* MudBlazor's text-variant input reserves a helper-text line and draws an underline. The empty line
   is most of the wasted height in the strip, and a lone underline reads as an unfinished form field
   sitting on the table. The search icon carries the affordance instead. */
.qm-scope .rp-search .mud-input-control-helper-container {
    display: none;
}

.qm-scope .rp-search .mud-input-underline:before,
.qm-scope .rp-search .mud-input-underline:after {
    display: none;
}

/* Below sm the strip wraps, which needs the height to follow the content — again via the variable. */
@media (max-width: 599.98px) {
    .qm-scope .rp-toolbar {
        --mud-internal-toolbar-height: auto;
        flex-wrap: wrap;
        padding-top: 8px;
        padding-bottom: 8px;
    }

    .qm-scope .rp-search {
        flex: 1 1 100%;
    }
}

/* Bảng phóng kín màn hình.

   Đặt trên cái vỏ ngoài chứ không trên thẻ, vì `position: fixed` xoá luôn mọi ràng buộc kích thước
   của thẻ — để thẻ tự làm cái khung cố định thì phần cuộn bên trong không còn gì để tính chiều cao và
   nó kéo dài quá đáy màn hình. Vỏ ngoài giữ khung, thẻ co giãn trong đó, vùng cuộn ăn hết phần còn lại.

   z-index 1200: trên thanh ứng dụng (1100) — bảng phủ kín thì thanh điều hướng thò ra một nửa là vô
   nghĩa — nhưng dưới hộp thoại (1300) và popover (1400), nên ngăn lọc và mấy ô chọn ngày bên trong nó
   vẫn mở lên trên bảng. */
.qm-scope .rp-shell {
    outline: none;
}

.qm-scope .rp-shell--full {
    position: fixed;
    inset: 0;
    z-index: 1200;
    display: flex;
    padding: 8px;
    background-color: var(--mud-palette-background);
}

.qm-scope .rp-shell--full .rp-card {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;

    /* Không có nó thì một flex item không chịu co xuống dưới chiều cao nội dung, và vùng cuộn lại
       tràn khỏi đáy màn hình đúng như lúc chưa phóng to. */
    min-height: 0;
}

/* Co theo nội dung, giãn hết chỗ trống thì thôi — `flex: 1 1 auto` sẽ kéo khung cuộn xuống kín màn
   hình kể cả khi bảng rỗng, và lời nhắn "không có dữ liệu" bị đẩy xuống tận đáy, cách chỗ nó nói đến
   cả một màn hình trắng. min-height: 0 là thứ cho phép nó co lại khi bảng dài hơn màn hình. */
.qm-scope .rp-shell--full .rp-scroll {
    flex: 0 1 auto;
    min-height: 0;
    max-height: none;
}

.qm-scope .rp-scroll {
    overflow: auto;
    max-height: 70vh;

    /* No border or surface of its own: MatrixTable puts it inside an outlined MudPaper, which draws
       both. Two nested borders read as a box in a box. */
    background-color: var(--mud-palette-surface);
}

.qm-scope .rp-table {
    border-collapse: separate;
    border-spacing: 0;
    font-size: 12.5px;
    white-space: nowrap;
}

/* --mud-palette-table-lines and the dense 6px/8px cell padding are MudDataGrid's own, so a report
   sheet and a DataTable screen draw the same weight of rule at the same rhythm. */
.qm-scope .rp-table th,
.qm-scope .rp-table td {
    border-right: 1px solid color-mix(in srgb, var(--mud-palette-table-lines) 55%, transparent);
    border-bottom: 1px solid var(--mud-palette-table-lines);
    padding: 6px 8px;
}

.qm-scope .rp-table thead th {
    position: sticky;
    background-color: var(--mud-palette-surface);
    color: var(--mud-palette-text-primary);
    font-weight: 600;
    text-align: center;
    z-index: 2;
}

/* Striped on odd rows, the parity MudBlazor itself uses — a report sheet striped the other way from
   every list screen in the app reads as a different control rather than the same one. */
.qm-scope .rp-table tbody tr:nth-of-type(odd) > td {
    background-color: var(--mud-palette-table-striped);
}

.qm-scope .rp-table tbody tr:nth-of-type(odd) > td.dt-frozen-cell {
    background-color: var(--mud-palette-table-striped);
}

/* Three header rows, each pinned at the height of the ones above it. Stated rather than measured
   because a sticky row's `top` cannot be "wherever the previous one ended" in CSS. */
.qm-scope .rp-table thead tr:nth-child(1) th { top: 0; height: 28px; }
.qm-scope .rp-table thead tr:nth-child(2) th { top: 28px; height: 24px; }
.qm-scope .rp-table thead tr:nth-child(3) th { top: 52px; height: 34px; font-weight: 700; font-size: 10.5px; white-space: normal; }

/* The identity columns pin sideways as well, so they out-stack the header band where the two cross. */
.qm-scope .rp-table .rp-ident {
    text-align: left;
    font-weight: 600;
}

/* Frozen columns, on DataTable's own machinery rather than a second set invented here: the leading
   headers carry MudBlazor's `sticky-left`, appDataTable.syncFrozen measures them after each render
   and writes each cell's running-total offset plus .dt-frozen-cell. The rules below are therefore
   index-free and hold for any number of frozen columns — the previous version declared the offsets
   in CSS, which needs one rule per column index and silently stopped working past whatever number
   someone had written rules up to. It had two, against QM-RP-02's six and QM-RP-03's five.

   Not shared with the .mud-data-grid rules further up: those are selected under that class, and this
   is a plain table. Same tokens and same reasoning, restated for this element. */
.qm-scope .rp-table th.sticky-left,
.qm-scope .rp-table td.dt-frozen-cell {
    position: sticky;
    z-index: 3;
}

.qm-scope .rp-table thead th.sticky-left {
    z-index: 4;
}

/* Opaque, and following the row's own state. A pinned cell left transparent shows the scrolled
   columns through it — a hovered row would display values from underneath the frozen band. */
.qm-scope .rp-table tbody td.dt-frozen-cell,
.qm-scope .rp-table tfoot td.dt-frozen-cell {
    background-color: var(--mud-palette-surface);
}

.qm-scope .rp-table tbody tr:hover > td.dt-frozen-cell {
    background-color: var(--mud-palette-table-hover);
}

/* The seam where the frozen band ends, so the reader can see where the scrolling half starts. */
.qm-scope .rp-table td.dt-frozen-cell--last,
.qm-scope .rp-table th.sticky-left:last-of-type {
    border-right-color: var(--mud-palette-table-lines);
}

/* Hai hàng tiêu đề trên cùng là một dải navy đặc, hàng thứ ba là dải nhạt — đúng ba tầng
   `th.grp` / `th.sub` của mockup QM-RP-01. Đây là chỗ duy nhất trong bảng dùng nền đặc, và nó có việc
   phải làm: bảng rộng chín mươi cột số giống hệt nhau, không có dải màu thì cuộn ngang ba màn hình
   xong không còn biết cụm số đang xem là chỉ tiêu nào. */
.qm-scope .rp-table thead th.rp-char,
.qm-scope .rp-table thead th.rp-point {
    background-color: var(--qm-report-band);
    color: var(--qm-report-band-ink);
    border-bottom-color: color-mix(in srgb, var(--qm-report-band-ink) 22%, transparent);
    text-transform: uppercase;
    letter-spacing: .3px;
}

/* Điểm đo là chú thích của chỉ tiêu ngay trên nó, không phải một tiêu đề ngang hàng — nên nhẹ hơn,
   nhỏ hơn và không viết hoa, y như <div> lồng trong ô `th.grp` của mockup. */
.qm-scope .rp-table thead th.rp-point {
    font-weight: 400;
    font-size: 10px;
    text-transform: none;
    opacity: .86;
}

.qm-scope .rp-table thead th.rp-stat {
    background-color: var(--qm-report-subhead);
    color: var(--qm-report-subhead-ink);
    text-transform: uppercase;
    letter-spacing: .2px;
    min-width: 74px;
}

/* Mấy cột định danh phía trước dùng dải nhạt ấy cho cả ba tầng — chúng là một ô rowspan, không thể
   vừa navy vừa nhạt, mà để navy thì tên nguyên liệu bị dải màu nuốt mất. */
.qm-scope .rp-table thead th.sticky-left {
    background-color: var(--qm-report-subhead);
    color: var(--qm-report-subhead-ink);
    text-transform: uppercase;
    letter-spacing: .2px;
    vertical-align: bottom;
}

/* Ranh giới giữa hai cụm ô, hai bề dày như mockup: hết một điểm đo là một vạch, hết hẳn một chỉ tiêu
   là một vạch đậm hơn. Trên dải navy thì vạch ấy là màu nền thẻ, nên hai khối trông như tách rời;
   trong thân bảng thì là màu kẻ bảng, vì ở đó nền đã trắng rồi. */
.qm-scope .rp-table .rp-sec-end {
    border-right: 2px solid var(--mud-palette-table-lines);
}

.qm-scope .rp-table .rp-band-end {
    border-right: 3px solid var(--mud-palette-lines-default);
}

.qm-scope .rp-table thead .rp-sec-end {
    border-right: 2px solid var(--qm-report-band-gap);
}

.qm-scope .rp-table thead .rp-band-end {
    border-right: 3px solid var(--qm-report-band-gap);
}

/* Số mẫu vượt ngưỡng khác 0 — chỗ duy nhất mockup tô màu chữ trong thân bảng. Số 0 để nguyên, vì tô
   đỏ cả cột thì không còn ai nhìn màu nữa. */
.qm-scope .rp-table .rp-exceed {
    color: var(--qm-fail);
    font-weight: 700;
}

.qm-scope .rp-table .rp-value {
    text-align: right;
    font-variant-numeric: tabular-nums;
}

.qm-scope .rp-table tbody tr:hover > td {
    background-color: var(--mud-palette-table-hover);
}

/* Print: the pane is a scrolling box on screen and a page in a printer, so the cap and the sticky
   positioning both have to come off or the printout is one screenful of a ninety-column table. */
@media print {
    .qm-scope .rp-scroll {
        overflow: visible;
        max-height: none;
        border: none;
    }

    .qm-scope .rp-table thead th,
    .qm-scope .rp-table th.sticky-left,
    .qm-scope .rp-table td.dt-frozen-cell {
        position: static;
    }
}

/* Row tinting from QM-RP-02's mockup: a product below 90% is a failure to look at, 90–95% a warning.
   Tinted rather than badged, because the signal belongs to the whole line rather than to one cell. */
.qm-scope .rp-table tbody tr.rp-row-fail td { background-color: var(--qm-fail-soft); }
.qm-scope .rp-table tbody tr.rp-row-warn td { background-color: var(--qm-warn-soft); }

/* QM-TX-10 §2.1-④: an open alert is tinted by severity — đỏ nhạt for Cao (rách sàng), vàng nhạt for
   Trung bình (nhiệt độ). Keyed off the row class alone rather than a table class, because DataTable puts
   RowClass on MudDataGrid's own <tr> and the board has no wrapper table of its own. */
.qm-scope tr.qc-alert-row--high td { background-color: var(--qm-fail-soft); }
.qm-scope tr.qc-alert-row--medium td { background-color: var(--qm-warn-soft); }

/* Tỷ lệ đạt chung — pinned beside the identity columns because it is the number the report is read for. */
.qm-scope .rp-table .rp-rate {
    text-align: right;
    font-variant-numeric: tabular-nums;
    min-width: 88px;
}

.qm-scope .rp-table .rp-muted { color: var(--mud-palette-text-secondary); }

/* TỔNG CỘNG, pinned to the foot so it stays visible while the body scrolls. */
.qm-scope .rp-table tfoot td {
    position: sticky;
    bottom: 0;
    background-color: var(--mud-palette-background-gray);
    font-weight: 600;
    border-top: 2px solid var(--mud-palette-lines-default);
    z-index: 2;
}

.qm-scope .rp-table tfoot .rp-ident { z-index: 3; }

/* Bảng hướng dẫn "Báo cáo này cần dữ liệu gì" — ReportHelp.razor. Rộng cố định vì nội dung là
   danh sách mã công đoạn và mã chỉ tiêu: để nó co theo viewport thì mã bị ngắt dòng giữa chừng,
   mà mã ngắt dòng là thứ người đọc đang cố đối chiếu với màn hình phiếu. */
.report-help {
    max-width: min(30rem, calc(100vw - 2rem));
}

/* ReportHelp is a native popover now (see the component), so these replace the UA's centred-dialog
   defaults: no border/inset/margin of its own, and placed under its button by anchor positioning —
   right edges aligned, flipping above or to the other side when it would leave the viewport. A
   browser without anchor positioning ignores position-area and shows it centred, still usable. */
.report-help[popover] {
    inset: auto;
    margin: 4px 0 0;
    border: none;
    max-height: min(70vh, 40rem);
    overflow-y: auto;
    position-area: bottom span-left;
    position-try-fallbacks: flip-block, flip-inline;
}

.report-help-list {
    margin: 0.25rem 0 0;
    padding-left: 1.25rem;
}

.report-help-list li {
    font-size: 0.8125rem;
    line-height: 1.6;
}

/* ── Màn hẹp: QM thu thập dữ liệu ──────────────────────────────────────────────

   Ngưỡng 599.98px trùng với khối .rp-toolbar ở trên và với Breakpoint.Sm của MudBlazor, tức cùng một
   đường mà ViewportState gọi là "compact" và ngăn điều hướng chuyển sang phủ lên trang. Ba chỗ nói về
   cùng một bề rộng thì phải nói cùng một con số. */
@media (max-width: 599.98px) {
    /* Thẻ KPI trên điện thoại.

       Đo được trước khi sửa: bốn thẻ trên màn Phiếu kiểm tra chiếm khoảng 2,5 màn hình cuộn trước khi
       thấy dòng dữ liệu đầu tiên, vì mỗi thẻ ăn trọn bề ngang với padding 16px và con số cỡ h5. Xếp
       hai thẻ một hàng và rút gọn lại thì cả bốn nằm trong một phần sáu chỗ cũ, vẫn đọc được con số —
       thứ duy nhất người ta nhìn thẻ KPI để lấy. */
    .metric-card {
        padding: 8px 10px !important;
    }

    .metric-card .metric-card__value {
        font-size: 1.25rem;
    }

    .metric-card .metric-card__label {
        font-size: 0.6875rem;
        line-height: 1.3;
    }

    /* Vùng bấm tối thiểu. Đo được 36 nút nhỏ hơn 40px trên màn Phiếu kiểm tra; ngón tay đeo găng trên
       sàn sản xuất cần rộng hơn thế. 44px là ngưỡng cả Apple lẫn Google cùng khuyến nghị. */
    .qm-scope .mud-button-root:not(.mud-button-text),
    .qm-scope .mud-icon-button {
        min-width: 44px;
        min-height: 44px;
    }

    /* Ô nhập số liệu. 16px là cỡ chữ nhỏ nhất mà Safari trên iOS không tự phóng to trang khi ô được
       chọn — dưới ngưỡng ấy, chạm vào một ô là cả bảng nhảy ra khỏi khung nhìn. */
    .qm-scope input,
    .qm-scope textarea {
        font-size: 16px;
    }
}

/* ── QcSheetTable trên màn hẹp ─────────────────────────────────────────────────

   Ma trận chỉ tiêu × mẫu đổi thành danh sách thẻ. Chỉ là cách bày, không phải cách khác để lưu: cả hai
   nhánh cùng gọi RaiseAsync, nên con số đi xuống máy chủ giống hệt nhau. */
.qm-scope .iqc-cards {
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 8px;
}

.qm-scope .iqc-card {
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 6px;
    padding: 10px 12px;
    background: var(--mud-palette-surface);
}

/* Chỉ tiêu ngoài phạm vi quy cách vẫn hiện, mờ đi — cùng lập luận với dòng "không dùng" của bảng: để
   người nhập thấy chỉ tiêu đã được xét và cố ý không yêu cầu, chứ không phải biến mất. */
.qm-scope .iqc-card--na {
    opacity: 0.6;
}

.qm-scope .iqc-card__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 8px;
}

.qm-scope .iqc-card__name {
    font-weight: 600;
    font-size: 0.9375rem;
    min-width: 0;
}

.qm-scope .iqc-card__verdict {
    font-size: 0.75rem;
    font-weight: 600;
    white-space: nowrap;
}

.qm-scope .iqc-card__threshold {
    color: var(--mud-palette-text-secondary);
    font-size: 0.8125rem;
    margin-bottom: 8px;
}

.qm-scope .iqc-card__entry {
    display: flex;
    align-items: center;
    gap: 8px;
}

.qm-scope .iqc-card__entry + .iqc-card__entry {
    margin-top: 6px;
}

.qm-scope .iqc-card__rowlabel {
    color: var(--mud-palette-text-secondary);
    font-size: 0.8125rem;
    min-width: 3.5rem;
}

/* 44px cao để ngón tay đeo găng bấm trúng, 16px chữ để Safari không phóng to trang khi ô được chọn. */
.qm-scope .iqc-card__editor {
    flex: 1 1 auto;
    min-width: 0;
    height: 44px;
    padding: 0 10px;
    font-size: 16px;
    font-variant-numeric: tabular-nums;
    border: 1px solid var(--mud-palette-lines-input);
    border-radius: 4px;
    background: var(--mud-palette-background);
    color: inherit;
}

.qm-scope .iqc-card__editor:focus {
    outline: 2px solid var(--mud-palette-primary);
    outline-offset: -2px;
}

.qm-scope .iqc-card__editor:disabled {
    background: var(--mud-palette-action-disabled-background);
}

.qm-scope .iqc-card__passfail {
    display: flex;
    gap: 8px;
    flex: 1 1 auto;
}

/* ── ProductSheetGrid trên màn hẹp ─────────────────────────────────────────────

   Lưới 62 cột đổi thành danh sách phiếu gập lại được. Gập theo phiếu chứ không theo chỉ tiêu — ngược
   với .iqc-cards — vì ở màn này một kỳ có hàng chục phiếu và người nhập làm xong hẳn một phiếu rồi mới
   sang phiếu sau. */
.qm-scope.sheet-cards {
    padding: 4px 0;
}

.qm-scope .sheet-cards__empty {
    padding: 16px;
    text-align: center;
    color: var(--mud-palette-text-secondary);
}

.qm-scope .sheet-cards__title {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

.qm-scope .sheet-cards__ticket {
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

.qm-scope .sheet-cards__note {
    font-size: 0.75rem;
    color: var(--mud-palette-text-secondary);
}

/* Nhãn trên, giá trị dưới. Xếp cạnh nhau thì tên chỉ tiêu dài — "Kim loại nặng (As/Pb/Hg/Cd/F)" —
   ép ô nhập còn vài ký tự, mà ô nhập mới là thứ ngón tay phải trúng. */
.qm-scope .sheet-cards__field {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 6px 0;
    border-bottom: 1px solid var(--mud-palette-lines-default);
}

.qm-scope .sheet-cards__field:last-child {
    border-bottom: none;
}

.qm-scope .sheet-cards__label {
    font-size: 0.75rem;
    color: var(--mud-palette-text-secondary);
}

.qm-scope .sheet-cards__readonly {
    font-size: 0.875rem;
    min-height: 24px;
}

.qm-scope .sheet-cards__input,
.qm-scope .sheet-cards__select {
    height: 44px;
    padding: 0 10px;
    font-size: 16px;
    font-variant-numeric: tabular-nums;
    border: 1px solid var(--mud-palette-lines-input);
    border-radius: 4px;
    background: var(--mud-palette-background);
    color: inherit;
    width: 100%;
}

.qm-scope .sheet-cards__input:focus,
.qm-scope .sheet-cards__select:focus {
    outline: 2px solid var(--mud-palette-primary);
    outline-offset: -2px;
}

/* OI v2: client-side press feedback — shows instantly, no Blazor Server round-trip. */
.oi2-board .mud-button-root { touch-action: manipulation; transition: transform 80ms ease-out, opacity 80ms ease-out; }
.oi2-board .mud-button-root:active:not(:disabled) { transform: scale(0.97); opacity: 0.85; }

/* OI shift report (BM-QTSX-02/03) — blueprint look: tinted merged headers, machine group rows, SUM strip. */
.oi-bp th { background: var(--mud-palette-background-gray); font-size: 0.72rem; text-transform: uppercase; }
.oi-bp tr.oi-bp__grp td { background: color-mix(in srgb, var(--mud-palette-primary) 10%, var(--mud-palette-surface)); color: var(--mud-palette-primary); font-weight: 800; }
.oi-bp tr.oi-bp__sum td { background: var(--mud-palette-background-gray); font-size: 0.8125rem; }
.oi-bp__sumbox { display: flex; flex-wrap: wrap; gap: 4px 22px; color: var(--mud-palette-text-secondary); }
.oi-bp__sumbox b { color: var(--mud-palette-text-primary); margin-left: 4px; }

/* OI v2 machine board — generous tiles, scaled up on wide/tablet screens (blueprint PM-TX-06-nhap-nhanh-OI). */
.oi2-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr)); gap: 16px; }
.oi2-slot { border-radius: 14px !important; padding: 28px 24px; text-align: center; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px; min-height: max(380px, calc(100vh - 300px)); }
.oi2-slot--run { border: 2px solid var(--mud-palette-success) !important; background: color-mix(in srgb, var(--mud-palette-success) 8%, var(--mud-palette-surface)); }
.oi2-slot--idle { border: 2px dashed var(--mud-palette-lines-inputs) !important; background: var(--mud-palette-background-gray); }
.oi2-slot__m { font-size: clamp(22px, 2.2vw, 34px) !important; font-weight: 800 !important; letter-spacing: .5px; text-transform: uppercase; line-height: 1.2; }
.oi2-slot__chip { font-size: clamp(14px, 1.1vw, 18px) !important; font-weight: 700; padding: 4px 8px; height: auto !important; }
.oi2-slot__pn { font-size: clamp(20px, 2vw, 32px) !important; font-weight: 800 !important; color: var(--mud-palette-success-darken); margin-top: 18px; line-height: 1.2; }
.oi2-slot__el { font-size: clamp(48px, 5.5vw, 96px) !important; font-weight: 800 !important; font-variant-numeric: tabular-nums; color: var(--mud-palette-success-darken); line-height: 1.1; }
.oi2-slot__st { font-size: clamp(14px, 1.2vw, 20px) !important; color: var(--mud-palette-text-secondary); }
.oi2-slot--idle .oi2-slot__st { margin-top: 18px; }
.oi2-slot .oi2-slot__go { min-height: clamp(64px, 8vh, 96px) !important; font-size: clamp(20px, 1.8vw, 28px) !important; font-weight: 800 !important; border-radius: 12px; margin-top: 12px; }
.oi2-slot__st + .oi2-slot__go, .oi2-slot__el + .oi2-slot__st + .oi2-slot__go { margin-top: 28px; }

/* OI2 shift report DataTable — blueprint look: machine group row tinted, header tinted, tabular numbers. */
.oi2-board .mud-table-head .mud-table-cell { background: var(--mud-palette-background-gray); font-size: 0.72rem; text-transform: uppercase; }
.oi2-board .mud-table-body .mud-table-row-group .mud-table-cell, .oi2-board .mud-data-grid-group .mud-table-cell { background: var(--mud-palette-background-gray); color: var(--mud-palette-text-primary); font-weight: 700; }
.oi2-board .mud-table-body .mud-table-cell { font-variant-numeric: tabular-nums; }
.oi2-slot__plan { font-size: clamp(12px, 1vw, 15px) !important; margin-top: 2px; }

/* OI2 shift report: merged group header row (built by data-table.js bindHeaderGroups). The real header sits one row lower. */
.oi2-hgroups { --oi2-hg-h: 24px; }
.oi2-hgroups tr.oi2-hg-row th { height: var(--oi2-hg-h); padding: 2px 8px; text-align: center; font-size: 0.7rem; font-weight: 800; letter-spacing: .3px; text-transform: uppercase; white-space: nowrap; border-top: 2px solid var(--mud-palette-lines-inputs); background: var(--mud-palette-background-gray); color: var(--mud-palette-text-secondary); }
.oi2-hgroups tr.oi2-hg-row th.oi2-hg-th--blank { border-top-color: transparent; background: var(--mud-palette-surface); }
.oi2-hgroups .mud-table-sticky-header thead tr.oi2-hg-row th { position: sticky; top: 0; z-index: 11; }
.oi2-hgroups .mud-table-sticky-header thead tr:not(.oi2-hg-row) th { top: var(--oi2-hg-h); }
