/* ===========================================================================
   IDHOME core components — v2

   Restrained Linear × Stripe vocabulary. All classes prefixed `ui-*`.
   Every interactive component supports the 7 states named in product.md:
     default · hover · focus · active · disabled · loading · error

   File order: form controls → buttons → cards → badges → tables → states
   → micro-utilities. Keep tokens out of this file; only consume CSS vars
   from tokens.css.
   =========================================================================== */

/* ---------- Buttons ---------- */

.ui-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  height: var(--h-input);
  padding: 0 var(--space-4);
  border: 1px solid var(--border-strong);
  border-radius: var(--r);
  background: var(--surface);
  color: var(--text);
  font-weight: 500;
  font-size: var(--font-base);
  letter-spacing: var(--tracking-normal);
  cursor: pointer;
  white-space: nowrap;
  text-decoration: none;
  user-select: none;
  transition:
    background-color var(--dur-fast) var(--ease-out),
    border-color    var(--dur-fast) var(--ease-out),
    color           var(--dur-fast) var(--ease-out),
    box-shadow      var(--dur-fast) var(--ease-out),
    transform       var(--dur-fast) var(--ease-out);
}
.ui-btn:hover { background: var(--surface-2); border-color: var(--text-muted); }
.ui-btn:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px var(--accent-ring);
  border-color: var(--accent);
}
.ui-btn:active:not(:disabled) { transform: scale(0.98); }
.ui-btn:disabled,
.ui-btn[aria-disabled="true"] {
  opacity: 0.55;
  cursor: not-allowed;
}
.ui-btn[aria-busy="true"] { cursor: progress; pointer-events: none; }

/* Variants */
.ui-btn-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-on);
  /* Thai script on saturated brand-red needs heavier stroke than Latin —
     w500 lets vowel marks (above/below) blend into the ground. w600
     restores legibility without changing the brand color. */
  font-weight: 600;
}
.ui-btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
.ui-btn-primary:focus-visible { box-shadow: 0 0 0 3px var(--accent-ring); }
/* Small primary buttons override the size baseline's `font-sm` and keep
   `font-base`. 12.5px Thai on bright red reads as washy even at w600;
   13.5px with the heavier weight keeps the same 30px height with much
   better contrast perception. Outline / ghost / danger small buttons
   stay on font-sm — only primary needs the bump. */
.ui-btn-primary.ui-btn-sm { font-size: var(--font-base); }

.ui-btn-ghost {
  background: transparent;
  border-color: transparent;
}
.ui-btn-ghost:hover { background: var(--surface-2); border-color: transparent; }

/* Success / "go" action — emerald fill. Use only for actions whose
   semantic is literally "start / proceed" (warehouse: "เริ่มตรวจ",
   attendance: "Clock in"). Brand-red `.ui-btn-primary` remains the
   default identity action elsewhere. Emerald reads cleaner than red
   in warehouse / showroom lighting on small Thai labels. */
.ui-btn-success {
  background: var(--success);
  border-color: var(--success);
  color: #ffffff;
  font-weight: 600;
}
.ui-btn-success:hover {
  background: color-mix(in oklab, var(--success) 85%, black);
  border-color: color-mix(in oklab, var(--success) 85%, black);
}
.ui-btn-success:focus-visible {
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--success) 30%, transparent);
}
/* Same small-size legibility treatment as primary — keep 30px height
   but lift font to font-base so Thai vowel marks don't collapse. */
.ui-btn-success.ui-btn-sm { font-size: var(--font-base); }

/* Warning / "continue in-progress" action — vivid orange fill. Used
   when the user has already started something and is being prompted to
   keep going (e.g. goods_release "ทำต่อ" on a record that's mid-
   inspection). Distinct from `.ui-btn-primary` (start new) and
   `.ui-btn-success` (go forward) by signaling "this work is open,
   finish it". Brand-picked #e96725; not derived from the warning token
   ramp so it stays a local button colour (the soft warning badges keep
   their amber token). White text needs weight 600 to clear contrast
   floor at this lightness — keep that paired with any future tweaks. */
.ui-btn-warning {
  background: #e96725;
  border-color: #e96725;
  color: #ffffff;
  font-weight: 600;
}
.ui-btn-warning:hover {
  background: color-mix(in oklab, #e96725 85%, black);
  border-color: color-mix(in oklab, #e96725 85%, black);
}
.ui-btn-warning:focus-visible {
  box-shadow: 0 0 0 3px color-mix(in oklab, #e96725 30%, transparent);
}
.ui-btn-warning.ui-btn-sm { font-size: var(--font-base); }

.ui-btn-danger {
  background: var(--danger);
  border-color: var(--danger);
  color: var(--text-inverse);
}
.ui-btn-danger:hover {
  background: color-mix(in oklab, var(--danger) 80%, black);
  border-color: color-mix(in oklab, var(--danger) 80%, black);
}

.ui-btn-outline-danger {
  background: var(--surface);
  border-color: var(--danger);
  color: var(--danger);
}
.ui-btn-outline-danger:hover {
  background: var(--danger-soft);
  border-color: var(--danger);
  color: var(--danger);
}

/* Sizes */
.ui-btn-sm { height: var(--h-input-sm); padding: 0 var(--space-3); font-size: var(--font-sm); }
.ui-btn-lg { height: var(--h-input-lg); padding: 0 var(--space-5); font-size: var(--font-md); }
.ui-btn-block { width: 100%; }

/* Icon-only button — square + visually centered icon */
.ui-btn-icon {
  width: var(--h-input);
  padding: 0;
}
.ui-btn-icon.ui-btn-sm { width: var(--h-input-sm); }
.ui-btn-icon.ui-btn-lg { width: var(--h-input-lg); }

/* ---------- Form controls ---------- */

.ui-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}

.ui-field-label {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--font-sm);
  font-weight: 500;
  color: var(--text);
  line-height: 1.2;
}
.ui-field-label .req { color: var(--danger); }
.ui-field-hint { font-size: var(--font-xs); color: var(--text-muted); line-height: 1.4; }
.ui-field-err {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--font-xs);
  color: var(--danger);
  line-height: 1.4;
}

.ui-input,
.ui-select,
.ui-textarea {
  display: block;
  width: 100%;
  height: var(--h-input);
  padding: 0 var(--space-3);
  border: 1px solid var(--border-strong);
  border-radius: var(--r);
  background: var(--surface);
  color: var(--text);
  font-size: var(--font-base);
  line-height: 1;
  transition:
    border-color var(--dur-fast) var(--ease-out),
    box-shadow   var(--dur-fast) var(--ease-out);
}
.ui-textarea {
  height: auto;
  min-height: 80px;
  padding: var(--space-3);
  line-height: var(--leading-base);
  resize: vertical;
}

.ui-input:hover,
.ui-select:hover,
.ui-textarea:hover { border-color: var(--text-muted); }

