/* ===========================================================================
   IDHOME app shell — v3 (card-grid menu, no sidebar)

   Single-view design: every viewport uses the same bold-tile card grid
   menu, opened from a button in the topbar. The legacy sidebar (column
   on PC, slide-in drawer on mobile) is removed entirely — the overlay
   IS the menu now, on every screen size.

   Layout:
     .app-shell             single-column wrapper, locks viewport to 100dvh
     .ui-main               content area, scrollable, takes full width
     .ui-topbar             top stripe (menu trigger + brand + notif + user)
     .ui-content            inner wrapper with max-width + padding
     .ui-mobile-nav         full-screen card-grid menu overlay (universal)
     .ui-fab                floating action button (bottom-right, settings)

   Menu overlay visibility via `html[data-mobile-menu="open"]` (the
   attribute name kept its original mobile-only spelling; the behaviour
   is now universal).
   =========================================================================== */

/* Lock the viewport at the document level. `.app-shell` already pins itself
   to 100vh + overflow:hidden, but on some routes (release/detail desktop)
   Chrome still reports html.scrollHeight > viewport — likely from the
   position:sticky sidebar's natural-flow height — which surfaces a phantom
   outer scrollbar and an empty band below the sidebar. Locking html/body
   removes that side channel without affecting the inner `.ui-main` scroll. */
html,
body {
  height: 100%;
  margin: 0;
  overflow: hidden;
}

.app-shell {
  /* Single-column wrapper. No more sidebar — the menu lives in an
     overlay triggered from the topbar — so `.app-shell` is just a
     viewport lock around `.ui-main`. Scrolling lives inside
     `.ui-main`; the document itself does not scroll (see
     `html, body { overflow: hidden }` above). `dvh` accounts for the
     mobile URL bar collapse/expand. */
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
  background: var(--bg);
}

/* ---------- Brand mark (used by topbar + overlay header) ---------- */

.ui-brand-mark {
  width: 32px; height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--accent);
  color: var(--accent-on);
  border-radius: var(--r-sm);
  font-weight: 700;
  font-size: 12px;
  letter-spacing: 0.04em;
  flex-shrink: 0;
}
.ui-brand-text {
  display: flex;
  flex-direction: column;
  gap: 1px;
  line-height: 1.1;
  min-width: 0;
}
.ui-brand-name {
  font-weight: 600;
  font-size: 14px;
  color: var(--text);
}
.ui-brand-sub {
  font-size: 10.5px;
  color: var(--text-muted);
  letter-spacing: 0.02em;
}

/* ---------- Main + topbar + content ---------- */

.ui-main {
  /* Only child of .app-shell now (sidebar removed). Takes the full
     viewport height; topbar pins to its top via position: sticky, the
     content scrolls below. */
  height: 100%;
  min-width: 0;
  display: flex;
  flex-direction: column;
  /* App-shell contract: the content column must never make the whole
     page scroll horizontally. Content that is legitimately wide and
     meant to scroll (data tables via `.ui-table-wrap`, the reports bar
     chart, chip rows) carries its OWN `overflow-x: auto`, so it still
     scrolls internally. Anything else that accidentally exceeds the
     viewport (e.g. a CSS-grid `1fr` track expanding to its min-content,
     a long unbreakable token) gets clipped here instead of pushing the
     topbar + header off-screen.
     Vertical scroll lives HERE now, not on the document — see
     `.app-shell { overflow: hidden }` above for the why. */
  overflow-x: clip;
  overflow-y: auto;
  /* Reserve gutter so content doesn't reflow the instant the scrollbar
     appears (e.g. mid row-stagger when the entrance briefly bumps
     intrinsic height a few pixels). */
  scrollbar-gutter: stable;
}

.ui-topbar {
  /* Stay at the top of the scrolling main column instead of riding the
     content down. `position: sticky` against the new `.ui-main` scroll
     container keeps the topbar in place exactly the way `position:
     fixed` would, without taking the topbar out of flow (which would
     re-introduce the layout coupling the Phase 2 sidebar redesign
     avoided). */
  position: sticky;
  top: 0;
  z-index: var(--z-sticky, 10);
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-6);
  background: var(--bg-elev);
  border-bottom: 1px solid var(--border);
  min-height: 56px;
  flex-shrink: 0;
}
.ui-topbar-spacer { flex: 1; }

.ui-menu-trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  cursor: pointer;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--r);
  font-weight: 600;
  font-size: var(--font-sm);
  transition: background var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}
.ui-menu-trigger:hover { background: var(--surface-2); border-color: var(--border-strong); }
.ui-menu-trigger [data-lucide] { width: 18px; height: 18px; }
.ui-menu-trigger-label { display: inline; }
@media (max-width: 540px) {
  /* Icon-only at very narrow widths to keep the topbar uncluttered. */
  .ui-menu-trigger-label { display: none; }
  .ui-menu-trigger { padding: var(--space-2); }
}

.ui-topbar-brand {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  text-decoration: none;
  color: var(--text);
  min-width: 0;
}
.ui-topbar-brand:hover { color: var(--text); }
@media (max-width: 720px) {
  /* Drop the brand text on phones — the iD mark is enough, and the
     menu trigger is already labelled. */
  .ui-topbar-brand .ui-brand-text { display: none; }
}

.ui-content {
  padding: var(--space-6);
  max-width: 1440px;
  width: 100%;
  margin: 0 auto;
}

/* ---------- Auth shell (login, password reset) ---------- */

.ui-auth-shell {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: var(--space-6);
  background:
    radial-gradient(ellipse at top, color-mix(in oklab, var(--accent) 6%, var(--bg)) 0%, var(--bg) 60%);
}

/* ---------- Auth — split-pane login ---------- */

