/* ================= */
/* VARIABLES         */
/* ================= */

*, body, html {
  user-select: none !important;
  -webkit-user-select: none !important;
  -webkit-touch-callout: none !important;
}

[onclick], [onclick] * {
  cursor: pointer;
}

*:focus-visible {
  outline-color: #6c757d !important;
  box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.35) !important;
}

/* Escala global: todos los tamaños en rem derivan de acá */
html[data-font-size="large"],
html[data-font-size="xlarge"] {
  font-size: 20px;
}

/* xlarge: sube solo las clases de texto, no el layout rem */
html[data-font-size="xlarge"] .t1 { font-size: 1.4rem; }
html[data-font-size="xlarge"] .t2 { font-size: 1.25rem; }
html[data-font-size="xlarge"] .t3 { font-size: 1.1rem; }
html[data-font-size="xlarge"] .t4 { font-size: 0.95rem; }
html[data-font-size="xlarge"] .t5 { font-size: 1.55rem; }

/* xlarge: card de categoría más ancha para acomodar el texto más grande */
html[data-font-size="xlarge"] .app-category-item {
  width: 6.5rem;
  padding: 0.625rem 0.5rem;
}

/* ======================================== */
/* RESPONSIVE BREAKPOINTS × FONT-SIZE TIERS  */
/* ======================================== */
/*
 * Every @media (max-width: ...) rule in this file is written in raw px, calibrated against the
 * "normal" font-size tier (the browser default, ~16px root). Under the "large"/"xlarge" tiers
 * (html[data-font-size], 20px root — see above, a 1.25x scale) the SAME content needs more room
 * to render without wrapping/overflowing at the SAME viewport width, so every such breakpoint
 * needs a second, 1.25x-scaled copy scoped to those two attribute selectors.
 *
 * This can't be done with em/rem units on the @media rule itself: per spec, em/rem in a media
 * feature always resolve against the browser's INITIAL default font-size, never against an
 * author stylesheet's html { font-size } override — so max-width: 25em is just a fixed 400px,
 * completely blind to data-font-size. CSS custom properties aren't usable in @media conditions
 * either (no browser support for that yet). Hand-duplicating each threshold is the only reliable
 * option without a build-time preprocessor.
 *
 * The pattern for every existing and future px breakpoint that depends on text/content fitting a
 * width (not for one that's a pure layout/device concern, like a two-column-vs-one-column
 * decision that has nothing to do with font size):
 *   @media (max-width: Npx) { .some-class { ... } }
 *   @media (max-width: round(N*1.25)px) {
 *     html[data-font-size="large"] .some-class,
 *     html[data-font-size="xlarge"] .some-class {
 *       ... same declarations ...
 *     }
 *   }
 * The attribute-scoped selector has higher specificity than the plain one, so it always wins
 * when both could match — no dependence on which is declared first. Relative order between
 * multiple font-scaled rules for the SAME selector still has to mirror their plain counterparts.
 */

body {
  --x-background-color-primary: #2563EB;
  --x-border-color-primary: #2563EB;

  --x-background-color-secondary: #1D4ED8;
  --x-border-color-secondary: #1D4ED8;

  --x-background-color-accent: #F0B43C;
  --x-border-color-accent: #E5A828;

  --x-stripe-color-primary: #000000;
  --x-stripe-color-secondary: #FFFFFF;

  --x-background-color-violet: #6F42C1;
  --x-border-color-violet: #6F42C1;

  --x-background-color-neutral-light: #F5F5F5;
  --x-border-color-neutral-light: #F5F5F5;

  --x-background-color-neutral-dark: #CCCCCC;
  --x-border-color-neutral-dark: #CCCCCC;

  --x-font-color-light: #666666;
  --x-font-color-dark: #333333;
}

/* ================= */
/* LAYOUT            */
/* ================= */

/* client.mjs's App: caps every staff screen at 900px, centered, with the body's own background
   showing on both sides as gray side columns once the viewport is wider than that. */
.app-viewport {
  max-width: 900px;
  margin: 0 auto;
  background: #fff;
}

@media (min-width: 901px) {
  body {
    background: var(--x-background-color-neutral-light);
  }
}

/* The app's own root shell (PosShell.mjs and Login.mjs's two screens) — one level above
   Layout.mjs's per-screen LayoutContainer, clipped so an inner screen's own scrolling never
   leaks out to the whole page. */