.ui-input:focus,
.ui-select:focus,
.ui-textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-ring);
}

.ui-input:disabled,
.ui-select:disabled,
.ui-textarea:disabled {
  opacity: 0.55;
  cursor: not-allowed;
  background: var(--surface-2);
}

.ui-input.has-error,
.ui-select.has-error,
.ui-textarea.has-error {
  border-color: var(--danger);
}
.ui-input.has-error:focus,
.ui-select.has-error:focus,
.ui-textarea.has-error:focus {
  box-shadow: 0 0 0 3px oklch(55% 0.21 25 / 0.25);
}

/* Native select arrow — use Lucide-flavored chevron via inline SVG */
.ui-select {
  appearance: none;
  padding-right: 36px;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>");
  background-repeat: no-repeat;
  background-position: right 10px center;
}

.ui-input-with-icon {
  position: relative;
}
.ui-input-with-icon > [data-lucide],
.ui-input-with-icon > svg.lucide {
  position: absolute;
  left: 12px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--text-muted);
  pointer-events: none;
}
.ui-input-with-icon > .ui-input { padding-left: 36px; }

/* Trailing reveal/clear button inside a ui-input-with-icon wrapper.
   Position absolute on the right so the input doesn't need extra padding
   unless reveal is actually present. */
.ui-input-reveal {
  position: absolute;
  right: 4px;
  top: 50%;
  transform: translateY(-50%);
  width: 30px;
  height: 30px;
  border: 0;
  background: transparent;
  color: var(--text-muted);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  border-radius: var(--r-sm);
  transition: color var(--dur-fast) var(--ease-out),
              background-color var(--dur-fast) var(--ease-out);
}
.ui-input-reveal:hover { color: var(--text); background: var(--surface-2); }
.ui-input-reveal[aria-pressed="true"] { color: var(--accent); }
.ui-input-reveal svg.lucide,
.ui-input-reveal [data-lucide] { width: 16px; height: 16px; }
/* When a reveal button is present, give the input a right padding so the
   typed value doesn't slide under the icon. `:has()` is widely supported
   (96%+); for older browsers, the fallback class below opts in explicitly. */
.ui-input-with-icon:has(.ui-input-reveal) > .ui-input,
.ui-input-with-icon.has-reveal > .ui-input { padding-right: 40px; }

/* Checkbox + radio — leverage native, restyle minimal */
.ui-check {
  display: inline-flex;
  align-items: flex-start;
  gap: var(--space-2);
  cursor: pointer;
  font-size: var(--font-base);
  line-height: 1.4;
  user-select: none;
}
.ui-check input[type="checkbox"],
.ui-check input[type="radio"] {
  width: 16px;
  height: 16px;
  margin: 2px 0 0;
  accent-color: var(--accent);
  flex-shrink: 0;
  cursor: pointer;
}

/* Form-row layout helper — used by Django ModelForm rendering. */
.ui-form-row {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-3);
}
@media (min-width: 768px) {
  .ui-form-row-2 { grid-template-columns: 1fr 1fr; }
  .ui-form-row-3 { grid-template-columns: 1fr 1fr 1fr; }
}

/* ---------- Card ---------- */

.ui-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-xl);
  padding: var(--pad);
}
.ui-card-lg  { padding: var(--pad-lg); }
.ui-card-flat {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: 0;
}

/* Collapsible card — wraps a `<details>` element so the section can
   show/hide via native browser semantics (tap summary toggles, ESC
   closes when focused). The card's own `padding` already provides the
   surface; the summary just reuses that padding so the heading sits
   where it always does. Chevron at the right rotates on open. */
details.ui-card-collapsible > summary {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  list-style: none;
  cursor: pointer;
  user-select: none;
}
details.ui-card-collapsible > summary::-webkit-details-marker { display: none; }
details.ui-card-collapsible > summary::marker { content: ''; }
.ui-card-collapsible-chevron {
  /* 40px outer box for touch-target, 20px inner glyph. The whole
     `<summary>` is the toggle, so the user can tap anywhere on it,
     but the chevron's hit area now reads as a real button (no more
     "pixel-hunting" the 20px icon). Padding keeps the glyph visually
     unchanged; box-sizing: content-box means width/height refers to
     the inner SVG, padding adds on the outside up to the 40px floor. */
  width: 20px;
  height: 20px;
  padding: 10px;
  box-sizing: content-box;
  color: var(--text-muted);
  transition: transform var(--dur-fast) var(--ease-out);
}
details.ui-card-collapsible[open] .ui-card-collapsible-chevron {
  transform: rotate(180deg);
}
@media (prefers-reduced-motion: reduce) {
  .ui-card-collapsible-chevron { transition: none; }
}
.ui-card-header {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--pad-sm) var(--pad);
  border-bottom: 1px solid var(--divider);
}
.ui-card-body { padding: var(--pad); }
.ui-card-footer {
  padding: var(--pad-sm) var(--pad);
  border-top: 1px solid var(--divider);
  background: var(--surface-2);
  display: flex;
  gap: var(--space-2);
  justify-content: flex-end;
  border-radius: 0 0 var(--r-md) var(--r-md);
}

/* ---------- Menu page (standalone card-grid landing) ----------
   Wraps `_menu_grid.html` when it's rendered as a real page
   (`pages/dashboard/menu.html`). The overlay version has its own
   `.ui-mobile-nav-body` wrapper; this one matches the page's content
   width and spacing instead. */
.ui-menu-page {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
}
@media (max-width: 540px) {
  .ui-menu-page { gap: var(--space-5); }
}

/* ---------- Menu-card grid (bold tile) ----------
   Touch-first square-ish tile: big icon centered, label centered
   below. Used by the org-structure landing page and (Phase B) the
   mobile primary nav. The icon sits in a tinted square plate that
   takes the tone color (primary/accent/info/warning…). On hover
   the tile lifts and the plate brightens; the whole card is one
   tap target.

   Grid: auto-fill with a 200px floor on desktop → 2-up forced on
   phones (≤540px) so each tile stays a comfortable touch size
   without the right column collapsing. */
.ui-menu-card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: var(--space-2);
}
.ui-menu-card {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--space-4) var(--space-3);
  aspect-ratio: 1 / 0.82;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-xl);
  color: var(--text);
  text-align: center;
  text-decoration: none;
  overflow: hidden;
  transition:
    transform var(--dur-slow) var(--ease-out),
    border-color var(--dur-slow) var(--ease-out),
    box-shadow var(--dur-slow) var(--ease-out);
}
/* Subtle tone wash on the surface — picks up the same color family
   as the icon plate but at very low chroma so the whole card reads
   as "of a kind" with its category without shouting. */
.ui-menu-card::before {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, transparent 0%, var(--menu-card-wash, transparent) 100%);
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-out);
  pointer-events: none;
}
.ui-menu-card:hover::before,
.ui-menu-card:focus-visible::before { opacity: 1; }