.ui-auth-split {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(360px, 480px);
  background: var(--bg);
  min-height: 100vh;
  width: 100%;
  max-width: 1280px;
  margin: 0 auto;
  border: 1px solid var(--border);
  border-radius: var(--r-xl);
  overflow: hidden;
  box-shadow: var(--shadow-md);
}
.ui-auth-hero {
  background: var(--nav-bg);
  color: var(--nav-text-active);
  padding: var(--space-12);
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: var(--space-8);
  min-height: 540px;
  position: relative;
  overflow: hidden;
}
.ui-auth-hero::after {
  content: "";
  position: absolute;
  top: -20%; right: -20%;
  width: 50%; height: 70%;
  background: radial-gradient(circle at top right, var(--accent) 0%, transparent 70%);
  opacity: 0.18;
  pointer-events: none;
}
.ui-auth-hero .brand {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  position: relative;
  z-index: 1;
}
.ui-auth-hero .brand .mark {
  width: 36px; height: 36px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--accent);
  color: var(--accent-on);
  border-radius: var(--r-sm);
  font-weight: 700;
  font-size: 13px;
  letter-spacing: 0.04em;
}
.ui-auth-hero .brand .label {
  font-size: var(--font-md);
  font-weight: 600;
  letter-spacing: -0.005em;
}
.ui-auth-hero .brand .sub {
  font-size: 11px;
  color: var(--nav-text-muted);
  letter-spacing: 0.02em;
}
.ui-auth-hero .body { position: relative; z-index: 1; display: flex; flex-direction: column; gap: var(--space-4); }
.ui-auth-hero .chip {
  align-self: flex-start;
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.16);
  color: var(--nav-text-active);
  padding: 3px 10px;
  border-radius: var(--r-pill);
  font-size: var(--font-xs);
  font-weight: 500;
  letter-spacing: var(--tracking-wide);
}
.ui-auth-hero h1 {
  font-size: 36px;
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: -0.02em;
  color: #fff;
  margin: 0;
}
.ui-auth-hero h1 .accent { color: var(--accent); }
.ui-auth-hero .lead {
  font-size: var(--font-md);
  color: var(--nav-text);
  max-width: 36ch;
}
.ui-auth-hero .stats {
  display: flex;
  align-items: center;
  gap: var(--space-6);
  margin-top: var(--space-4);
}
.ui-auth-hero .stats .stat-num {
  font-size: 26px;
  font-weight: 600;
  color: #fff;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}
.ui-auth-hero .stats .stat-lbl {
  font-size: 11.5px;
  color: var(--nav-text-muted);
}
.ui-auth-hero .stats .divider {
  width: 1px; height: 28px;
  background: rgba(255, 255, 255, 0.15);
}
.ui-auth-hero .copyright {
  position: relative;
  z-index: 1;
  font-size: 11px;
  color: var(--nav-text-muted);
}

.ui-auth-side {
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: var(--space-12) var(--space-10);
  background: var(--surface);
}
.ui-auth-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  width: 100%;
  max-width: 360px;
  margin: 0 auto;
}
.ui-auth-form h1 {
  font-size: 22px;
  font-weight: 600;
  letter-spacing: -0.012em;
  margin: 0;
}
.ui-auth-form .lead {
  color: var(--text-muted);
  margin: 4px 0 var(--space-2);
  font-size: var(--font-sm);
}
.ui-auth-form .helpdesk {
  text-align: center;
  font-size: var(--font-xs);
  color: var(--text-muted);
  margin-top: var(--space-2);
}

@media (max-width: 880px) {
  .ui-auth-split {
    grid-template-columns: 1fr;
    border-radius: 0;
    border: 0;
    min-height: 100vh;
    box-shadow: none;
  }
  .ui-auth-hero { display: none; }
  .ui-auth-side { padding: var(--space-6); justify-content: flex-start; padding-top: var(--space-16); }
}

/* ---------- Auth — single-card layout (password reset / confirm) ----- */

.ui-auth-card {
  width: 100%;
  max-width: 420px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: var(--space-8);
  box-shadow: var(--shadow-md);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.ui-auth-card .brand-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}
.ui-auth-card .brand-row .mark {
  width: 32px; height: 32px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--accent); color: var(--accent-on);
  border-radius: var(--r-sm);
  font-weight: 700; font-size: 12px; letter-spacing: 0.04em;
}
.ui-auth-card h1 {
  font-size: var(--font-xl);
  font-weight: 600;
  letter-spacing: var(--tracking-tight);
  margin: 0;
}
.ui-auth-card .sub {
  color: var(--text-muted);
  font-size: var(--font-sm);
  margin: 0 0 var(--space-2);
}

/* ---------- Settings FAB ---------- */

.ui-fab {
  position: fixed;
  right: var(--space-6);
  bottom: var(--space-6);
  width: 44px;
  height: 44px;
  border-radius: var(--r-pill);
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border-strong);
  box-shadow: var(--shadow-md);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  z-index: var(--z-fab);
  transition: background-color var(--dur-fast) var(--ease-out),
              transform        var(--dur-fast) var(--ease-out);
}
.ui-fab:hover { background: var(--surface-2); transform: translateY(-1px); }
.ui-fab svg.lucide,
.ui-fab [data-lucide] { width: 18px; height: 18px; }

/* Hide FAB when modal is open (modal owns the visual space) */
html[data-modal-open="true"] .ui-fab { display: none; }

/* ---------- Modal / dialog (used by settings page) ---------- */

.ui-modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
  backdrop-filter: blur(3px);
  z-index: var(--z-modal-bg);
  display: grid;
  place-items: center;
  padding: var(--space-4);
  animation: ui-modal-fade-in var(--dur-base) var(--ease-out);
}
@keyframes ui-modal-fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
.ui-modal {
  background: var(--surface);
  border-radius: var(--r-lg);
  border: 1px solid var(--border-strong);
  box-shadow: var(--shadow-xl);
  max-width: 480px;
  width: 100%;
  max-height: 92vh;
  overflow-y: auto;
  z-index: var(--z-modal);
  animation: ui-modal-slide-in var(--dur-base) var(--ease-out);
}
@keyframes ui-modal-slide-in {
  from { opacity: 0; transform: translateY(8px) scale(0.98); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}
.ui-modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-4) var(--space-5);
  border-bottom: 1px solid var(--divider);
}
.ui-modal-title {
  font-size: var(--font-md);
  font-weight: 600;
  color: var(--text);
}
.ui-modal-body { padding: var(--space-5); }
.ui-modal-footer {
  padding: var(--space-3) var(--space-5);
  border-top: 1px solid var(--divider);
  display: flex;
  gap: var(--space-2);
  justify-content: flex-end;
  background: var(--surface-2);
  border-radius: 0 0 var(--r-lg) var(--r-lg);
}