.app-root-shell {
  height: 100dvh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* PosShell.mjs: the history tab panel and each ticket's TicketView panel — all siblings stacked
   in the same spot, only one visible at a time (display toggled via Bootstrap's d-flex/d-none so
   this class only needs to carry what's common to both states). */
.app-pos-tab-panel {
  flex: 1;
  overflow: hidden;
  flex-direction: column;
}

/* Layout.mjs: LayoutContainer/Header/Content/Footer, the header+scroll-body+footer flex shell
   every screen is built from. */
.app-layout-container {
  height: 100dvh;
  display: flex;
  flex-direction: column;
}

.app-layout-header {
  flex-shrink: 0;
}

/* overflow-x:hidden is deliberate: wide content (e.g. a table with many columns) must scroll
   inside its own overflow-x:auto container (see .table-responsive) — without this, that
   horizontal overflow leaks up into this shared scroll area instead, showing a scrollbar under
   the whole screen (including content that never asked to scroll horizontally) rather than just
   under the wide element itself. */
.app-layout-content {
  flex: 1;
  overflow-y: auto;
  overflow-x: hidden;
  padding-bottom: 2rem;
  padding-top: 1.5rem;
}

/* z-index:20, not 1 — .payment-method-card's own remove-badge (.app-tile-remove-badge) sits at
   z-index:2, and neither .app-layout-content nor anything between it and the card establishes its
   own stacking context, so that badge's z-index was being compared directly against this element's
   in the SAME top-level context. Whenever a card scrolled to sit right where the sticky bottom
   bar's collapse-toggle floats (PaymentMethodConfigurator.mjs, itself z-index:2 for the same
   reason), the badge's 2 beat this footer's old 1 and painted its red X on top of the bar
   regardless of DOM order. 20 clears every in-content z-index used today (2) with room to spare,
   while staying well below the modal (1200) / expandable-popover (1000) tiers. */
.app-layout-footer {
  flex-shrink: 0;
  margin-bottom: 10px;
  position: relative;
  z-index: 20;
  background: white;
}

/* Pairs with .app-bottom-bar-slot-inline-body's own padding-bottom below — see BottomBarSlot.mjs's
   own doc on why its INLINE mode needs .app-layout-content's usual padding-bottom suppressed
   here instead of just living with it. */
.app-layout-content--flush-bottom {
  padding-bottom: 0;
}

/* BottomBarSlot.mjs's INLINE mode — see its own doc for why min-height:100% + margin-top:auto on
   the last child (the bottom bar itself) is what makes it sit at the bottom of the screen for
   short content, the same as STICKY, while still scrolling to the real end of the list for tall
   content instead of staying fixed. */
.app-bottom-bar-slot-inline {
  display: flex;
  flex-direction: column;
  min-height: 100%;
}

.app-bottom-bar-slot-inline > :last-child {
  margin-top: auto;
  margin-bottom: 12px;
}

/* The gap .app-layout-content would otherwise leave AFTER everything (its own padding-bottom,
   suppressed above) lives here instead — between the list and the bottom bar, where it actually
   belongs, rather than as dead space below a bar nothing ever scrolls past.
   flex:1 (rather than the default content-driven size) gives this a genuinely resolved height —
   needed so a payment step's own .payment-content-fill (CashPayment.mjs/CardPayment.mjs/etc,
   already the exact same min-height:100% pattern one level up) keeps resolving that percentage
   correctly once BottomBarSlot.mjs nests it inside this wrapper instead of directly inside
   .app-layout-content. */
.app-bottom-bar-slot-inline-body {
  flex: 1;
  padding-bottom: 1.5rem;
}

/* BrandIcon.mjs's own wrapper div — almost always used as the trailing "end of the list" marker
   (see the mt-4 audit), so it just needs to center the logo image inside whatever width it sits
   in. */
.app-brand-icon {
  text-align: center;
}

/* ================= */
/* TYPOGRAPHY        */
/* ================= */

.t1, .t2 {
  font-family: "Poppins", serif;
  font-style: normal;
}

.t1 {
  font-size: 1.125rem;
  font-weight: 200;
  color: var(--x-font-color-dark);
}

.t2 {
  font-size: 1rem;
  font-weight: 300;
  color: var(--x-font-color-light);
}

.t3, .t4 {
  font-family: "Roboto", serif;
  font-style: normal;
  line-height: 1.2;
}

.t3 {
  font-size: 0.875rem;
  font-weight: 300;
  color: var(--x-font-color-light);
}

.t4 {
  font-size: 0.75rem;
  font-weight: 300;
  color: var(--x-font-color-light);
}

.t5 {
  font-size: 1.25rem;
  font-weight: 500;
  color: var(--x-font-color-dark);
}

/* ================= */
/* TOAST             */
/* ================= */

@keyframes toast-enter {
  from { opacity: 0; transform: translateY(-10px) scaleX(0.96); }
  to   { opacity: 1; transform: translateY(0)     scaleX(1);    }
}

@keyframes toast-leave {
  from { opacity: 1; transform: translateY(0)    scaleX(1);    }
  to   { opacity: 0; transform: translateY(-6px) scaleX(0.96); }
}

/* ================= */
/* BLINKING CURSOR   */
/* ================= */

@keyframes cursor-blink {
  0% {
    opacity: 0;
  }
}

.blink-cursor::after {
  content: "";
  position: relative;
  top: 5px;
  width: 10px;
  height: 25px;
  background: #000;
  display: inline-block;
  animation: cursor-blink 1s steps(2) infinite;
}

/* ================= */
/* RADIO ICON BUTTON */
/* ================= */

.radio-icon-button > div {
  padding: 0.5rem 1rem;
  font-size: 1.25rem;
  line-height: 1;
  border-radius: 0.5rem;
  background-color: #e9e9e9;
  border-color: #cdcbcb;
  color: black;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.25rem;
  min-height: 1.25rem;
}

.radio-icon-button > input:checked ~ div,
.radio-icon-button.checked-static > div {
  color: white;
  background-color: var(--x-background-color-primary);
}

.radio-icon-button.violet > input:checked ~ div,
.radio-icon-button.checked-static.violet > div {
  background-color: var(--x-background-color-violet);
}

/* Payment-method-only checked style: light tint + colored border + corner checkmark badge,
   instead of the plain solid-fill checked look other `.radio-icon-button` consumers keep using. */
.radio-icon-button.selected-badge > div {
  position: relative;
  border: 2px solid transparent;
}

.radio-icon-button.selected-badge > input:checked ~ div,
.radio-icon-button.selected-badge.checked-static > div {
  color: black;
  background-color: color-mix(in srgb, var(--x-background-color-secondary) 18%, #e9e9e9);
  border-color: var(--x-background-color-secondary);
}

.radio-icon-button.selected-badge.violet > input:checked ~ div,
.radio-icon-button.selected-badge.checked-static.violet > div {
  background-color: color-mix(in srgb, var(--x-background-color-violet) 18%, #e9e9e9);
  border-color: var(--x-background-color-violet);
}

.payment-method-check {
  position: absolute;
  top: -6px;
  right: -6px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--x-background-color-secondary);
  color: #fff;
  font-size: 0.65rem;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  transform: scale(0.5);
  transition: opacity 0.12s ease, transform 0.12s ease;
}

.radio-icon-button.selected-badge > input:checked ~ div .payment-method-check,
.radio-icon-button.selected-badge.checked-static .payment-method-check {
  opacity: 1;
  transform: scale(1);
}

.radio-icon-button.selected-badge.violet > input:checked ~ div .payment-method-check,
.radio-icon-button.selected-badge.checked-static.violet .payment-method-check {
  background: var(--x-background-color-violet);
}

.radio-icon-button:not(.points-disabled):hover > div,
.radio-icon-button:not(.points-disabled) > input:hover ~ div {
  filter: brightness(0.9);
}

.radio-icon-button:not(.points-disabled):active > div,
.radio-icon-button:not(.points-disabled) > input:active ~ div {
  filter: brightness(0.8);
}

.radio-icon-button.points-disabled {
  cursor: default;
}

.radio-icon-button.points-disabled > div {
  opacity: 0.2;
  box-shadow: 0 0 0 1.5px #6c757d;
}

.radio-icon-button.points-insufficient > div {
  box-shadow: 0 0 0 1.5px #dc3545;
}

.radio-icon-button.selected-badge.points-insufficient > input:checked ~ div,
.radio-icon-button.selected-badge.points-insufficient.checked-static > div {
  background-color: color-mix(in srgb, #dc3545 25%, #e9e9e9);
  border-color: #dc3545;
  box-shadow: none;
}

.radio-icon-button.selected-badge.points-insufficient > input:checked ~ div .payment-method-check,
.radio-icon-button.selected-badge.points-insufficient.checked-static .payment-method-check {
  opacity: 0;
  transform: scale(0.5);
}

@keyframes points-shake-kf {
  0%, 100% { transform: translateX(0);    box-shadow: none;                          }
  20%      { transform: translateX(-6px); box-shadow: 0 0 0 2px #adb5bd;             }
  40%      { transform: translateX(6px);  box-shadow: 0 0 0 2px #adb5bd;             }
  60%      { transform: translateX(-4px); box-shadow: 0 0 0 2px #adb5bd;             }
  80%      { transform: translateX(3px);  box-shadow: 0 0 0 1px rgba(173,181,189,0.5); }
}

@keyframes points-shake-red-kf {
  0%, 100% { transform: translateX(0);    box-shadow: none;                        }
  15%      { transform: translateX(-6px); box-shadow: 0 0 0 2px #dc3545;           }
  35%      { transform: translateX(6px);  box-shadow: 0 0 0 2px #dc3545;           }
  55%      { transform: translateX(-4px); box-shadow: 0 0 0 2px #dc3545;           }
  75%      { transform: translateX(3px);  box-shadow: 0 0 0 1px rgba(220,53,69,0.5); }
  90%      { transform: translateX(0);    box-shadow: none;                        }
}

.radio-icon-button {
  display: inline-block;
  border-radius: 0.5rem;
  cursor: pointer;
}

.radio-icon-button.points-shake-neutral { animation: points-shake-kf 0.3s ease; }
.radio-icon-button.points-shake-red     { animation: points-shake-red-kf 0.3s ease; }
.badge.points-shake-neutral             { animation: points-shake-kf 0.3s ease; }

/* ================= */
/* SEGMENTED CONTROL */
/* ================= */

.app-segmented-control-stack {
  display: inline-flex;
  flex-direction: column;
  align-items: stretch;
}

.app-segmented-control {
  display: inline-flex;
  background: #e0e0e0;
  border-radius: 99px;
  padding: 3px;
  gap: 2px;
  /* Paints over the finish tab tucked up behind it, so the two read as one shape. */
  position: relative;
  z-index: 1;
}

/* The way out of edit mode: a tab growing out of the bottom of the active segment. Every segment
   puts a zero-height ghost of its own label in this row, so a cell is exactly as wide as the
   segment above it and the tab lands under the right one — by layout, with nothing measured, and
   it moves along when the selection changes. */
.app-segmented-control-tabs {
  display: flex;
  padding: 0 3px;
  gap: 2px;
}

.app-segmented-control-tab-cell {
  position: relative;
  display: block;
}

.app-segmented-control-tab-ghost {
  display: block;
  height: 0;
  overflow: hidden;
  padding: 0 22px;
  font-size: 0.9rem;
  font-weight: 500;
  visibility: hidden;
}

@keyframes app-segmented-control-tab-in-kf {
  from { opacity: 0; transform: translateY(-8px); }
  to   { opacity: 1; transform: translateY(0); }
}

@keyframes app-scan-indicator-flip-kf {
  from { opacity: 0; transform: rotateY(90deg); }
  to   { opacity: 1; transform: rotateY(0); }
}

.app-scan-indicator-flip-kf {
  display: inline-block;
  animation: app-scan-indicator-flip-kf 0.3s ease-out;
}

.app-segmented-control-tab {
  display: block;
  width: 100%;
  margin-top: -16px;
  padding: 20px 4px 6px;
  border: none;
  border-radius: 0 0 14px 14px;
  background: #dc3545;
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.18);
  color: white;
  font-size: 0.78rem;
  font-weight: 600;
  cursor: pointer;
  animation: app-segmented-control-tab-in-kf 0.18s ease-out;
}

.app-segmented-control-tab:active {
  filter: brightness(0.94);
}

.app-segmented-control-option {
  border-radius: 99px;
  border: none;
  padding: 5px 22px;
  font-size: 0.9rem;
  font-weight: 500;
  transition: background 0.15s ease, box-shadow 0.15s ease;
  cursor: pointer;
  background: transparent;
  color: #777;
}

.app-segmented-control-option:disabled {
  cursor: not-allowed;
  opacity: 0.55;
}

.app-segmented-control.small .app-segmented-control-option {
  padding: 2px 12px;
  font-size: 0.8rem;
}

.app-segmented-control-option.active {
  background: white;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.18);
  color: #222;
}

/* Below 480px: only the horizontal padding shrinks (22px -> 12px, same value the .small variant
   already uses) — padding-left/right rather than the padding shorthand, so the vertical padding
   (5px normal, 2px .small) is left untouched. */
@media (max-width: 479px) {
  .app-segmented-control-option {
    padding-left: 12px;
    padding-right: 12px;
  }
}

/* Below 380px: narrower still — 8px horizontal padding and a smaller font, on top of (declared
   after, so it wins over) the 479px step above. */
@media (max-width: 379px) {
  .app-segmented-control-option {
    padding-left: 8px;
    padding-right: 8px;
    font-size: 0.8rem;
  }
}

/* FONT-SIZE-TIER-SCALED BREAKPOINTS — see the "RESPONSIVE BREAKPOINTS × FONT-SIZE TIERS" doc
   comment near the top of the file: every px-based @media step above needs a 1.25x-scaled twin
   here, scoped to html[data-font-size="large"/"xlarge"], since @media units never track that
   attribute's own font-size override. Same relative order as the plain rules above (narrower wins
   via source order only where specificity ties; the attribute-scoped selector's higher specificity
   already makes it win over the plain rule whenever both match, regardless of order). */
@media (max-width: 599px) {
  html[data-font-size="large"] .app-segmented-control-option,
  html[data-font-size="xlarge"] .app-segmented-control-option {
    padding-left: 12px;
    padding-right: 12px;
  }
}
@media (max-width: 474px) {
  html[data-font-size="large"] .app-segmented-control-option,
  html[data-font-size="xlarge"] .app-segmented-control-option {
    padding-left: 8px;
    padding-right: 8px;
    font-size: 0.8rem;
  }
}

/* ======================= */
/* CATALOG EDIT MODE       */
/* ======================= */

.app-catalog-editing .app-segmented-control-option.active {
  animation: app-catalog-edit-glow-kf 1.2s ease-in-out infinite alternate;
}

@keyframes app-catalog-edit-glow-kf {
  from { box-shadow: 0 0 0 1px #dc3545, 0 0 1px 1px rgba(220, 53, 69, 0.5); }
  to   { box-shadow: 0 0 0 1px #dc3545, 0 0 5px 2px rgba(220, 53, 69, 0.8); }
}

/* Same red pulsing glow as app-catalog-finish-button's "Finalizar" — flags a badge that needs
   attention (e.g. a CONFIRMED PurchaseOrder that's still UNPAID, see purchaseOrderBadges.mjs). */
.app-badge-glow-red {
  animation: app-catalog-edit-glow-kf 1.2s ease-in-out infinite alternate;
}

/* Sits where the next group would go, at the end of the list, rather than up by the view selector
   — an outline of the panel it creates. */
.app-add-category-group-button {
  width: calc(100% - 1rem);
  margin: 0 0.5rem;
  padding: 0.75rem;
  border: 2px dashed #d0d0d0;
  border-radius: 20px;
  background: transparent;
  color: #999;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  cursor: pointer;
  transition: filter 0.1s ease, border-color 0.1s ease, color 0.1s ease;
}

.app-add-category-group-button:hover {
  border-color: #b8b8b8;
  color: #777;
}

.app-add-category-group-button:active {
  filter: brightness(0.9);
}

.app-add-category-group-button:disabled {
  opacity: 0.5;
}

.app-catalog-finish-button {
  padding: 6px 26px;
  border: none;
  border-radius: 99px;
  background: #dc3545;
  color: white;
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  animation: app-catalog-edit-glow-kf 1.2s ease-in-out infinite alternate;
}

/* ======================= */
/* DRAG TO REORDER         */
/* ======================= */

.app-drag-slot {
  position: relative;
}

/* [data-drag-handle] only ever renders on the one cell useSlotDrag.mjs has selected for dragging
   (a plain tap elsewhere selects THAT cell instead — see the hook's own doc) — every other cell
   carries none of this, so touch-action:none here can never block scrolling anywhere but the
   single tile someone deliberately chose to pick up. Only the handle opts out, never the whole
   cell: a category group's cell is an entire panel, and the page still has to scroll when dragged
   anywhere but its grip. */
[data-drag-handle] {
  touch-action: none;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}

/* The tap-selected cell (see useSlotDrag.mjs) — a further press-and-move drags it; anywhere else
   still just scrolls. Marked on the tile's own image and label rather than a box around the whole
   cell, since both a product and a category tile already share this exact markup
   (ProductWithLabel.mjs/Category.mjs) regardless of which one ends up inside a slot. */
.app-drag-slot-selected .app-glass-image-wrap {
  border: 2px solid var(--x-border-color-secondary);
}

/* Same border, as a plain press-down cue, on the read-only catalog's own product/category tiles
   (.app-long-pressable) — the one piece of tactile feedback tapping one otherwise has, since it
   has no :active state of its own beyond that. Explicitly excluded once a tile is reachable
   through useSlotDrag.mjs (:not(.app-catalog-editing-tile) — set by Category.mjs, the one
   .app-long-pressable this ever actually matters for while editing, since EditableProductTile.mjs
   never carries the class at all): the SAME finger-down that arms this border can also be the one
   that's about to select or deselect the tile, so its own native :active toggling and
   .app-drag-slot-selected's own JS-driven class toggling were fighting over the identical border on
   the identical element within the same gesture — a mismatch mobile Safari renders as a visible
   glitch rather than the two cleanly replacing one another. */
.app-long-pressable:not(.app-catalog-editing-tile):active .app-glass-image-wrap {
  border: 2px solid var(--x-border-color-secondary);
}

.app-drag-slot-selected p {
  display: inline-block;
  background-color: var(--x-background-color-primary);
  color: #fff;
  padding: 0 0.4rem;
  border-radius: 4px;
}

/* Safari starts its own image drag before any dragstart handler gets a say, and that cancels the
   pointer stream the reorder and the long press both run on. */
[data-drag-handle] img,
.app-long-pressable img {
  -webkit-user-drag: none;
}

.app-drag-slot-dragging {
  z-index: 20;
}

/* A group is dragged by a column down the inside of its panel, not by a grip tucked next to the
   name: a target as tall as the group itself, and it reads as picking up the group rather than its
   title — pressing anywhere in this column starts the drag, not just on the grip glyph itself.
   The grip icon is only ever its own normal size (standard drag-handle iconography, not a rail
   drawn to fill the space) and sits vertically centred via the auto margins on
   .app-category-panel-grip below — the two move buttons anchor to the top/bottom of this same
   column instead of floating loose elsewhere on the card. */
.app-category-panel-handle {
  flex-shrink: 0;
  width: 1.75rem;
  cursor: grab;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 0.25rem 0;
}

.app-category-panel-grip {
  margin: auto 0;
  color: #cfcfcf;
}

.app-category-panel-handle:active .app-category-panel-grip {
  color: #a5a5a5;
}

/* Swaps this group with the previous/next one — the same move a one-slot drag would make, offered
   as a plain tap for anyone who'd rather not drag. onpointerdown stopPropagation (see
   CategoryGroup.mjs) keeps the press from also starting the handle's own drag. */
.app-group-move-button {
  width: 1.75rem;
  height: 1.75rem;
  padding: 0;
  border: none;
  background: transparent;
  color: #999;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}

.app-group-move-button:disabled {
  opacity: 0.3;
  cursor: default;
}

/* Suppresses the native press-and-hold callout/selection, which would otherwise fire in the
   middle of the long press that enters edit mode and cancel the gesture. */
.app-long-pressable {
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}

/* ======================= */
/* EDITABLE TILES          */
/* ======================= */

.app-editable-tile {
  position: relative;
  cursor: pointer;
  border-radius: 12px;
}

/* A category or group's name while editing, styled like the very input it turns into on tap rather
   than a full-bleed divider bar — the whole box is the tap target, not just the pencil, so entering
   rename doesn't require aiming at a small icon. No justify-content of its own: left-aligned (the
   default) fits a group's name docked next to its own row; a caller centering its own name (the
   category screen's page-width title) adds justify-content-center itself. */
.app-editable-name {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  max-width: 100%;
  padding: 0.375rem 0.75rem;
  border: 1px solid #ced4da;
  border-radius: 0.375rem;
  background: white;
  cursor: pointer;
}

.app-editable-name:active {
  background: rgba(0, 0, 0, 0.03);
}

/* The box sizes to its own content and no further (see above) — an <input> can't do that on its
   own the way a <div> can, so EditableName.mjs measures the closed box just before swapping to this
   and pins the input to that exact width in an inline style, rather than either one guessing at a
   fixed width that would drift from the other. */
input.app-editable-name {
  cursor: text;
}

/* Wraps just the image so the remove badge has something image-sized to anchor to. Without it the
   badge falls back to the tile, whose size depends on the grid cell and on how many lines the name
   wraps to — the same `top`/`right` then lands somewhere different in every grid. It can't live
   inside .app-glass-image-wrap itself, which clips to the circle. */
.app-tile-image-slot {
  position: relative;
  width: fit-content;
}

.app-tile-remove-badge {
  position: absolute;
  top: -3px;
  right: -4px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: #dc3545;
  color: white;
  border: none;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.8rem;
  z-index: 2;
  cursor: pointer;
  transition: filter 0.1s ease, transform 0.1s ease;
}

.app-tile-remove-badge:hover {
  filter: brightness(0.9);
  transform: scale(1.15);
}

.app-tile-remove-badge:active {
  filter: brightness(0.75);
  transform: scale(0.9);
}

.app-tile-rename-button {
  border: none;
  background: transparent;
  color: #888;
  padding: 0 2px;
  font-size: 0.85rem;
  cursor: pointer;
}

.app-tile-rename-button:active {
  color: #555;
}

@keyframes app-add-tile-in-kf {
  from { opacity: 0; transform: scale(0); }
  to   { opacity: 1; transform: scale(1); }
}

.app-add-tile-cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 4px;
  cursor: pointer;
}

.app-add-tile {
  width: 70px;
  height: 70px;
  border-radius: 50%;
  background: #ced4da;
  color: white;
  font-size: 1.5rem;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: filter 0.1s ease, transform 0.1s ease;
  animation: app-add-tile-in-kf 0.25s ease-out;
}

.app-add-tile:hover {
  filter: brightness(0.9);
  transform: scale(1.1);
}

.app-add-tile:active {
  filter: brightness(0.75);
  transform: scale(0.9);
}

/* The add tile for a grid whose cells are cards: the outline of the card it would add, the same
   size and shape as its neighbours, holding a bare (+) with nothing behind it. */
.app-add-tile-card {
  width: 5.625rem;
  align-self: stretch;
  /* Matches a card with a one-line name, for when it wraps onto a line of its own and has no
     neighbour to stretch up to. */
  min-height: 7rem;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 2px dashed #d5d5d5;
  border-radius: 14px;
  color: #b5b5b5;
  font-size: 1.75rem;
  cursor: pointer;
  transition: border-color 0.12s ease, color 0.12s ease;
  animation: app-add-tile-in-kf 0.25s ease-out;
}

.app-add-tile-card:hover {
  border-color: #bdbdbd;
  color: #999;
}

.app-add-tile-card:active {
  border-color: #a8a8a8;
  color: #777;
}

/* ================= */
/* INPUT RE-SKIN     */
/* ================= */

input[type="text"]:not(.form-control),input[type="number"]:not(.form-control),input[type="search"]:not(.form-control) {
  border: 0;
  outline: 0;
  border-radius: 0;
  background: transparent;
  border-bottom: 1px solid #666;
  -webkit-appearance: none;
  appearance: none;
}

/* Safari/Chrome inject their own search icon + clear "x" for type="search" — these inputs
   already render their own (app-filter-pill__clear, or the bi-search icon), so drop the
   native ones to avoid a duplicate. */
input[type="search"]::-webkit-search-decoration,
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-results-button,
input[type="search"]::-webkit-search-results-decoration {
  display: none;
}

/* ================= */
/* BUTTON RE-SKIN    */
/* ================= */

.app-keypad-key {
  font-family: "Poppins", serif;
  font-size: 1.25rem;
  font-weight: 100;
  font-style: normal;
  border: 1px solid #ced4da;
}

/* AmountKeypadModal.mjs's own bigger keys — scoped to its card so NumericKeypad.mjs's other
   callers (CashPayment.mjs, etc., which still use .app-keypad-key at its original 1.25rem)
   aren't affected. */
.app-amount-keypad-card .app-keypad-key {
  font-size: 2.25rem;
}

.app-keypad-key--utility {
  background: #e9ecef;
}

.btn-primary {
  font-family: "Poppins", serif;
  font-size: 1.25rem;
  font-weight: 100;
  font-style: normal;

  --bs-btn-color: #fff;
  --bs-btn-bg: var(--x-background-color-primary);
  --bs-btn-border-color: var(--x-border-color-primary);
  --bs-btn-hover-color: #fff;
  --bs-btn-hover-bg: var(--x-background-color-primary);
  --bs-btn-hover-border-color: var(--x-border-color-primary);
  --bs-btn-focus-shadow-rgb: 49, 132, 253;
  --bs-btn-active-color: #fff;
  --bs-btn-active-bg: var(--x-background-color-primary);
  --bs-btn-active-border-color: var(--x-border-color-primary);
  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
  --bs-btn-disabled-color: #fff;
  --bs-btn-disabled-bg: var(--x-background-color-primary);
  --bs-btn-disabled-border-color: var(--x-border-color-primary);
}

.btn-secondary {
  font-family: "Poppins", serif;
  font-size: 1.25rem;
  font-weight: 100;
  font-style: normal;

  --bs-btn-color: #fff;
  --bs-btn-bg: var(--x-background-color-secondary);
  --bs-btn-border-color: var(--x-border-color-secondary);
  --bs-btn-hover-color: #fff;
  --bs-btn-hover-bg: var(--x-background-color-secondary);
  --bs-btn-hover-border-color: var(--x-border-color-secondary);
  --bs-btn-focus-shadow-rgb: 49, 132, 253;
  --bs-btn-active-color: #fff;
  --bs-btn-active-bg: var(--x-background-color-secondary);
  --bs-btn-active-border-color: var(--x-border-color-secondary);
  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
  --bs-btn-disabled-color: #fff;
  --bs-btn-disabled-bg: var(--x-background-color-secondary);
  --bs-btn-disabled-border-color: var(--x-border-color-secondary);
}

.btn {
  transition: filter 0.12s ease, transform 0.45s cubic-bezier(0.34, 1.56, 0.64, 1);
}

@media (max-width: 449px) {
  /* .btn-primary/.btn-secondary above set their own big 1.25rem font-size for large action
     buttons — that reads as oversized once the viewport itself is this narrow, so pull every
     .btn (button text in general, not just those two) down to a flat 1rem here. */
  .btn {
    font-size: 1rem;
  }
}
/* See "RESPONSIVE BREAKPOINTS × FONT-SIZE TIERS" near the top of the file. */
@media (max-width: 561px) {
  html[data-font-size="large"] .btn,
  html[data-font-size="xlarge"] .btn {
    font-size: 1rem;
  }
}

.btn-primary:hover, .btn-secondary:hover {
  filter: brightness(1.12);
}

.btn-primary:active {
  filter: brightness(0.7);
  transform: scale(1.05);
  transition: filter 0.12s ease, transform 0.08s ease;
}

.btn-secondary:active {
  filter: brightness(0.7);
  transform: scale(1.12);
  transition: filter 0.12s ease, transform 0.08s ease;
}

.btn-outline-secondary {
  --bs-btn-color: #6c757d;
  --bs-btn-bg: transparent;
  --bs-btn-border-color: #6c757d;
  --bs-btn-hover-color: #6c757d;
  --bs-btn-hover-bg: transparent;
  --bs-btn-hover-border-color: #6c757d;
  --bs-btn-focus-shadow-rgb: 49, 132, 253;
  --bs-btn-active-color: #6c757d;
  --bs-btn-active-bg: transparent;
  --bs-btn-active-border-color: #6c757d;
  --bs-btn-active-shadow: none;
  --bs-btn-disabled-color: #6c757d;
  --bs-btn-disabled-bg: transparent;
  --bs-btn-disabled-border-color: #6c757d;
}

.btn-outline-primary {
  --bs-btn-color: var(--x-font-color-dark);
  --bs-btn-bg: transparent;
  --bs-btn-border-color: #6c757d;
  --bs-btn-hover-color: var(--x-font-color-dark);
  --bs-btn-hover-bg: transparent;
  --bs-btn-hover-border-color: #6c757d;
  --bs-btn-focus-shadow-rgb: 49, 132, 253;
  --bs-btn-active-color: var(--x-font-color-dark);
  --bs-btn-active-bg: transparent;
  --bs-btn-active-border-color: #6c757d;
  --bs-btn-active-shadow: none;
  --bs-btn-disabled-color: var(--x-font-color-dark);
  --bs-btn-disabled-bg: transparent;
  --bs-btn-disabled-border-color: #6c757d;
}

/* Every bordered/outline-style button in the app shares this one press-down "expand" feedback
   (darken + scale up) instead of each duplicating it — .btn-outline-secondary/-primary,
   VariantComboRow's/VariantComboRowMeasured's stepper buttons (app-variant-stepper-btn — -50/
   +50/+250, ^5/^6, etc., which also carry .btn-outline-secondary already, listed explicitly here
   so the sharing is documented rather than incidental) and VariantValueToggle
   (app-variant-toggle, :not(--active) since a selected pill uses its own solid-fill treatment
   instead — see .app-variant-toggle--active:hover below). */
.btn-outline-secondary:active,
.btn-outline-primary:active,
.app-variant-stepper-btn:active,
.app-variant-toggle:active:not(.app-variant-toggle--active) {
  filter: brightness(0.82);
  transform: scale(1.12);
  transition: filter 0.12s ease, transform 0.08s ease;
}

/* Same sharing for the focus ring: these are plain .btn-based classes, so without this they'd
   each fall through to Bootstrap's raw default focus box-shadow. */
.btn.btn-outline-secondary:focus,
.btn.btn-outline-secondary:focus-visible,
.btn.btn-outline-primary:focus,
.btn.btn-outline-primary:focus-visible,
.app-variant-toggle:focus,
.app-variant-toggle:focus-visible {
  box-shadow: none;
}

/* VariantValueToggle (variant selection modal): a plain .btn-primary/.btn-outline-secondary
   pairing here would visibly mismatch in size — .btn-primary above sets its own font-size
   (1.25rem) and font-weight (100) for the app's big display buttons, while
   .btn-outline-secondary keeps Bootstrap's smaller default, so the "active" pill would render
   noticeably larger than the rest. This class keeps both states on the same font metrics and
   only swaps color. */
/* Sizing matches .radio-icon-button > div above — the app's established minimum touch target
   for icon/pill action buttons — so these toggles and the stepper buttons below aren't smaller
   than every other tappable control in the app. */
.app-variant-toggle {
  padding: 0.5rem 1rem;
  font-size: 0.8rem;
  line-height: 1;
  min-width: 1.25rem;
  min-height: 1.25rem;
  color: #6c757d;
  background: transparent;
  border: 1px solid #6c757d;
}

/* GoodsReceiptForm.mjs's quick-filter pills sit on a gray (bg-light) panel — the shared
   .app-variant-toggle's transparent default would just show that gray through, so this scopes a
   white fill to this one screen instead of touching the shared class (VariantSelectModal.mjs
   etc. render these on plain white and are fine with transparent). */
.app-goods-receipt-quick-filters .app-variant-toggle:not(.app-variant-toggle--active) {
  background: #fff;
}

/* CardPayment.mjs's Débito/Crédito pair — a two-option choice on its own screen reads better at
   the app's normal text size than the shared .app-variant-toggle's own smaller default (tuned for
   VariantSelectModal's denser rows of many pills), so this scopes a size bump to just this one row
   instead of touching the shared class. */
.app-card-type-toggle-row .app-variant-toggle {
  font-size: 1rem;
}

/* PaymentMethodCard.mjs's own cuotas row (1x/3x/6x) and register row (account icons) — a fixed
   size for both instead of the shared .app-variant-toggle's own padding-driven sizing, which made
   the icon variant (a 20px image) render noticeably bigger than the plain-text cuotas pills right
   above it despite using the exact same class. */
.payment-method-card-toggle-row .app-variant-toggle {
  padding: 0;
  min-width: 3rem;
  min-height: 2rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* Selected here reads as the same light "secondary" wash every .app-variant-toggle already uses
   for hover/press (see the shared rule below) instead of the solid brand-color fill --active uses
   everywhere else — tried on these two rows specifically, not the shared class. */
.payment-method-card-toggle-row .app-variant-toggle--active,
.payment-method-card-toggle-row .app-variant-toggle--active:hover {
  color: #6c757d;
  background: color-mix(in srgb, var(--x-background-color-secondary) 18%, transparent);
  border-color: var(--x-border-color-secondary);
  filter: none;
}

/* Bootstrap's own .btn:disabled default (just opacity:0.65) barely reads as disabled here — the
   pill's border stays full-strength and a colorful account logo/icon still shows through fairly
   vividly at 65%. A lighter border plus a stronger wash makes "this one isn't pickable" obvious
   at a glance instead of needing a second look. */
.payment-method-card-toggle-row .app-variant-toggle:disabled {
  opacity: 0.4;
  border-color: #dee2e6;
}

.app-variant-toggle--active {
  color: #fff;
  background: var(--x-background-color-primary);
  border-color: var(--x-border-color-primary);
}

/* brightness(1.12) — the app's usual hover feedback for .btn-primary — washes out to near-white
   on themes whose primary color is already light (e.g. this tenant's pastel green), so this
   darkens instead: correct regardless of how light or dark the tenant's brand color is.
   Restates color/background/border-color (not just filter) so an active+hovered pill still
   reads as selected even if this rule's own specificity/order ever changes relative to the
   shared hover/press-down green wash below (which already excludes --active via :not(), so in
   practice the two don't currently fight — this is just not relying on that). */
.app-variant-toggle--active:hover {
  color: #fff;
  background: var(--x-background-color-primary);
  border-color: var(--x-border-color-primary);
  filter: brightness(0.9);
}

/* Transient flash — see previewCombo in VariantSelectModal.mjs — not a selection state, so it
   only touches border/background, never the (unrelated) --active green. */
.app-variant-toggle--preview {
  border-color: var(--x-border-color-secondary);
  background: color-mix(in srgb, var(--x-background-color-secondary) 18%, transparent);
}

/* LocationToggle.mjs's "no stock" warning — just the border, a real Bootstrap danger red rather
   than the tenant-branded --x- primary/secondary vars above, since this is a universal
   error/warning signal, not a brand color. Still selectable (see its own doc — this feature never
   blocks on stock, only warns), so this has to win over --active's own border color when both
   apply; the doubled class in the selector beats --active's single-class specificity. */
.app-variant-toggle--danger.app-variant-toggle--danger {
  border-color: var(--bs-danger);
}

/* The same light green wash — hover, press-down, and the transient --preview flash all read as
   one consistent "this is interactive/about to be picked" color across the variant toggles and
   the measured/UNIT stepper buttons (-50/+50/+250, ^5/^6, etc.) alike. The shared scale/
   brightness/focus-ring rules near .btn-outline-secondary above already cover the :active
   selectors too — this just adds the color on top. Scoped to these two families only, not
   .btn-outline-secondary/-primary in general — elsewhere in the app (Cancel buttons, payment
   buttons, ...) hovering/pressing shouldn't turn green. :not(--active) on the toggle rules so
   they don't fight the solid primary fill an already-selected pill uses instead (see
   --active:hover above, which restates its own color/background/border-color for the same
   equal-specificity reason).
   .btn.btn-outline-secondary.app-variant-stepper-btn (the stepper buttons' actual full class
   list, not just .app-variant-stepper-btn alone) — Bootstrap's own :active rule for
   .btn-outline-secondary is itself 3 classes deep (:not(.btn-check) + .btn:active), so a
   2-class selector here loses the background/border-color fight even from later in the
   cascade; this needs to match or beat that specificity, not just come after it. */
.app-variant-toggle:hover:not(.app-variant-toggle--active),
.app-variant-toggle:active:not(.app-variant-toggle--active),
.btn.btn-outline-secondary.app-variant-stepper-btn:active {
  border-color: var(--x-border-color-secondary);
  background: color-mix(in srgb, var(--x-background-color-secondary) 18%, transparent);
}

/* VariantQuantityGrid.mjs's 1-dimension case: a plain list stacked vertically on mobile (each
   value's replicate-input/label/quantity-input on its own line, the natural reading order on a
   narrow screen), switching to a horizontal row of value+input pairs once there's enough width —
   the same "table" the 2+ dimension case renders, just without a second axis to justify a real
   <table>. */
.app-variant-quantity-list {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

/* Label above its input(s), same as the 2/3-dimension table's row/column headers below — the
   nested row only ever holds the replicate input plus the cell's own quantity input, never more,
   so a plain flex row (not a grid) is enough. */
.app-variant-quantity-list__cell {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

.app-variant-quantity-list__cell > div {
  display: flex;
  gap: 0.5rem;
}

/* The "replicate to all" cell has no label/paired row — just its own input. */
.app-variant-quantity-list__replicate {
  align-items: flex-end;
}

@media (min-width: 480px) {
  .app-variant-quantity-list {
    flex-direction: row;
    flex-wrap: wrap;
  }

  .app-variant-quantity-list__cell span {
    text-align: center;
  }

  .app-variant-quantity-list__replicate {
    align-items: center;
  }
}
/* See "RESPONSIVE BREAKPOINTS × FONT-SIZE TIERS" near the top of the file. */
@media (min-width: 600px) {
  html[data-font-size="large"] .app-variant-quantity-list,
  html[data-font-size="xlarge"] .app-variant-quantity-list {
    flex-direction: row;
    flex-wrap: wrap;
  }

  html[data-font-size="large"] .app-variant-quantity-list__cell span,
  html[data-font-size="xlarge"] .app-variant-quantity-list__cell span {
    text-align: center;
  }

  html[data-font-size="large"] .app-variant-quantity-list__replicate,
  html[data-font-size="xlarge"] .app-variant-quantity-list__replicate {
    align-items: center;
  }
}

/* VariantQuantityGrid.mjs's 2/3-dimension case — cells stay compact, the quantity input owns the
   whole cell instead of leaving Bootstrap's default table padding around a much smaller control.
   Every cell's content (a bare input, or a column/row header's label+input pair) centers
   horizontally and bottom-anchors vertically, so a header's taller label+input stack still lines
   its input up with the shorter single-input cells around it. */
.app-variant-quantity-grid td,
.app-variant-quantity-grid th {
  padding: 0.25rem;
  text-align: center;
  vertical-align: bottom;
}

.app-variant-quantity-grid .app-variant-quantity-input {
  min-width: 40px;
}

/* Every input in VariantQuantityGrid.mjs (corner/row/column replicate inputs and each cell's own
   quantity) is capped at this width, in both the table and the 1-dimension list layout —
   comfortable for up to 3 digits, deliberately not wider, so a stray 4th digit just scrolls within
   the input instead of the whole grid growing lopsided around one bigger box. `.form-control`
   already sets width:100%; the margin auto is what actually centers it once max-width caps that
   below its cell's full width. */
.app-variant-quantity-input {
  max-width: 50px;
  margin-inline: auto;
  text-align: center;
  font-size: 0.8rem;
}

/* position:relative anchors .app-tile-remove-badge (the card's own top-right X, same one
   VariantComboRowMeasured uses) below. */
/* Centered by default — the single truly vacuous combo (no variants, no stock control) has no
   header to share the line with, so its stepper is centered like any other lone flex item
   instead of sitting flush left. Once there is a header (--labeled), it goes to the left and the
   stepper to the right instead. */
.app-variant-combo-row {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid #dee2e6;
  border-radius: 0.375rem;
  padding: 0.5rem;
  margin-bottom: 0.5rem;
}

.app-variant-combo-row--labeled {
  justify-content: space-between;
}

.app-variant-combo-row--preview {
  border-color: var(--x-border-color-secondary);
}

/* A "cambio/devolución" combo (negative quantity — see VariantComboRow.mjs's own doc) — same
   violet as the checkout's own Reintegro badge, so the card itself reads as that concept at a
   glance, not just its own text/icon underneath the label. */
.app-variant-combo-row--exchange-return {
  border-color: #8b5cf6;
}

/* A 0-quantity combo — see VariantComboRow.mjs's own doc — same red as the card's own remove
   badge (.app-tile-remove-badge), warning the cashier this row won't be saved as-is. */
.app-variant-combo-row--zero {
  border-color: #dc3545;
}

.app-variant-combo-row-header {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.app-variant-combo-row-actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

/* margin-left:auto pins the stepper flush right regardless of the label's own width (empty,
   short, or long — every row's buttons land in the same place) — a small margin-right keeps it
   clear of the row's own remove badge (.app-tile-remove-badge, pinned to the row's top-right
   corner). Only when there's actually a header to clear from (--labeled) — the unlabeled row has
   no remove badge either (see VariantComboRow.mjs), and its stepper is centered above instead.
   Below the 549px breakpoint this wraps onto its own full-width line under the label instead,
   where the mobile rule right after this one re-centers it. */
.app-variant-combo-row--labeled .app-variant-combo-row-actions {
  margin-left: auto;
  margin-right: 2.5rem;
}

/* A labeled row with no remove badge (VariantComboRow.mjs's `removable: false`) has nothing to
   clear on its right, so its stepper sits flush right instead. */
.app-variant-combo-row--labeled.app-variant-combo-row--unremovable .app-variant-combo-row-actions {
  margin-right: 0;
}

/* Below 550px the header (trash + label) + 5-button stepper (-, number, +, ^5, ^6) no longer fit
   on one line — stack them instead: header on its own line, the button row centered under it. */
@media (max-width: 549px) {
  .app-variant-combo-row {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.5rem;
  }

  .app-variant-combo-row-actions {
    width: 100%;
    justify-content: center;
  }
}
/* See "RESPONSIVE BREAKPOINTS × FONT-SIZE TIERS" near the top of the file. */
@media (max-width: 686px) {
  html[data-font-size="large"] .app-variant-combo-row,
  html[data-font-size="xlarge"] .app-variant-combo-row {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.5rem;
  }

  html[data-font-size="large"] .app-variant-combo-row-actions,
  html[data-font-size="xlarge"] .app-variant-combo-row-actions {
    width: 100%;
    justify-content: center;
  }
}

/* VariantComboRowMeasured — always a vertical stack (header, fine-adjust botonera, slider),
   regardless of screen width, unlike the integer VariantComboRow's row/column breakpoint above.
   position:relative anchors .app-tile-remove-badge (the card's own top-right X, reused from
   EditableProductTile.mjs/Category.mjs — same red circle badge, negatively offset so it pokes
   past the card's corner) below. */
.app-variant-combo-row--measured {
  position: relative;
  flex-direction: column;
  align-items: stretch;
  gap: 0.5rem;
}

/* The whole-unit (kg/lt) stepper — same -/input/+/↑5/↑6 pattern as VariantComboRow's own
   integer stepper, since a whole kg is edited exactly like a whole UNIT product. Centered when
   there's no label to share the line with (the no-variant synthetic combo); once there is one
   (--labeled), it goes to the left and the stepper to the right instead of both being centered
   together. */
.app-variant-measured-whole {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 0.4rem;
}

.app-variant-measured-whole--labeled {
  justify-content: space-between;
}

/* -/input/+/Kg as one tight, fixed-gap unit. */
.app-variant-measured-whole-controls {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.4rem;
}

/* space-between above only pushes this whole group away from the label, it must never spread the
   controls themselves apart from each other. margin-left:auto pins it flush right regardless of
   the label's own width, same convention (and same margin-right) as .app-variant-combo-row-actions,
   for clearance from the row's own remove badge (.app-tile-remove-badge, pinned to the row's
   top-right corner) — only relevant when there's actually a label+badge to clear (--labeled); the
   unlabeled row has neither, and its controls are centered above instead. */
.app-variant-measured-whole--labeled .app-variant-measured-whole-controls {
  margin-left: auto;
  margin-right: 2.5rem;
}

.app-variant-measured-whole-input {
  min-width: 100px;
}

.app-variant-measured-whole-label {
  margin-left: 0.2rem;
}

.app-variant-measured-whole-input-wrap {
  position: relative;
}

/* Generic version of the wrapper above — anchors FloatingHorizontalSlider.mjs's absolute
   positioning for any draggable quantity input (VariantComboRow.mjs's plain UNIT stepper), not
   just the measured kg/lt one. */
.app-drag-slider-anchor {
  position: relative;
}

/* Floating horizontal slider — shared by FloatingHorizontalSlider.mjs, driven by
   AmountStepperInput.mjs's own drag gesture (used for both measured quantities/price and plain
   UNIT counts alike). Centered ABOVE the input, not on top of it. AmountStepperInput's own
   trackWidthPx (useHorizontalDragSlider.mjs) must match this track's width. */
.app-hslider {
  position: absolute;
  bottom: calc(100% + 12px);
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  z-index: 20;
  pointer-events: none;
  padding: 0.7rem 1.1rem 1rem;
  background: #fff;
  border-radius: 12px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.55);
}

.app-hslider-value {
  width: 76px;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--x-background-color-primary);
  white-space: nowrap;
  text-align: center;
}

.app-hslider-track {
  position: relative;
  width: 160px;
  height: 10px;
  border-radius: 5px;
  background: var(--x-background-color-neutral-dark);
}

.app-hslider-fill {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  border-radius: 5px;
  background: var(--x-background-color-primary);
}

/* Reference ticks on the track — the measured slider's major ruler marks (250/500/750, where
   snapToRuler() pulls the value toward while dragging) or the plain UNIT slider's uniform
   every-20 marks (no snapping there, just a visual guide). */
.app-hslider-tick {
  position: absolute;
  top: -3px;
  width: 2px;
  height: 16px;
  background: rgba(0, 0, 0, 0.25);
  transform: translateX(-50%);
}

.app-hslider-thumb {
  position: absolute;
  top: 50%;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--x-background-color-primary);
  transform: translate(-50%, -50%);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
}

/* Below 360px: shrink every button/text in the modal EXCEPT the footer's Guardar/Agregar
   (btn-primary) and trash (btn-danger) — those keep their normal touch-target size on purpose,
   everything else (variant toggles, combo steppers, labels) gets tighter to fit the narrower
   screen. Scoped to .app-variant-modal-card / dedicated app-variant-* classes only, so this
   never leaks into .t1/.t3/.t4 or .btn usage anywhere else in the app. */
@media (max-width: 359px) {
  .app-variant-modal-card .t1 {
    font-size: 0.95rem;
  }

  .app-variant-modal-card .t3 {
    font-size: 0.75rem;
  }

  .app-variant-modal-card .t4 {
    font-size: 0.68rem;
  }

  .app-variant-toggle {
    padding: 0.35rem 0.6rem;
    font-size: 0.8rem;
    min-width: 1rem;
    min-height: 1rem;
  }
}
/* See "RESPONSIVE BREAKPOINTS × FONT-SIZE TIERS" near the top of the file. */
@media (max-width: 449px) {
  html[data-font-size="large"] .app-variant-modal-card .t1,
  html[data-font-size="xlarge"] .app-variant-modal-card .t1 {
    font-size: 0.95rem;
  }

  html[data-font-size="large"] .app-variant-modal-card .t3,
  html[data-font-size="xlarge"] .app-variant-modal-card .t3 {
    font-size: 0.75rem;
  }

  html[data-font-size="large"] .app-variant-modal-card .t4,
  html[data-font-size="xlarge"] .app-variant-modal-card .t4 {
    font-size: 0.68rem;
  }

  html[data-font-size="large"] .app-variant-toggle,
  html[data-font-size="xlarge"] .app-variant-toggle {
    padding: 0.35rem 0.6rem;
    font-size: 0.8rem;
    min-width: 1rem;
    min-height: 1rem;
  }
}

/* transition:none overrides .btn's own slow bouncy transform transition (see .btn above) — these
   buttons should snap back instantly on release, same as .app-variant-toggle's own fast snap
   (which gets there by overriding .btn's transition property with its own border/background-only
   list instead). The shared :active rule near .btn-outline-secondary above still gets its own
   fast transition applied while *entering* the pressed state, since that comes from the :active
   rule itself, not this base one. */
.app-variant-stepper-btn {
  padding: 0.5rem 0.7rem;
  font-size: 1.25rem;
  line-height: 1;
  min-width: 1.25rem;
  min-height: 1.25rem;
  transition: none;
}

/* Below 450px the -50/50/250 (or -100/100/500) stepper buttons get cramped next to the rest of
   their row (input, Gr/$ mode toggle) — same shrink already applied under 360px, just extended up
   to 450px so these buttons stay comfortable across the full narrow-phone range, not just the
   very narrowest. */
@media (max-width: 449px) {
  .app-variant-stepper-btn {
    padding: 0.5rem;
    font-size: 1rem;
    min-width: 1rem;
    min-height: 1rem;
  }
}
/* See "RESPONSIVE BREAKPOINTS × FONT-SIZE TIERS" near the top of the file. */
@media (max-width: 561px) {
  html[data-font-size="large"] .app-variant-stepper-btn,
  html[data-font-size="xlarge"] .app-variant-stepper-btn {
    padding: 0.5rem;
    font-size: 1rem;
    min-width: 1rem;
    min-height: 1rem;
  }
}

/* touch-action:none — the horizontal drag gesture (see useHorizontalDragSlider.mjs) starts with
   a pointerdown here, on both of this class's uses (VariantComboRow.mjs/VariantComboRowMeasured's
   whole-unit input). Without it, iOS Safari can still hand a slightly off-axis touch to its own
   page-scroll mid-drag, cutting the gesture short. This is the reliable half of that fix;
   e.preventDefault() in the drag's own pointermove handler is the (less reliable on its own)
   other half. */
.app-variant-stepper-input {
  width: 3.5rem;
  padding: 0.5rem;
  font-size: 1.25rem;
  touch-action: none;
}

/* Below 450px the stepper's own value input gets cramped too — same shrink already applied under
   360px, just extended up to 450px (see .app-variant-stepper-btn's own identical-range rule
   above). `.app-variant-measured-whole-input`'s min-width is sized in `ch` (a "0" character's own
   width in THIS input's font) rather than a guessed px number, so it stays exactly wide enough for
   6 digits (appConfig.measuredMax's own ceiling) at whatever font-size applies here, plus this same
   rule's own padding (border-box — see Bootstrap's global box-sizing reset — so padding must be
   added back on top of the content width `ch` measures, not already included in it). */
@media (max-width: 449px) {
  .app-variant-stepper-input {
    width: 2.75rem;
    padding: 0.5rem 0.35rem;
    font-size: 1rem;
  }

  .app-variant-measured-whole-input {
    min-width: calc(6ch + 0.7rem);
  }
}
/* See "RESPONSIVE BREAKPOINTS × FONT-SIZE TIERS" near the top of the file. */
@media (max-width: 561px) {
  html[data-font-size="large"] .app-variant-stepper-input,
  html[data-font-size="xlarge"] .app-variant-stepper-input {
    width: 2.75rem;
    padding: 0.5rem 0.35rem;
    font-size: 1rem;
  }

  html[data-font-size="large"] .app-variant-measured-whole-input,
  html[data-font-size="xlarge"] .app-variant-measured-whole-input {
    min-width: calc(6ch + 0.7rem);
  }
}

/* GoodsReceiptDetailLine.mjs's −/count/+ — same stepper, scoped smaller so a list of these lines
   stays compact instead of each one taking up as much room as the POS variant-combo stepper. */
.app-goods-receipt-stepper .app-variant-stepper-btn {
  padding: 0.3rem 0.5rem;
  font-size: 1rem;
  width: 2.5rem;
  height: 2.2rem;
}

.app-goods-receipt-stepper .app-variant-stepper-input {
  width: 3.2rem;
  height: 2.2rem;
  padding: 0.3rem 0.5rem;
  font-size: 1rem;
}

/* Shared fixed width for FundsSettings.mjs's payment-methods "Costos" column — same class on the
   header label and on each row's %/edit cluster keeps them pixel-aligned without repeating the
   width as an inline style in both places. */
.app-register-cost-col {
  width: 5.6rem;
}

/* The ↑ in the ↑5/↑6 shortcuts, lighter than the digit next to it so the number (the actual
   value) reads as the heavier, more prominent part of the label. */
.app-variant-multiple-arrow {
  font-weight: 50;
}

/* Generic modal close button — a gray circle, sized the same everywhere it's used. Originated in
   VariantSelectModal.mjs (replacing its old "Cancelar" footer button); every modal in the app now
   renders it through Modal.mjs, inline at the end of its header row (a normal flex sibling, not
   absolutely positioned over the title). */
.app-modal-close {
  width: 2.25rem;
  height: 2.25rem;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Modal.mjs's own card — a flex column so its header/footer can stay put while only
   .app-modal-body scrolls, instead of the whole card (title and close button included)
   scrolling as one block the way every hand-rolled modal used to. */
.app-modal-card {
  display: flex;
  flex-direction: column;
}

.app-modal-body {
  overflow-y: auto;
  flex: 1 1 auto;
  min-height: 0;
}

/* Same shadow .app-navbar casts over .app-layout-content below it — the header sits in normal
   flow right above .app-modal-body (not fixed/sticky over it), so this only reads once the body
   actually scrolls some content up underneath it; position+z-index+opaque background make sure
   that content paints below this row instead of covering the shadow (plain DOM order alone would
   let it, since .app-modal-body comes after this in the card's own flex column). */
.app-modal-header {
  position: relative;
  z-index: 1;
  background: white;
  box-shadow: rgba(33, 35, 38, 0.3) 0 10px 10px -10px;
}

/* Same shadow .app-bottom-bar casts upward over the content behind it — same reasoning as
   .app-modal-header's own, mirrored: the footer comes AFTER .app-modal-body in DOM, so it already
   paints on top without the z-index bump, but still needs position+background to keep that shadow
   (and the footer itself) opaque over whatever the body scrolls up against it. */
.app-modal-footer {
  position: relative;
  flex-shrink: 0;
  background: white;
  box-shadow: rgba(0, 0, 0, 0.2) 0 -5px 20px 5px;
}

/* Below 320px the -/number/+ core plus the ^5/^6 shortcuts no longer fit — the shortcuts go,
   the core stepper (the part that actually matters) stays. */
@media (max-width: 319px) {
  .app-variant-multiple-btn {
    display: none;
  }
}
/* See "RESPONSIVE BREAKPOINTS × FONT-SIZE TIERS" near the top of the file. */
@media (max-width: 399px) {
  html[data-font-size="large"] .app-variant-multiple-btn,
  html[data-font-size="xlarge"] .app-variant-multiple-btn {
    display: none;
  }
}

/* AmountKeypadModal — quick scale/fade for the digit-entry overlay's own backdrop/card, driven
   by its `closing` signal switching between the -enter/-leave keyframe names (same pattern as
   ToastManagerImpl's leaving flag / FlashToast.mjs). */
@keyframes app-amount-keypad-backdrop-enter {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes app-amount-keypad-backdrop-leave {
  from { opacity: 1; }
  to   { opacity: 0; }
}

@keyframes app-amount-keypad-card-enter {
  from { opacity: 0; transform: scale(0.92); }
  to   { opacity: 1; transform: scale(1); }
}

@keyframes app-amount-keypad-card-leave {
  from { opacity: 1; transform: scale(1); }
  to   { opacity: 0; transform: scale(0.92); }
}

/* The typed value readout — same 2.25rem as .app-keypad-key below it, no existing t1-t5 class
   matches that size. */
.app-amount-keypad-value {
  font-size: 2.25rem;
  font-weight: 500;
  color: var(--x-font-color-dark);
}

/* AmountKeypadModal.mjs applies this manually (via pressedKey/flashKey) to a button struck from
   a real keyboard, since there's no native :active for that — same look as the app's usual
   press-down "expand" feedback (see .btn-outline-secondary:active and friends). */
.app-key-pressed {
  filter: brightness(0.82);
  transform: scale(1.12);
  transition: filter 0.12s ease, transform 0.08s ease;
}

/* AmountStepperInput.mjs's own amount input is readonly (tapping it opens AmountKeypadModal
   instead) — suppress every native text-field affordance so it doesn't look editable. Safari/iOS
   ignores plain user-select on <input>/<textarea> (WebKit only honors it on non-form elements),
   which is why AmountStepperInput.mjs also calls preventDefault() on the gesture's own
   pointerdown — this covers other browsers/the visual caret, that covers the actual selection.
   Scoped to .app-variant-stepper-input, not just the wider .app-variant-measured-whole-input
   variant (see below) — every stepper (UNIT or measured) shares this input now. */
.app-variant-stepper-input[readonly] {
  caret-color: transparent;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  cursor: pointer;
}

.app-variant-stepper-input[readonly]:disabled {
  cursor: default;
}

/* ================= */
/* OTHER CLASSES     */
/* ================= */

.app-card {
  --bs-card-border-color: #cfcfcf;
  border-radius: 12px;
  overflow: hidden;
  box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .075);
}

/* Opts a card out of the clip-to-rounded-corners above — for a card whose content needs to float
   outside its own bounds, e.g. PurchaseRequestsList.mjs's order cards, whose PurchaseOrderSendMenu.mjs
   popover would otherwise get cut off at the card edge instead of floating above it. */
.app-card--overflow-visible {
  overflow: visible;
}

.app-card--clickable {
  cursor: pointer;
  transition: background-color 0.12s ease;
}

/* A card whose own row is currently disabled (e.g. PromotionListItem.mjs's `enabled: no`) — reads
   as slightly washed-out rather than gone, since its own toggle/edit/duplicate buttons stay fully
   interactive. */
.app-card--disabled {
  opacity: .6;
}

.app-card--clickable:hover {
  background-color: rgba(0, 0, 0, 0.04);
}

.app-card--clickable:active {
  background-color: rgba(0, 0, 0, 0.10);
  transition: none;
}

.app-card--merge-bottom {
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
  border-bottom: 0;
  margin-bottom: 0 !important;
}

.app-card--merge-top {
  border-top-left-radius: 0;
  border-top-right-radius: 0;
  border-top: 1px dashed #cfcfcf;
  margin-top: 0 !important;
  box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .075);
}

/* An Inventory row whose stock has gone below 1 unit (InventoryStock.mjs) — a light tint flags it
   without the alarm a full bg-danger would raise, since a negative count is an accepted data state
   here (oversold/backordered), not an error. */
.app-card--negative-stock {
  background-color: rgba(220, 53, 69, 0.08);
}

.app-account-card {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: .15rem;
  min-width: 8rem;
  height: 4rem;
  border: 2px solid #cfcfcf;
  border-radius: 12px;
  padding: .5rem .75rem;
  cursor: pointer;
  transition: background-color .12s ease, border-color .12s ease, color .12s ease, opacity .12s ease;
}

@media (max-width: 499px) {
  /* Account tiles (AccountSelectCard, used by every account-family picker across the app) —
     narrower phones get a smaller card so more of them fit per row instead of always wrapping to
     one-per-line. */
  .app-account-card {
    min-width: 6rem;
    height: 3.25rem;
    gap: .1rem;
    border-radius: 10px;
    padding: .35rem .6rem;
  }
}
/* See "RESPONSIVE BREAKPOINTS × FONT-SIZE TIERS" near the top of the file. */
@media (max-width: 624px) {
  html[data-font-size="large"] .app-account-card,
  html[data-font-size="xlarge"] .app-account-card {
    min-width: 6rem;
    height: 3.25rem;
    gap: .1rem;
    border-radius: 10px;
    padding: .35rem .6rem;
  }
}

.app-account-card:hover {
  background-color: rgba(0, 0, 0, .04);
}

.app-account-card:active {
  background-color: rgba(0, 0, 0, .10);
}

.app-account-card--selected {
  background: color-mix(in srgb, var(--account-card-accent, var(--x-background-color-primary)) 18%, #fff);
  border: 2px solid var(--account-card-accent, var(--x-background-color-primary));
}

.app-account-card--selected:hover,
.app-account-card--selected:active {
  background: color-mix(in srgb, var(--account-card-accent, var(--x-background-color-primary)) 18%, #fff);
}

.app-account-card--disabled {
  opacity: .5;
  cursor: not-allowed;
}

.color-swatch-btn,
.color-customize-btn {
  transition: filter 0.08s ease, transform 0.08s ease;
}

.color-swatch-btn:hover,
.color-customize-btn:hover {
  filter: brightness(0.9);
}

.color-swatch-btn:active,
.color-customize-btn:active {
  transform: scale(0.9);
  filter: brightness(0.8);
}

.app-badge {
  display: inline-flex;
  align-items: center;
  height: 1.35rem;
  line-height: 1;
}

.app-badge-closed {
  background-color: #7cb342;
  color: #fff;
}

.app-badge-superseded {
  background-color: #fafafa;
  color: #a5a4a4;
  border: 1px solid #c9c6c6;
}

.app-badge-validated {
  background-color: #7cb342;
  color: #fff;
}

/* Finalizar's own ARCA mark (PaymentMethodConfigurator.mjs/CheckoutBottomBar.mjs) — pinned inside
   the button's right end, out of the flow so the label stays centered exactly as without it, and
   never catching the tap itself. */
.app-btn-with-trailing-icon {
  position: relative;
}

.app-btn-trailing-icon {
  position: absolute;
  right: 1rem;
  top: 50%;
  transform: translateY(-50%);
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 50%;
  pointer-events: none;
}

.app-badge-icon {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  margin-right: .3rem;
  flex-shrink: 0;
}

.app-navbar {
  background-color: white;
  box-shadow: rgba(33, 35, 38, 0.3) 0 10px 10px -10px;
  flex-shrink: 0;
}

/* .t1 sets no line-height of its own (see TYPOGRAPHY), so a page title that wraps to two lines
   (a long screen name on a narrow phone) gets the browser's default spacing — noticeably looser
   than the rest of the navbar. Scoped to the title specifically, not .t1 itself, which is reused
   all over the app in contexts this shouldn't affect. */
.app-navbar-title {
  line-height: 1;
}

.app-tab-row {
  display: flex;
  flex-direction: row;
  background: #f8f9fa;
  margin-top: 15px;
}

.app-tab-spacer {
  flex: 1;
  border-bottom: 1px solid #d0d0d0;
  background: white;
}

.app-light-panel {
  display: inline-block;
  background-color: #F5F5F5;
  padding: 10px;
  border-radius: 20px;
}

.app-light-panel.border-solid {
  border: 1px solid grey;
}

.app-light-panel-small {
  display: inline-block;
  background-color: #F5F5F5;
  padding: 10px;
  border-radius: 10px;
}

.app-bottom-bar {
  background-color: #F5F5F5;
  margin: 0 10px;
  padding: 20px 20px 20px;
  border-radius: 20px;
  box-shadow: rgba(0, 0, 0, 0.2) 0 -5px 20px 5px;
  position: relative;
}

/* Collapse/expand toggle — pinned OUTSIDE .app-bottom-bar's own top-right corner (see
   PaymentMethodConfigurator.mjs), same "floats on the border, never over the content" placement
   as .app-tile-remove-badge, so it can never overlap the Total row (expanded) or Finalizar
   (collapsed) regardless of how tall the panel currently is. */
.app-bottom-bar-collapse-toggle {
  position: absolute;
  top: -14px;
  right: 14px;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: #dee2e6;
  border: 1px solid #a0a0a0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1rem;
  color: #212529;
  cursor: pointer;
  z-index: 2;
  transition: filter 0.1s ease, transform 0.1s ease;
}

.app-bottom-bar-collapse-toggle:hover {
  filter: brightness(0.95);
}

.app-bottom-bar-collapse-toggle:active {
  filter: brightness(0.9);
  transform: scale(0.92);
}

/* Bottom margin wider than the top's — extra clearance from the bottom bar's own
   collapse-toggle badge (PaymentMethodConfigurator.mjs), which floats above the bar's border and
   would otherwise land close enough to the last card's own remove-badge for the two to visually
   overlap. */
.payment-content {
  max-width: 500px;
  margin: 10px auto 28px;
  padding: 0 28px;
}

/* The full-height flex column each payment screen wraps its .payment-content(s) + bottom
   instruction banner in, so the banner can sit at mt-auto and the whole thing fills LayoutContent
   instead of collapsing to its content's own height. */
.payment-content-fill {
  min-height: 100%;
  display: flex;
  flex-direction: column;
}

.checkout-content-wrapper {
  width: fit-content;
  margin: -20px auto 0;
  padding-top: 20px;
  position: relative;
}

/* CheckoutBottomBar.mjs's points/deposit/discount/surcharge badges — a single row, always (no
   width-based toggle: the previous "original independent floats above 450px, merged row below"
   split was dropped because the "original" layout turned out to still stack/overlap badges at
   ordinary phone/tablet widths, e.g. 490px, not just very narrow ones).
   Breaks out to the full SCREEN width rather than staying inside .checkout-content-wrapper (the
   positioning ancestor, width:fit-content sized to its widest row — the payment-methods row,
   narrower than the full viewport) — left:50%/width:min(100vw, 900px)/translateX(-50%) is the
   standard "escape a centered, narrower parent" trick: the box spans edge-to-edge of the actual
   screen (capped at .app-viewport's own 900px max-width) regardless of how wide that parent is, with its own padding standing in for the small edge
   margin a bare 100vw would otherwise skip. justify-content:center then centers the badges INSIDE
   that full-width strip, so the row visually reads as "centered on screen" the same as before, but
   now has the entire screen's width available before flex-wrap on the inner row (its own
   justify-content:center, so a wrapped 2nd line reads as a second centered row, not a ragged one)
   ever needs to kick in — a fixed right-aligned offset (tried previously) only worked for however
   many badges it was tuned for; add a 4th (recargo, alongside puntos/seña/descuento) and the row
   grows wide enough to spill off the LEFT edge of the screen instead, unable to shrink since
   nothing bounded its width to begin with. */
.checkout-badges-float {
  position: absolute;
  top: 0;
  left: 50%;
  width: min(100vw, 900px);
  padding: 0 12px;
  transform: translate(-50%, -50%);
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  box-sizing: border-box;
}

/* Shared by every checkout-badges-float badge (points/deposit/discount/reintegro) — color comes
   from Bootstrap's own bg-*/text-bg-* utility classes (bg-info text-dark for deposit — the same
   turquoise TicketHistoryPanel.mjs's own "Señado" badge uses — or .checkout-badge-reintegro
   below, which has no matching utility class), this only owns the sizing/spacing every one of
   them shares. */
.checkout-badge {
  white-space: nowrap;
  font-size: 0.85rem;
  padding: 0.4em 0.7em;
}

/* A "cambio/devolución" line's own credit (CheckoutBottomBar.mjs) — deliberately its own color,
   never reused from checkout-badge-discount/-surcharge, so it never reads as an ordinary
   promotion/payment-method adjustment (see CheckoutBottomBar.mjs's own doc). */
.checkout-badge-reintegro {
  background-color: #8b5cf6;
  color: #fff;
}

/* Below 420px there isn't room for two badges side by side at their normal size — same
   sizing-to-fit-content reasoning as the rest of the checkout basket's own breakpoints. */
@media (max-width: 419px) {
  .checkout-badge {
    font-size: 0.7rem;
  }
}
/* See "RESPONSIVE BREAKPOINTS × FONT-SIZE TIERS" near the top of the file. */
@media (max-width: 524px) {
  html[data-font-size="large"] .checkout-badge,
  html[data-font-size="xlarge"] .checkout-badge {
    font-size: 0.7rem;
  }
}

@keyframes customer-attention {
  0%   { transform: scale(1)    translateX(0); }
  15%  { transform: scale(1.25) translateX(-4px); }
  30%  { transform: scale(1.25) translateX(4px); }
  45%  { transform: scale(1.2)  translateX(-3px); }
  60%  { transform: scale(1.15) translateX(3px); }
  75%  { transform: scale(1.05) translateX(-1px); }
  100% { transform: scale(1)    translateX(0); }
}

.customer-attention {
  display: inline-block;
  animation: customer-attention 0.6s ease;
}

/* The panel is the grey box; the cells wrap inside it, next to the drag rail when editing. */
.app-category-panel {
  display: flex;
  align-items: stretch;
  gap: 0.375rem;
  background-color: #F5F5F5;
  padding: 0.625rem;
  border-radius: 20px;
  width: 100%;
}

.app-category-cells {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.375rem;
}

.app-category-panel a {
  text-decoration: none;
  color: inherit;
}

.app-category-item {
  width: 5.625rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: 0.5rem 0.375rem;
  cursor: pointer;
  background: white;
  border-radius: 14px;
  box-shadow: 0 1px 4px rgba(0,0,0,0.08);
  transition: filter 0.12s ease;
}

.app-category-item:hover {
  filter: brightness(0.97);
}

.app-category-item:active {
  filter: brightness(0.82);
}

.app-glass-image-wrap {
  position: relative;
  width: 4.375rem;
  height: 4.375rem;
  border-radius: 50%;
  overflow: hidden;
  box-shadow: 0 2px 6px rgba(0,0,0,0.15);
}

.app-glass-image-wrap::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: linear-gradient(
    135deg,
    rgba(255,255,255,0.50) 0%,
    rgba(255,255,255,0.15) 45%,
    transparent 65%
  );
  pointer-events: none;
}

.app-category-item-image {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.app-product-item {
  display: inline-block;
  cursor: pointer;
}

/* display:block (not inline-block) — on a wide enough viewport, inline-block items wrap onto the
   same line side by side, which reads as a multi-column product list. The checkout basket is
   always exactly one column, whatever the viewport; margin auto keeps it centered where the
   parent's own text-align:center used to do that job for the inline-block version. */
.app-basket-item {
  width: 100%;
  max-width: 500px;
  display: block;
  margin-left: auto;
  margin-right: auto;
  cursor: pointer;
}

/* TicketItem.mjs's own final line total — always this fixed neutral color/weight, never the
   `.t2`/`.text-success` default, regardless of whether a promotion/payment-pricing adjustment
   changed the price. */
.app-ticket-item-final-price {
  color: #242424 !important;
  font-weight: 400;
}

/* TicketItem.mjs's own "Elegir variantes y ubicación" warning badge — too long to fit its yellow
   pill below 460px, so it swaps to the short Texts.CUSTOMIZE ("Personalizar") instead. Both spans
   always render (see that component's own doc); only visibility toggles here. */
.app-ticket-item-warning-badge-short {
  display: none;
}
@media (max-width: 459px) {
  .app-ticket-item-warning-badge-full {
    display: none;
  }
  .app-ticket-item-warning-badge-short {
    display: inline;
  }
}
/* See "RESPONSIVE BREAKPOINTS × FONT-SIZE TIERS" near the top of the file. */
@media (max-width: 574px) {
  html[data-font-size="large"] .app-ticket-item-warning-badge-full,
  html[data-font-size="xlarge"] .app-ticket-item-warning-badge-full {
    display: none;
  }
  html[data-font-size="large"] .app-ticket-item-warning-badge-short,
  html[data-font-size="xlarge"] .app-ticket-item-warning-badge-short {
    display: inline;
  }
}

/* TicketItemList.mjs's applied-promotions/payment-adjustment summary — the two-column grid of
   label/amount pairs (see that component's own doc). Same light-gray fill as .app-category-panel
   elsewhere in the app, so the block reads as its own contained group rather than loose text
   floating between the basket items and the checkout bottom bar. */
.app-applied-discounts {
  display: grid;
  grid-template-columns: auto auto;
  column-gap: 1.5rem;
  row-gap: 0.3rem;
  justify-content: center;
  background-color: #F5F5F5;
  border-radius: 12px;
  padding: 0.75rem 1rem;
}

/* .t1/.t2 (unlike .t3/.t4) set no line-height of their own, so a long product name that wraps to
   two lines gets the browser's default spacing for "Poppins" — visibly looser than the rest of
   this row. Setting line-height directly on the wrapping .app-basket-item-name span only tightens
   that span's OWN internal line box, not the gap it leaves before its next sibling (the variant/
   location .t4 rows) — line-height has to sit on the shared block parent (this class, not
   .text-start itself, which is a bare Bootstrap utility reused all over the app) so every line in
   the block, wrapped title included, inherits the same tight spacing. */
.app-basket-item-info {
  line-height: 1;
}

/* Below 550px the image is shrunk to half its usual 50x50 (the width/height HTML attributes still
   say 50, so this has to win on specificity via the CSS properties, not just override a default
   rule) to give the name/price row more room. Declared BEFORE the narrower 399px step below, which
   drops the image entirely — that one must win the cascade at widths under 400px, not this one. */
@media (max-width: 549px) {
  .app-basket-item-image {
    width: 25px;
    height: 25px;
  }
}
/* See "RESPONSIVE BREAKPOINTS × FONT-SIZE TIERS" near the top of the file. */
@media (max-width: 686px) {
  html[data-font-size="large"] .app-basket-item-image,
  html[data-font-size="xlarge"] .app-basket-item-image {
    width: 25px;
    height: 25px;
  }
}

/* Below 440px the product name steps down from .t1 to .t2 — a first, smaller step before the
   399px rule below drops it further to .t3. Declared BEFORE that narrower step so its .t3
   properties win the cascade under 400px, not these .t2 ones. */
@media (max-width: 439px) {
  .app-basket-item-name {
    font-family: "Poppins", serif;
    font-size: 1rem;
    font-weight: 300;
    color: var(--x-font-color-light);
  }
}
/* See "RESPONSIVE BREAKPOINTS × FONT-SIZE TIERS" near the top of the file. */
@media (max-width: 549px) {
  html[data-font-size="large"] .app-basket-item-name,
  html[data-font-size="xlarge"] .app-basket-item-name {
    font-family: "Poppins", serif;
    font-size: 1rem;
    font-weight: 300;
    color: var(--x-font-color-light);
  }
}

/* Below 400px the product name steps down further, from .t2 (see the 439px step above) to .t3
   (every property that differs between them besides font-size — family and weight; line-height is
   already 1.2 for .app-basket-item-name at every breakpoint, see above). */
@media (max-width: 399px) {
  .app-basket-item-name {
    font-family: "Roboto", serif;
    font-size: 0.875rem;
    font-weight: 300;
    color: var(--x-font-color-light);
  }
}
/* See "RESPONSIVE BREAKPOINTS × FONT-SIZE TIERS" near the top of the file. */
@media (max-width: 499px) {
  html[data-font-size="large"] .app-basket-item-name,
  html[data-font-size="xlarge"] .app-basket-item-name {
    font-family: "Roboto", serif;
    font-size: 0.875rem;
    font-weight: 300;
    color: var(--x-font-color-light);
  }
}

/* Below 320px there isn't room for a product image alongside its name/price without wrapping to
   two lines — dropped so the name/price row gets the full width back instead. */
@media (max-width: 319px) {
  .app-basket-item-image {
    display: none;
  }
}
/* See "RESPONSIVE BREAKPOINTS × FONT-SIZE TIERS" near the top of the file. */
@media (max-width: 399px) {
  html[data-font-size="large"] .app-basket-item-image,
  html[data-font-size="xlarge"] .app-basket-item-image {
    display: none;
  }
}

/* TicketItem.mjs's own FREE_PRODUCTS grant marker, in its trailing icon column — a little
   twinkle so a "gratis" line reads as a small bit of magic, not just a plain $0. */
@keyframes grant-sparkle-twinkle {
  0%, 100% { transform: scale(0.85) rotate(0deg); opacity: 0.5; }
  50%      { transform: scale(1.15) rotate(15deg); opacity: 1;  }
}

.app-grant-sparkle-icon {
  display: inline-block;
  animation: grant-sparkle-twinkle 1.4s ease-in-out infinite;
}

.app-tab-btn:active {
  transform: none;
  transition: filter 0.12s ease;
}

.app-tab-btn {
  height: 42px;
}

.app-tab-btn.btn-primary,
.app-tab-btn.btn-primary:hover,
.app-tab-btn.btn-primary:active {
  background-color: white;
  border-color: #d0d0d0;
  border-bottom-color: white;
  color: var(--x-background-color-primary);
  box-shadow: inset 0 2px 0 var(--x-background-color-primary);
  filter: none;
  transform: none;
}

.app-tab-btn.btn-outline-secondary {
  --bs-btn-border-color: #d0d0d0;
  --bs-btn-hover-border-color: #d0d0d0;
  --bs-btn-active-border-color: #d0d0d0;
  --bs-btn-color: #888;
  --bs-btn-hover-color: #888;
  --bs-btn-active-color: #888;
}

.app-tab-btn.btn-outline-secondary:hover {
  background-color: rgba(0, 0, 0, 0.05);
}

.app-tab-btn.btn-outline-secondary:active {
  background-color: rgba(0, 0, 0, 0.1);
  transform: none;
  filter: none;
  transition: background-color 0.08s ease;
}

.app-tab-btn.app-tab-other-user,
.app-tab-btn.app-tab-other-user:hover,
.app-tab-btn.app-tab-other-user:active {
  color: var(--x-background-color-secondary);
  box-shadow: inset 0 2px 0 var(--x-background-color-secondary);
}

.app-tab-add-btn {
  background: transparent;
  border: 1px solid #d0d0d0;
  border-bottom: 1px solid #d0d0d0;
  color: var(--x-background-color-secondary);
  font-size: 1rem;
  padding: 0 0.75rem;
  box-shadow: 4px 0 8px rgba(0, 0, 0, 0.08);
}

.app-tab-add-btn:hover {
  background-color: rgba(0, 0, 0, 0.05);
  color: var(--x-background-color-secondary);
  filter: brightness(1.1);
}

.app-tab-add-btn:active {
  background-color: rgba(0, 0, 0, 0.05);
  color: var(--x-background-color-secondary);
  filter: brightness(0.85);
  transform: none;
  transition: background-color 0.08s ease;
}

.btn-square {
  min-width: 2.7em;
}

.app-tab-close {
  opacity: 0.7;
  font-size: 1.5rem;
  font-weight: 600;
  vertical-align: middle;
  line-height: 1;
}

.app-tab-close:hover {
  opacity: 1;
}

.app-shortcut-btn:hover {
  background-color: rgba(0, 0, 0, 0.05) !important;
  color: inherit !important;
}

.app-shortcut-btn:active {
  background-color: rgba(0, 0, 0, 0.1) !important;
  color: inherit !important;
}

.expense-row {
  border-bottom: 1px solid rgba(0, 0, 0, 0.08);
  transition: background-color 0.12s ease;
}
.expense-row:last-child {
  border-bottom: none;
}
.expense-row:hover {
  background-color: rgba(0, 0, 0, 0.04);
}
.expense-row:active {
  background-color: rgba(0, 0, 0, 0.10);
  transition: none;
}
.app-stat--clickable {
  cursor: pointer;
  border-radius: 8px;
  transition: background-color 0.12s ease;
}
.app-stat--clickable:hover {
  background-color: rgba(0, 0, 0, 0.04);
}
.app-stat--clickable:active {
  background-color: rgba(0, 0, 0, 0.10);
  transition: none;
}
.table {
  /* Bootstrap's own .table rule sets this same variable directly on the table element (not just
     as an inheritable default), so overriding it from an ancestor like body never wins — explicit
     wins over inherited regardless of the ancestor's specificity. Has to be redeclared at (at
     least) this same selector, loaded after bootstrap's CSS, to actually take effect. */
  --bs-table-hover-bg: rgba(0, 0, 0, 0.03);
}
/* Cursor-only affordance for a clickable <tr> inside a .table-hover table — the hover/active
   background itself is already owned by bootstrap's --bs-table-hover-bg above; adding
   .app-stat--clickable's own hover/active background on the row would compete with it instead of
   layering cleanly (that class targets the row itself, bootstrap paints per-cell). */
.app-table-row--clickable {
  cursor: pointer;
}
.app-table-header th {
  background: #f1f3f5;
  color: #495057;
  font-weight: 600;
  border-bottom: 2px solid #dee2e6;
}
.app-table-footer td {
  background: #f1f3f5;
  border-top: 2px solid #dee2e6;
}
.app-search-result-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  cursor: pointer;
  border-bottom: 1px solid rgba(0, 0, 0, 0.06);
  transition: background-color 0.1s ease;
}

.app-search-result-row:last-child {
  border-bottom: none;
}

.app-search-result-row:hover {
  background-color: rgba(0, 0, 0, 0.04);
}

.app-search-result-row:active {
  background-color: rgba(0, 0, 0, 0.08);
  transition: none;
}

/* RegisterSessions.mjs's monthly history table, narrowest phones (below 400px): the grouped
   Resultado and Diferencia cells drop their leading symbol badge entirely, bare amount only —
   no room to spare there yet. */
.symbol-badge {
  display: none !important;
}

@media (min-width: 400px) {
  .col-400-4 {
    flex: 0 0 auto;
    width: 33.3333%;
  }
  .flex-400-row {
    flex-direction: row;
    align-items: center;
    gap: 0.25rem;
  }
  .d-400-none {
    display: none !important;
  }
  .d-400-inline {
    display: inline !important;
  }
}
/* See "RESPONSIVE BREAKPOINTS × FONT-SIZE TIERS" near the top of the file. Same class names — the
   "400" in each is just a naming convention, not a hardcoded value, so no markup change needed. */
@media (min-width: 500px) {
  html[data-font-size="large"] .col-400-4,
  html[data-font-size="xlarge"] .col-400-4 {
    flex: 0 0 auto;
    width: 33.3333%;
  }
  html[data-font-size="large"] .flex-400-row,
  html[data-font-size="xlarge"] .flex-400-row {
    flex-direction: row;
    align-items: center;
    gap: 0.25rem;
  }
  html[data-font-size="large"] .d-400-none,
  html[data-font-size="xlarge"] .d-400-none {
    display: none !important;
  }
  html[data-font-size="large"] .d-400-inline,
  html[data-font-size="xlarge"] .d-400-inline {
    display: inline !important;
  }
}

@media (min-width: 340px) {
  /* RegisterSessions.mjs's monthly history table, CASH accounts — "ver movimientos" shows up
     here, same small size as the pencil (correct session) button until 500px. */
  .d-340-inline-block {
    display: inline-block !important;
  }
}
/* See "RESPONSIVE BREAKPOINTS × FONT-SIZE TIERS" near the top of the file. */
@media (min-width: 425px) {
  html[data-font-size="large"] .d-340-inline-block,
  html[data-font-size="xlarge"] .d-340-inline-block {
    display: inline-block !important;
  }
}

@media (max-width: 499px) {
  /* RegisterSessions.mjs's monthly history table, CASH accounts — the pencil (correct session)
     and "ver movimientos" buttons both shrink to leave more room for the table's own columns on
     narrow phones. */
  .app-table-icon-button-cash > div {
    padding: 0.35rem 0.65rem;
    font-size: 1.1rem;
    min-width: 1.1rem;
    min-height: 1.1rem;
  }
}
/* See "RESPONSIVE BREAKPOINTS × FONT-SIZE TIERS" near the top of the file. */
@media (max-width: 624px) {
  html[data-font-size="large"] .app-table-icon-button-cash > div,
  html[data-font-size="xlarge"] .app-table-icon-button-cash > div {
    padding: 0.35rem 0.65rem;
    font-size: 1.1rem;
    min-width: 1.1rem;
    min-height: 1.1rem;
  }
}

.app-table-cell-bottom {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  height: 100%;
}

@media (min-width: 320px) and (max-width: 479px) {
  /* RegisterSessions.mjs's monthly history table, DIGITAL only — it's down to just 2-3 columns
     in this range (everything folds into one combined column below 480px), so a full-bleed,
     edge-to-edge table just leaves a lot of empty cell padding. Pulling it in from the edges and
     centering it reads as intentional instead of wasted space. CASH keeps its own 5 grouped
     columns at this width and stays edge-to-edge. */
  .app-history-table-wrap.app-history-table-wrap--digital {
    max-width: 90%;
    margin-left: auto;
    margin-right: auto;
  }
}
/* See "RESPONSIVE BREAKPOINTS × FONT-SIZE TIERS" near the top of the file. */
@media (min-width: 400px) and (max-width: 599px) {
  html[data-font-size="large"] .app-history-table-wrap.app-history-table-wrap--digital,
  html[data-font-size="xlarge"] .app-history-table-wrap.app-history-table-wrap--digital {
    max-width: 90%;
    margin-left: auto;
    margin-right: auto;
  }
}

@media (min-width: 420px) {
  /* RegisterSessions.mjs's monthly history table, CASH accounts, narrowest phones: below this,
     Fecha and Inicial fold into one combined "Inicio" column (date/time, then starting amount
     below) — from here on they get their own columns again. */
  .d-420-none {
    display: none !important;
  }
  .d-420-table-cell {
    display: table-cell !important;
  }
}
/* See "RESPONSIVE BREAKPOINTS × FONT-SIZE TIERS" near the top of the file. */
@media (min-width: 525px) {
  html[data-font-size="large"] .d-420-none,
  html[data-font-size="xlarge"] .d-420-none {
    display: none !important;
  }
  html[data-font-size="large"] .d-420-table-cell,
  html[data-font-size="xlarge"] .d-420-table-cell {
    display: table-cell !important;
  }
}

@media (min-width: 350px) and (max-width: 479px) {
  /* Narrows further still, same reasoning. */
  .app-history-table-wrap.app-history-table-wrap--digital {
    max-width: 75%;
  }
}
/* See "RESPONSIVE BREAKPOINTS × FONT-SIZE TIERS" near the top of the file. */
@media (min-width: 438px) and (max-width: 599px) {
  html[data-font-size="large"] .app-history-table-wrap.app-history-table-wrap--digital,
  html[data-font-size="xlarge"] .app-history-table-wrap.app-history-table-wrap--digital {
    max-width: 75%;
  }
}

@media (max-width: 299px) {
  /* RegisterSessions.mjs's monthly history table, DIGITAL accounts — "ver movimientos" is always
     visible there (no breakpoint gates it), just smaller on the very narrowest phones. */
  .app-table-view-button > div {
    padding: 0.35rem 0.65rem;
    font-size: 1.1rem;
    min-width: 1.1rem;
    min-height: 1.1rem;
  }
}
/* See "RESPONSIVE BREAKPOINTS × FONT-SIZE TIERS" near the top of the file. */
@media (max-width: 374px) {
  html[data-font-size="large"] .app-table-view-button > div,
  html[data-font-size="xlarge"] .app-table-view-button > div {
    padding: 0.35rem 0.65rem;
    font-size: 1.1rem;
    min-width: 1.1rem;
    min-height: 1.1rem;
  }
}

@media (min-width: 480px) {
  /* RegisterSessions.mjs's monthly history table, DIGITAL accounts, narrowest phones: below this,
     Egresos and Resultado fold into the same combined "Resultado" column as Ingresos Brutos/Netos
     (gross struck-through, then net/egresos/resultado each with their own badge) — from here on
     Egresos and Resultado get their own columns again, and Ingresos Brutos/Netos stay combined
     (struck-through gross, net below) until 640px. */
  .d-480-none {
    display: none !important;
  }
  .d-480-table-cell {
    display: table-cell !important;
  }
  /* CASH accounts — the symbol badges (plus, minus, equals, asterisk, question mark) are dropped
     entirely below this, bare amount only, no room to spare — independent of the 420px
     Fecha/Inicial split. */
  .symbol-badge {
    display: inline-flex !important;
  }
}
/* See "RESPONSIVE BREAKPOINTS × FONT-SIZE TIERS" near the top of the file. */
@media (min-width: 600px) {
  html[data-font-size="large"] .d-480-none,
  html[data-font-size="xlarge"] .d-480-none {
    display: none !important;
  }
  html[data-font-size="large"] .d-480-table-cell,
  html[data-font-size="xlarge"] .d-480-table-cell {
    display: table-cell !important;
  }
  html[data-font-size="large"] .symbol-badge,
  html[data-font-size="xlarge"] .symbol-badge {
    display: inline-flex !important;
  }
}

/* RegisterSessions.mjs's monthly history table: Resultado/Diferencia start out grouped
   (+crédito/-débito/=resultado, +contado/-esperado/=diferencia stacked in one cell) to fit narrow
   screens, then expand into independent columns as space allows — Diferencia first (it's the one
   a Dueño actually needs to scan for problems), Resultado once there's room for both. */
@media (min-width: 640px) {
  .d-640-none {
    display: none !important;
  }
  .d-640-table-cell {
    display: table-cell !important;
  }
}
/* See "RESPONSIVE BREAKPOINTS × FONT-SIZE TIERS" near the top of the file. */
@media (min-width: 800px) {
  html[data-font-size="large"] .d-640-none,
  html[data-font-size="xlarge"] .d-640-none {
    display: none !important;
  }
  html[data-font-size="large"] .d-640-table-cell,
  html[data-font-size="xlarge"] .d-640-table-cell {
    display: table-cell !important;
  }
}

@media (min-width: 860px) {
  .d-860-none {
    display: none !important;
  }
  .d-860-table-cell {
    display: table-cell !important;
  }
}
/* See "RESPONSIVE BREAKPOINTS × FONT-SIZE TIERS" near the top of the file. */
@media (min-width: 1075px) {
  html[data-font-size="large"] .d-860-none,
  html[data-font-size="xlarge"] .d-860-none {
    display: none !important;
  }
  html[data-font-size="large"] .d-860-table-cell,
  html[data-font-size="xlarge"] .d-860-table-cell {
    display: table-cell !important;
  }
}

/* ================= */
/* PUBLIC PAGES      */
/* ================= */

.pub-page .form-control::placeholder {
  color: #bbb;
  opacity: .5;
}

.pub-page {
  min-height: 100dvh;
  background: #F5F5F5;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.pub-stripe {
  width: 100%;
  height: 110px;
  flex-shrink: 0;
  background: repeating-linear-gradient(
    90deg,
    var(--x-stripe-color-primary) 0px, var(--x-stripe-color-primary) 34px,
    var(--x-stripe-color-secondary) 34px, var(--x-stripe-color-secondary) 68px
  );
}

.pub-logo-wrap {
  margin: -62px auto 1.125rem;
  width: 124px;
  height: 124px;
  border-radius: 50%;
  background: #F5F5F5;
  border: 6px solid #F5F5F5;
  box-shadow: 0 6px 28px rgba(0,0,0,0.16);
  overflow: hidden;
  flex-shrink: 0;
}

.pub-logo {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}

.pub-tag {
  font-family: 'Poppins', sans-serif;
  font-size: 0.6rem;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: #999;
  margin: 0 0 1.5rem;
  text-align: center;
}

.pin-box {
  font-family: 'Poppins', sans-serif;
  font-size: 2.5rem;
  font-weight: 600;
  letter-spacing: .3em;
  background: var(--x-background-color-accent);
  border-radius: 12px;
  padding: 16px 28px;
  display: inline-block;
  color: #333;
}

.pub-footer {
  margin-top: auto;
  padding: 2rem 1.5rem 2.5rem;
  width: 100%;
  max-width: 480px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  border-top: 1px solid #E0E0E0;
}

.pub-footer-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-family: 'Poppins', sans-serif;
  font-size: 0.72rem;
  font-weight: 400;
  color: #888;
  text-align: center;
  line-height: 1.4;
}

.pub-footer-row svg {
  flex-shrink: 0;
  color: #B8B8B8;
}

/* Landing */

.landing-main {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  max-width: 340px;
  padding: 0 1.5rem 3.5rem;
}

.landing-kawaii {
  width: 100%;
  border-radius: 16px;
  display: block;
  margin-bottom: 1.5rem;
}

.landing-links {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  list-style: none;
  margin: 0;
  padding: 0;
}

.landing-btn {
  display: flex;
  align-items: center;
  gap: 0.875rem;
  padding: 0 1.375rem;
  height: 54px;
  border-radius: 10px;
  text-decoration: none;
  font-family: 'Poppins', sans-serif;
  font-size: 0.875rem;
  font-weight: 600;
  border: none;
  transition: opacity 0.14s ease, transform 0.14s ease;
  letter-spacing: 0.01em;
  cursor: pointer;
}

.landing-btn:focus {
  outline: none;
}

.landing-btn:focus-visible {
  outline: 2px solid var(--x-border-color-primary) !important;
  outline-offset: 2px;
  box-shadow: none !important;
}

.landing-btn:active {
  transform: scale(0.975);
  opacity: 0.85;
}

.landing-btn--instagram,
.landing-btn--whatsapp {
  background: #2C2C2C;
  color: #FFFFFF;
}

.landing-btn--instagram:link,
.landing-btn--instagram:visited,
.landing-btn--whatsapp:link,
.landing-btn--whatsapp:visited {
  color: #FFFFFF;
}

.landing-btn--register {
  background: var(--x-background-color-primary);
  color: #2C2C2C;
}

.landing-btn--register:link,
.landing-btn--register:visited {
  color: #2C2C2C;
}

.landing-btn--recover {
  height: 32px;
  background: color-mix(in srgb, var(--x-background-color-primary) 20%, transparent);
  color: #2C2C2C;
  border: 1px solid var(--x-border-color-primary);
  font-size: 0.8rem;
  justify-content: center;
}

.landing-btn--recover:link,
.landing-btn--recover:visited {
  color: #2C2C2C;
}

.landing-btn svg {
  flex-shrink: 0;
}

.landing-register-wrap {
  position: relative;
}

.landing-social-group {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  width: 100%;
  margin-top: 0.5rem;
}

.landing-register-badge {
  position: absolute;
  top: 0;
  right: -20px;
  background: #FF1D69;
  color: #FFFFFF;
  font-family: 'Poppins', sans-serif;
  font-size: 0.6rem;
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: 0.01em;
  padding: 4px 8px;
  border-radius: 6px;
  transform: rotate(15deg);
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25);
  max-width: 128px;
  text-align: center;
  pointer-events: none;
  z-index: 1;
}

.landing-register-note {
  font-family: 'Poppins', sans-serif;
  font-size: 0.65rem;
  color: #999;
  text-align: center;
  line-height: 1.4;
  margin: 0.75rem 0 0;
}

@media (hover: hover) {
  .landing-btn--instagram:hover,
  .landing-btn--whatsapp:hover {
    background: #3E3E3E;
  }
  .landing-btn--register:hover {
    filter: brightness(0.92);
  }
  .landing-btn--recover:hover {
    background: color-mix(in srgb, var(--x-background-color-primary) 35%, transparent);
  }
}

@media (prefers-reduced-motion: no-preference) {
  .landing-main,
  .pub-body {
    animation: landing-rise 0.45s cubic-bezier(0.22, 1, 0.36, 1) both;
    animation-delay: 0.05s;
  }
  @keyframes landing-rise {
    from { opacity: 0; transform: translateY(14px); }
    to   { opacity: 1; transform: translateY(0); }
  }
}

/* No fixed gap — justify-content-between (see PaymentMethodChipRow.mjs) spaces the icons evenly
   across whatever width the row actually has, so it never overflows no matter the screen size,
   without needing a breakpoint table of gap values to keep it from doing so. */
.payment-methods-row--hidden { height: 0; overflow: hidden; pointer-events: none; }

/* It's a plain <p> (ToggleIconButton.mjs) — without this it keeps the browser/Bootstrap default
   margin-bottom:1rem past its own visible text, which read as a big excess of empty space at the
   bottom of the dashed .payment-add-method-panel it now sits inside. */
.payment-method-label {
  margin-bottom: 0;
}

/* Dashed "add something" panel the chip row now sits inside, at the end of the payment-method
   card list — same look CategoryGroupList.mjs's own "+ Nuevo grupo" button uses
   (.app-add-category-group-button), just a plain container instead of a single-line button since
   this one wraps an entire row of icons rather than one "+ label". */
/* Starts fully visible (opacity:1) and only fades to this once the screen's own idle timer
   (PaymentMethodConfigurator.mjs's ADD_METHOD_PANEL_IDLE_DELAY_MS) fires with no touch/click
   anywhere on the screen — the :hover rule below covers anyone actually on a mouse instead, where
   that idle timer's own document-wide pointerdown listener doesn't apply (hovering isn't a
   pointerdown). */
.payment-add-method-panel {
  border: 2px dashed #d0d0d0;
  border-radius: 20px;
  padding: 0.75rem 0.5rem;
  background-color: #f9f9f9;
  opacity: 1;
  transition: opacity 0.2s ease;
}

.payment-add-method-panel--idle {
  opacity: 0.3;
}

.payment-add-method-panel--idle:hover {
  opacity: 1;
}

/* Below 420px, six payment method buttons in a row need to give up some space: the label drops
   from .t3 to .t4's font-size (the only property that actually differs between them — see the
   TYPOGRAPHY section) and the button itself loses half its horizontal padding. Scoped to
   .payment-methods-row specifically, never .radio-icon-button on its own, which is the generic
   edit/delete icon-button style used all over the app. */
@media (max-width: 419px) {
  .payment-methods-row .radio-icon-button > div {
    padding-left: 0.5rem;
    padding-right: 0.5rem;
  }
  .payment-methods-row .payment-method-label {
    font-size: 0.75rem;
  }
}
/* See "RESPONSIVE BREAKPOINTS × FONT-SIZE TIERS" near the top of the file. */
@media (max-width: 499px) {
  html[data-font-size="large"] .payment-methods-row .radio-icon-button > div,
  html[data-font-size="xlarge"] .payment-methods-row .radio-icon-button > div {
    padding-left: 0.5rem;
    padding-right: 0.5rem;
  }
  html[data-font-size="large"] .payment-methods-row .payment-method-label,
  html[data-font-size="xlarge"] .payment-methods-row .payment-method-label {
    font-size: 0.75rem;
  }
}

.checkout-summary-row {
  opacity: 1;
  transition: opacity 0.15s ease;
}
.checkout-summary-row--hidden {
  opacity: 0.05;
  pointer-events: none;
}

/* ===================== */
/* PAYMENT METHOD ICON MORPH */
/* ===================== */

.icon-morph {
  position: relative;
  width: 1.25rem;
  height: 1.25rem;
}
.icon-morph i {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: opacity 0.15s ease, transform 0.15s ease;
}
.icon-morph .icon-morph-default {
  opacity: 1;
  transform: rotate(0deg) scale(1);
}
.icon-morph .icon-morph-close {
  opacity: 0;
  transform: rotate(-45deg) scale(0.7);
}
.icon-morph.is-close .icon-morph-default {
  opacity: 0;
  transform: rotate(45deg) scale(0.7);
}
.icon-morph.is-close .icon-morph-close {
  opacity: 1;
  transform: rotate(0deg) scale(1);
}

/* ===================== */
/* REGISTER OPTION STACK */
/* ===================== */

/* `left`/`top` are set inline (ExpandableOptionStack.mjs), computed from the trigger's own
   getBoundingClientRect() — position:fixed (not absolute) since this renders through a Portal,
   straight into document.body, with no positioned ancestor of its own left to anchor against.
   z-index below every full-screen modal's own 1200 on purpose (see ExpandableOptionStack.mjs's
   own doc) — a modal opened from one of this stack's options must always cover it. */
.register-option-stack {
  position: fixed;
  display: flex;
  gap: 15px;
  align-items: center;
  z-index: 1000;
  /* Only the icon square itself was opaque — the label under it (and the gaps between slots)
     let whatever's behind the floating stack show through and bleed into the text. Wrapping the
     whole stack in its own opaque card fixes that at the source instead of per-slot. */
  background: #fff;
  border-radius: 1rem;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.45);
}
/* Stacked upward, closest option nearest the (lower) trigger — column-reverse puts the array's
   FIRST entry at the bottom of the stack, right next to it. translate(-50%,-100%) anchors the
   stack's own BOTTOM-center at the inline left/top coordinate instead of its top-left corner. */
.register-option-stack--up {
  flex-direction: column-reverse;
  transform: translate(-50%, -100%);
  padding: 18px 12px 12px;
}
/* Mirror of --up for a trigger in the top half of the screen — plain column order puts the
   array's first entry at the TOP of the stack, right next to the (higher) trigger above it. */
.register-option-stack--down {
  flex-direction: column;
  transform: translate(-50%, 0);
  padding: 12px 12px 18px;
}
.register-option-icon {
  appearance: none;
  -webkit-appearance: none;
  font: inherit;
  line-height: 1;
  margin: 0;
  padding: 0.5rem 1rem;
  min-width: 1.25rem;
  min-height: 1.25rem;
  font-size: 1.25rem;
  border-radius: 0.5rem;
  background-color: #e9e9e9;
  border: 2px solid transparent;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  position: relative;
  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
}
.register-option-icon.selected {
  border-color: var(--stack-color, var(--x-background-color-primary));
  background-color: color-mix(in srgb, var(--stack-color, var(--x-background-color-primary)) 15%, #e9e9e9);
}
.register-option-icon .register-option-check {
  position: absolute;
  top: -6px;
  right: -6px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--stack-color, var(--x-background-color-primary));
  color: #fff;
  font-size: 0.65rem;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  transform: scale(0.5);
  transition: opacity 0.12s ease, transform 0.12s ease;
}
.register-option-icon.selected .register-option-check {
  opacity: 1;
  transform: scale(1);
}
.register-option-icon:hover {
  filter: brightness(0.9);
}
.register-option-icon:active {
  filter: brightness(0.8);
}
.register-option-slot {
  transform-origin: bottom center;
  animation: register-option-enter 0.16s cubic-bezier(0.34, 1.56, 0.64, 1) both;
  animation-delay: calc(var(--stack-index, 0) * 0.045s);
}

@keyframes register-option-enter {
  from { opacity: 0; transform: translateY(32px) scale(0.7); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}

.ticket-history-separator { border-top: 1px solid #f0f0f0; }
.ticket-history-items { font-size: 0.82rem; border-collapse: collapse; }


/* One row of ProductFilters.mjs — 2 filter widgets side by side, collapsing to a single stacked
   column on narrow screens (e.g. the catalog's bulk-add screen, which runs much narrower than the
   admin products list this layout was designed for). */
.app-product-filters-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: .5rem;
}

/* ======================= */
/* SELECTABLE PRODUCT LIST */
/* ======================= */

/* Fixed height + its own scroll — CategoryAddProducts.mjs/QuantityPricingSelectProducts.mjs can
   both list hundreds of products, and this used to grow the whole page along with them instead of
   staying put while the checklist itself scrolled. */
.app-selectable-product-list {
  border-radius: 12px;
  height: 400px;
  overflow-y: auto;
}

.app-selectable-product-row {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  padding: 0.5rem 0.75rem;
  cursor: pointer;
  border-left: 3px solid transparent;
  border-bottom: 1px solid;
}

.app-selectable-product-row:last-child {
  border-bottom: none;
}

/* Neutral: the list still being filtered through — plain gray, deliberately unremarkable next to
   the primary-colored final list below it. */
.app-selectable-product-list--neutral .app-selectable-product-row {
  background: var(--x-background-color-neutral-light);
  border-bottom-color: #e5e5e5;
}

.app-selectable-product-list--neutral .app-selectable-product-row .form-check-input {
  accent-color: var(--x-background-color-neutral-dark);
}

/* Primary: the committed final list — the tenant's own brand color (green here, whatever else
   another tenant sets), so it reads as "this is what's actually going to be saved" at a glance,
   not just another gray list. */
.app-selectable-product-list--primary .app-selectable-product-row {
  background: color-mix(in srgb, var(--x-background-color-primary) 12%, white);
  border-bottom-color: color-mix(in srgb, var(--x-background-color-primary) 25%, white);
  border-left-color: var(--x-background-color-primary);
}

.app-selectable-product-list--primary .app-selectable-product-row .form-check-input {
  accent-color: var(--x-background-color-primary);
}

@media (max-width: 350px) {
  .app-product-filters-row {
    grid-template-columns: 1fr;
  }
}
/* See "RESPONSIVE BREAKPOINTS × FONT-SIZE TIERS" near the top of the file. */
@media (max-width: 438px) {
  html[data-font-size="large"] .app-product-filters-row,
  html[data-font-size="xlarge"] .app-product-filters-row {
    grid-template-columns: 1fr;
  }
}

.app-filter-pill {
  display: flex;
  align-items: center;
  gap: 8px;
  border-radius: 20px;
  padding: .4rem .6rem;
  border: 1px solid;
  font-size: .9rem;
  line-height: 1.5;
  width: 100%;
  min-width: 0;
  user-select: none;
}

.app-filter-pill--empty {
  background: #f1f3f5;
  border-color: #dee2e6;
  color: #6c757d;
  cursor: pointer;
}

.app-filter-pill--selected {
  background: var(--x-background-color-primary);
  border-color: var(--x-border-color-primary);
  color: #fff;
  cursor: pointer;
}

.app-filter-pill--selected .t2 {
  color: #fff;
}

.app-filter-pill--expanded {
  background: #f1f3f5;
  border-color: #dee2e6;
  color: #6c757d;
  cursor: default;
}

.app-filter-pill--empty:focus-within,
.app-filter-pill--expanded:focus-within {
  border-color: #adb5bd;
}

.app-filter-pill__input {
  flex: 1;
  min-width: 0;
  width: 0;
  background: transparent;
  border: none !important;
  outline: none !important;
  box-shadow: none !important;
  padding: 0;
  color: inherit;
  font-size: inherit;
  line-height: inherit;
}

input.app-filter-pill {
  display: block;
  outline: none !important;
  box-shadow: none !important;
  cursor: pointer;
}

input.app-filter-pill:focus {
  border-color: #adb5bd;
}

.app-filter-pill--open {
  border-color: #adb5bd;
}

.app-filter-pill__clear {
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.18);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}

/* ===================== */
/* PRODUCT IMPORT MAPPING */
/* ===================== */

/* One shared column template for every row of ProductImport.mjs's mapping table (header, each
   field row, the variants toggle row, each advanced-mode dupla row) so the 4 columns line up
   pixel-for-pixel across all of them, instead of each row separately replicating the same
   flex-basis math (which drifted out of sync between rows more than once before this). */
.app-import-mapping-row {
  display: grid;
  grid-template-columns: 180px 1fr 1fr 60px;
  column-gap: 0.5rem;
  align-items: center;
}

/* Avanzado variant duplas: same row pattern, with an extra 1fr track for the optional value
   (code) column select. */
.app-import-variant-dupla-row {
  display: grid;
  grid-template-columns: 180px 1fr 1fr 1fr 60px;
  column-gap: 0.5rem;
  align-items: center;
}

/* Avanzado tags: one row per filter -> tags rule — filter text, tags to assign, remove button.
   Same leading 180px track as .app-import-mapping-row so these rows line up under the "Columnas
   evaluadas" field above them. */
.app-import-tag-dupla-row {
  display: grid;
  grid-template-columns: 180px 1fr 1fr 60px;
  column-gap: 0.5rem;
  align-items: center;
}

/* ===================== */
/* PRODUCT GRID           */
/* ===================== */

.app-product-grid-container {
  max-width: 960px;
  margin: 20px auto;
  padding: 0 8px;
}

.app-product-grid {
  display: grid;
  gap: 8px;
  /* Every row the height of the tallest one. Cells differ in how much they carry under the image —
     nothing at all for an add tile, one or two lines for a product name — and without this the
     rows end up visibly different heights, with the circles no longer lining up across them. */
  grid-auto-rows: 1fr;
}

/* Centers the auto grid as a group instead of stretching it: a fixed (not 1fr) tile width
   means auto-fit only creates as many tracks as there are items to fill, collapsing the rest
   to 0 — so width:fit-content shrinks the grid to just those tracks and the flex wrapper
   centers it. A constant tile size also means no visible jump when a resize crosses into
   more columns; only the column count changes, never the tile size. */
.app-product-grid-auto-wrap {
  display: flex;
  justify-content: center;
}

.app-product-grid-auto {
  grid-template-columns: repeat(auto-fit, 117px);
  width: fit-content;
  /* Caps auto-fit at 6 columns (6*117px tiles + 5*8px gaps) even though the 960px container
     has room for a 7th — 6 was the requested ceiling regardless of extra width. */
  max-width: min(100%, 742px);
}

/* printOffscreen.mjs — #app-print-portal sits off-screen during normal use (not display:none, so
   printable content like images actually loads/renders), and only becomes the page's sole visible
   content while body.app-printing is set and print media applies. */
#app-print-portal {
  position: fixed;
  top: 0;
  left: -99999px;
  width: 720px;
}

@media print {
  body.app-printing > *:not(#app-print-portal) {
    display: none !important;
  }
  body.app-printing #app-print-portal {
    position: static;
    left: auto;
  }
}

/* ===================== */
/* PHOTO UPLOAD           */
/* ===================== */

/* PhotoUpload.mjs's circular badges — the collapsed pencil toggle and, once expanded, each action
   (trash/paste/upload) sitting to its left. Solid color + white border, same look the toggle
   always had. :hover/:active darken via --app-photo-upload-btn-bg + color-mix() rather than a
   filter — a filter darkens the whole rendered element, border included, leaving the white border
   looking dirty/gray on hover; color-mix() only touches the fill, so the border stays crisp white
   regardless of variant (green/red/gray, each just overriding the custom property). :active also
   scales the badge up while held — transition on transform (not just background-color) is what
   makes it shrink back smoothly on release instead of snapping. */
.app-photo-upload-btn {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  flex-shrink: 0;
  --app-photo-upload-btn-bg: var(--x-background-color-secondary);
  background: var(--app-photo-upload-btn-bg);
  color: #fff;
  border: 2px solid #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  font-size: 1rem;
  cursor: pointer;
  transition: background-color 0.15s ease, transform 0.1s ease;
}

.app-photo-upload-btn:hover:not(:disabled) {
  background: color-mix(in srgb, var(--app-photo-upload-btn-bg) 82%, black);
}

.app-photo-upload-btn:active:not(:disabled) {
  background: color-mix(in srgb, var(--app-photo-upload-btn-bg) 72%, black);
  transform: scale(1.15);
}

.app-photo-upload-btn:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.app-photo-upload-btn.danger {
  --app-photo-upload-btn-bg: #dc3545;
}

/* The toggle badge once expanded (now an X) — a smaller round body of its own, same neutral gray
   as the shared pill behind it, so it still reads as "close" rather than another action. */
.app-photo-upload-btn.close {
  width: 30px;
  height: 30px;
  --app-photo-upload-btn-bg: #e9ecef;
  color: #6c757d;
}

/* ============================= */
/* PAYMENT METHOD CONFIGURATOR   */
/* ============================= */

/* Same role as .app-variant-stepper-btn (VariantComboRow.mjs's own +/-/quick-jump buttons) but
   smaller — PaymentMethodCard.mjs packs 5 of these plus a wider amount box on one compact row, so
   it gets its own class instead of shrinking the catalog's stepper everywhere else too. */
.payment-stepper-btn {
  padding: 0.6rem;
  font-size: 0.8rem;
  line-height: 1;
  min-width: 3rem;
  min-height: 1rem;
  transition: none;
}

/* Bootstrap's own disabled opacity (0.65) barely reads as disabled at this button's own small
   size/light outline style — washed out further so it's unmistakable at a glance. */
.payment-stepper-btn:disabled {
  opacity: 0.3;
}

/* Progressive hide, least useful first — same idea as .app-variant-multiple-btn's own narrow-width
   rule above, but staged in two steps instead of an all-or-nothing hide: +10k goes first (the
   least likely to be needed for a typical amount), +1k only once things get even tighter, always
   leaving the core -/amount/+ stepper (the part that actually matters) untouched. */
@media (max-width: 449px) {
  .payment-stepper-btn--10k {
    display: none;
  }
}
@media (max-width: 379px) {
  .payment-stepper-btn--1k {
    display: none;
  }
}

.payment-method-card {
  position: relative;
  margin-bottom: 1rem;
}

/* The cashier's own "I actually checked this one" per card (PaymentMethodConfigurator.mjs) —
   double .app-tile-remove-badge's own 22px diameter, vertically centered on the card instead of
   pinned to a corner, straddling its right edge (half in, half out) the same way the remove badge
   straddles the top-right corner. Starts neutral gray (unconfirmed); Finalizar stays disabled
   until every card's own badge is green. */
.payment-method-card-confirm-badge {
  position: absolute;
  top: 50%;
  right: 0;
  transform: translate(50%, -50%);
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: #adb5bd;
  color: #fff;
  border: 1px solid #dc3545;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.6rem;
  cursor: pointer;
  z-index: 2;
  transition: filter 0.1s ease, transform 0.1s ease, background-color 0.15s ease;
}

.payment-method-card-confirm-badge:hover {
  filter: brightness(0.95);
}

.payment-method-card-confirm-badge:active {
  transform: translate(50%, -50%) scale(0.9);
}

.payment-method-card-confirm-badge--confirmed {
  background: #198754;
  border-color: transparent;
}

/* Tapped while the card is still red (no amount yet, or cuotas/cuenta still pending) — same
   points-shake-red-kf feedback .payment-method-card-amount--shake already uses, just anchored to
   this badge's own translate(50%,-50%) positioning instead of a plain 0,0 origin. */
.payment-method-card-confirm-badge--shake {
  animation: payment-method-card-confirm-badge-shake-kf 0.3s ease;
}

@keyframes payment-method-card-confirm-badge-shake-kf {
  10%, 90% { transform: translate(50%, -50%) translateX(-1px); }
  20%, 80% { transform: translate(50%, -50%) translateX(2px); }
  30%, 50%, 70% { transform: translate(50%, -50%) translateX(-4px); }
  40%, 60% { transform: translate(50%, -50%) translateX(4px); }
}

/* Plays once, at the moment the row's own PaymentMethodCard first mounts (key=${row.rowId} in
   PaymentMethodConfigurator.mjs keeps the DOM node stable across re-renders after that, so this
   never replays on an amount/cuotas/etc change). Animating grid-template-rows (0fr -> 1fr)
   instead of a transform: scaleY — a transform is purely visual and doesn't affect layout, so the
   card would reserve its full final height from frame one and everything below it (the dashed
   add-method panel) would just sit there already, not actually get pushed down as the card opens.
   The 1-row grid + overflow:hidden inner is the standard trick for animating to an unknown
   "auto" content height, since height itself can't be animated to auto directly. Width is
   unaffected either way — the card is always 100% wide, only its own row track height changes. */
@keyframes payment-method-card-grow-kf {
  from { grid-template-rows: 0fr; opacity: 0; }
  to   { grid-template-rows: 1fr; opacity: 1; }
}

.payment-method-card-grow {
  display: grid;
  animation: payment-method-card-grow-kf 0.15s ease both;
}

/* overflow:hidden only for the duration of the grow-in above (clipping the card's real content
   against the still-collapsing grid row track it sits in) — snapped back to visible right as the
   animation ends (and kept there via fill-mode:both) so the amount stepper's own floating drag
   slider (FloatingHorizontalSlider.mjs, positioned to float ABOVE the input) isn't clipped by this
   same wrapper afterward. overflow isn't a smoothly-interpolatable property, so this just flips at
   99% instead of easing — that's fine, nothing visually depends on a gradual overflow change. */
@keyframes payment-method-card-grow-inner-kf {
  from, 99% { overflow: hidden; }
  to        { overflow: visible; }
}

/* Right padding wider than the rest — clearance from .payment-method-card-confirm-badge, which
   pokes 22px into the card from its right edge (half of its own 44px diameter). */
.payment-method-card-grow-inner {
  min-height: 0;
  padding: 0.6rem 1.5rem 0.6rem 0.75rem;
  animation: payment-method-card-grow-inner-kf 0.15s linear both;
}

/* PaymentMethodCard.mjs's stepper row + whatever sits under it (transfer alias, Calcular vuelto)
   — as wide as the stepper row's own buttons and centered, so the rows below line up exactly with
   its two ends instead of stretching across the whole card. */
.payment-method-card-amount-block {
  width: fit-content;
  max-width: 100%;
  margin: 0 auto;
}

/* The DEPOSIT-only pickup date/time card (PaymentMethodConfigurator.mjs) is always present, never
   mounted/removed, so it skips the grow/inner wrapper the animated payment-method cards use —
   same padding value, applied directly since there's no separate "inner" layer to carry it here. */
.payment-method-card--static {
  padding: 0.6rem 1.5rem 0.6rem 0.75rem;
}

/* ByPriceQuantityCard.mjs's own lines — inset on both sides so they line up with the other cards'
   content (PaymentMethodCard.mjs's stepper row and Calcular vuelto), which sits the same distance in
   from the card's left edge and from .payment-method-card-confirm-badge. */
.by-price-quantity-card-item {
  padding: 0 1.25rem;
}

/* The balance badge — a notch above Bootstrap's own 0.75em badge size. */
.by-price-quantity-card-badge {
  font-size: 0.9em;
}

/* Double .payment-method-card--static's own 0.6rem bottom padding. */
.by-price-quantity-card {
  padding-bottom: 1.2rem;
}

/* $0, or a still-pending cuotas/cuenta selection, both block Finalizar (see canFinalize's own
   `hasZeroAmountRow`/`hasPendingSelection` checks in PaymentMethodConfigurator.mjs) — flagged red
   here so it's obvious at a glance which card still needs attention. */
.payment-method-card--invalid {
  --bs-card-border-color: #dc3545;
}

.payment-method-card-header {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

/* align-self:flex-start (overriding .payment-method-card-header's own align-items:center — the
   adjustment badge still needs that centering, see its own rule) — a label with extra lines below
   it (Puntos'/Transferencia's own pointsHint/alias, see PaymentMethodCard.mjs) would otherwise
   center this against the WHOLE multi-line block, dropping the icon below the label's own line
   instead of sitting next to it. */
.payment-method-card-icons {
  position: relative;
  display: flex;
  align-items: center;
  align-self: flex-start;
  gap: 0.35rem;
  font-size: 1.1rem;
  flex-shrink: 0;
}

/* Fixed width, not flex:1 — same convention as the catalog's own .app-variant-stepper-input
   (VariantComboRow.mjs's own amount box, sized to its content instead of stretching to fill
   whatever room a flex row happens to have), just wider to fit a peso amount instead of a bare
   quantity. .app-variant-combo-row-actions itself is only ever squeezed flush right by its OWN
   --labeled parent (.app-variant-combo-row), which this card doesn't use — so with no flex-grow
   here the whole stepper row just sits at its natural width under the header, same as any other
   .app-variant-combo-row-actions consumer. */
.payment-method-card-amount {
  width: 6rem;
  text-align: center;
  cursor: pointer;
  font-weight: 600;
  padding: 0.35rem 0.5rem;
  border: 1px solid #dee2e6;
  border-radius: 0.375rem;
  background: #fff;
}

/* $0 flagged directly on the amount input itself, not just the card's own outer border — this is
   specifically an amount problem, unlike a pending cuenta/cuotas selection (see
   .payment-method-card--invalid), which the outer border alone already covers. */
.payment-method-card-amount--invalid {
  border-color: #dc3545;
}

/* Tapping the input when ceiling===0 (nothing else to give it, see PaymentMethodCard.mjs's own
   handleBlockedAmountTap) — reuses the same red shake every other "can't do that right now"
   feedback in the app already uses (see points-shake-red-kf's own doc). */
.payment-method-card-amount--shake {
  animation: points-shake-red-kf 0.3s ease;
}

.payment-method-card-adjustment {
  margin-left: auto;
  white-space: nowrap;
}

/* Below 400px the card header is too tight for the full "20% descuento -$12.175" text — only the
   bare signed peso figure (or bare rate alone with no delta yet) still fits. */
.payment-method-card-adjustment-short {
  display: none;
}
@media (max-width: 399px) {
  .payment-method-card-adjustment-full {
    display: none;
  }
  .payment-method-card-adjustment-short {
    display: inline;
  }
}

/* .app-light-panel's own default (padding:10px, border-radius:20px) is shared with
   CustomerSearchFilter.mjs/CustomerSearch.mjs — this scopes a flatter, less rounded look to just
   this card's own hint panels (Puntos balance, Transferencia alias) instead of touching that
   shared class. */
.payment-method-card-hint-panel {
  padding: 5px 15px;
  border-radius: 5px;
}

/* Blocks the stepper entirely (including tapping the amount to open AmountKeypadModal) while a
   Crédito card still needs its cuotas picked, or any card with 2+ eligible accounts still needs
   one picked — see PaymentMethodCard.mjs's own `selectionPending`. */
.payment-method-card-actions-disabled {
  opacity: 0.4;
  pointer-events: none;
}

/* TemplateImageManagerModal.mjs: below Bootstrap's md breakpoint the thumbnail and the delete
   button share their own row (delete pinned to the right via justify-content:space-between),
   while the Nombre/Ancho de impresión fields stack as separate rows below. At md+ this wrapper
   dissolves (display:contents) so the thumbnail and the button rejoin the outer flex-md-row as
   direct items — the button's own `order-md-5`/`ms-md-auto` then push it to the end of that row,
   in line with the inputs, matching the already-confirmed desktop layout. Pure layout breakpoint,
   not font-size dependent — no 1.25x twin needed (see the file-level comment above). */
.tmpl-image-thumb-actions-row {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  width: 100%;
}
@media (min-width: 768px) {
  .tmpl-image-thumb-actions-row {
    display: contents;
  }
}

/* TemplateMarkdownEditor.mjs: the editor/preview two-column layout, as a 2x2 grid — CSS Grid
   (not flexbox) specifically because each column's HEADER row (help+toolbar buttons on the
   left, "Vista previa"+printer select on the right) needs to end up the exact same width as
   that column's CONTENT row below it (textarea vs. the preview box, whose width tracks the
   printer's own numCols and isn't known ahead of time) — a shared grid column track sizes
   itself once from every cell in it, header and content included, so the two rows can never
   drift apart the way they would nesting/duplicating a flex-basis by hand across two separate
   flex rows. `minmax(200px, 1fr)` keeps the textarea column from collapsing to its own
   min-content (a bare textarea's is near 0); `auto` keeps the preview column exactly as wide as
   its own content (the ticket box) and no wider, matching the "let the preview keep its full
   width, the editor gives up space first" requirement.
   Named `grid-template-areas` (not DOM order) place the 4 cells — below 700px there's no room
   for both columns side by side and it collapses to one column, but each header still needs to
   render directly above ITS OWN content ("Vista previa" above the preview box, not above the
   textarea) rather than in DOM order (both headers, then both bodies) — areas let the desktop
   and mobile arrangements each place the same 4 elements independently of where they sit in the
   markup. Pure layout breakpoint, not font-size dependent — no 1.25x twin needed (see the
   file-level comment above). */
.tmpl-markdown-editor-grid {
  display: grid;
  grid-template-columns: minmax(200px, 1fr) auto;
  grid-template-areas: "edit-header preview-header" "edit-body preview-body";
  column-gap: 16px;
  row-gap: 4px;
}
.tmpl-markdown-editor-edit-header { grid-area: edit-header; }
.tmpl-markdown-editor-edit-body { grid-area: edit-body; }
/* min-height matches .btn-primary's own rendered height, so the label keeps the same vertical
   room as the edit column's header even where (stacked below, on its own row) nothing forces
   the two to stretch to a shared row height any more. */
.tmpl-markdown-editor-preview-header { grid-area: preview-header; min-height: 40px; }
.tmpl-markdown-editor-preview-body { grid-area: preview-body; }
/* Hides the textarea's own native scrollbars (both axes) without disabling scroll itself — an
   unstyled horizontal scrollbar consumes real height from the textarea's own content box on
   platforms without overlay scrollbars (Windows, some Linux), which the line-number gutter/
   error-highlight overlay beside it never loses (they have no horizontal overflow of their own),
   throwing their line rows out of sync with the textarea's the further down a long template goes.
   Hiding both here (rather than only horizontal) keeps the fix simple and cross-browser —
   Firefox's scrollbar-width has no per-axis variant. */
.tmpl-markdown-editor-textarea {
  scrollbar-width: none;
}
.tmpl-markdown-editor-textarea::-webkit-scrollbar {
  display: none;
}
@media (max-width: 699px) {
  .tmpl-markdown-editor-grid {
    grid-template-columns: 1fr;
    grid-template-areas: "edit-header" "edit-body" "preview-header" "preview-body";
  }
  /* Stacked, "Vista previa" now sits directly under the editor's own bordered box instead of
     beside it — needs its own breathing room above it that the shared row-gap (kept tight for
     the desktop side-by-side case) doesn't provide. */
  .tmpl-markdown-editor-preview-header {
    margin-top: 16px;
  }
}
