:root {
  --dp-color-bg: #f5f6f8;
  --dp-color-surface: #ffffff;

  --dp-space-1: 0.25rem;
  --dp-space-2: 0.5rem;
  --dp-space-3: 1rem;
  --dp-space-4: 1.5rem;
  --dp-space-5: 3rem;

  --dp-radius-sm: 0.25rem;
  --dp-radius-md: 0.5rem;

  --dp-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.08);

  --dp-font-size-sm: 0.875rem;
  --dp-font-size-base: 1rem;
  --dp-font-size-lg: 1.25rem;

  --dp-sidebar-width: 15rem;
  --dp-sidebar-width-collapsed: 3.5rem;
  --dp-viewport-min-h: 100vh;
  --dp-message-min-h: 60vh;

  --dp-z-sidebar: 100;
  --dp-z-topbar: 200;
  /* Real Bootstrap stack (not this app's own numbers): form modal 1055, its backdrop 1050.
     Everything below is layered relative to those, low to high — the chooser is a modal
     stacked over the form modal that opened it, so it must clear both. */
  --dp-z-chooser-backdrop: 1059;
  --dp-z-chooser: 1060;
  --dp-z-toast: 1080;

  --dp-color-muted: #6c757d;
  --dp-empty-icon-size: 2.5rem;
  --dp-toast-offset: 1.5rem;
  --dp-toast-width: 22rem;

  --dp-auth-card-width: 22rem;

  --dp-thumb-size: 2.5rem;
  --dp-image-preview-size: 5rem;
  --dp-repeater-label-w: 9rem;

  /* Phase 10A — order list responsive columns / column-choice dropdown, cashier goods browser.
     Phase 13a-1 §4.7 raised the cap to `min(24rem, 60vh)`, which still produced a scroll box for
     a long column list — a scroll box is what "cropped" meant. Phase 13b §4 replaces the cap
     with a multi-column grid (`--dp-columns-menu-max-rows` above) sized to its own content;
     `--dp-columns-menu-min-w`/`-max-w` bound that content's width instead of its height. */
  --dp-columns-menu-min-w: 14rem;
  --dp-columns-menu-max-w: 32rem;
  --dp-goods-browser-thumb-size: 3rem;
  /* Phase 13a-3 §3.1: grid thumbnails used a fixed height (`--dp-goods-browser-grid-thumb-h`,
     now removed) that changed the crop's aspect ratio with column width, so the same product
     looked like a different shape in grid vs list. `aspect-ratio: 1 / 1` (below, on the thumb
     and its placeholder) makes both views the exact same 1:1 crop as the list's own
     `--dp-goods-browser-thumb-size` square. */
  --dp-goods-browser-grid-min-w: 8.5rem;

  /* Phase 13b §2.1 — the filter bar's control cell (list-screen.ejs) is capped so an input does
     not stretch across a wide screen and become harder to scan. */
  --dp-list-filter-control-max-w: 28rem;
  /* Phase 13c §2 — even spacing between filter rows; one token so every filter bar in the app
     gets the same rhythm rather than a per-row `mb-2`. */
  --dp-list-filter-row-gap: var(--dp-space-2);
  /* Phase 13b §4 — the Kolom dropdown's multi-column layout: fourteen entries become two columns
     of seven instead of one scrolling column, so nothing is clipped. */
  --dp-columns-menu-max-rows: 8;
  /* Phase 13b §5.2 — the goods name's fixed two-line box (line-height * 2), so a one-line and a
     two-line name produce identically sized cards regardless of the line-clamp above it.
     Phase 13d — was 2.5em, which is not two lines: the inherited line-height is 1.5, so two lines
     is 3em. Measured live, a one-line name box came out 35px against a two-line name's 42px, and
     that 7px difference propagated straight into the thumbnail height. */
  --dp-goods-browser-grid-name-h: 3em;

  /* Phase 13a-1 §4.9 — order module's six inline `style=` widths, moved into tokens (invariant
     #13: no raw hex/magic pixel numbers outside this block, and no inline styles anywhere). */
  --dp-order-timeline-date-w: 8.125rem;
  --dp-order-qty-stepper-w: 8.75rem;
  --dp-order-materials-col-w: 12.5rem;
  --dp-order-note-col-w: 10rem;

  /* Phase 13a-3 §5 — the cashier's Detail Pelanggan/Konfirmasi column sticks under the topbar
     on `lg`+ so Confirm stays reachable without scrolling back up a long cart; the offset token
     matches `main`'s own top padding (Bootstrap's `p-4`) rather than a magic number. */
  --dp-cashier-sticky-top: var(--dp-space-4);
  /* Phase 13a-3 §7.2 — the shared success/failure result modal's icon size. */
  --dp-result-icon-size: 3rem;

  /* Phase 13a-4 §6 — the shared table-cell padding, applied by `list-screen.ejs` to every
     `.dp-list-table` in the app (all eleven Phase 13a-1 screens plus this phase's items/ledger/
     timeline tables) instead of a per-table guess. Matches Bootstrap's own default `.table` cell
     padding (0.5rem) so no existing screen's spacing visibly changes. `-dense` is for a table
     rendered inside a modal (`dense: true`, `import-modal.ejs`'s preview table is the first
     caller), where vertical space is tighter than a full page card. */
  --dp-table-cell-padding-y: 0.5rem;
  --dp-table-cell-padding-x: 0.5rem;
  --dp-table-cell-padding-y-dense: 0.35rem;
  --dp-table-cell-padding-x-dense: 0.5rem;

  /* Phase 13a-4 §4 — the border colour a `readonly` `form-field.ejs` input flattens to (Bootstrap's
     own default `.form-control` border grey — not a new colour, just named here per invariant #13). */
  --dp-color-border-muted: #dee2e6;
  /* Phase 13c §5 — a general-purpose border colour for any bordered surface (the goods grid card
     is the first; every future one should reach for this rather than a new raw hex). Bootstrap's
     own default border grey, same value as `-muted` above but named separately since the two
     tokens are free to diverge later. */
  --dp-color-border: #dee2e6;

  /* Phase 13a-4 §3 — `DP.confirmDialog` can now open while another modal is already showing (the
     order cancel modal), the same stacking problem phase-07a's chooser modal already solved for
     a picker stacked over a form modal. Above the chooser stack (1059/1060) so a confirm can, in
     principle, also stack over an open chooser; below the toast layer. */
  --dp-z-confirm-backdrop: 1069;
  --dp-z-confirm: 1070;

  /* Phase 13c §8 — `components/autocomplete.ejs`'s listbox, positioned by `DP.initAutocomplete`
     against the input's own `getBoundingClientRect()` (see app.js and the rule below on why it
     must be `position: fixed`, not `absolute`). Above the form modal (1055) and the chooser
     (1060) — an autocomplete can open inside either — below the confirm dialog's own backdrop
     (1069) and the toast (1080). No caller opens an autocomplete inside a modal today; this is
     what stops the first one that does from being a bug. */
  --dp-z-autocomplete: 1065;

  /* Phase 15 §4.1 — the client's brand colour. Used only on the sidebar (§4.2) and, as its own
     separate custom property, in the printed documents' self-contained `<style>` block (§4.4) —
     never referenced across that boundary since documents.ejs has no `/assets` access. The dark
     shade is a fixed derivation (not a hand-picked second hex) so there is exactly one colour
     decision to revisit if the brand colour ever changes. */
  --dp-color-brand: #015ae0;
  --dp-color-brand-dark: color-mix(in srgb, var(--dp-color-brand) 80%, black);
  --dp-color-brand-text: #ffffff;
  /* Phase 15 §4.3 — the sidebar wordmark logo is a horizontal lockup: sized by height, width
     follows, never a tall narrow slot. */
  --dp-sidebar-logo-h: 2rem;
}

