/* ==========================================================================
   TSS Quick Bill — application shell and shared components.

   Loaded on every signed-in page, after tokens.css. Everything here is either
   the frame (app, page, topbar, navbar) or a component used on more than one
   surface (button, field, card, table, toast, sheet). Anything used on one
   screen only lives in that screen's own stylesheet.

   Written against the MUNGARU tokens. No value is hard-coded that a token
   already names — a colour written literally here is a colour that will not
   follow the dark theme.
   ========================================================================== */

/* ---------------------------------------------------------------- hiding */

/* The browser hides `[hidden]` with a UA rule of `display: none`, and ANY
   author rule that sets `display` beats it — including a bare
   `.sheet-scrim { display: flex }`. That is not a subtle bug: it shipped, and
   the cancel-bill confirmation and the type-code panel were both stuck on
   screen over the camera on a real cashier's phone, with no way to dismiss
   them.

   So the attribute is made to win outright. `!important` is nearly always the
   wrong tool; here it is the right one, because "hidden means hidden" is not
   a rule any later stylesheet should be able to argue with. Every overlay in
   this product is toggled by this attribute, so this single declaration is
   what stands between a working screen and an unusable one. */
[hidden] { display: none !important; }

/* ------------------------------------------------------------------ shell */

.app {
  min-height: 100svh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  background: var(--bg);
}

.page {
  flex: 1 1 auto;
  width: 100%;
  max-width: 44rem;
  margin-inline: auto;
  padding: var(--sp-4) var(--sp-4) calc(var(--sp-8) + env(safe-area-inset-bottom));
}

.page--wide { max-width: 72rem; }
.page--flush { padding: 0; max-width: none; }

/* A page that owns the whole viewport — the scan screen, the gate — cannot
   scroll as a document or the viewfinder would drift off the top. */
.app--fixed { height: 100svh; height: 100dvh; overflow: hidden; }
.app--fixed .page { overflow: hidden; padding: 0; }

.stack > * + * { margin-top: var(--sp-4); }
.stack-tight > * + * { margin-top: var(--sp-2); }
.row { display: flex; align-items: center; gap: var(--sp-3); }
.row--between { justify-content: space-between; }
.row--wrap { flex-wrap: wrap; }
.push { margin-left: auto; }
.grow { flex: 1 1 auto; min-width: 0; }

/* ----------------------------------------------------------------- topbar */

.topbar {
  position: sticky;
  top: 0;
  z-index: var(--z-bar);
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  min-height: 3rem;
  padding: var(--sp-2) var(--sp-4);
  padding-top: calc(var(--sp-2) + env(safe-area-inset-top));
  background: var(--surface);
  border-bottom: var(--bw-hair) solid var(--rule);
  color: var(--ink);
}

.topbar__brand {
  /* The fix belongs on the parent, not on the mark: letting the brand shrink
     is what stopped the mode pill sitting on top of it at 360px. */
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-weight: var(--w-strong);
  font-size: var(--fs-meta);
  letter-spacing: .02em;
  text-decoration: none;
  color: inherit;
}

.topbar__mark {
  width: 1.5rem;
  height: 1.5rem;
  display: grid;
  place-items: center;
  border-radius: var(--r-sm);
  background: var(--accent);
  color: var(--on-accent);
  font-weight: var(--w-heavy);
  font-size: .7rem;
}

.topbar__meta {
  font-size: var(--fs-meta);
  color: var(--ink-mute);
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  /* The mode pill ("Demo catalogue", "Shadow mode") is a whole phrase, and
     with the brand pinned at `flex: none` it pushed the row 28px past a 360px
     screen. The meta side is what gives: it may shrink, and the pill inside it
     may lose its label before anything leaves the viewport. */
  min-width: 0;
  flex: 0 1 auto;
}

/* The pill keeps its icon and its colour at every width. Below 380px it drops
   the words rather than the row losing the theme toggle and the menu — the
   icon plus the `title` still say which mode this is, and the state is also
   printed on the billing screen itself. */