.ui-menu-card:hover,
.ui-menu-card:focus-visible {
  transform: translateY(-4px);
  border-color: var(--menu-card-tone, var(--accent));
  /* Three-layer shadow:
     1) Tone-coloured outer glow — signals which category the card belongs to
     2) Ambient depth shadow — grounds the lift against the page
     3) Inset top highlight — a 1px tone-tinted line at the top edge,
        the touch that separates "lifted" from "just bigger". Subtle
        enough that you don't see the highlight itself, only the
        depth it implies. */
  box-shadow:
    0 14px 30px -14px color-mix(in oklab, var(--menu-card-tone, var(--accent)) 55%, transparent),
    0 4px 12px -4px color-mix(in oklab, var(--text) 12%, transparent),
    inset 0 1px 0 0 color-mix(in oklab, var(--menu-card-tone, var(--accent)) 20%, transparent);
  text-decoration: none;
  color: var(--text);
  outline: none;
}
.ui-menu-card:active {
  transform: translateY(-1px);
  /* Snap back faster than the lift took — gives the press a tactile feel
     instead of dragging through the 250ms hover curve in reverse. */
  transition-duration: var(--dur-fast);
}

.ui-menu-card-icon {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 52px;
  height: 52px;
  border-radius: var(--r-lg, var(--r-md));
  background: var(--menu-card-plate, var(--accent-soft));
  color: var(--menu-card-tone, var(--accent));
  /* Hard-lock the plate to a square. Without flex-shrink: 0 a tight
     card height (e.g. multi-col mobile) would compress only the
     height because width is explicit, leaving a wide-rectangle plate.
     aspect-ratio is belt-and-suspenders in case future flex contexts
     try to stretch it again. */
  flex-shrink: 0;
  aspect-ratio: 1 / 1;
  transition:
    transform var(--dur-slow) var(--ease-out),
    background var(--dur-slow) var(--ease-out),
    box-shadow var(--dur-slow) var(--ease-out);
}
.ui-menu-card:hover .ui-menu-card-icon,
.ui-menu-card:focus-visible .ui-menu-card-icon {
  transform: scale(1.08) translateY(-1px);
  /* Plate background blends in more of the card's tone so the icon
     reads as "filled in" without losing its shape. Color-mix keeps the
     soft base + adds 16% tone for a subtle saturation bump. */
  background: color-mix(in oklab, var(--menu-card-tone, var(--accent)) 16%, var(--menu-card-plate, var(--accent-soft)));
  /* Soft tone halo — 4px ring around the plate at 10% opacity. Reads
     as glow, not border. Pairs with the card's tone-coloured outer
     shadow so the whole card feels like one cohesive lit object. */
  box-shadow: 0 0 0 4px color-mix(in oklab, var(--menu-card-tone, var(--accent)) 10%, transparent);
}
.ui-menu-card:active .ui-menu-card-icon {
  transform: scale(1.02) translateY(0);
  transition-duration: var(--dur-fast);
}
.ui-menu-card-icon [data-lucide] { width: 26px; height: 26px; }

/* Per-tone palette — set `--menu-card-tone` + `--menu-card-plate` +
   `--menu-card-wash` so the hover lift, border glow, icon plate and
   surface wash all line up. */
.ui-menu-card.tone-primary,
.ui-menu-card.tone-accent {
  --menu-card-tone:  var(--accent);
  --menu-card-plate: var(--accent-soft);
  --menu-card-wash:  color-mix(in oklab, var(--accent) 5%, transparent);
}
.ui-menu-card.tone-info {
  --menu-card-tone:  var(--info);
  --menu-card-plate: var(--info-soft);
  --menu-card-wash:  color-mix(in oklab, var(--info) 5%, transparent);
}
.ui-menu-card.tone-success {
  --menu-card-tone:  var(--success);
  --menu-card-plate: var(--success-soft);
  --menu-card-wash:  color-mix(in oklab, var(--success) 5%, transparent);
}
.ui-menu-card.tone-warning {
  --menu-card-tone:  var(--warning-strong, var(--warning));
  --menu-card-plate: var(--warning-soft);
  --menu-card-wash:  color-mix(in oklab, var(--warning) 5%, transparent);
}
.ui-menu-card.tone-danger {
  --menu-card-tone:  var(--danger);
  --menu-card-plate: var(--danger-soft);
  --menu-card-wash:  color-mix(in oklab, var(--danger) 5%, transparent);
}

.ui-menu-card-label {
  position: relative;
  font-size: var(--font-sm);
  font-weight: 600;
  color: var(--text);
  letter-spacing: var(--tracking-tight);
  line-height: 1.25;
  transition: color var(--dur-slow) var(--ease-out);
}
/* Label adopts the card's tone on hover — closes the loop with the
   plate fill + border + glow so the whole card reads as a single
   category-coloured surface, not four independently-tinted parts. */
.ui-menu-card:hover .ui-menu-card-label,
.ui-menu-card:focus-visible .ui-menu-card-label {
  color: var(--menu-card-tone, var(--accent));
}

/* Bold tile hides hint by default; keep the class around so the
   same markup also works for the asymmetric variant later. */
.ui-menu-card-hint { display: none; }

@media (max-width: 540px) {
  /* Default mobile (auto = 2-up). Each tile is a comfortable thumb
     target without the right column collapsing into wasted space. */
  .ui-menu-card-grid { grid-template-columns: repeat(2, 1fr); gap: var(--space-2); }
  .ui-menu-card      {
    aspect-ratio: 1 / 0.78;
    padding: var(--space-3) var(--space-2);
    gap: var(--space-2);
  }
  .ui-menu-card-icon { width: 44px; height: 44px; }
  .ui-menu-card-icon [data-lucide] { width: 22px; height: 22px; }
  .ui-menu-card-label { font-size: var(--font-xs); }

  /* When the user picks an explicit column count higher than 2 on a
     phone-sized viewport, scale the tile internals down so the icon
     plate stays square and the label still fits. Each tier sets:
       - card aspect-ratio: taller to accommodate the vertical stack
       - card padding + gap: tighter to claw back content area
       - icon plate + glyph: scaled down to match the narrower card
       - label font: smaller at the tightest tiers */
  html[data-menu-cols="3"] .ui-menu-card      { aspect-ratio: 1 / 0.95; padding: var(--space-2); gap: var(--space-1); }
  html[data-menu-cols="3"] .ui-menu-card-icon { width: 36px; height: 36px; }
  html[data-menu-cols="3"] .ui-menu-card-icon [data-lucide] { width: 18px; height: 18px; }

  html[data-menu-cols="4"] .ui-menu-card      { aspect-ratio: 1 / 1.1;  padding: var(--space-1) var(--space-1); gap: var(--space-1); }
  html[data-menu-cols="4"] .ui-menu-card-icon { width: 30px; height: 30px; }
  html[data-menu-cols="4"] .ui-menu-card-icon [data-lucide] { width: 15px; height: 15px; }
  html[data-menu-cols="4"] .ui-menu-card-label { font-size: 10px; }

  /* 5/6/7 on a sub-540px viewport are normally disabled by the
     viewport-fit clamp in the picker JS, but if the saved value ever
     leaks through (e.g. saved on PC, then resized) these rules keep
     it readable instead of broken. */
  html[data-menu-cols="5"] .ui-menu-card,
  html[data-menu-cols="6"] .ui-menu-card,
  html[data-menu-cols="7"] .ui-menu-card      { aspect-ratio: 1 / 1.2; padding: 2px; gap: 2px; }
  html[data-menu-cols="5"] .ui-menu-card-icon,
  html[data-menu-cols="6"] .ui-menu-card-icon,
  html[data-menu-cols="7"] .ui-menu-card-icon { width: 24px; height: 24px; }
  html[data-menu-cols="5"] .ui-menu-card-icon [data-lucide],
  html[data-menu-cols="6"] .ui-menu-card-icon [data-lucide],
  html[data-menu-cols="7"] .ui-menu-card-icon [data-lucide] { width: 13px; height: 13px; }
  html[data-menu-cols="5"] .ui-menu-card-label,
  html[data-menu-cols="6"] .ui-menu-card-label,
  html[data-menu-cols="7"] .ui-menu-card-label { font-size: 9px; }
}