/* ---------- Settings modal — swatch picker, font picker, segmented ----- */

.ui-swatch-row {
  display: flex;
  gap: var(--space-2);
  flex-wrap: wrap;
}
.ui-swatch {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px; height: 36px;
  border-radius: var(--r-pill);
  border: 2px solid var(--border);
  cursor: pointer;
  background: var(--c);
  transition:
    border-color var(--dur-fast) var(--ease-out),
    transform    var(--dur-fast) var(--ease-out);
}
.ui-swatch:hover { transform: scale(1.06); }
.ui-swatch.is-active {
  border-color: var(--text);
  box-shadow: 0 0 0 3px var(--accent-ring);
}
.ui-swatch.is-active::after {
  content: "";
  width: 12px; height: 12px;
  border-radius: var(--r-pill);
  background: rgba(255, 255, 255, 0.85);
}

.ui-segmented {
  display: inline-flex;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r);
  padding: 3px;
  gap: 2px;
}
.ui-segmented > button {
  border: 0;
  background: transparent;
  padding: 6px 12px;
  font-size: var(--font-sm);
  font-weight: 500;
  color: var(--text-secondary);
  border-radius: calc(var(--r) - 3px);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
}
.ui-segmented > button:hover { color: var(--text); }
.ui-segmented > button.is-active {
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow-sm);
}
.ui-segmented svg.lucide,
.ui-segmented [data-lucide] { width: 14px; height: 14px; }

/* ---------- Toast notifications (Django messages → popup) ----------
   Replaces the inline `.ui-banner` rendering of Django messages with a
   fixed-position overlay. Container `.ui-messages` is positioned by the
   shell — top-right on desktop, top-full-width on mobile (where the
   operator's eyes land after tapping submit). Individual toasts use a
   left stripe + tinted icon circle to signal kind without flooding the
   whole card with colour. Auto-dismiss is wired by `_messages.html` via
   `data-auto-dismiss="<ms>"`; errors set 0 to stay until acknowledged.
*/

.ui-messages {
  position: fixed;
  top: var(--space-4);
  right: var(--space-4);
  z-index: var(--z-toast);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  max-width: 380px;
  /* The gaps between toasts shouldn't intercept clicks meant for the
     page beneath. Individual `> *` (the toasts) re-enable pointer
     events so the close button + hover-pause still work. */
  pointer-events: none;
}
.ui-messages > * { pointer-events: auto; }

@media (max-width: 720px) {
  .ui-messages {
    top: calc(var(--space-2) + env(safe-area-inset-top));
    right: var(--space-3);
    left: var(--space-3);
    max-width: none;
  }
}

.ui-toast {
  position: relative;
  background: var(--surface);
  color: var(--text);
  border-radius: var(--r-md);
  padding: var(--space-3) calc(var(--space-3) + 32px) var(--space-3) var(--space-4);
  box-shadow:
    0 12px 32px -8px rgba(0, 0, 0, 0.20),
    0 4px 12px -4px rgba(0, 0, 0, 0.10);
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-3);
  align-items: center;
  font-size: var(--font-sm);
  overflow: hidden;  /* keep progress-bar within rounded corners */
  /* Defensive baseline — guarantees the toast is visible even if the
     entrance animation gets skipped (older browsers without keyframes,
     a parser error elsewhere in the stylesheet, etc.). The animation
     below overrides this for the first 360ms; after it completes the
     fill-mode keeps opacity at 1 — same end state, just safer if the
     animation never runs. */
  opacity: 1;
  /* Soft rise entrance — overshoot-free cubic-bezier so the toast
     reads as "appearing" not "popping in". Stagger via inline
     animation-delay (set by _messages.html script) so multiple
     messages tumble in sequentially instead of stacking on top of
     each other in a single frame. */
  animation: ui-toast-in 360ms cubic-bezier(0.16, 1, 0.3, 1) both;
  animation-delay: var(--ui-toast-delay, 0ms);
}
.ui-toast.is-leaving {
  animation: ui-toast-out 220ms ease-in forwards;
}

@keyframes ui-toast-in {
  from { opacity: 0; transform: translateY(-12px) scale(0.96); }
  to   { opacity: 1; transform: translateY(0)    scale(1);    }
}
@keyframes ui-toast-out {
  from { opacity: 1; transform: translateY(0); }
  to   { opacity: 0; transform: translateY(-8px); }
}

/* Icon circle — tinted bg + glyph in kind colour. 32px sits in the
   visual sweet spot for a toast at this scale: smaller and the icon
   competes with the text x-height, larger and it dwarfs the line. */
.ui-toast-icon {
  display: inline-flex;
  width: 32px;
  height: 32px;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-pill);
  flex-shrink: 0;
  background: var(--surface-2);
  color: var(--text-secondary);
}
.ui-toast-icon [data-lucide],
.ui-toast-icon svg.lucide { width: 18px; height: 18px; }

.ui-toast-body {
  line-height: 1.5;
  color: var(--text);
  font-weight: 500;
}

/* Close — corner placement so it doesn't compete with the message
   itself. The `padding-right` on `.ui-toast` reserves the column. */
.ui-toast-close {
  position: absolute;
  top: 6px;
  right: 6px;
  width: 28px;
  height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 0;
  border-radius: var(--r-sm);
  color: var(--text-muted);
  cursor: pointer;
  transition:
    background-color var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out);
}
.ui-toast-close:hover {
  background: var(--surface-2);
  color: var(--text);
}
.ui-toast-close:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--accent-ring);
}
.ui-toast-close [data-lucide],
.ui-toast-close svg.lucide { width: 14px; height: 14px; }