.topbar__meta .pill {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

@media (max-width: 23.75rem) {
  .topbar__meta .pill { font-size: 0; gap: 0; padding: .3rem; }
  .topbar__meta .pill .icon { font-size: initial; }
}

.topbar__who {
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  border-radius: var(--r-full);
  background: var(--accent-wash);
  color: var(--accent);
  font-size: .72rem;
  font-weight: var(--w-strong);
  text-decoration: none;
}

/* A connection dot with no text would be colour-alone meaning, so it carries
   a title and the offline state also swaps in an icon. */
.dot {
  width: .5rem;
  height: .5rem;
  border-radius: var(--r-full);
  background: var(--ok);
  flex: none;
}
.dot--off { background: var(--warn); }

/* ----------------------------------------------------------------- navbar */

/* Bill detail renders a navbar it does not want on a phone: the rail is the
   right frame at a desk, and a frame that vanishes one click into Bills makes
   one product feel like two. The suppression lives HERE rather than in
   desk.css so it is not contingent on a stylesheet fetch — a cashier hands
   this screen across the counter, and a tab bar appearing under it is a
   mis-tap into another part of the app. desk.css turns it back on at ≥64rem. */
.app--desknav > .navbar { display: none; }

.navbar {
  position: sticky;
  bottom: 0;
  z-index: var(--z-bar);
  display: flex;
  background: var(--surface);
  border-top: var(--bw-hair) solid var(--rule);
  padding-bottom: env(safe-area-inset-bottom);
}

.navbar__item {
  flex: 1 1 0;
  min-height: var(--tap-anchor);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: .15rem;
  padding: var(--sp-2) var(--sp-1);
  color: var(--ink-mute);
  text-decoration: none;
  font-size: .68rem;
  font-weight: var(--w-strong);
  letter-spacing: .01em;
}

.navbar__item[aria-current="page"] { color: var(--accent); }
.navbar__item[aria-current="page"] .icon { transform: translateY(-1px); }

/* --------------------------------------------------------------- headings */

.title {
  font-size: var(--fs-title);
  line-height: var(--lh-title);
  font-weight: var(--w-strong);
  color: var(--ink);
  margin: 0;
}

.head {
  font-size: var(--fs-head);
  line-height: var(--lh-title);
  font-weight: var(--w-strong);
  color: var(--ink);
  margin: 0;
}

.lead { font-size: var(--fs-lead); color: var(--ink-mute); margin: 0; }
.meta { font-size: var(--fs-meta); color: var(--ink-mute); margin: 0; }
.faint { color: var(--ink-faint); }

.eyebrow {
  font-size: var(--fs-meta);
  font-weight: var(--w-strong);
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 0 0 var(--sp-2);
}

/* Kannada and English are given the same size, weight and colour. Kannada has
   no upper case, so ranking them by any of those three would quietly make one
   language the subtitle of the other. */
.bilingual { display: flex; flex-wrap: wrap; gap: .45rem; align-items: baseline; }
.bilingual__kn, .bilingual__en { font-size: inherit; font-weight: inherit; color: inherit; }
.bilingual__sep { color: var(--ink-faint); }

/* --------------------------------------------------------------- surfaces */

.card {
  background: var(--surface);
  border: var(--bw-hair) solid var(--rule);
  border-radius: var(--r-lg);
  padding: var(--sp-4);
}

.card--sunken { background: var(--surface-sunken); }
.card--raised { background: var(--surface-raised); box-shadow: var(--shadow-1); }
.card--flush { padding: 0; overflow: hidden; }

.card__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-3);
  margin-bottom: var(--sp-3);
}

.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
  gap: var(--sp-3);
}

.tile {
  background: var(--surface);
  border: var(--bw-hair) solid var(--rule);
  border-radius: var(--r-lg);
  padding: var(--sp-4);
  display: flex;
  flex-direction: column;
  gap: .3rem;
}