.ui-section-title {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--font-md);
  font-weight: 600;
  letter-spacing: var(--tracking-tight);
  color: var(--text);
  margin: 0 0 var(--space-3);
}
.ui-section-title svg.lucide,
.ui-section-title [data-lucide] {
  width: 16px;
  height: 16px;
  color: var(--text-secondary);
}

.ui-overline {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--font-xs);
  font-weight: 600;
  letter-spacing: var(--tracking-overline);
  text-transform: uppercase;
  color: var(--text-muted);
}

/* ---------- Badge ---------- */

.ui-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: 2px 8px;
  border-radius: var(--r-pill);
  background: var(--surface-2);
  color: var(--text-secondary);
  border: 1px solid var(--border);
  font-size: var(--font-xs);
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  white-space: nowrap;
  line-height: 1.6;
}
.ui-badge svg.lucide,
.ui-badge [data-lucide] { width: 12px; height: 12px; }

.ui-badge-accent  { background: var(--accent-soft);  color: var(--accent);         border-color: transparent; }
.ui-badge-success { background: var(--success-soft); color: var(--success);        border-color: transparent; }
.ui-badge-warning { background: var(--warning-soft); color: var(--warning-strong); border-color: transparent; }
.ui-badge-danger  { background: var(--danger-soft);  color: var(--danger);         border-color: transparent; }
.ui-badge-info    { background: var(--info-soft);    color: var(--info);           border-color: transparent; }
.ui-badge-outline { background: transparent; }

/* Anchor-as-badge — when a badge is used as an interactive filter chip
   (queue + history date / scope / staff filters). Adds the hover and
   focus affordances that a static badge doesn't need. */
a.ui-badge {
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}
a.ui-badge:hover { background: var(--surface-3); color: var(--text); }
a.ui-badge-accent:hover  { background: var(--accent-soft);  color: var(--accent); filter: brightness(1.04); }
a.ui-badge-info:hover    { background: var(--info-soft);    color: var(--info);   filter: brightness(1.04); }
a.ui-badge-success:hover { background: var(--success-soft); color: var(--success); filter: brightness(1.04); }
a.ui-badge:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px var(--accent-ring);
}
a.ui-badge[aria-current="true"] { font-weight: 700; }

.ui-badge-stack {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-1);
}

/* Mobile touch-target bump — the default `.ui-badge` is information-
   density-first (compact, 11px text, 2px padding) which makes it
   genuinely hard to tap on phones. Bump every badge a notch at ≤720px
   for legibility, then add real touch-target floor (≥40px) on the
   interactive `a.ui-badge` / `button.ui-badge` variants so filter
   chips on `/release/sr/`, `/release/wh/`, `/release/history/` clear
   the Material 48dp / Apple HIG 44pt guideline. Icons inside scale
   along so glyphs stay proportional. */
@media (max-width: 720px) {
  .ui-badge {
    padding: 4px 10px;
    font-size: var(--font-sm);
    line-height: 1.4;
  }
  .ui-badge [data-lucide],
  .ui-badge svg.lucide {
    width: 14px;
    height: 14px;
  }
  a.ui-badge,
  button.ui-badge {
    min-height: 40px;
    padding: 8px 14px;
    gap: var(--space-2);
  }
  a.ui-badge [data-lucide],
  a.ui-badge svg.lucide,
  button.ui-badge [data-lucide],
  button.ui-badge svg.lucide {
    width: 16px;
    height: 16px;
  }
  /* Chip rows lay out two-per-line nicely with a slightly larger row
     gap so taps on adjacent chips don't graze. */
  .ui-chip-row { gap: var(--space-2); row-gap: var(--space-2); }

  /* Display-only badges (`<span class="ui-badge">`) sitting alongside
     interactive ones (`<a class="ui-badge">`) inside a chip row should
     match the touch-target size of their neighbours — a tiny
     information-density chip next to 40px tap-chips reads as a layout
     accident. Used by queue.html for the "ช่วงเอง: yyyy-mm-dd → ..."
     indicator chip that appears when a custom date range is active,
     and by any future "current state" tag that needs to live in a
     filter row without being itself a link. Scoped to `.ui-chip-row`
     so status tags in table cells (e.g. release state in the queue
     table) keep their compact size. */
  .ui-chip-row .ui-badge {
    min-height: 40px;
    padding: 8px 14px;
    gap: var(--space-2);
    font-size: var(--font-sm);
  }
  .ui-chip-row .ui-badge [data-lucide],
  .ui-chip-row .ui-badge svg.lucide {
    width: 16px;
    height: 16px;
  }

  /* iOS Safari auto-zooms into any input whose computed `font-size` is
     below 16px when it gains focus — disorienting and forces the user
     to pinch out. The desktop default is `--font-base` (13.5px). Bump
     to 16px on phones to defuse the zoom. Applied to every text-input-
     ish surface; .ui-textarea inherits the rule via the selector list
     in the base block. */
  .ui-input,
  .ui-select,
  .ui-textarea {
    font-size: 16px;
  }
  /* Buttons get a small font lift so labels (Thai vowel marks tend to
     fall thin at 13.5px) read cleanly at the new 44px height. */
  .ui-btn {
    font-size: var(--font-md);
  }
  .ui-btn-sm { font-size: var(--font-sm); }
  .ui-btn-lg { font-size: var(--font-lg); }
}

/* ---------- Table ---------- */

.ui-table-wrap {
  width: 100%;
  overflow-x: auto;
  border-radius: var(--r-md);
}