/* Auto-dismiss countdown — bottom hairline that scales from full to
   empty over the TTL. Acts as both a progress affordance ("this is
   going away soon") and a kind reinforcement (the strip is in the
   toast's accent colour). Persistent toasts (errors) skip this. */
.ui-toast-progress {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: 2px;
  background: currentColor;
  opacity: 0.22;
  transform-origin: left center;
  animation: ui-toast-progress var(--ui-toast-ttl, 5000ms) linear forwards;
  animation-delay: var(--ui-toast-delay, 0ms);
}
.ui-toast.is-paused .ui-toast-progress { animation-play-state: paused; }
.ui-toast[data-auto-dismiss="0"] .ui-toast-progress { display: none; }

@keyframes ui-toast-progress {
  from { transform: scaleX(1); }
  to   { transform: scaleX(0); }
}

/* Kind variants — left stripe + icon tint. Card body stays neutral
   surface so long Thai messages remain comfortable to read; only the
   accents carry colour. */
.ui-toast-success { border-left: 4px solid var(--success); }
.ui-toast-success .ui-toast-icon { background: var(--success-soft); color: var(--success); }
.ui-toast-success .ui-toast-progress { color: var(--success); }

.ui-toast-info { border-left: 4px solid var(--info); }
.ui-toast-info .ui-toast-icon { background: var(--info-soft); color: var(--info); }
.ui-toast-info .ui-toast-progress { color: var(--info); }

.ui-toast-warning { border-left: 4px solid var(--warning); }
.ui-toast-warning .ui-toast-icon { background: var(--warning-soft); color: var(--warning-strong); }
.ui-toast-warning .ui-toast-progress { color: var(--warning); }

.ui-toast-danger { border-left: 4px solid var(--danger); }
.ui-toast-danger .ui-toast-icon { background: var(--danger-soft); color: var(--danger); }
.ui-toast-danger .ui-toast-progress { color: var(--danger); }

/* Mobile bump — body text reads at `--font-md` so the message is
   comfortable in one glance while wearing gloves on a warehouse floor. */
@media (max-width: 540px) {
  .ui-toast-body { font-size: var(--font-md); }
  .ui-toast { padding-block: var(--space-3); }
}

@media (prefers-reduced-motion: reduce) {
  .ui-toast,
  .ui-toast.is-leaving,
  .ui-toast-progress { animation: none; }
}

/* ---------- App menu — bold-tile card grid overlay (universal) ----------
   The single primary-navigation surface across all viewports. Lives in
   `.ui-mobile-nav` (name kept for backwards compat with class names —
   it's no longer mobile-only) and toggled via the topbar's
   `.ui-menu-trigger`. Behind the scenes still uses
   `html[data-mobile-menu="open"]` as the open-state attribute. */

html[data-mobile-menu="open"] body { overflow: hidden; }

.ui-mobile-nav {
  display: flex;
  flex-direction: column;
  position: fixed;
  inset: 0;
  z-index: 1100;
  background: var(--bg);
  transform: translateY(-100%);
  transition: transform var(--dur-base) var(--ease-out);
  visibility: hidden;
}
html[data-mobile-menu="open"] .ui-mobile-nav {
  transform: translateY(0);
  visibility: visible;
}

/* Sticky top bar — background + border-bottom span edge-to-edge so the
   bar reads as a real top stripe. The inner wrapper constrains the cog
   to the body's left content edge (max-width 1280, padding space-6),
   while the close button is pinned to the right edge of the bar itself
   (viewport corner) via absolute positioning. */
.ui-mobile-nav-bar {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  /* iOS notch / Dynamic Island reserve — pad the bar's top so the cog
     and X don't slide under the status-bar inset on phones. */
  padding-top: env(safe-area-inset-top, 0px);
}
.ui-mobile-nav-bar-inner {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-6);
  max-width: 1280px;
  margin: 0 auto;
  width: 100%;
}
.ui-mobile-nav-bar-spacer { flex: 1; }
/* Close button pinned to the bar's right corner (viewport edge). Offset
   from the iOS notch via the same `env(safe-area-inset-top)` used by
   the bar's `padding-top`, then nudged in by `--space-3` to vertically
   center against the cog (which sits inside the inner row's space-3
   vertical padding). */
.ui-mobile-nav-close-end {
  position: absolute;
  top: calc(env(safe-area-inset-top, 0px) + var(--space-3));
  right: var(--space-4);
}
.ui-mobile-nav-brand {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  color: var(--text);
  text-decoration: none;
  flex: 1;
  min-width: 0;
}
.ui-mobile-nav-brand .ui-brand-mark {
  width: 36px; height: 36px;
  border-radius: var(--r-md);
  background: var(--accent);
  color: var(--accent-on);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: var(--font-sm);
}
.ui-mobile-nav-brand .ui-brand-text {
  display: flex;
  flex-direction: column;
  line-height: 1.15;
}
.ui-mobile-nav-brand .ui-brand-name {
  font-weight: 600;
  color: var(--text);
  font-size: var(--font-md);
}
.ui-mobile-nav-brand .ui-brand-sub {
  font-size: var(--font-xs);
  color: var(--text-muted);
  letter-spacing: var(--tracking-overline);
  text-transform: uppercase;
}
.ui-mobile-nav-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}
.ui-mobile-nav-close:hover { border-color: var(--border-strong); }

/* Column-count picker — circular trigger that matches the close button,
   with a small popover beneath it for the column choice. The popover is
   positioned `absolute` against `.ui-menu-cols` (the bar lives in flow
   so `position: relative` on the wrapper is enough — no portal/JS-
   anchored math). */