body {
  background-color: var(--dp-color-bg);
}

.dp-app-shell {
  min-height: var(--dp-viewport-min-h);
}

.dp-sidebar {
  width: var(--dp-sidebar-width);
  /* Phase 13a-1 §3.1: a flex item defaults to flex-shrink: 1, so without this an over-wide
     sibling (a wide table before the min-width:0 fix below) squeezes the sidebar narrower on
     some pages than others — "same width like when accessing other menu" was the reported
     symptom. `flex: 0 0 <width>` means the sidebar's own width is authoritative, period. */
  flex: 0 0 var(--dp-sidebar-width);
  transition: flex-basis 0.15s ease, width 0.15s ease;
  background-color: var(--dp-color-brand);

  /* Phase 15 §5 — locks the sidebar so it stays in view while the page scrolls. `position:
     sticky`, never `fixed`: fixing it would take the sidebar out of flow and re-break the
     `min-width: 0` containment below (`.dp-app-main`) that keeps a wide table's horizontal
     scrollbar on `.table-responsive` instead of the page. `height` (not `min-height`, which the
     rule used before this phase) so the box never exceeds the viewport — a sticky element taller
     than the viewport has nothing to stick within. `overflow-y: auto` lets the menu list scroll
     inside the rail if it ever outgrows eleven items. */
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
}