.ui-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--font-base);
  font-variant-numeric: tabular-nums;
}
.ui-table th,
.ui-table td {
  text-align: left;
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--divider);
  vertical-align: middle;
}
.ui-table th {
  background: var(--surface-2);
  font-size: var(--font-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
  color: var(--text-muted);
  white-space: nowrap;
}
.ui-table tbody tr { transition: background-color var(--dur-fast) var(--ease-out); }
.ui-table tbody tr:hover { background: var(--surface-2); }
.ui-table tbody tr:last-child td { border-bottom: 0; }
.ui-table tbody tr.is-archived { color: var(--text-muted); }
.ui-table tbody tr.is-selected { background: var(--accent-soft); }
.ui-table tbody tr.is-checked  { background: color-mix(in oklab, var(--accent) 6%, transparent); }
.ui-table tbody tr.is-checked td.ui-cell-primary > div:first-child { text-decoration: line-through; color: var(--text-muted); }

/* Body-cell typography helpers (replaces inline styles entirely) */
.ui-table .ui-cell-id {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-weight: 500;
  color: var(--text);
  letter-spacing: -0.005em;
  /* Identifiers (docno, employee_code, permission_code) never wrap —
     a split like "RC202606\n0001" reads as two values, not one. */
  white-space: nowrap;
}
.ui-table .ui-cell-num {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--text);
  white-space: nowrap;
}
.ui-table .ui-cell-num-right { text-align: right; }
.ui-table .ui-cell-date {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--text);
  white-space: nowrap;
}
.ui-table .ui-cell-name {
  font-weight: 500;
  color: var(--text);
}
.ui-table .ui-cell-sub {
  font-family: var(--font-mono);
  font-size: var(--font-xs);
  color: var(--text-muted);
  margin-top: 2px;
  display: flex;
  align-items: center;
  gap: var(--space-1);
}
.ui-table .ui-cell-actions {
  text-align: right;
  white-space: nowrap;
}

/* Primary-cell links inside tables get accent color — but NOT when the
   `<a>` is also a `.ui-btn`. Buttons own their full color/background
   contract (e.g. `.ui-btn-primary` = white on accent); a table-cell
   override would silently steal the foreground and leave white-text
   buttons rendering with the cell's body color. The `:not(.ui-btn)`
   guard preserves both behaviours. */
.ui-table tbody td > a:not(.ui-btn) {
  color: var(--accent);
  font-weight: 500;
}
.ui-table tbody td > a:not(.ui-btn):hover { color: var(--accent-hover); }
.ui-table .ui-cell-actions a:not(.ui-btn),
.ui-table .ui-cell-actions button:not(.ui-btn) { color: inherit; font-weight: 500; }
.ui-table .ui-cell-actions a:not(.ui-btn):hover { text-decoration: none; }

.ui-table td.ui-empty-row {
  padding: var(--space-16) var(--space-6) !important;
  text-align: center;
  vertical-align: middle;
  color: var(--text-muted);
  font-size: var(--font-sm);
}

/* ---------- Banner / alert ---------- */

.ui-banner {
  display: flex;
  gap: var(--space-3);
  align-items: flex-start;
  padding: var(--space-3) var(--space-4);
  /* Operator request 2026-06-23 — "ทำมุมของ toggle ให้โค้งแรงๆ":
     bump from --r (6px) to --r-xl (14px) for a softer, friendlier
     rounded-card silhouette. Inner toggle button (--r-pill 50%) and
     details row inherit from inside; the outer banner outline is what
     reads as "very rounded". */
  border-radius: var(--r-xl);
  border: 1px solid var(--border);
  background: var(--surface);
  font-size: var(--font-sm);
}
.ui-banner svg.lucide,
.ui-banner [data-lucide] {
  width: 18px; height: 18px;
  flex-shrink: 0;
  margin-top: 1px;
}
.ui-banner-body { flex: 1; line-height: 1.5; color: var(--text); font-size: 18px; }
.ui-banner-body > strong { font-size: 18px; }
/* Detail blob under the heading — kept smaller than the heading but
   still legible. Inline `font-size` set on this div by detail /
   history templates is overridden by the more-specific selector here. */
.ui-banner-body > .muted { font-size: 15px; }
.ui-banner-success { background: var(--success-soft); border-color: transparent; color: var(--success); }
.ui-banner-warning { background: var(--warning-soft); border-color: transparent; color: var(--warning-strong); }
.ui-banner-danger  { background: var(--danger-soft);  border-color: transparent; color: var(--danger);  }
.ui-banner-info    { background: var(--info-soft);    border-color: transparent; color: var(--info);    }

/* Banner head — label on the left, optional icon-only toggle pushed
   to the far right via margin-left:auto. Display:flex (not inline-
   flex) so the head fills the banner-body width and the auto-margin
   resolves to "push toggle to the right edge". */
.ui-banner-head {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}

/* Details toggle — icon-only chevron button at the far right of the
   banner head. Chevron rotates 180° on expand (CSS aria-expanded
   selector). Operator request 2026-06-23: hide detail tooltip by
   default + provide affordance to reveal. */
.ui-banner-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-left: auto;  /* push to far right within .ui-banner-head */
  padding: 0;
  width: 28px;
  height: 28px;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: inherit;
  cursor: pointer;
  opacity: 0.7;
  transition: opacity 160ms ease, background 160ms ease;
}
.ui-banner-toggle:hover {
  opacity: 1;
  background: color-mix(in oklab, currentColor 12%, transparent);
}
.ui-banner-toggle:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 2px;
  opacity: 1;
}
.ui-banner-toggle > i,
.ui-banner-toggle > svg {
  width: 18px;
  height: 18px;
  transition: transform 220ms ease;
}
.ui-banner-toggle[aria-expanded="true"] > i,
.ui-banner-toggle[aria-expanded="true"] > svg {
  transform: rotate(180deg);
}
.ui-banner-details[hidden] { display: none; }

/* ---------- KV — key-value pair list (used in detail panels) ---------- */

.ui-kv {
  display: grid;
  /* `minmax(0, 1fr)` not bare `1fr`: a default `1fr` track has a
     min-width of `min-content`, so a long unbreakable value (a mono
     docno, a wide ฿ amount) forces the track wider than its share and
     overflows the card → horizontal page scroll. `minmax(0, 1fr)`
     lets the value track shrink below its content width so
     `word-break` can wrap instead of pushing the layout wide. */
  grid-template-columns: 130px minmax(0, 1fr);
  row-gap: var(--space-2);
  column-gap: var(--space-4);
  font-size: var(--font-sm);
}
.ui-kv .k { color: var(--text-muted); }
.ui-kv .v { color: var(--text); word-break: break-word; }
@media (max-width: 540px) {
  /* Tight 2-col layout on phones — label + value on the same row.
     Label width 100px fits the longest Thai labels in this app
     (รหัสลูกค้า, ชื่อลูกค้า) at font-sm without truncating; values
     get the remaining width. Row-gap kept compact so the list reads
     as a quick scan card, not a record-by-record block. */
  .ui-kv {
    grid-template-columns: 100px minmax(0, 1fr);
    row-gap: var(--space-2);
    column-gap: var(--space-3);
  }
  .ui-kv .k {
    font-weight: 500;
    font-size: var(--font-sm);
    color: var(--text-muted);
    align-self: baseline;
  }
  .ui-kv .v {
    font-size: var(--font-md);
    align-self: baseline;
  }
}