.ui-menu-cols {
  position: relative;
  display: inline-flex;
}
.ui-menu-cols-popover {
  position: absolute;
  top: calc(100% + var(--space-2));
  left: 0;
  min-width: 200px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg, var(--r-md));
  box-shadow:
    0 12px 28px -14px color-mix(in oklab, var(--text) 30%, transparent),
    0 4px 10px -4px color-mix(in oklab, var(--text) 14%, transparent);
  padding: var(--space-3);
  z-index: 10;
  /* Animated entrance — `visibility` (not display) plus opacity+transform
     so the popover can transition smoothly into view. Pointer-events is
     gated by the closed state so the hidden popover never traps clicks.
     150ms (base tier) keeps the picker snappy — the user is mid-task. */
  visibility: hidden;
  opacity: 0;
  transform: translateY(-6px) scale(0.94);
  transform-origin: top left;
  pointer-events: none;
  transition:
    opacity   var(--dur-base) var(--ease-out),
    transform var(--dur-base) var(--ease-out),
    visibility 0s linear var(--dur-base);
}
.ui-menu-cols[data-open="true"] .ui-menu-cols-popover {
  visibility: visible;
  opacity: 1;
  transform: translateY(0) scale(1);
  pointer-events: auto;
  transition:
    opacity   var(--dur-base) var(--ease-out),
    transform var(--dur-base) var(--ease-out),
    visibility 0s linear 0s;
}

/* Cog rotation — signals the popover's open state without a separate
   chevron. Uses the existing icon as both affordance + state indicator. */
.ui-menu-cols-trigger [data-lucide] {
  transition: transform var(--dur-base) var(--ease-out);
}
.ui-menu-cols[data-open="true"] .ui-menu-cols-trigger [data-lucide] {
  transform: rotate(90deg);
}

/* Popover interior cascade — title fades in first, then each option
   in source order. Tightened to ~270ms total so the user isn't waiting
   on the picker; stagger 14ms reads as a quick ripple, not a wait. */
@keyframes ui-menu-cols-item-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: translateY(0); }
}
.ui-menu-cols[data-open="true"] .ui-menu-cols-title {
  animation: ui-menu-cols-item-in 140ms var(--ease-out) backwards;
  animation-delay: 15ms;
}
.ui-menu-cols[data-open="true"] .ui-menu-cols-opt {
  animation: ui-menu-cols-item-in 140ms var(--ease-out) backwards;
}
.ui-menu-cols[data-open="true"] .ui-menu-cols-opt:nth-child(1) { animation-delay:  50ms; }
.ui-menu-cols[data-open="true"] .ui-menu-cols-opt:nth-child(2) { animation-delay:  64ms; }
.ui-menu-cols[data-open="true"] .ui-menu-cols-opt:nth-child(3) { animation-delay:  78ms; }
.ui-menu-cols[data-open="true"] .ui-menu-cols-opt:nth-child(4) { animation-delay:  92ms; }
.ui-menu-cols[data-open="true"] .ui-menu-cols-opt:nth-child(5) { animation-delay: 106ms; }
.ui-menu-cols[data-open="true"] .ui-menu-cols-opt:nth-child(6) { animation-delay: 120ms; }
.ui-menu-cols[data-open="true"] .ui-menu-cols-opt:nth-child(7) { animation-delay: 134ms; }

/* Grid layout morph — when the user picks a new column count, the
   View Transitions API (triggered in apply() in _mobile_nav.html)
   takes a snapshot of the old layout and crossfades it into the new
   one. These rules customize the default crossfade duration to match
   the picker's pacing. Browsers without View Transitions just see an
   instant re-flow — graceful fallback. */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: var(--dur-base);
  animation-timing-function: var(--ease-out);
}
.ui-menu-cols-title {
  font-size: var(--font-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--tracking-overline);
  color: var(--text-muted);
  margin-bottom: var(--space-2);
}
.ui-menu-cols-options {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-1);
}
.ui-menu-cols-opt {
  appearance: none;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  border-radius: var(--r-md);
  padding: var(--space-2);
  font-size: var(--font-sm);
  font-weight: 600;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.ui-menu-cols-opt:hover { background: var(--surface-2); border-color: var(--border-strong); }
.ui-menu-cols-opt.is-active {
  background: var(--accent);
  color: var(--accent-on);
  border-color: var(--accent);
}
.ui-menu-cols-opt.is-disabled,
.ui-menu-cols-opt:disabled {
  opacity: 0.42;
  cursor: not-allowed;
  background: var(--surface);
  border-color: var(--border);
  color: var(--text-muted);
}
.ui-menu-cols-opt.is-disabled:hover,
.ui-menu-cols-opt:disabled:hover { background: var(--surface); border-color: var(--border); }
.ui-menu-cols-opt[data-cols="auto"] {
  grid-column: span 3;
}

/* Explicit column overrides — win over the default `auto-fill` rule and
   the `≤540px → 2-up` media query because `html[data-menu-cols="N"]`
   has higher specificity (0,2,1 vs 0,1,0 for the bare selector). */
html[data-menu-cols="2"] .ui-menu-card-grid { grid-template-columns: repeat(2, 1fr); }
html[data-menu-cols="3"] .ui-menu-card-grid { grid-template-columns: repeat(3, 1fr); }
html[data-menu-cols="4"] .ui-menu-card-grid { grid-template-columns: repeat(4, 1fr); }
html[data-menu-cols="5"] .ui-menu-card-grid { grid-template-columns: repeat(5, 1fr); }
html[data-menu-cols="6"] .ui-menu-card-grid { grid-template-columns: repeat(6, 1fr); }
html[data-menu-cols="7"] .ui-menu-card-grid { grid-template-columns: repeat(7, 1fr); }
.ui-mobile-nav-close:hover { background: var(--surface-2); }
.ui-mobile-nav-close [data-lucide] { width: 22px; height: 22px; }

.ui-mobile-nav-body {
  flex: 1;
  position: relative;
  overflow: hidden;
  /* Drill-down panels stack inside this positioned container — each
     panel is `position: absolute; inset: 0` and scrolls internally.
     The body itself doesn't scroll; padding/max-width are handled per
     panel so the centering survives the absolute positioning. */
  padding: 0;
  margin: 0;
  width: 100%;
  max-width: none;
}

/* ---------- Drill-down panel stack ---------------------------------
   Each panel is a full-bleed surface inside `.ui-mobile-nav-body`.
   Sliding is `translateX` only — never layout properties — so the
   transition stays on the compositor. State is driven by data-*
   attributes set by the drill JS:

     data-current        → onscreen (translateX 0)
     data-leaving-left   → slid off to the left (we drilled into it)
     data-leaving-right  → slid off to the right (we backed out of it)

   Default (no attr) is "off-screen right", waiting to be drilled into. */
.mc-nav-panel {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  transform: translateX(100%);
  transition: transform var(--dur-slow) var(--ease-out);
  /* The "I haven't been touched yet" baseline state. */
}
.mc-nav-panel[hidden] { display: none; }
.mc-nav-panel[data-current] {
  transform: translateX(0);
  display: flex !important;  /* override the hidden attr while active */
}
.mc-nav-panel[data-leaving-left] {
  transform: translateX(-100%);
  display: flex !important;
}
.mc-nav-panel[data-leaving-right] {
  transform: translateX(100%);
  display: flex !important;
}

.mc-nav-panel-head {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--bg);
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border);
}
.mc-nav-back {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-md);
  color: var(--text);
  cursor: pointer;
  font-weight: 600;
  font-size: var(--font-md);
  transition: background var(--dur-fast) var(--ease-out);
}
.mc-nav-back:hover { background: var(--surface-2); }
.mc-nav-back [data-lucide] { width: 20px; height: 20px; flex-shrink: 0; }
.mc-nav-back-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