.tile__label {
  font-size: var(--fs-meta);
  font-weight: var(--w-strong);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.tile__value {
  font-size: var(--fs-head);
  line-height: 1.1;
  font-weight: var(--w-strong);
  color: var(--ink);
}

.tile__note { font-size: var(--fs-meta); color: var(--ink-mute); }

/* ---------------------------------------------------------------- buttons */

.btn {
  --btn-bg: var(--surface);
  --btn-ink: var(--ink);
  --btn-edge: var(--rule-strong);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  min-height: var(--tap-min);
  padding: 0 var(--sp-4);
  border: var(--bw-hair) solid var(--btn-edge);
  border-radius: var(--r-md);
  background: var(--btn-bg);
  color: var(--btn-ink);
  font: inherit;
  font-weight: var(--w-strong);
  font-size: var(--fs-body);
  text-decoration: none;
  cursor: pointer;
  transition: transform var(--dur-press) var(--ease-press),
              background-color var(--dur-press) var(--ease-press);
  -webkit-tap-highlight-color: transparent;
}

.btn:active { transform: scale(.97); }
.btn[disabled], .btn[aria-disabled="true"] { opacity: .5; pointer-events: none; }

.btn--primary {
  --btn-bg: var(--accent);
  --btn-ink: var(--on-accent);
  --btn-edge: transparent;
  min-height: var(--tap-primary);
}
.btn--primary:active { --btn-bg: var(--accent-press); }

.btn--anchor {
  /* The one button the whole screen exists for. Full width, tallest target,
     bottom of the thumb zone. */
  width: 100%;
  min-height: var(--tap-anchor);
  font-size: var(--fs-lead);
}

.btn--quiet { --btn-edge: transparent; --btn-bg: transparent; --btn-ink: var(--ink-mute); }
.btn--ghost { --btn-bg: transparent; }
.btn--danger { --btn-ink: var(--danger); --btn-edge: var(--danger); --btn-bg: transparent; }
.btn--full { width: 100%; }
/* No reduced height. 2.25rem is 36px, under the 48px floor, and this class
   is on "Send a new code" during sign-in and on the camera's "Try again" —
   both of which are the only way forward when they appear. */
.btn--small { padding: 0 var(--sp-3); font-size: var(--fs-meta); }

.btn--icon {
  width: var(--tap-min);
  min-height: var(--tap-min);
  padding: 0;
  border-radius: var(--r-md);
}

.btn-row { display: flex; gap: var(--sp-3); }
.btn-row > .btn { flex: 1 1 0; }

/* ----------------------------------------------------------------- fields */

.field { display: block; position: relative; }
.field + .field { margin-top: var(--sp-4); }

.field__label {
  display: block;
  font-size: var(--fs-meta);
  font-weight: var(--w-strong);
  color: var(--ink-mute);
  margin-bottom: var(--sp-1);
}

.field__input {
  width: 100%;
  min-height: var(--tap-primary);
  padding: var(--sp-3);
  border: var(--bw-strong) solid var(--rule-strong);
  border-radius: var(--r-md);
  background: var(--surface);
  color: var(--ink);
  /* 16px minimum: anything smaller makes iOS Safari zoom the page on focus,
     which on a scan screen throws the viewfinder off the top. */
  font: inherit;
  font-size: var(--fs-body);
}

.field__input:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 1px;
  border-color: var(--accent);
}

.field__input--code {
  font-family: var(--font-mono);
  font-size: var(--fs-head);
  letter-spacing: .5em;
  text-align: center;
  padding-inline: 0;
  text-indent: .5em;
}

.field__hint { font-size: var(--fs-meta); color: var(--ink-faint); margin-top: var(--sp-1); }
/* A hint that is telling the staffer something is off rather than
   explaining the field. Amber, which this palette keeps for a warning. */
.field__hint--warn { color: var(--warn); }
.field__error { font-size: var(--fs-meta); color: var(--danger); margin-top: var(--sp-1); font-weight: var(--w-strong); }
.field--bad .field__input { border-color: var(--danger); }

/* A value shown for reference, not for changing — the username on the staff
   edit form. Readonly rather than disabled: an admin should still be able to
   select and copy it, and disabled controls drop below the contrast floor. */
.field__input[readonly] {
  background: var(--surface-sunken);
  color: var(--ink-mute);
  cursor: default;
}

/* A select has to LOOK like one. `appearance: none` strips the platform's own
   dropdown arrow, and the right padding here was reserving room for a
   replacement that was never drawn — so every select in the app rendered as a
   filled text box, and a cashier looking for the machine chooser on the bank
   screen reported it missing while looking straight at it.

   Drawn as a background chevron rather than a pseudo-element: `select` cannot
   carry ::after in any engine, so the alternative is a wrapper element around
   every one of them.

   `currentColor` cannot be used inside a `url()`, so the stroke is the ink
   token's own value, restated per theme below. */
select.field__input {
  appearance: none;
  padding-right: var(--sp-6);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='%235B6B60' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--sp-3) center;
  background-size: 20px 20px;
}
:root:not([data-theme="light"]) select.field__input {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='%239FB0A4' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
}
:root[data-theme="dark"] select.field__input {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='%239FB0A4' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
}
:root[data-theme="light"] select.field__input {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='%235B6B60' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
}

/* "required", said in the label rather than left to a browser tooltip. Lives
   here rather than in one surface's stylesheet: it was written for the
   shifts page, reused on the bank screen, and rendered there as plain prose
   because pos.css does not load ledger.css — so the label read "Which machine
   required" as one sentence. */
.req {
  font-size: var(--fs-meta);
  font-weight: var(--w-regular);
  color: var(--warn);
  text-transform: none;
}

/* ------------------------------------------------------------------ money */

/* .money is defined in tokens.css (tabular figures). These are the roles. */
.money--credit { color: var(--money-credit); }
.money--debit { color: var(--money-debit); }
.money--pending { color: var(--money-pending); }