/* Phase 13a-1 §3.1 — "the whole bug": a flex item's default min-width is `auto`, i.e. it refuses
   to shrink below its own content's intrinsic width. A wide table therefore pushed this column
   wider than the viewport instead of overflowing inside its own `.table-responsive`, which is
   also why the sidebar got squeezed (see `.dp-sidebar` above). Do not "fix" this with
   `overflow-x: hidden` on body or a fixed-position sidebar — both hide the overflow instead of
   containing it; the horizontal scrollbar has to land on `.table-responsive`, not the page. */
.dp-app-main {
  min-width: 0;
}

.dp-app-shell.dp-sidebar-collapsed .dp-sidebar {
  width: var(--dp-sidebar-width-collapsed);
  flex: 0 0 var(--dp-sidebar-width-collapsed);
}

/* Collapsed rail: icon-only. Labels become `visually-hidden` (Bootstrap utility, still readable
   by a screen reader); each link's own `title`/`aria-label` (see partials/sidebar.ejs) is what
   makes the rail still usable sighted — a native tooltip on hover. */
.dp-app-shell.dp-sidebar-collapsed .dp-sidebar-label,
.dp-app-shell.dp-sidebar-collapsed .dp-sidebar-brand {
  position: absolute !important;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Phase 13b §9.1 — with the brand hidden (rule above), the toggle is the row's only remaining
   flex item; without this its `justify-content: space-between` snaps it to one edge instead of
   centring it in the rail. */
.dp-app-shell.dp-sidebar-collapsed .dp-sidebar-brand-row {
  justify-content: center;
}

.dp-app-shell.dp-sidebar-collapsed .nav-link {
  text-align: center;
}

.dp-app-shell.dp-sidebar-collapsed .nav-link i {
  margin-right: 0 !important;
}

/* Phase 15 §4.2 — on a mid-blue background, Bootstrap's default `.nav-pills .nav-link.active`
   blue is nearly invisible against `--dp-color-brand`; the derived dark shade keeps the
   active/hover pill visibly distinct. `:focus-visible` gets its own ring for the same reason —
   the default browser outline barely shows on this background either. */
.dp-sidebar .nav-link:hover,
.dp-sidebar .nav-link.active {
  background-color: var(--dp-color-brand-dark);
  color: var(--dp-color-brand-text);
}

.dp-sidebar .nav-link:focus-visible {
  outline: 2px solid var(--dp-color-brand-text);
  outline-offset: 2px;
}

/* Phase 15 §4.3 — the wordmark is a horizontal lockup: sized by height, width follows, and
   `max-width: 100%` stops it from ever forcing the rail wider than `--dp-sidebar-width`. */
.dp-sidebar-logo {
  height: var(--dp-sidebar-logo-h);
  max-width: 100%;
}

.dp-topbar-select {
  width: auto;
}

.dp-centered-message {
  min-height: var(--dp-message-min-h);
}

/* Shared components — src/views/components/ */

.dp-page-header {
  gap: var(--dp-space-3);
}

.dp-card {
  border-radius: var(--dp-radius-md);
}

.dp-card-header {
  border-bottom: 1px solid var(--dp-color-bg);
}

.dp-data-table-wrapper {
  width: 100%;
}

.dp-empty-state-icon {
  display: block;
  font-size: var(--dp-empty-icon-size);
  margin-bottom: var(--dp-space-2);
}

.dp-money {
  font-variant-numeric: tabular-nums;
}

.dp-status-badge {
  font-weight: 500;
}

.dp-form-actions {
  gap: var(--dp-space-2);
}

.dp-auth-wrapper {
  min-height: var(--dp-viewport-min-h);
}

.dp-auth-card {
  width: 100%;
  max-width: var(--dp-auth-card-width);
}

.dp-table-thumb {
  width: var(--dp-thumb-size);
  height: var(--dp-thumb-size);
  object-fit: cover;
  border-radius: var(--dp-radius-sm);
}

.dp-table-thumb-placeholder {
  width: var(--dp-thumb-size);
  height: var(--dp-thumb-size);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--dp-color-muted);
  background-color: var(--dp-color-bg);
  border-radius: var(--dp-radius-sm);
}