.mc-nav-panel-body {
  flex: 1;
  overflow-y: auto;
  padding: var(--space-6);
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
  max-width: 1280px;
  margin: 0 auto;
  width: 100%;
  box-sizing: border-box;
  /* Hide scrollbar but keep wheel/touch scroll. */
  scrollbar-width: none;
}
.mc-nav-panel-body::-webkit-scrollbar { width: 0; height: 0; }

/* Drill arrow on the right edge of a parent (group) card. Subtle by
   default; tone-coloured on hover to echo the rest of the card's
   hover state. */
.ui-menu-card.mc-has-children {
  /* Reserve a hair more padding on the right so the arrow doesn't
     crowd the label on narrow tiles. */
  padding-right: calc(var(--space-3) + 8px);
}
.mc-drill-arrow {
  position: absolute;
  top: 50%;
  right: 8px;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  color: var(--text-muted);
  opacity: 0.7;
  transition:
    transform var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out),
    opacity var(--dur-fast) var(--ease-out);
}
.mc-drill-arrow [data-lucide] { width: 16px; height: 16px; }
.ui-menu-card.mc-has-children:hover .mc-drill-arrow,
.ui-menu-card.mc-has-children:focus-visible .mc-drill-arrow {
  color: var(--menu-card-tone, var(--accent));
  opacity: 1;
  transform: translateY(-50%) translateX(2px);
}

@media (prefers-reduced-motion: reduce) {
  .mc-nav-panel { transition: none; }
  .mc-drill-arrow { transition: none; }
}
.ui-mobile-nav-body::-webkit-scrollbar { width: 0; height: 0; }

.ui-mobile-nav-section { display: flex; flex-direction: column; gap: var(--space-3); }

/* ----- Overlay entrance choreography ---------------------------------
   Two-level cascade: sections fade+rise in sequence, and each section's
   cards stagger a few ms behind their section. Total reveal is bounded
   at ~380ms (well under impeccable's 500ms cap) so the overlay never
   feels slow. Fires only when `data-mobile-menu="open"` toggles — not
   on initial render of the menu landing page (product register's
   "no page-load choreography" rule).

   The reduced-motion fallback is free: project tokens already flatten
   `--dur-*` to 0.01ms under `prefers-reduced-motion: reduce`, so the
   keyframes complete instantly with no choreography. */