.amount {
  display: inline-flex;
  align-items: baseline;
  gap: .1em;
  font-variant-numeric: tabular-nums;
  font-weight: var(--w-strong);
}
.amount__sym { font-size: .68em; color: var(--ink-mute); }
.amount__paise { font-size: .72em; color: var(--ink-mute); }
/* 01-identity.md §2 rule 4: weight 700 for totals. `.amount` is 600, which
   is right for a line and wrong for the figure someone pays. */
.amount--total { font-size: var(--fs-total); line-height: 1; font-weight: var(--w-heavy); }
.amount--display { font-size: var(--fs-display); line-height: var(--lh-display);
                   font-weight: var(--w-heavy); }

/* ---------------------------------------------------------------- notices */

.notice {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-md);
  border: var(--bw-hair) solid var(--rule);
  background: var(--surface-sunken);
  color: var(--ink);
  font-size: var(--fs-meta);
}

.notice .icon { flex: none; margin-top: .1rem; }
.notice--ok { background: var(--ok-wash); border-color: transparent; color: var(--ok); }
.notice--warn { background: var(--warn-wash); border-color: transparent; color: var(--warn); }
.notice--bad { background: var(--danger-wash); border-color: transparent; color: var(--danger); }
.notice--info { background: var(--info-wash); border-color: transparent; color: var(--info); }

/* Every status carries an icon and a word. Colour alone would be unreadable
   for the one cashier in twelve who cannot separate red from green. */
.pill {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  padding: .2rem .55rem;
  border-radius: var(--r-full);
  font-size: var(--fs-meta);
  font-weight: var(--w-strong);
  background: var(--surface-sunken);
  color: var(--ink-mute);
  white-space: nowrap;
}
.pill--ok { background: var(--ok-wash); color: var(--ok); }
.pill--warn { background: var(--warn-wash); color: var(--warn); }
.pill--bad { background: var(--danger-wash); color: var(--danger); }
.pill--info { background: var(--info-wash); color: var(--info); }

/* ----------------------------------------------------------------- tables */

.table { width: 100%; border-collapse: collapse; font-size: var(--fs-meta); }
.table th {
  text-align: left;
  font-size: var(--fs-meta);
  font-weight: var(--w-strong);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-faint);
  padding: var(--sp-2) var(--sp-2);
  border-bottom: var(--bw-hair) solid var(--rule);
  white-space: nowrap;
}
.table td {
  padding: var(--sp-3) var(--sp-2);
  border-bottom: var(--bw-hair) solid var(--rule);
  color: var(--ink);
  vertical-align: top;
}
.table tr:last-child td { border-bottom: 0; }
.table .num { text-align: right; font-variant-numeric: tabular-nums; }

/* Money columns are closed with the accountant's double rule. It means "the
   addition ends here", and it is the one piece of KHATA the working screens
   keep, because a ruled column is not decoration — it is how the number is
   read. */
.rule-double {
  border-top: var(--bw-strong) solid var(--ink);
  box-shadow: 0 3px 0 -2px var(--ink);
  padding-top: var(--sp-2);
}

.scroll-x { overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* ------------------------------------------------------------------ lists */

.rows { list-style: none; margin: 0; padding: 0; }
.rows > li { border-bottom: var(--bw-hair) solid var(--rule); }
.rows > li:last-child { border-bottom: 0; }

.rowlink {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  min-height: var(--tap-min);
  padding: var(--sp-3) var(--sp-4);
  color: inherit;
  text-decoration: none;
}

/* ----------------------------------------------------------------- sheets */

.sheet-scrim {
  position: fixed;
  inset: 0;
  z-index: var(--z-sheet);
  background: var(--scrim);
  display: flex;
  align-items: flex-end;
}

.sheet {
  width: 100%;
  max-height: 88svh;
  overflow-y: auto;
  background: var(--surface);
  border-radius: var(--r-lg) var(--r-lg) 0 0;
  padding: var(--sp-4) var(--sp-4) calc(var(--sp-6) + env(safe-area-inset-bottom));
  box-shadow: var(--shadow-3);
  animation: sheet-in var(--dur-sheet) var(--ease-arrive);
}

.sheet__grip {
  width: 2.5rem;
  height: .25rem;
  border-radius: var(--r-full);
  background: var(--rule-strong);
  margin: 0 auto var(--sp-4);
}

@keyframes sheet-in {
  from { transform: translateY(12%); opacity: .6; }
  to { transform: none; opacity: 1; }
}

/* ----------------------------------------------------------------- toasts */

.toasts {
  position: fixed;
  left: 50%;
  bottom: calc(var(--tap-anchor) + var(--sp-4) + env(safe-area-inset-bottom));
  transform: translateX(-50%);
  z-index: var(--z-toast);
  width: min(30rem, calc(100vw - var(--sp-6)));
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  pointer-events: none;
}

.toast {
  pointer-events: auto;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-md);
  background: var(--ink);
  color: var(--bg);
  font-size: var(--fs-meta);
  box-shadow: var(--shadow-2);
  animation: toast-in var(--dur-swap) var(--ease-arrive);
}