.dp-goods-image-preview {
  width: var(--dp-image-preview-size);
  height: var(--dp-image-preview-size);
  object-fit: cover;
  border-radius: var(--dp-radius-md);
  box-shadow: var(--dp-shadow-sm);
}

.dp-chooser-selected:empty {
  display: none;
}

.dp-chooser-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--dp-space-1);
}

.dp-chooser-modal {
  z-index: var(--dp-z-chooser);
}

.dp-chooser-backdrop {
  z-index: var(--dp-z-chooser-backdrop);
}

.dp-chooser-table {
  width: 100%;
}

.dp-chooser-modal-count {
  white-space: nowrap;
}

/* Phase 17 §2.1 — components/repeater.ejs: a row is [label][material chooser][remove], one row
   per (material, label) pair. `.dp-repeater-row-label-input`/`-static` share one width token so
   an editable label and a goods-defined static one line up in the same column. */
.dp-repeater-row {
  display: flex;
  align-items: center;
  gap: var(--dp-space-2);
  margin-bottom: var(--dp-space-2);
}

.dp-repeater-row-label-input,
.dp-repeater-row-label-static {
  width: var(--dp-repeater-label-w);
  flex: 0 0 auto;
}

.dp-repeater-row-label-static {
  color: var(--dp-color-muted);
}

.dp-repeater-row-material {
  flex: 1 1 auto;
  min-width: 0;
}

.dp-repeater-row-material .dp-chooser-selected {
  margin-bottom: 0;
}

/* Phase 17 fix — a repeater living inside a TABLE CELL (the cashier cart line, §2.4) has nowhere
   near the horizontal room the goods form has. Side by side, a 9rem label box plus a chooser plus
   the remove button gave the Material column an intrinsic width of 342px out of an 853px cart
   table: `Barang` collapsed to 75px (a three-line wrap of the goods name), `Harga Satuan` to 70px
   (the price input squeezed to nothing), and Total/remove fell off the end behind a horizontal
   scrollbar. That is the "row height becomes a problem" escape hatch §2.4 named — taken as a
   layout change, not as the summary-plus-edit-modal fallback, because the cell still fits inline
   editing once the label stops competing with the picker for the same line.

   Compact mode stacks instead: the label and the row's own remove button share line 1, the picker
   gets line 2 to itself, so a row is only as wide as its picker. (Leaving remove beside the picker
   pushed it onto a third line whenever a material name was long — the chooser badge is
   `nowrap`, so the picker's min-content width is the badge, and the two together no longer fit.)
   Opt-in via `.dp-repeater-compact` — the goods form keeps the wide side-by-side layout above,
   where it reads correctly. */