@keyframes ui-menu-section-in {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Group cascade — each section (label + all its cards) arrives as one
   unit. Cards don't get their own animation; they inherit the parent
   section's opacity/transform so the whole group appears together.
   Stagger between sections is wide enough (90ms) to feel sequential
   without dragging — total reveal ≈ 720ms for 8 sections. */
html[data-mobile-menu="open"] .ui-mobile-nav-section {
  animation: ui-menu-section-in 360ms var(--ease-out) backwards;
}
html[data-mobile-menu="open"] .ui-mobile-nav-section:nth-of-type(1) { animation-delay:  40ms; }
html[data-mobile-menu="open"] .ui-mobile-nav-section:nth-of-type(2) { animation-delay: 130ms; }
html[data-mobile-menu="open"] .ui-mobile-nav-section:nth-of-type(3) { animation-delay: 220ms; }
html[data-mobile-menu="open"] .ui-mobile-nav-section:nth-of-type(4) { animation-delay: 310ms; }
html[data-mobile-menu="open"] .ui-mobile-nav-section:nth-of-type(5) { animation-delay: 400ms; }
html[data-mobile-menu="open"] .ui-mobile-nav-section:nth-of-type(6) { animation-delay: 490ms; }
html[data-mobile-menu="open"] .ui-mobile-nav-section:nth-of-type(7) { animation-delay: 580ms; }
html[data-mobile-menu="open"] .ui-mobile-nav-section:nth-of-type(8) { animation-delay: 670ms; }

.ui-mobile-nav-section-label {
  font-size: var(--font-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--tracking-overline);
  color: var(--text-muted);
  padding: 0 var(--space-1);
  text-align: left;
}
@media (max-width: 720px) {
  /* Centered on mobile so the grouped cards read as a clear block;
     left-aligned on desktop where the label anchors a wider row. */
  .ui-mobile-nav-section-label { text-align: center; }
}

.ui-mobile-nav-footer {
  padding: var(--space-3) var(--space-4) calc(var(--space-3) + env(safe-area-inset-bottom, 0px));
  font-size: var(--font-xs);
  color: var(--text-muted);
  text-align: center;
  border-top: 1px solid var(--border);
  background: var(--surface);
}

/* Unread-count badge on notification card icon plate. */
.ui-menu-card-icon { position: relative; }
.ui-menu-card-badge {
  position: absolute;
  top: -6px;
  right: -6px;
  min-width: 20px;
  height: 20px;
  padding: 0 6px;
  border-radius: 999px;
  background: var(--accent);
  color: var(--accent-on);
  font-size: var(--font-xs);
  font-weight: 700;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 0 0 2px var(--surface);
}

@media (max-width: 540px) {
  .ui-mobile-nav-body { padding: var(--space-4); gap: var(--space-5); }
}

@media (prefers-reduced-motion: reduce) {
  .ui-mobile-nav { transition: none; }
}

@media (max-width: 540px) {
  .ui-content { padding: var(--space-4); }
  .ui-topbar { padding: var(--space-3) var(--space-4); }
  .ui-page-header { flex-direction: column; align-items: stretch; }
  /* Stretch action buttons to fill the row — empty space next to the
     CSV / scan / history buttons was visually dead weight. With each
     `.ui-btn` at 100% width, single-button headers fill the row and
     multi-button headers stack as full-width rows (clean mobile
     pattern). justify-content stays `stretch` rather than `flex-end`
     so the children honour width: 100%. */
  .ui-page-header-actions {
    width: 100%;
    justify-content: stretch;
    flex-direction: column;
  }
  .ui-page-header-actions > .ui-btn {
    width: 100%;
    justify-content: center;
  }
}

/* ---------- Mobile cards transform (tables → cards at ≤720px) ----------

   The desktop table flips into a stack of real cards. Each `<tr>` becomes
   a surfaced record with its own border + soft shadow so users on phone /
   tablet can chunk records visually instead of scanning a vertical list
   of bordered rows.

   Inside each card, every `<td>` is a 2-column CSS grid: a fixed-width
   label key (read from `data-label`) on the left and the value on the
   right. Multiple value blocks (e.g. customer name + arcode under one
   `<td data-label="ลูกค้า">`) auto-stack onto extra rows in column 2 —
   no template change required to support multi-line cells.

   Three semantic column classes own special treatment:
     - `.ui-cell-primary`  → card heading (larger, flush top, anchors the card)
     - `.ui-cell-actions`  → full-width CTA at the card footer
     - `.ui-cell-optional` → hidden on mobile
*/

@media (max-width: 720px) {
  .ui-table thead { display: none; }

  /* When `.ui-card-flat` wraps a table, neutralise its surface + border
     on mobile so the inner `<tr>` cards are the only "card" element on
     screen. Otherwise the wrapper's hairline border reads as a
     continuous horizontal line between gaps, defeating the chunked-
     records look the per-row cards aim for. */
  .ui-card-flat:has(.ui-table) {
    background: transparent;
    border: 0;
    padding: 0;
    box-shadow: none;
  }

  /* tbody becomes a vertical stack of cards. Gap on the tbody (not
     border-bottom on rows) is what chunks the records — gives each
     card real breathing room instead of the "long list" feel. */
  .ui-table-wrap { overflow: visible; }
  .ui-table tbody { display: flex; flex-direction: column; gap: var(--space-3); }

  /* Each row → card surface. Same `--surface` (white in light, dark
     elevation in dark) as the filter panel above so the cards read as
     siblings of that panel, not as a different material. Hairline
     border + medium shadow give the card real definition against the
     page bg. */
  .ui-table tbody tr {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-4);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    box-shadow: var(--shadow-md);
  }
  .ui-table tbody tr:hover { background: var(--surface); }

  /* Label-key / value-stack grid. `align-items: start` keeps the key
     aligned with the top of multi-line value blocks. `text-align: right`
     on the cell catches bare text nodes (e.g. `฿178.00` as direct td
     content) so they right-align too — the `> *` rule alone misses
     text nodes. The pseudo-label opts back to left below. */
  .ui-table tbody td {
    display: grid;
    grid-template-columns: 96px 1fr;
    align-items: start;
    column-gap: var(--space-3);
    row-gap: 2px;
    /* Tightened vertical gap between key/value rows in mobile cards —
       4px top+bottom (was 8px) keeps the card readable while shrinking
       the dead vertical space between glance-reads. `.ui-cell-primary`
       and `.ui-cell-actions` opt out below (they have their own
       padding + divider treatment to anchor the heading and footer
       button). */
    padding: var(--space-1) 0;
    border: 0;
    text-align: right;
    font-size: var(--font-md);
  }
  .ui-table tbody td::before {
    content: attr(data-label);
    font-size: var(--font-sm);
    color: var(--text-muted);
    letter-spacing: var(--tracking-normal);
    font-weight: 500;
    line-height: 1.6;
    grid-column: 1;
    text-align: left;
  }
  /* The customer arcode (rendered as `<div class="ui-cell-sub">` below
     the name) is a system-side reference — operators read by name on
     mobile. Hide the secondary line to reclaim a row. Desktop still
     shows arcode where horizontal space is cheap. */
  .ui-table tbody td[data-label="ลูกค้า"] .ui-cell-sub {
    display: none;
  }
  /* All real children land in column 2 and right-align. Auto-placement
     stacks additional children (e.g. arcode below arname) onto extra
     rows of column 2 — without needing a wrapper div in the template. */
  .ui-table tbody td > * {
    grid-column: 2;
    text-align: right;
    min-width: 0;
  }
  /* `.ui-cell-sub` defaults to `display: flex` on desktop (used inline
     below a primary name in tables). On mobile cards the flex shell
     defeats `text-align: right`, leaving secondary lines like arcode
     glued to the label column. Reset to block at any depth inside td
     so it inherits the col 2 right alignment and sits directly under
     the primary name. */
  .ui-table tbody td .ui-cell-sub {
    display: block;
    text-align: right;
  }

  /* Primary identifier — the card's leading heading. Larger + bolder,
     flush at the card's top edge; subtle divider before meta rows.
     Switches back to block layout so the heading reads left-aligned. */
  .ui-table tbody td.ui-cell-primary {
    display: block;
    order: -1;
    padding: 0 0 var(--space-2);
    margin-bottom: var(--space-1);
    border-bottom: 1px solid var(--divider);
  }
  .ui-table tbody td.ui-cell-primary::before { display: none; }
  .ui-table tbody td.ui-cell-primary > * {
    text-align: left;
  }
  .ui-table tbody td.ui-cell-primary .ui-cell-id {
    font-size: var(--font-lg);
    font-weight: 700;
    color: var(--text);
    letter-spacing: -0.01em;
  }

  /* Action cell at card footer. Block layout so the button can fill
     full width; divider above signals "this is the action you take on
     this card". Button height bumped from `ui-btn-sm` (30px) to default
     (38px) for a comfortable tap target on touch surfaces. */
  .ui-table tbody td.ui-cell-actions {
    display: block;
    padding-top: var(--space-3);
    margin-top: var(--space-2);
    border-top: 1px solid var(--divider);
  }
  .ui-table tbody td.ui-cell-actions::before { display: none; }
  /* Anchors, buttons, AND span.ui-btn (disabled-look placeholder for
     blocked actions, e.g. "รอตรวจสอบ" on the DL queue) all get the same
     full-width treatment so a row's action area reads the same whether
     the action is fireable or waiting. */
  .ui-table tbody td.ui-cell-actions > a,
  .ui-table tbody td.ui-cell-actions > button,
  .ui-table tbody td.ui-cell-actions > span.ui-btn {
    display: flex;
    width: 100%;
    height: var(--h-input);
    font-size: var(--font-base);
    text-align: center;
  }
  /* Non-action span (e.g. "ดูอย่างเดียว" for view-only users) — left
     aligned as a quiet note instead of being styled like a button.
     Excludes .ui-btn spans which are explicitly button-styled and
     handled by the rule above. */
  .ui-table tbody td.ui-cell-actions > span:not(.ui-btn) {
    display: block;
    text-align: left;
    color: var(--text-muted);
  }

  /* Optional columns hidden by markup. */
  .ui-table tbody td.ui-cell-optional { display: none; }

  /* Empty-state row keeps its own centered layout — opt out of the
     2-col grid so the "no results" message stays centered inside the
     card surface, not crammed into the value column. */
  .ui-table tbody td.ui-empty-row {
    display: block;
  }
  .ui-table tbody td.ui-empty-row::before { display: none; }
  .ui-table tbody td.ui-empty-row > * {
    text-align: center;
  }

  /* Queue "สถานะ" cell — stretch the state badge across the value
     column so the chip reads as a row-spanning status panel instead of
     a small pill floating mid-row. Scoped to `data-label="สถานะ"` so
     the history page's `data-label="สถานะ Smile"` (different label) is
     untouched. The stack wrapper switches from `inline-flex` to flex
     so the child badges (primary state + optional POS warning) get the
     full width.  */
  .ui-table tbody td[data-label="สถานะ"] .ui-badge-stack {
    display: flex;
    width: 100%;
    align-items: stretch;
  }
  .ui-table tbody td[data-label="สถานะ"] .ui-badge {
    width: 100%;
    justify-content: center;
  }

  /* State-tinted card surface (goods_release). When the row carries a
     `data-release-state` (history) or `data-started` / `data-pos-warning`
     (queue) attribute, paint the card with a very faint wash (~4-6%
     of the matching semantic colour) so a long list becomes glance-
     scannable. Color is reinforcement, not the only signal — every
     row still carries its `.ui-badge` icon+label. */
  .ui-table tbody tr[data-release-state="delivered"] {
    background: color-mix(in oklab, var(--success) 0%, var(--surface));
  }
  .ui-table tbody tr[data-release-state="awaiting_other_scope"] {
    background: color-mix(in oklab, var(--info) 4%, var(--surface));
  }
  .ui-table tbody tr[data-release-state="awaiting_pos_confirm"] {
    background: color-mix(in oklab, var(--warning) 5%, var(--surface));
  }
  .ui-table tbody tr[data-release-state="pos_writeback_failed"] {
    background: color-mix(in oklab, var(--danger) 6%, var(--surface));
  }
  .ui-table tbody tr[data-started="true"] {
    background: color-mix(in oklab, var(--warning) 4%, var(--surface));
  }
  /* When a queue row ALSO carries the POS-incomplete flag, the danger
     wash wins — it's the more attention-worthy signal. */
  .ui-table tbody tr[data-pos-warning="true"] {
    background: color-mix(in oklab, var(--danger) 5%, var(--surface));
  }

  /* "รายละเอียด" button takes the `.ui-badge-warning` palette —
     `--warning-soft` fill + `--warning-strong` text, transparent
     border — so it reads as a clear warm-amber chip on every card
     regardless of POS state. Both tokens are theme-aware in
     `tokens.css` (light: amber-soft + deep amber; dark: dark amber +
     bright amber) so the button stays legible in either mode without
     a per-theme override here. We override `.ui-btn-primary` via the
     attribute selector chain so no template change is needed. */
  .ui-table tbody tr[data-release-state] .ui-cell-actions > a {
    background: var(--warning-wash);
    color: var(--warning-strong);
    border-color: transparent;
  }
  .ui-table tbody tr[data-release-state] .ui-cell-actions > a:hover {
    filter: brightness(1.04);
  }

  /* `.release-item-row` mobile-card overrides removed — the
     /release/detail/ items list no longer uses a <table> + the
     generic mobile-card transform. It's rendered as uniform
     `.release-item-card` blocks across all viewports; see
     `apps/goods_release/static/goods_release/css/release.css`. */
}