.toast__msg { flex: 1 1 auto; }
.toast__close { background: none; border: 0; color: inherit; opacity: .7; cursor: pointer; padding: .25rem; }
/* Ink on a wash, not white on a saturated fill. The literal #fff measured
   1.94:1 against --danger in dark, and error toasts never auto-hide — so the
   one unreadable thing on screen was also the one that stayed. The border
   keeps it reading as raised on the dark ground. */
.toast--error {
  background: var(--danger-wash); color: var(--danger);
  border: var(--bw-hair) solid var(--danger);
}
.toast--success {
  background: var(--ok-wash); color: var(--money-credit);
  border: var(--bw-hair) solid var(--money-credit);
}
.toast--warning {
  background: var(--warn-wash); color: var(--warn);
  border: var(--bw-hair) solid var(--warn);
}
.toast.is-going { opacity: 0; transform: translateY(.5rem); transition: all var(--dur-swap) var(--ease-leave); }

@keyframes toast-in {
  from { opacity: 0; transform: translateY(.75rem); }
  to { opacity: 1; transform: none; }
}

/* ------------------------------------------------------------------ misc */

.icon { flex: none; }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

.empty {
  text-align: center;
  padding: var(--sp-7) var(--sp-4);
  color: var(--ink-mute);
}
.empty__art { color: var(--rule-strong); margin-bottom: var(--sp-3); }

.divider { height: var(--bw-hair); background: var(--rule); border: 0; margin: var(--sp-4) 0; }

.tabs { display: flex; gap: var(--sp-1); border-bottom: var(--bw-hair) solid var(--rule); }
.tabs__item {
  padding: var(--sp-3) var(--sp-3);
  color: var(--ink-mute);
  text-decoration: none;
  font-size: var(--fs-meta);
  font-weight: var(--w-strong);
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}
.tabs__item[aria-current="page"] { color: var(--accent); border-bottom-color: var(--accent); }

/* HTMX gives us a request marker for free; use it rather than inventing a
   spinner state that has to be turned off by hand. */
.htmx-request .is-busy { opacity: .55; pointer-events: none; }
.htmx-indicator { opacity: 0; transition: opacity var(--dur-swap) var(--ease-arrive); }
.htmx-request .htmx-indicator, .htmx-request.htmx-indicator { opacity: 1; }