.dp-repeater-compact .dp-repeater-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    'label remove'
    'material material';
  gap: var(--dp-space-1) var(--dp-space-2);
}

.dp-repeater-compact .dp-repeater-row-material {
  grid-area: material;
}

.dp-repeater-compact .dp-repeater-row-remove {
  grid-area: remove;
}

.dp-repeater-compact .dp-repeater-row + .dp-repeater-row {
  border-top: 1px solid var(--dp-color-border);
  padding-top: var(--dp-space-2);
}

.dp-repeater-compact .dp-repeater-row-label-input,
.dp-repeater-compact .dp-repeater-row-label-static {
  grid-area: label;
  width: auto;
}

.dp-repeater-compact .dp-repeater-row-label-static {
  font-size: var(--dp-font-size-sm);
  line-height: 1.2;
}

/* Phase 17 §3.6 — components/phone-input.ejs: the country select stacks above the national
   number box, each full width of the field — this control lives in narrow sidebar/summary
   columns (cashier customer card, order detail's `col-xl-3`) where a side-by-side row leaves the
   national box too narrow to type into. */
.dp-phone-input-row {
  display: flex;
  flex-direction: column;
  gap: var(--dp-space-1);
}

.dp-phone-input-code {
  width: 100%;
}

/* Phase 13b §8 — components/autocomplete.ejs. The listbox floats under the input; Bootstrap's
   own `list-group`/`list-group-item` classes give every row its styling, so no new rule is
   needed beyond positioning the list itself and marking the keyboard-active row.
   Phase 13c §8 — the material control sits inside `.table-responsive` (13A-1 §3.1's own
   `overflow-x: auto`, which the browser resolves the other axis of to `auto` too, clipping any
   `position: absolute` list that extends past the wrapper's own bottom edge — a short cart
   cropped the list almost entirely). `position: fixed` escapes any clipping ancestor by
   definition; `DP.initAutocomplete` computes `top`/`left`/`width` from the input's own
   `getBoundingClientRect()` and keeps them updated on scroll/resize, so the static `top: 100%` /
   `left/right: 0` rules are gone — there is nothing left for CSS alone to position. This rule
   generalises: ANY floating layer inside `.table-responsive`, a card body or a modal must be
   `position: fixed`, never `absolute` — `data-bs-boundary="viewport"` (13B §4, the Kolom
   dropdown) only constrains Popper's overflow *calculation*, it does not let an element escape a
   clipping ancestor. */
.dp-autocomplete {
  position: relative;
}

.dp-autocomplete-listbox {
  position: fixed;
  z-index: var(--dp-z-autocomplete);
  max-height: 16rem;
  overflow-y: auto;
  box-shadow: var(--dp-shadow-sm);
}

.dp-autocomplete-listbox li {
  cursor: pointer;
}

.dp-search-card {
  border-radius: var(--dp-radius-md);
}

.dp-search-card-actions {
  gap: var(--dp-space-2);
}

.dp-cashier-cart-table {
  min-width: 640px;
}

.dp-cashier-col-qty {
  width: 9rem;
}

.dp-cashier-col-price {
  width: 10rem;
}

.dp-cashier-col-materials {
  min-width: 12rem;
}

.dp-cashier-line-thumb {
  width: var(--dp-thumb-size);
  height: var(--dp-thumb-size);
  object-fit: cover;
  border-radius: var(--dp-radius-sm);
}

.dp-cashier-qty-stepper {
  width: 8.5rem;
}

.dp-cashier-pct-row .dp-cashier-pct-btn {
  flex: 1;
}