/* ---------- Breadcrumbs ---------- */

.ui-breadcrumbs {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--font-xs);
  color: var(--text-muted);
  flex-wrap: wrap;
}
.ui-breadcrumbs a { color: var(--text-muted); }
.ui-breadcrumbs a:hover { color: var(--accent); }
.ui-breadcrumbs .sep { opacity: 0.5; }
.ui-breadcrumbs > :last-child { color: var(--text-secondary); }

/* Mobile breadcrumb bump — desktop is information-density-first
   (compact 11px muted text). On a phone:
     1. Font-size to `font-md` (15px) for real legibility.
     2. Text stays in the regular ink colour (`--text`) so the row
        reads as a navigation trail, not a row of buttons.
     3. The accent affordance is a `text-decoration: underline` —
        spans EXACTLY the text width (no box-padding bleed) and the
        offset keeps it tight against the glyph baseline. This is the
        difference from a `border-bottom` on a padded `inline-block`,
        which paints the line at the box edge (far below the text).
     4. `align-items: baseline` overrides the row's default `center`
        so siblings (`<span>` separators, current-page label) line up
        with the link's text baseline.
     5. Modest 2px horizontal padding gives the tap area a bit of
        breathing room without making the line look detached. */
@media (max-width: 720px) {
  .ui-breadcrumbs {
    font-size: var(--font-md);
    row-gap: var(--space-1);
    align-items: baseline;
  }
  .ui-breadcrumbs a {
    color: var(--text);
    padding: 4px 2px;
    text-decoration: underline;
    /* Omit `text-decoration-color` so the underline tracks
       `currentColor` — same colour as the text in every state
       (default, hover, focus). */
    text-decoration-thickness: 2px;
    text-underline-offset: 3px;
  }
  .ui-breadcrumbs a:hover {
    color: var(--accent);
  }
  .ui-breadcrumbs a:focus-visible {
    outline: 2px solid var(--accent-ring);
    outline-offset: 2px;
    border-radius: 2px;
  }
  /* Breathing room between the breadcrumb row and the page title.
     The desktop default (4px) reads as cramped once the breadcrumb
     font bumps to 15px and the title shares the same column. `space-8`
     (48px) lets the title land as a clear "next section" with real
     mental separation from the crumb trail. */
  .ui-page-header h1 {
    margin-top: var(--space-8);
  }
}

/* ---------- Page header ---------- */

.ui-page-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-4);
  margin-bottom: var(--space-6);
  flex-wrap: wrap;
}
.ui-page-header h1 {
  font-size: var(--font-xl);
  font-weight: 600;
  letter-spacing: var(--tracking-tight);
  color: var(--text);
  margin: var(--space-1) 0 var(--space-1);
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.ui-page-header h1 svg.lucide,
.ui-page-header h1 [data-lucide] { width: 22px; height: 22px; color: var(--text-secondary); }
.ui-page-header .subtitle {
  font-size: var(--font-sm);
  color: var(--text-muted);
  margin: 0;
}
.ui-page-header-actions {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  flex-wrap: wrap;
}

/* ---------- Pagination ---------- */

.ui-pagination {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-top: 1px solid var(--divider);
}
.ui-pagination .info {
  font-size: var(--font-xs);
  color: var(--text-muted);
}

/* ---------- Loading skeleton ---------- */

.ui-skel {
  display: block;
  background: linear-gradient(
    90deg,
    var(--surface-2) 0%,
    var(--surface-3) 50%,
    var(--surface-2) 100%
  );
  background-size: 200% 100%;
  animation: ui-skel-shimmer 1.4s linear infinite;
  border-radius: var(--r-sm);
}
.ui-skel-line { height: 12px; margin: 6px 0; }
.ui-skel-line.is-short { width: 40%; }
.ui-skel-line.is-medium { width: 65%; }
.ui-skel-line.is-long { width: 90%; }

@keyframes ui-skel-shimmer {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

@media (prefers-reduced-motion: reduce) {
  .ui-skel { animation: none; }
}

/* ---------- Spinner ---------- */

@keyframes ui-spin {
  to { transform: rotate(360deg); }
}
.ui-spin {
  display: inline-block;
  animation: ui-spin 0.9s linear infinite;
}

/* ---------- Notif popover + user menu (topbar) ---------- */

.ui-notif { position: relative; }

.ui-notif-trigger { position: relative; }
.ui-notif-dot {
  position: absolute;
  top: 6px; right: 6px;
  width: 7px; height: 7px;
  background: var(--danger);
  border-radius: var(--r-pill);
  border: 2px solid var(--surface);
}

.ui-notif-popover {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  width: 340px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-lg);
  display: none;
  z-index: var(--z-dropdown);
}
.ui-notif[data-open="true"] .ui-notif-popover { display: block; }
.ui-notif-panel { max-height: 480px; overflow-y: auto; }
.ui-notif-empty {
  padding: var(--space-6);
  text-align: center;
  color: var(--text-muted);
  font-size: var(--font-sm);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
}
.ui-notif-empty svg.lucide,
.ui-notif-empty [data-lucide] { width: 24px; height: 24px; opacity: 0.5; }

.ui-notif-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--divider);
  background: var(--surface-2);
  font-size: var(--font-sm);
  font-weight: 600;
}

.ui-notif-list {
  list-style: none;
  padding: 0;
  margin: 0;
}
.ui-notif-list > li { border-bottom: 1px solid var(--divider); }
.ui-notif-list > li:last-child { border-bottom: 0; }

.ui-notif-item {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--space-3) var(--space-4);
  text-decoration: none;
  color: var(--text);
  background: transparent;
  transition: background-color var(--dur-fast) var(--ease-out);
}
.ui-notif-item:hover {
  background: var(--surface-2);
  text-decoration: none;
}
.ui-notif-item.is-unread {
  background: var(--accent-soft);
}
.ui-notif-item.is-unread:hover {
  background: color-mix(in oklab, var(--accent) 12%, var(--surface));
}

.ui-notif-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2);
}
.ui-notif-title { font-size: var(--font-sm); font-weight: 500; }
.ui-notif-time { font-size: var(--font-xs); color: var(--text-muted); flex-shrink: 0; }
.ui-notif-body { font-size: var(--font-xs); color: var(--text-secondary); line-height: var(--leading-base); }