@media print {
  .topbar, .navbar, .toasts, .no-print { display: none !important; }
  .app, .page { display: block; max-width: none; padding: 0; }
  body { background: #fff; color: #000; }
}

/* ------------------------------------------------------------- brand mark */

/* The society's own logo, used as supplied. It is red on white and predates
   this app by a century, so it is not recoloured, not themed and not redrawn
   — it sits on a white chip that keeps its own ground intact in both themes.
   A logo that changes with the theme is a logo somebody has taken liberties
   with. */
.brandmark {
  display: block;
  height: auto;
  background: #fff;
  border-radius: var(--r-sm);
  padding: 2px 5px;
  /* The white chip needs an edge of its own on a light ground, or it floats. */
  box-shadow: 0 0 0 var(--bw-hair) color-mix(in oklab, #000 12%, transparent);
}

.topbar__logo { width: 3.4rem; padding: 1px 3px; }

@media print {
  .brandmark { box-shadow: none; }
}

/* ==========================================================================
   THE CAMERA SCANNER — shared, not billing's

   Two roles point a camera at something: the cashier at a barcode and the
   guard at an exit pass. While this lived in pos.css the gate loaded none of
   it, so the guard's video rendered at the browser's default 300x150 with no
   brackets, no tool row and no fallback panel — and a denied camera
   permission left that role with no way to work at all.
   ========================================================================== */

/* ---------------------------------------------------------- the viewfinder
   ========================================================================== */

.scanner {
  position: relative;
  overflow: hidden;
  /* The token the system itself names "scan tray". Before video arrives the
     band is a themed well, not a black hole; live video covers it fully. */
  background: var(--surface-sunken);
  display: grid;
  place-items: center;
  /* Bias the reticle up between the hint band and the tool row. */
  padding-top: var(--sp-6);
  padding-bottom: 3.75rem;
}

.scanner__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity var(--dur-swap) var(--ease-arrive);
}
.scanner.is-live .scanner__video { opacity: 1; }

/* ------------------------------------------------------------ the window */

.scanner__window {
  position: relative;
  width: min(76%, 19rem);
  aspect-ratio: 5 / 3;                /* wide, for EAN strip codes */
  border-radius: var(--r-md);
  /* The surround mask: one shadow instead of four positioned veils. --scrim
     is translucent dark in both themes, which is what live video needs. */
  box-shadow: 0 0 0 100vmax var(--scrim);
}

.scanner__brackets {
  position: absolute;
  inset: -2px;
  width: calc(100% + 4px);
  height: calc(100% + 4px);
  fill: none;
  stroke: #fff;         /* optics exemption: strokes over arbitrary video */
  stroke-width: 2.5;
  stroke-linecap: round;
  opacity: .9;
  transition: stroke var(--dur-press) var(--ease-press);
}
.scanner[data-state="locked"] .scanner__brackets { stroke: var(--ok); }
.scanner[data-state="miss"]   .scanner__brackets { stroke: var(--warn); }

/* The window's own edge marks a DECODE — "read", not "billed".
   -------------------------------------------------------------------------
   There used to be a full-window green wash here as well. Two green events
   fired on one scan, in the same place, a third of a second apart: the wash on
   decode and the count overlay on the server's verdict. In doorway glare that
   is one signal fewer that can be read, and every retired flash raises the
   salience of the ones that remain — above all the amber "already on the bill".

   Green now means exactly one thing on this screen: TSS took it. The edge is
   white, like the brackets and the tool pills, because it is chrome over live
   video rather than a themed surface — and because it is honest about what it
   knows. The scanner deliberately makes no sound here for the same reason
   (scanner.js `accept`): the server answers ~300ms later, and confirming
   before it does confirms items the server goes on to refuse. */
.scanner[data-state="locked"] .scanner__window {
  animation: lock-edge 420ms var(--ease-press);
}
@keyframes lock-edge {
  0%   { box-shadow: 0 0 0 100vmax var(--scrim),
                     0 0 0 3px rgb(255 255 255 / .95); }
  100% { box-shadow: 0 0 0 100vmax var(--scrim),
                     0 0 0 3px rgb(255 255 255 / 0); }
}

/* ------------------------------------------------------- bilingual hint */

/* Two equal lines, Kannada first, no separator glyph — hiding the middot
   here is what removes the dangling "·" when the pair used to wrap. A full-
   width gradient band, not a floating pill: nothing to clip, nothing to
   overflow at 320px. */
.scanner__hint {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  margin: 0;
  padding: var(--sp-3) var(--sp-4) var(--sp-5);
  display: grid;
  justify-items: center;
  row-gap: var(--sp-0);
  text-align: center;
  background: linear-gradient(to bottom, var(--scrim), transparent);
  color: #fff;          /* optics exemption: type over live video */
  font-size: var(--fs-body);   /* both scripts, one size — no subtitles */
  line-height: var(--lh-title);
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--dur-swap) var(--ease-arrive);
}
.scanner__hint .bilingual__sep { display: none; }
.scanner.is-live .scanner__hint { opacity: 1; }
/* After the first successful add the strip has taken over as teacher; JS
   adds .has-hit and the lesson retires for the rest of the bill. */
.scanner.has-hit .scanner__hint { opacity: 0; }

/* ------------------------------------------------------------- tool row */

.scanner__tools {
  position: absolute;
  left: var(--scan-pad);
  right: var(--scan-pad);
  bottom: var(--scan-pad);
  display: flex;
  flex-wrap: nowrap;          /* wrapped tools were the "Type coo" bug */
  align-items: center;
  justify-content: space-between;
  gap: var(--tap-gap);
  z-index: 2;                 /* above the fallback panel: the bill is never
                                 hostage to the camera — Type code must stay
                                 reachable while the camera is dead */
}