.dp-cashier-totals dt,
.dp-cashier-totals dd {
  margin-bottom: var(--dp-space-1);
}

/* Phase 13a-3 §5 — the Detail Pelanggan/Konfirmasi column sticks under the topbar on `lg`+ only;
   below `lg` everything stacks in plain DOM order (no sticky behaviour to fight while stacked). */
@media (min-width: 992px) {
  .dp-cashier-sticky-col {
    position: sticky;
    top: var(--dp-cashier-sticky-top);
  }
}

/* Phase 15 §8.2 — the per-line materials chooser (one or more materials per line); no custom
   sizing needed beyond containing it in its own table cell. */
.dp-cashier-line-material {
  min-width: 0;
}

/* Phase 13a-1 §4 — the one list-screen component: filter bar, toolbar, responsive expand
   column, Kolom dropdown. Replaces the order-module-only versions of these same rules (every
   list screen gets them now, not just order — see components/list-screen.ejs). */

.dp-list-filter-group .dp-chooser {
  margin-bottom: 0;
}

/* Phase 13c §2 — even, visible spacing between filter rows (previously touching — the flex column
   had no gap at all). One token on the shared container, not a per-row `mb-2`. */
.dp-list-filters {
  gap: var(--dp-list-filter-row-gap);
}

/* Phase 13b §2.1 — one column of label+control rows; the control itself is capped so it does not
   stretch across a wide screen. */
.dp-list-filter-control {
  max-width: var(--dp-list-filter-control-max-w);
}

/* Phase 13c §3 — a chooser filter's control cell holds a button plus badges, not one input;
   opting it out of the shared cap (rather than raising the cap for every filter type) keeps every
   other filter's width unchanged. */
.dp-list-filter-control-chooser {
  max-width: none;
}

/* Phase 13c §2 — the `s/d`/`to` separator between a date range's two inputs, vertically centred
   against the date inputs beside it. */
.dp-list-filter-daterange-sep {
  white-space: nowrap;
}

/* Phase 13b §2.2 — an unset chooser filter reads "Semua" instead of a blank badge row. `:empty`
   ignores generated content, so this fires only when no badge has been rendered into the div. */
.dp-list-filter-group .dp-chooser-selected:empty::before {
  content: attr(data-dp-filter-empty-label);
  color: var(--dp-color-muted);
}

.dp-list-toolbar {
  min-height: 2.25rem;
}

.dp-list-select-cell {
  width: 2.5rem;
  text-align: center;
}

.dp-list-expand-cell {
  width: 2rem;
  text-align: center;
  cursor: pointer;
  color: var(--dp-color-muted);
}

.dp-list-row-expanded .dp-list-expand-cell i {
  transform: rotate(90deg);
  display: inline-block;
}

.dp-list-child-row dt,
.dp-list-child-row dd {
  margin-bottom: var(--dp-space-1);
}

.dp-search-card-plain .dp-search-card-form {
  margin-bottom: 0;
}

/* Phase 13a-4 §6 — see the `--dp-table-cell-padding-*` token comment above. */
.dp-list-table > :not(caption) > * > * {
  padding: var(--dp-table-cell-padding-y) var(--dp-table-cell-padding-x);
}

.dp-list-table.dp-table-dense > :not(caption) > * > * {
  padding: var(--dp-table-cell-padding-y-dense) var(--dp-table-cell-padding-x-dense);
}

/* Phase 13a-4 §4 — `form-field.ejs`'s `readonly: true` param: a flattened control (no focus
   ring, muted border, unchanged text colour) — never `disabled`, see that param's own doc
   comment for why. */
.dp-readonly {
  background-color: var(--dp-color-bg);
  border-color: var(--dp-color-border-muted);
  color: inherit;
}

.dp-readonly:focus {
  box-shadow: none;
  border-color: var(--dp-color-border-muted);
}