.ui-notif-foot {
  padding: var(--space-2) var(--space-4);
  text-align: center;
  border-top: 1px solid var(--divider);
  background: var(--surface-2);
}
.ui-notif-foot a {
  font-size: var(--font-xs);
  font-weight: 500;
}

.ui-user-menu {
  position: relative;
}

.ui-user-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 4px 8px 4px 4px;
  border-radius: var(--r);
  border: 1px solid transparent;
  background: transparent;
  cursor: pointer;
  color: var(--text);
  transition: background-color var(--dur-fast) var(--ease-out);
}
.ui-user-chip:hover { background: var(--surface-2); }
.ui-user-chip svg.lucide,
.ui-user-chip [data-lucide] { width: 14px; height: 14px; color: var(--text-muted); }

.ui-avatar {
  width: 30px; height: 30px;
  border-radius: var(--r-pill);
  background: var(--accent-soft);
  color: var(--accent);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-weight: 600;
  font-size: 11.5px;
  letter-spacing: 0.03em;
  flex-shrink: 0;
}
.ui-user-meta {
  display: flex;
  flex-direction: column;
  gap: 1px;
  line-height: 1.15;
  text-align: left;
}
.ui-user-meta b { font-size: 12.5px; font-weight: 600; color: var(--text); }
.ui-user-meta span { font-size: 10.5px; color: var(--text-muted); }

.ui-menu {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  min-width: 220px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
  display: none;
  z-index: var(--z-dropdown);
}
.ui-user-menu[data-open="true"] .ui-menu { display: block; }

.ui-menu-header {
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--divider);
  background: var(--surface-2);
  font-size: var(--font-xs);
  color: var(--text-muted);
  font-family: var(--font-mono);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ui-menu-item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-4);
  width: 100%;
  border: 0;
  background: transparent;
  color: var(--text);
  font-size: var(--font-sm);
  cursor: pointer;
  text-align: left;
  text-decoration: none;
}
.ui-menu-item:hover { background: var(--surface-2); }
.ui-menu-item svg.lucide,
.ui-menu-item [data-lucide] { width: 14px; height: 14px; color: var(--text-muted); }
.ui-menu-item-danger { color: var(--danger); }
.ui-menu-item-danger svg.lucide,
.ui-menu-item-danger [data-lucide] { color: var(--danger); }
.ui-menu-item-danger:hover { background: var(--danger-soft); }

.ui-menu-divider {
  height: 1px;
  background: var(--divider);
}

/* ---------- Quicklink card (home page) ---------- */

.ui-quicklink {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: var(--space-5);
  text-decoration: none;
  color: var(--text);
  position: relative;
  height: 100%;
  min-height: 160px;
  transition:
    border-color var(--dur-fast) var(--ease-out),
    transform    var(--dur-fast) var(--ease-out);
}
.ui-quicklink:hover {
  border-color: var(--accent);
  transform: translateY(-1px);
  text-decoration: none;
  color: var(--text);
}
.ui-quicklink .ico {
  width: 40px;
  height: 40px;
  border-radius: var(--r);
  background: var(--accent-soft);
  color: var(--accent);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.ui-quicklink .ico svg.lucide,
.ui-quicklink .ico [data-lucide] {
  width: 20px;
  height: 20px;
}
.ui-quicklink h3 {
  font-size: var(--font-md);
  font-weight: 600;
  margin: 0;
  letter-spacing: var(--tracking-tight);
}
.ui-quicklink p {
  font-size: var(--font-sm);
  color: var(--text-muted);
  margin: 0;
  line-height: var(--leading-base);
}
.ui-quicklink-status {
  position: absolute;
  top: var(--space-3);
  right: var(--space-3);
  font-size: var(--font-xs);
  color: var(--text-muted);
  background: var(--surface-2);
  padding: 2px 8px;
  border-radius: var(--r-pill);
}
.ui-quicklink.is-disabled {
  pointer-events: none;
  opacity: 0.6;
}

/* ---------- Photo preview slot (goods_release detail) ---------- */

/* `.ui-photo-preview` removed 2026-06-03 — superseded by `.ui-photo-slot`
   in features.css which integrates the empty-state tap target and the
   filled-state preview into one component instead of stacking them. */

/* ---------- Dashboard tiles ---------- */

.ui-stat {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  overflow: hidden;
}
.ui-stat-label {
  font-size: var(--font-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
  color: var(--text-muted);
}
.ui-stat-value {
  font-size: 28px;
  font-weight: 600;
  color: var(--text);
  letter-spacing: -0.018em;
  font-variant-numeric: tabular-nums;
}
.ui-stat-hint {
  font-size: var(--font-xs);
  color: var(--text-muted);
}
.ui-stat-icon {
  position: absolute;
  top: var(--space-3);
  right: var(--space-3);
  width: 32px;
  height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--r);
  background: var(--accent-soft);
  color: var(--accent);
  opacity: 0.85;
}
.ui-stat-icon.primary   { background: var(--accent-soft);  color: var(--accent); }
.ui-stat-icon.success   { background: var(--success-soft); color: var(--success); }
.ui-stat-icon.warning   { background: var(--warning-soft); color: var(--warning-strong); }
.ui-stat-icon.danger    { background: var(--danger-soft);  color: var(--danger); }
.ui-stat-icon.info      { background: var(--info-soft);    color: var(--info); }
.ui-stat-icon.secondary { background: var(--surface-2);    color: var(--text-muted); }

/* Clock + employee hero */

.ui-clock-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr);
  gap: var(--space-3);
  margin-bottom: var(--space-6);
}
@media (max-width: 720px) {
  .ui-clock-row { grid-template-columns: 1fr; }
}
.ui-clock-panel {
  background: var(--nav-bg);
  color: var(--nav-text-active);
  border-radius: var(--r-md);
  padding: var(--space-5) var(--space-6);
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  position: relative;
  overflow: hidden;
}
.ui-clock-panel::after {
  content: "";
  position: absolute;
  top: -40%; right: -20%;
  width: 60%; height: 180%;
  background: radial-gradient(circle, var(--accent) 0%, transparent 70%);
  opacity: 0.18;
  pointer-events: none;
}
.ui-clock-time {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 40px;
  font-weight: 500;
  letter-spacing: -0.01em;
  color: #fff;
  position: relative;
  z-index: 1;
}
.ui-clock-date {
  font-size: var(--font-sm);
  color: var(--nav-text-muted);
  position: relative;
  z-index: 1;
}