.scanner__tool {
  align-items: center;
  gap: var(--sp-2);
  min-height: var(--tap-min);   /* was 40px — under the floor */
  min-width: var(--tap-min);
  padding: 0 var(--sp-3);
  border: 0;
  border-radius: var(--r-full);
  background: var(--scrim);     /* token fill over video */
  color: #fff;                  /* optics exemption: type over live video */
  font: inherit;
  font-size: var(--fs-body);
  font-weight: var(--w-strong);
  backdrop-filter: blur(6px);
  cursor: pointer;
  /* A long label ellipsises inside the pill; it never pushes the layout. */
  max-width: 62%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* display behind :not([hidden]): the torch button ships hidden until the
   camera reports torch support. */
.scanner__tool:not([hidden]) { display: inline-flex; }

/* Torch on: accent fill — a state, not a status, so accent is honest. In
   dark theme this is a light pill, the brightest thing in a dim aisle. */
.scanner__tool[aria-pressed="true"] {
  background: var(--accent);
  color: var(--on-accent);
}
.scanner__tool--wide { margin-left: auto; }

/* -------------------------------------------------------- fallback panel */

/* Camera starting / denied / broken. Themed, static (no shimmer on billing,
   03 §2.2), and layered UNDER the tool row so [Type code] still works. */
.scanner__fallback {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: var(--sp-2);
  padding: var(--sp-5);
  text-align: center;
  color: var(--ink-mute);
  background: var(--surface-sunken);
}
.scanner.is-live .scanner__fallback { display: none; }
.scanner__fallback-msg { margin: 0; font-size: var(--fs-body); max-width: 22rem; }
.scanner__fallback .btn:not([hidden]) { display: inline-flex; }


/* ---------------------------------------------------- the typed-code panel

   Shared for the same reason as the viewfinder above: a camera permission can
   be refused on either screen, and when it is, typing the code is the only
   way anybody gets any work done.
   -------------------------------------------------------------------------- */

.typed {
  position: fixed;
  inset: auto 0 0 0;
  z-index: var(--z-sheet);
  margin-inline: auto;
  max-width: 30rem;
  max-height: min(55svh, 30rem);
  overflow: hidden;
  padding: var(--sp-4) var(--sp-4) calc(var(--sp-4) + env(safe-area-inset-bottom));
  background: var(--surface-raised);
  border-top: var(--bw-hair) solid var(--rule);
  border-radius: var(--r-lg) var(--r-lg) 0 0;
  box-shadow: var(--shadow-3);
  grid-template-rows: auto auto minmax(0, 1fr);
  gap: var(--sp-3);
}
/* display only behind :not([hidden]) — see the header comment. */
.typed:not([hidden]) {
  display: grid;
  animation: panel-up var(--dur-sheet) var(--ease-arrive);
}
@keyframes panel-up {
  from { transform: translateY(12%); opacity: .7; }
  to   { transform: none; opacity: 1; }
}

.typed__search {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  gap: var(--sp-2);
  min-height: 0;
  border-top: var(--bw-hair) solid var(--rule);
  padding-top: var(--sp-3);
}

/* The only scrolling region in the panel. Targets the id as a fallback until
   the template gains the class. */
.typed__results,
.typed #search-results {
  overflow-y: auto;
  min-height: 0;
  overscroll-behavior: contain;
}

.searchlist__row {
  width: 100%;
  min-height: var(--tap-min);
  border: 0;
  background: none;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.searchlist__row .grow { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.searchlist__row:active { background: var(--surface-sunken); }


/* --------------------------------------------------------------- bar lists

   Used by the accounts overview AND by two of the dashboard's five charts.
   While these lived in ledger.css the dashboard loaded none of them, so GST
   by slab and staff throughput rendered as bulleted lists with no bars.
   -------------------------------------------------------------------------- */


.barlist { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-2); }

.barlist__row {
  display: grid;
  grid-template-columns: minmax(6rem, 9rem) 1fr auto;
  align-items: center;
  gap: var(--sp-3);
}

.barlist__label { font-size: var(--fs-meta); color: var(--ink-mute); }

.barlist__track {
  height: .6rem;
  border-radius: var(--r-full);
  background: var(--surface-sunken);
  overflow: hidden;
}

.barlist__fill {
  display: block;
  height: 100%;
  border-radius: var(--r-full);
  background: var(--accent);
}

.barlist__value {
  font-size: var(--fs-meta);
  font-weight: var(--w-strong);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}


/* The way back. Used on payment, on the drawer count and on both staff
   forms — four apps, so it is not billing's to own. */
.pay__back {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  align-self: flex-start;
  min-height: var(--tap-min);
  color: var(--ink-mute);
  text-decoration: none;
  font-size: var(--fs-body);
}

/* The identity chip doubles as the sign-out menu. It is a `summary`, so it
   needs the pointer and none of the disclosure triangle. */
.topbar__more { position: relative; }
.topbar__more summary { list-style: none; cursor: pointer; }
.topbar__more summary::-webkit-details-marker { display: none; }
.topbar__more .scan__menu { position: absolute; right: 0; top: calc(100% + var(--sp-2)); }

/* The guard's sign-out: it was an 18px underlined text link on a screen whose
   every other target is 48px. */
.gate__out {
  min-height: var(--tap-min);
  padding: 0 var(--sp-3);
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  font-size: var(--fs-meta);
  text-decoration: underline;
  cursor: pointer;
}

/* --------------------------------------------------------- vertical rhythm

   One step of space between top-level blocks, as a class rather than an
   inline style. An inline `style` beats every stylesheet rule including a
   media query, so a page full of them cannot be re-laid-out for a wider
   screen without editing its markup. These exist so a layout can override
   them.
   -------------------------------------------------------------------------- */
.pushed { margin-top: var(--sp-4); }
.pushed--tight { margin-top: var(--sp-3); }
.pushed--none { margin-top: 0; }

/* ------------------------------------------------------------ the app menu

   The small menu behind a disclosure control. It began on the scan screen —
   hence the class names — but the shared topbar now uses the same markup, so
   it appears on the accounts screens, the admin screens and the bill history
   too. It lived in pos.css, which meant that on every page that did not load
   pos.css it rendered as bare text floating over the page.
   -------------------------------------------------------------------------- */

.scan__menu {
  position: absolute;
  right: 0;
  top: calc(100% + var(--sp-1));
  z-index: var(--z-sheet);
  min-width: 13.5rem;
  background: var(--surface-raised);
  border: var(--bw-hair) solid var(--rule);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-3);
  overflow: hidden;
  padding-block: var(--sp-1);
}