/* Phase 13a-4 §3 — `DP.confirmDialog` opened while another modal (the order cancel modal) is
   already showing. Lifted above the parent modal's own 1055/1050 default stack, mirroring
   `.dp-chooser-modal`/`.dp-chooser-backdrop` (phase-07a §2.2) — applied only when actually
   stacked (`confirmDialog`'s own JS), never unconditionally, so a normal (non-stacked) confirm
   keeps Bootstrap's plain default z-index. */
.dp-confirm-modal-stacked {
  z-index: var(--dp-z-confirm);
}

.dp-confirm-backdrop-stacked {
  z-index: var(--dp-z-confirm-backdrop);
}

/* Phase 13b §4 — the common case is now a multi-column grid sized to its content: nothing
   scrolls, nothing is clipped. `max-height`/`overflow-y` stay only as a last-resort guard for a
   genuinely huge table on a short viewport, so the failure mode is a scrollbar, not an
   unreachable row.
   Phase 13e §2 — `overflow-y: auto` here forces `overflow-x: auto` on this same element (CSS
   resolves a `visible` axis to `auto` when the other axis isn't `visible`). Nothing inside this
   menu may overhang its own box, because an overhanging element on the auto axis is silently
   clipped with no scrollbar to reveal it. In particular: never put a Bootstrap spacing utility
   (`px-*`/`ps-*`) on a `.form-check` — its input is positioned by `margin-left: -1.5em` against
   the class's own `padding-left: 1.5em`, and overriding just the padding leaves the negative
   margin pulling the input outside the box. Put horizontal inset on the menu or the grid instead. */
.dp-columns-menu {
  max-height: 80vh;
  overflow-y: auto;
  min-width: var(--dp-columns-menu-min-w);
  max-width: var(--dp-columns-menu-max-w);
  padding-top: var(--dp-space-1);
  padding-bottom: var(--dp-space-1);
}

.dp-columns-menu-grid {
  display: grid;
  grid-auto-flow: column;
  grid-template-rows: repeat(var(--dp-columns-menu-max-rows), auto);
  column-gap: var(--dp-space-3);
  padding-left: var(--dp-space-2);
  padding-right: var(--dp-space-2);
}

.dp-chooser-modal-per-item .dp-chooser-modal-per-item-row {
  display: flex;
  align-items: center;
  gap: var(--dp-space-2);
  padding: var(--dp-space-1) 0;
  border-bottom: 1px solid var(--dp-color-bg);
}

.dp-chooser-modal-per-item .dp-chooser-modal-per-item-label {
  flex: 1 1 auto;
  min-width: 0;
}

.dp-chooser-modal-per-item .dp-chooser-modal-per-item-control {
  flex: 0 0 auto;
  width: 12rem;
}

/* Phase 13a-1 §4.9 — order module inline `style=` widths, moved here */

.dp-order-timeline-date {
  min-width: var(--dp-order-timeline-date-w);
}

.dp-order-qty-stepper {
  max-width: var(--dp-order-qty-stepper-w);
}

.dp-order-col-materials {
  min-width: var(--dp-order-materials-col-w);
}

.dp-order-col-note {
  min-width: var(--dp-order-note-col-w);
}

/* Phase 10A — cashier goods browser (replaces the goods chooser modal on the register screen) */

.dp-goods-browser-thumb {
  width: var(--dp-goods-browser-thumb-size);
  height: var(--dp-goods-browser-thumb-size);
  object-fit: cover;
  border-radius: var(--dp-radius-sm);
}

.dp-goods-browser-thumb-placeholder {
  width: var(--dp-goods-browser-thumb-size);
  height: var(--dp-goods-browser-thumb-size);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--dp-color-muted);
  background-color: var(--dp-color-bg);
  border-radius: var(--dp-radius-sm);
}

/* Phase 13a-3 §3.2 — gap dropped one step (`--dp-space-3` -> `--dp-space-2`), cards narrower
   (`--dp-goods-browser-grid-min-w`, tokened above). */