.ui-emp-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: var(--space-5) var(--space-6);
  display: flex;
  gap: var(--space-4);
  align-items: center;
}
.ui-emp-avatar {
  width: 56px; height: 56px;
  border-radius: var(--r-pill);
  background: var(--accent-soft);
  color: var(--accent);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-weight: 600;
  font-size: 18px;
  letter-spacing: 0.03em;
  flex-shrink: 0;
}
.ui-emp-info { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.ui-emp-id {
  font-size: var(--font-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
  color: var(--text-muted);
  font-family: var(--font-mono);
}
.ui-emp-name {
  font-size: var(--font-lg);
  font-weight: 600;
  color: var(--text);
}
.ui-emp-meta {
  display: flex;
  gap: var(--space-1);
  flex-wrap: wrap;
  margin-top: var(--space-1);
}

/* ---------- Release-actions row (primary CTA + ghost save-progress) ---------- */

.ui-release-actions {
  display: flex;
  align-items: flex-start;
  gap: var(--space-4);
  flex-wrap: wrap;
}
.ui-release-actions-primary { flex: 1; min-width: 240px; }
.ui-release-actions-primary .ui-btn { width: 100%; justify-content: center; }
.ui-release-actions-secondary { padding-top: var(--space-1); }

@media (max-width: 720px) {
  /* Sticky-bottom action bar — `.ui-release-actions` is at the END of
     a long form (item checklist + photos + signature + notes), so the
     LP staff on a phone would scroll past several screens to reach
     "บันทึกส่งมอบเสร็จสิ้น". Pinning it to the viewport bottom matches
     what touch users expect from native checkout / save flows.
     `env(safe-area-inset-bottom)` keeps the button above the iOS home
     bar; `body:has()` reserves scroll space so the last form field
     isn't permanently hidden by the bar. */
  .ui-release-actions {
    flex-direction: column;
    position: fixed;
    bottom: 0; left: 0; right: 0;
    z-index: var(--z-sticky);
    margin: 0;
    padding: var(--space-3) var(--space-4)
            calc(var(--space-3) + env(safe-area-inset-bottom));
    background: var(--surface);
    border-top: 1px solid var(--border);
    box-shadow: 0 -4px 12px rgba(15, 23, 42, 0.06);
    gap: var(--space-2);
  }
  .ui-release-actions-primary,
  .ui-release-actions-secondary { width: 100%; padding: 0; min-width: 0; }
  .ui-release-actions-primary .ui-btn { width: 100%; }
  .ui-release-actions-secondary .ui-btn { width: 100%; justify-content: center; }
  /* Lock-warning meta line — preserve the message but compress to one
     line so it doesn't shove the bar past 1/3 of the viewport. */
  .ui-release-actions .ui-help-meta {
    margin-top: var(--space-1);
    font-size: var(--font-xs);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  /* Reserve scroll-bottom space so the sticky bar doesn't sit on top
     of the last visible form field. ~150px covers a 2-row bar (primary
     + secondary) plus the safe-area inset on most phones. */
  body:has(.ui-release-actions) {
    padding-bottom: 160px;
  }
}


/* =============================================================================
   Layout utility classes
   -----------------------------------------------------------------------------
   Small, focused utilities for the patterns we kept finding repeated inline
   in templates. NOT a general-purpose utility library — only extract a
   pattern here when it has repeated 2+ times across different surfaces.
   ============================================================================= */

/* Horizontal row of chips/badges/small interactive items. Wraps on overflow. */
.ui-chip-row {
  display: flex;
  gap: var(--space-1);
  flex-wrap: wrap;
}
/* Push the row to the right side of its flex parent. Compose with .ui-chip-row. */
.ui-chip-row-end { margin-left: auto; }

/* Filter form layout — search input + submit button + chip groups on one
   row. Wraps to a column on narrow viewports via natural flex-wrap. */
.ui-filter-form {
  display: flex;
  gap: var(--space-3);
  flex-wrap: wrap;
  align-items: center;
}

/* Horizontal search row — input + submit icon + clear icon stay on
   the same line even when `.ui-filter-form` collapses to column on
   mobile. Used by queue.html as the first row of the filter card. */
.ui-filter-search-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
}

@media (max-width: 720px) {
  /* Touch-first chip strip — wrapping rows of chips on a phone fragment
     vertical real estate and push real content below the fold. Switch to
     a single-row horizontal scroll with scroll-snap so users swipe through
     filters with their thumb. Hidden scrollbar (still scrollable) keeps
     the row visually clean. The `chip-row-end` right-alignment is dropped
     on mobile — there's no horizontal room to compete for. */
  .ui-chip-row {
    flex-wrap: nowrap;
    overflow-x: auto;
    overflow-y: visible;
    scroll-snap-type: x proximity;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    /* Slight padding so the first/last chip's focus ring doesn't clip,
     and negative margin so the bleed matches the card's edge. */
    padding: 2px var(--space-1);
    margin: 0 calc(-1 * var(--space-1));
  }
  .ui-chip-row::-webkit-scrollbar { display: none; }
  .ui-chip-row > * {
    scroll-snap-align: start;
    flex-shrink: 0;
  }
  .ui-chip-row-end { margin-left: 0; }

  /* `.ui-chip-row-wrap` opts a chip row out of the mobile horizontal-
     scroll default and back into multi-line wrap. Used when one chip
     in the row carries unique long content (e.g. a custom date-range
     summary like "2026-06-02 → 2026-06-07") that gets clipped at the
     scroll edge — wrapping to a new line keeps it fully readable
     without forcing the user to thumb-swipe to find it. The scroll-
     bleed negative margin from the default also resets here since
     there's no overflow to compensate for. */
  .ui-chip-row.ui-chip-row-wrap {
    flex-wrap: wrap;
    overflow-x: visible;
    scroll-snap-type: none;
    padding: 0;
    margin: 0;
  }
  .ui-chip-row.ui-chip-row-wrap > * {
    scroll-snap-align: none;
  }

  /* The filter form's children should each get their own row on mobile
     so the search input gets full width and chip strips can scroll
     horizontally underneath without competing. */
  .ui-filter-form { flex-direction: column; align-items: stretch; }
  .ui-filter-form > [role="group"] { width: 100%; }
}

/* Card or block that needs bottom spacing to the next card in a stacked page. */
.ui-mb-section { margin-bottom: var(--space-4); }

/* Top margin between stacked cards inside a section. `--space-3` is the
   chosen spacing for "related but distinct" blocks (form + checklist +
   submit row); use `.ui-mb-section` for major page-level breaks. */
.ui-mt-3 { margin-top: var(--space-3); }

/* Grid cell that should match its sibling's height — useful inside Bootstrap
   grid rows where two cards in adjacent columns should line up. */
.ui-full-h { height: 100%; }

/* Inline help/meta strip — small text, icon + label, used for irreversibility
   warnings, "ดูอย่างเดียว" notes, page-level captions under buttons. */
.ui-help-meta {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--font-xs);
  color: var(--text-muted);
}
.ui-help-meta [data-lucide] { width: 14px; height: 14px; }

/* Hint text under an empty-state row in a table — small + spaced from the
   primary empty message above it. */
.ui-empty-hint {
  font-size: var(--font-xs);
  margin-top: var(--space-1);
}