/* Who is signed in. A fact, checked once a shift — a menu row, not chrome. */
.scan__menu-head {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-4);
  font-size: var(--fs-meta);
  font-weight: var(--w-strong);
  color: var(--ink-mute);
  border-bottom: var(--bw-hair) solid var(--rule);
  margin-bottom: var(--sp-1);
}

.scan__menu-item {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  width: 100%;
  min-height: var(--tap-min);
  padding: 0 var(--sp-4);
  border: 0;
  background: none;
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-body);   /* 16px floor — these are real targets, not meta */
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
.scan__menu-item:active { background: var(--surface-sunken); }

/* The sound toggle's state is carried by its label text (JS swaps it); the
   pressed style only echoes it, because colour never stands alone. */
.scan__menu-item[aria-pressed="true"] { color: var(--ink-mute); }

/* The danger zone sits last, behind its own rule — the eye must cross a line
   to reach it, the same way the finger crossed the screen to open the menu. */
.scan__menu-rule {
  height: var(--bw-hair);
  background: var(--rule);
  border: 0;
  margin: var(--sp-1) 0;
}
.scan__menu-item--danger { color: var(--danger); }

/* ------------------------------------------------- item names in Kannada

   TSS added `allcodes.ukname`, a Unicode Kannada name, after the legacy
   column turned out to render only under the Nudi font. These are the classes
   that carry it.

   Kannada has no upper case, so it cannot be ranked below English by
   capitalisation the way a Latin subtitle would be — and ranking it by size or
   weight would make one language the caption of the other. It sits on its own
   line at the same weight, one step down in size only because two full-size
   lines per item would halve how many fit on a phone.
   -------------------------------------------------------------------- */

.line__kn,
.basket__kn,
.gate__kn,
.result__kn,
.invoice__kn {
  display: block;
  color: var(--ink-mute);
  font-size: var(--fs-meta);
  line-height: 1.35;
}

/* The guard reads at a metre, so this one does not step down at all. */
.gate__kn { color: inherit; font-size: inherit; }

/* A search row is a single nowrap line with an ellipsis, and `white-space`
   inherits while `text-overflow` does not — so the Kannada, being a block
   inside that row, could not wrap AND had no ellipsis, and a long name was
   sliced off mid-glyph at the row's edge. It needs its own. */
.searchlist__row .result__kn { overflow: hidden; text-overflow: ellipsis; }

/* ------------------------------------------------------------- attribution */

/* Who built it, on every page that has room for a line of text.
   Not on the two --fixed screens: the till and the gate scanner are
   full-height tools with `overflow: hidden`, and a footer there would either
   be clipped anyway or steal a row from the thing a cashier is working in. */
.credit {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  padding: var(--sp-4) var(--sp-3) calc(var(--sp-4) + env(safe-area-inset-bottom));
  font-size: 0.8rem;
  color: var(--ink-faint);
}
.credit a { display: inline-flex; align-items: center; gap: var(--sp-2); }
.credit img { height: 18px; width: auto; display: block; }

.app--fixed .credit { display: none; }

/* The wordmark is dark blue with a transparent ground, so on a dark page it
   disappears into it. Flattening it to white keeps a two-tone logo legible
   rather than printing a navy shape onto near-black. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .credit img { filter: brightness(0) invert(1); }
}
:root[data-theme="dark"] .credit img { filter: brightness(0) invert(1); }

/* The other ways to print, offered under the Print button rather than tried
   automatically. One container, emptied and refilled — appending left three
   presses showing three of everything. */
.print-alt { margin-top: var(--sp-2); }
.print-alt:empty { display: none; }