.dp-goods-browser-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--dp-goods-browser-grid-min-w), 1fr));
  gap: var(--dp-space-2);
}

/* Phase 13b §5.1 — `overflow: hidden` is what keeps the thumbnail's corners inside the card's
   own radius; before this, the image carried its own separate top-corner radius that could
   disagree with the card's, which read as "the image not included in the card".
   Phase 13c §5 — a real border (via `--dp-color-border`) is what makes the card readable against
   the browser's own white panel; `shadow-sm` alone was invisible at white-on-white. Every card
   here is a click target, so hovering strengthens both the shadow and the border. */
.dp-goods-browser-card {
  border-radius: var(--dp-radius-md);
  height: 100%;
  overflow: hidden;
  border: 1px solid var(--dp-color-border);
  transition: box-shadow 0.15s ease, border-color 0.15s ease;
}

.dp-goods-browser-card:hover {
  box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.12);
  border-color: var(--bs-primary);
}

/* Phase 13b §5.2 — the wrapper owns the 1:1 ratio; the image and placeholder just fill it
   (`object-fit: cover` on the image only, since the placeholder has no intrinsic image to fit).
   Phase 13d — `aspect-ratio` alone did NOT hold here, measured live: Bootstrap's `.card` is
   `display: flex; flex-direction: column`, so this wrapper is a flex item whose main size is its
   HEIGHT while `aspect-ratio` needs the cross size (width) to derive it — a circularity the flex
   algorithm resolves from the content's intrinsic size instead of the ratio. Two cards in one row
   measured 134x216 and 134x201: neither square, and not even equal to each other. A padding-based
   ratio box has no such interaction with flex sizing, so the square is guaranteed by construction;
   `flex: 0 0 auto` additionally stops the card's fixed row height from stretching or shrinking it.
   Do not "simplify" this back to `aspect-ratio` on the wrapper. */
.dp-goods-browser-grid-thumb-wrap {
  position: relative;
  width: 100%;
  padding-top: 100%; /* 1:1 — percentage padding resolves against WIDTH, so it is always square */
  flex: 0 0 auto;
  overflow: hidden;
}

.dp-goods-browser-grid-thumb,
.dp-goods-browser-grid-thumb-placeholder {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.dp-goods-browser-grid-thumb {
  object-fit: cover;
  display: block;
}

.dp-goods-browser-grid-thumb-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--dp-color-muted);
  background-color: var(--dp-color-bg);
  font-size: var(--dp-empty-icon-size);
}

/* Phase 13a-3 §3.2 — name clamped to two lines so a long product name never changes a card's
   height; the full name is still available via the element's own `title` attribute.
   Phase 13b §5.2 — `min-height` makes that true even for a one-line name: without it the clamp
   only bounds the tall case, so a one-word name still produced a shorter card than a two-line
   one in the same grid row. */
.dp-goods-browser-grid-name {
  font-size: var(--dp-font-size-sm);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  min-height: var(--dp-goods-browser-grid-name-h);
}

.dp-goods-browser-list-row {
  border-bottom: 1px solid var(--dp-color-bg);
}

/* Phase 13a-3 §7.2 — components/result-modal.ejs, the shared success/failure post-action modal
   (`DP.resultDialog`). One icon size; colour comes from Bootstrap's own `text-success`/
   `text-danger` utilities, applied by `app.js#resultDialog` per the `variant` option. */
.dp-result-icon {
  font-size: var(--dp-result-icon-size);
}

.dp-result-details li {
  margin-bottom: var(--dp-space-1);
}

.dp-toast-container {
  position: fixed;
  bottom: var(--dp-toast-offset);
  right: var(--dp-toast-offset);
  z-index: var(--dp-z-toast);
  display: flex;
  flex-direction: column;
  gap: var(--dp-space-2);
  width: var(--dp-toast-width);
  max-width: calc(100vw - 2 * var(--dp-toast-offset));
}
