/* ==========================================================================
   Quill Stock

   ONE system. Every screen is assembled from the parts below - do not invent
   a new height, radius, padding or button style for a new screen. If
   something genuinely does not exist yet, add it here first, as a named
   token, and then use it.

   Colours and type come from the Quill brand system in
   /root/Quill-Reference/dev/assets/css/site.css. Light, matching the main
   site. The only dark surface is the camera viewfinder, which is a necessity
   rather than a style choice, and it uses --panel-dark from the same system.
   ========================================================================== */

@font-face {
  font-family: "Helvetica Now";
  src: url("../fonts/HelveticaNowDisplay-Regular.otf") format("opentype");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Helvetica Now";
  src: url("../fonts/HelveticaNowDisplay-Medium.otf") format("opentype");
  font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Helvetica Now";
  src: url("../fonts/HelveticaNowDisplay-Bold.otf") format("opentype");
  font-weight: 700; font-style: normal; font-display: swap;
}

:root {
  /* ---- brand colour -------------------------------------------------- */
  --bg:          #f7f7f7;
  --bg-alt:      #f5f5f3;
  --ink:         #2e2e2e;
  --dim:         rgba(23,23,23,.55);
  --dimmer:      rgba(23,23,23,.35);
  --line:        rgba(23,23,23,.12);
  --line-strong: rgba(23,23,23,.2);

  --panel-dark:      #171717;
  --panel-dark-ink:  #f5f5f3;
  --panel-dark-line: rgba(255,255,255,.14);
  --panel-dark-dim:  rgba(245,245,243,.58);

  --surface:   #ffffff;
  --surface-2: #ececea;
  --danger:    #b3261e;
  --danger-line: #e6c4c1;
  --ok:        #2e7d32;

  /* ---- spacing: one scale, nothing between ---------------------------- */
  --s1: 4px;
  --s2: 8px;
  --s3: 12px;
  --s4: 16px;
  --s5: 24px;
  --s6: 32px;

  /* ---- radius: three, plus a pill ------------------------------------ */
  --r-sm:   10px;   /* chips, small controls */
  --r-md:   14px;   /* fields, buttons, cards, list rows */
  --r-lg:   18px;   /* dialogs, large panels */
  --r-full: 999px;

  /* ---- control heights: three, and nothing else ----------------------- */
  --h-sm: 44px;   /* chip, compact button */
  --h-md: 52px;   /* list row, secondary action, dialog button */
  --h-lg: 60px;   /* primary action, text field */

  /* ---- type ----------------------------------------------------------- */
  --t-xs:      12px;
  --t-sm:      14px;
  --t-body:    15px;
  --t-input:   17px;
  --t-title:   20px;
  --t-display: 24px;

  --ease: cubic-bezier(.23, 1, .32, 1);

  /* ---- page gutters ---------------------------------------------------- */
  --gutter: 20px;
}

/* ==========================================================================
   reset
   ========================================================================== */

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

/* No focus indication anywhere - native or custom. Outline only: blanket
   killing box-shadow would also wipe the inset shadow that marks a *selected*
   pill, which is a different thing. */
*:focus,
*:focus-visible,
*:focus-within { outline: none !important; }

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  /* Installed as a PWA there is no browser chrome to absorb overflow, so any
     layout taller than the viewport turns into a scrolling page. Screens size
     themselves to the viewport; scrolling happens inside .page__body. */
  overflow: hidden;
  overscroll-behavior: none;
  background: var(--bg);
  color: var(--ink);
  /* Helvetica Now carries no Thai glyphs, so Noto Sans Thai sits behind it -
     Latin renders in the brand face, Thai falls through to a face that has
     the characters instead of showing tofu. */
  font-family: "Helvetica Now", "Noto Sans Thai", -apple-system,
               BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
  font-size: var(--t-input);
  -webkit-font-smoothing: antialiased;
}

/* iOS will not let a fixed element paint into the home-indicator band, so
   whatever is behind it shows through there. On a dark screen a light strip is
   glaring, so the page behind the camera is dark too. */
body.bg-dark { background: var(--panel-dark); }

/* ==========================================================================
   page shell - EVERY screen uses this. head / body / foot.
   ========================================================================== */

/* Fixed + inset:0 pins all four edges to the real viewport. No height, no
   measurement, no dvh. Every attempt to compute this height has been wrong on
   some device or state - the keyboard, the first paint, the safe area. Pinning
   the edges cannot be wrong, so nothing here needs --vh at all. */
.page {
  position: fixed;
  inset: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--bg);
}

/* A page laid over another (the code and quantity steps over the camera). */
.page--over { z-index: 20; display: none; }
.page--over.is-open { display: flex; }

/* Centred single-column pages (login). Everything else is full width. */
.page--narrow > * { max-width: 460px; width: 100%; margin-inline: auto; }

.page__head {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: var(--s3);
  min-height: 56px;
  padding: calc(var(--s3) + env(safe-area-inset-top)) var(--gutter) var(--s3);
}
.page__head--line { border-bottom: 1px solid var(--line); }
.page__head--brand { flex-direction: column; gap: var(--s1); padding-bottom: var(--s2); }

/* The trailing control sits hard right. On headers with a title the flexible
   title already does this; on headers without one (a logo and a button) it
   would otherwise bunch up on the left. */
.page__head:not(.page__head--brand) > :last-child { margin-left: auto; }

.page__title { flex: 1 1 auto; font-size: var(--t-title); font-weight: 650; margin: 0; }

.page__body {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  padding: var(--s4) var(--gutter) 0;
}
.page__body--center { justify-content: center; }

/* Flex items shrink by default. In a column that means a 60px field gets
   squashed to whatever is left over once a long list asks for room - which is
   exactly what happened to the search box. Nothing shrinks unless it is one of
   the designated flexible elements below. */
.page__body > *,
.step > * { flex: 0 0 auto; }

.page__body > .picklist,
.page__body > .keypad,
.step > .picklist,
.step > .keypad { flex: 1 1 auto; min-height: 0; }

/* The only scrolling region in the app. */
.page__scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}

/* content column inside a scrolling body */
.page__inner {
  width: 100%;
  max-width: 640px;
  margin-inline: auto;
  padding-bottom: var(--s5);
}

/* title on the left, one action on the right */
.rowhead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  margin-bottom: var(--s2);
}

.page__foot {
  flex: 0 0 auto;
  padding: var(--s3) var(--gutter) calc(var(--s4) + env(safe-area-inset-bottom));
}

/* ==========================================================================
   brand mark
   The Quill wordmark is ALWAYS flat - never the gold bulb, never a gradient,
   on any background. The SVGs are pre-flattened; the filter is belt-and-braces
   so a future file swap cannot reintroduce colour.
   ========================================================================== */

.brand { display: flex; justify-content: center; }
.brand img,
.brand--sm,
.brand--onDark { height: 30px; width: auto; filter: brightness(0); }
.brand--sm { height: 20px; }
.surface-dark .brand img,
.brand--onDark { filter: brightness(0) invert(1); }

.brand-sub {
  font-size: var(--t-xs);
  line-height: 15px;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--dim);
  text-align: center;
}

/* ==========================================================================
   controls

   Three states, and there is never a fourth:
     .btn          solid black  - THE action on this screen, one per screen
     .btn--ghost   outlined     - the secondary action beside it
     .chip         outlined sm  - a secondary action anywhere else
   A *selected* option is outlined with an inset rule, never solid black -
   solid black means "press me", not "you chose me".
   ========================================================================== */

.btn {
  width: 100%;
  height: var(--h-lg);
  border: 0;
  border-radius: var(--r-md);
  font-size: var(--t-input);
  font-weight: 600;
  font-family: inherit;
  cursor: pointer;
  background: var(--panel-dark);
  color: var(--panel-dark-ink);
  transition: opacity .12s var(--ease);
}
.btn:disabled { opacity: .25; cursor: default; }
.btn:active:not(:disabled) { opacity: .8; }

.btn--ghost  { background: var(--bg); color: var(--ink); border: 1px solid var(--line-strong); }
.btn--danger { background: var(--danger); color: #fff; }
.btn--sm     { height: var(--h-sm); width: auto; padding: 0 var(--s4); font-size: var(--t-body); border-radius: var(--r-sm); }
.btn--onDark { background: rgba(255,255,255,.16); border: 1px solid rgba(255,255,255,.3); color: #fff; }

.chip {
  font-family: inherit;
  font-size: var(--t-sm);
  height: var(--h-sm);
  padding: 0 var(--s4);
  border-radius: var(--r-sm);
  border: 1px solid var(--line-strong);
  background: var(--bg);
  color: var(--ink);
  cursor: pointer;
}
.chip:active { background: var(--surface-2); }
.chip:disabled { opacity: .4; }
.chip--danger { color: var(--danger); border-color: var(--danger-line); }
.chip--onDark { background: rgba(255,255,255,.14); border-color: rgba(255,255,255,.28); color: #fff; }
.chip--onDark:active { background: rgba(255,255,255,.24); }

.iconbtn {
  flex: 0 0 auto;
  width: 40px; height: 40px;
  font-size: 30px;
  line-height: 1;
  border: 0;
  background: none;
  color: var(--ink);
  font-family: inherit;
  cursor: pointer;
  padding: 0;
}

.field {
  width: 100%;
  height: var(--h-lg);
  flex: 0 0 auto;         /* never let a flex column squash a field */
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-md);
  color: var(--ink);
  font-size: var(--t-input);
  font-family: inherit;
  padding: 0 var(--s4);
  outline: none;
}
.field::placeholder { color: var(--dimmer); }

.lbl {
  display: block;
  font-size: var(--t-sm);
  color: var(--dim);
  margin: 0 0 var(--s2);
}

/* a full-width action row, for lists of actions inside a dialog */
.rowbtn {
  width: 100%;
  min-height: var(--h-md);
  padding: var(--s3) var(--s4);
  text-align: left;
  font-family: inherit;
  font-size: var(--t-input);
  border-radius: var(--r-md);
  border: 1px solid var(--line-strong);
  background: var(--bg);
  color: var(--ink);
  cursor: pointer;
}
.rowbtn:active { background: var(--surface-2); }
.rowbtn--danger { color: var(--danger); border-color: var(--danger-line); }

.stack { display: grid; gap: var(--s2); }

/* Every option in a dialog is this, whatever it does. The options and the way
   out used to be two different components at two different sizes, which reads
   as a mistake rather than a hierarchy. */
.dlgbtn {
  width: 100%;
  min-height: var(--h-md);
  padding: var(--s3) var(--s4);
  text-align: center;
  font-family: inherit;
  font-size: var(--t-input);
  font-weight: 500;
  border-radius: var(--r-md);
  border: 1px solid var(--line-strong);
  background: var(--surface);
  color: var(--ink);
  cursor: pointer;
  word-break: break-all;
}
.dlgbtn:active { background: var(--surface-2); }
.dlgbtn--danger { color: var(--danger); border-color: var(--danger-line); }

/* selected, not actionable */
.segmented { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s2); }
.segmented__opt {
  height: var(--h-md);
  border-radius: var(--r-md);
  border: 1px solid var(--line-strong);
  background: var(--bg);
  color: var(--ink);
  font-family: inherit;
  font-size: var(--t-body);
  cursor: pointer;
}
.segmented__opt.is-on {
  background: var(--surface);
  border-color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--ink);   /* 2px weight, no layout shift */
  font-weight: 500;
}

/* ==========================================================================
   message slot
   ALWAYS a reserved fixed height. It sits above keypads and buttons, so a slot
   that grows when an error appears resizes everything below it - which is how
   the keypad used to shrink mid-entry.
   ========================================================================== */

.msg {
  flex: 0 0 auto;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  font-size: var(--t-body);
  line-height: 1.4;
  color: var(--danger);
}
.msg--auto { height: auto; min-height: 40px; }

/* ==========================================================================
   keypad
   The only flexible element on any screen that has one. Every sibling above it
   is a fixed height, so its height is a constant per device and the keys never
   move between steps or when an error appears.
   ========================================================================== */

.keypad {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: repeat(4, minmax(0, 1fr));
  gap: var(--s2);
  margin-top: auto;
  flex: 1 1 auto;
  min-height: 0;
  max-height: 420px;   /* stops keys becoming slabs on a tall phone */
}
.key {
  height: 100%;
  min-height: 40px;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--surface);
  color: var(--ink);
  font-size: 26px;
  font-weight: 500;
  font-family: inherit;
  cursor: pointer;
  transition: background .1s;
}
.key:active { background: var(--surface-2); }
.key--wide { font-size: var(--t-input); font-weight: 600; }

.pin-dots {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: var(--s3);
  height: 46px;
  flex: 0 0 auto;
}
.pin-dot {
  width: 16px; height: 16px;
  flex: 0 0 auto;
  border-radius: 50%;
  border: 1.5px solid var(--line-strong);
  transition: transform .1s var(--ease), background .1s;
}
.pin-dot.is-filled { background: var(--ink); border-color: var(--ink); transform: scale(1.08); }

/* ==========================================================================
   language switcher
   ========================================================================== */

.langbar { display: flex; justify-content: center; gap: var(--s2); }
.langbar a {
  color: var(--dim);
  text-decoration: none;
  font-size: var(--t-sm);
  padding: var(--s2) var(--s4);
  border-radius: var(--r-full);
  border: 1px solid transparent;
  transition: background .12s var(--ease), color .12s var(--ease);
}
.langbar a.is-on {
  color: var(--ink);
  background: var(--surface);
  border-color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--ink);
  font-weight: 500;
}

/* ==========================================================================
   login
   Every element fixed; the keypad absorbs the slack. See the keypad note.
   ========================================================================== */

.auth-title { font-size: var(--t-display); line-height: 29px; font-weight: 650; margin: 0 0 var(--s2); text-align: center; }
.auth-hint  { font-size: var(--t-body); line-height: 22px; color: var(--dim); margin: 0; text-align: center; }
.auth-head  { flex: 0 0 auto; padding: var(--s4) 0 0; }

/* Quiet by design - an escape hatch, not something to draw the eye away from
   the keypad. Full height so it is still an easy target. */
.backlink {
  align-self: flex-start;
  background: none;
  border: 0;
  font-family: inherit;
  font-size: var(--t-body);
  color: var(--dim);
  cursor: pointer;
  padding: var(--s2) var(--s3) var(--s2) 0;
  min-height: var(--h-sm);
}
.backlink:active { color: var(--ink); }

.step { display: none; }
.step.is-active { display: flex; flex-direction: column; min-height: 0; }
.page__body:not(.page__body--center) > .step.is-active { flex: 1 1 auto; }
.page__body > .btn { margin-top: var(--s4); }

/* While the keyboard is up the usable area is roughly half the screen, so a
   centred group sits behind it. Top-align while the field is focused. */
.page__body--center.is-keyboard { justify-content: flex-start; }

/* ==========================================================================
   lists and cards
   ========================================================================== */

.cardlist { list-style: none; margin: 0; padding: 0 0 var(--s5); display: grid; gap: var(--s3); }

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: var(--s4);
}
.card__name { font-size: var(--t-input); font-weight: 600; }
.card__muted { color: var(--dimmer); font-weight: 400; font-size: var(--t-body); }
.card__meta { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2); margin-top: var(--s2); }
.card__note { margin-top: var(--s2); font-size: var(--t-sm); color: var(--dim); line-height: 1.4; }
.card__actions {
  display: flex; flex-wrap: wrap; gap: var(--s2);
  margin-top: var(--s3); padding-top: var(--s3);
  border-top: 1px solid var(--line);
}

.tag {
  font-size: var(--t-xs);
  font-weight: 500;
  padding: var(--s1) var(--s3);
  border-radius: var(--r-full);
  border: 1px solid var(--line-strong);
  color: var(--dim);
}
.tag--admin         { background: var(--surface-2); color: var(--ink); font-weight: 600; }
.tag--active        { background: #e6f0e8; color: #24632c; border-color: #c5ddc9; }
.tag--enroll        { background: #fdf1d8; color: #8a5a00; border-color: #f0dcae; }
.tag--locked        { background: #fbe3e1; color: #93231c; border-color: #f0c7c3; }
.tag--claim_expired { background: var(--surface-2); color: #5c5c5c; }

.meta-text { font-size: var(--t-sm); color: var(--dim); }

/* scrolling list of selectable rows - the one flexible element on its screen */
.picklist {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  list-style: none;
  margin: var(--s3) 0 0;
  padding: 0;
}

/* selectable list rows (product codes) */
.pickitem {
  min-height: var(--h-md);
  display: flex;
  align-items: center;
  padding: var(--s3) var(--s4);
  font-size: var(--t-input);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--surface);
  margin-bottom: var(--s2);
  word-break: break-all;
}
.pickitem:active { background: var(--surface-2); }
.pick-empty { padding: var(--s5) var(--s1); color: var(--dim); font-size: var(--t-body); text-align: center; }

/* ==========================================================================
   dialogs
   ========================================================================== */

.sheet {
  border: 0;
  border-radius: var(--r-lg);
  padding: 0;
  width: min(440px, calc(100vw - var(--s6)));
  background: var(--bg);
  color: var(--ink);
}
.sheet--narrow { width: min(380px, calc(100vw - 40px)); }
.sheet::backdrop { background: rgba(23,23,23,.45); }
/* Even padding all round. No safe-area inset here - a dialog floats in the
   middle of the screen, so adding the home-indicator gap to its bottom just
   leaves dead space with nothing balancing it at the top. */
.sheet__inner { padding: var(--s5) var(--gutter); }
.sheet__title { font-size: var(--t-title); font-weight: 650; margin: 0 0 var(--s1); }
.sheet__sub   { font-size: var(--t-sm); color: var(--dim); margin-bottom: var(--s3); }
.sheet__body  { font-size: var(--t-body); line-height: 1.5; color: var(--dim); margin: var(--s2) 0 0; }
.sheet__group { margin-top: var(--s4); }
.sheet__actions { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s2); margin-top: var(--s5); }
.sheet__actions--single { grid-template-columns: 1fr; }
.sheet__actions .btn { height: var(--h-md); font-size: var(--t-input); }

/* ==========================================================================
   camera - the one dark surface
   ========================================================================== */

.surface-dark { background: var(--panel-dark); color: var(--panel-dark-ink); }
.surface-dark .btn { background: var(--panel-dark-ink); color: var(--panel-dark); }

/* inset:0 on a fixed element already pins it to all four edges. Setting an
   explicit height on top of that overrides the bottom pin - and if --vh is even
   slightly short, the page background shows through as a strip along the
   bottom. Let inset do the work. */
.cam { position: fixed; inset: 0; overflow: hidden; background: #000; }

.cam__video,
.cam__shot {
  position: absolute;
  top: 0; left: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  background: #000;
}

/* The viewfinder stays in the layout while the stream is (re)starting.
   Hiding a <video> while its metadata loads leaves iOS with a stale intrinsic
   size, and it comes back letterboxed in the middle of the screen instead of
   filling it - which is what happened after Retake. */
.cam__shot { display: none; }
.cam[data-state="review"] .cam__shot  { display: block; }
.cam[data-state="review"] .cam__video,
.cam[data-state="fail"]   .cam__video { display: none; }

.cam__top {
  position: absolute; top: 0; left: 0; right: 0;
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s3);
  padding: calc(var(--s3) + env(safe-area-inset-top)) var(--s4) var(--s3);
  /* keeps white controls readable over a bright shot */
  background: linear-gradient(rgba(0,0,0,.45), rgba(0,0,0,0));
}

.cam__topright { display: flex; align-items: center; gap: var(--s2); }

/* Only visible when receipts are waiting to reach the server. Amber, because
   it is not an error - the work is safe on the phone - but it is not "done"
   either, and the receiver should be able to see the number going down. */
.pending {
  font-size: var(--t-sm);
  font-weight: 600;
  padding: 0 var(--s3);
  height: var(--h-sm);
  display: inline-flex;
  align-items: center;
  border-radius: var(--r-full);
  background: rgba(255, 200, 60, .22);
  border: 1px solid rgba(255, 200, 60, .5);
  color: #ffd479;
  white-space: nowrap;
}
.pending[hidden] { display: none; }

.done__text { white-space: pre-line; }

.cam__bar {
  position: absolute; left: 0; right: 0; bottom: 0;
  display: none;
  padding: var(--s5) var(--gutter) calc(var(--s5) + env(safe-area-inset-bottom));
  background: linear-gradient(rgba(0,0,0,0), rgba(0,0,0,.5));
}
.cam[data-state="live"]   #bar-live   { display: flex; justify-content: center; }
.cam[data-state="review"] #bar-review { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s3); }

/* The one control on the live screen. Deliberately oversized - pressed
   hundreds of times a day, one-handed, sometimes with gloves. */
.shutter {
  width: 76px; height: 76px;
  border-radius: 50%;
  background: #fff;
  border: 5px solid rgba(255,255,255,.4);
  box-shadow: 0 0 0 2px rgba(0,0,0,.25);
  cursor: pointer;
  padding: 0;
}
.shutter:active { transform: scale(.94); }

.cam__fail {
  position: absolute; inset: 0;
  display: none;
  flex-direction: column; align-items: center; justify-content: center;
  gap: var(--s3);
  text-align: center;
  padding: var(--s6) var(--s5) calc(var(--s6) + env(safe-area-inset-bottom));
  background: var(--panel-dark);
}
.cam[data-state="fail"] .cam__fail { display: flex; }
.cam__fail h2 { font-size: 22px; font-weight: 650; margin: 0; }
.cam__fail p  { font-size: var(--t-input); line-height: 1.5; color: var(--panel-dark-dim); margin: 0 0 var(--s2); max-width: 340px; }
.cam__fail .btn { max-width: 260px; }

/* thumbnail of the shot, carried through the code and quantity steps */
.thumb {
  flex: 0 0 auto;
  width: 44px; height: 44px;
  object-fit: cover;
  border-radius: var(--r-sm);
  border: 1px solid var(--line);
  background: var(--surface-2);
}

/* ==========================================================================
   quantity
   ========================================================================== */

/* A screen whose flexible child is a scrolling list gives the bottom inset to
   the LIST, not to the container. Padding the container leaves a dead band of
   background below the last row; padding the list lets rows scroll right down
   to the bottom edge of the screen. */
#code-step .page__body { padding-bottom: 0; }
#code-step .picklist { padding-bottom: calc(var(--s4) + env(safe-area-inset-bottom)); }

/* The quantity screen ends in a button, so its inset stays on the container. */
#qty-step .page__body { padding-bottom: calc(var(--s4) + env(safe-area-inset-bottom)); }
.page__body--qty .msg { height: 34px; }
.page__body--qty > .btn { flex: 0 0 auto; margin-top: var(--s3); }

.qty-code  { font-size: var(--t-body); color: var(--dim); text-align: center; word-break: break-all; min-height: 22px; }
.qty-value { font-size: 56px; line-height: 66px; font-weight: 650; text-align: center; font-variant-numeric: tabular-nums; }

/* ==========================================================================
   saved confirmation
   ========================================================================== */

.done {
  position: fixed; inset: 0; z-index: 40;
  display: none;
  flex-direction: column; align-items: center; justify-content: center;
  gap: var(--s3);
  background: var(--bg);
}
.done.is-open { display: flex; }
.done__tick {
  width: 84px; height: 84px;
  border-radius: 50%;
  background: var(--panel-dark);
  color: var(--panel-dark-ink);
  display: flex; align-items: center; justify-content: center;
  font-size: 42px;
}
.done__text { font-size: var(--t-input); color: var(--dim); text-align: center; padding: 0 var(--s5); word-break: break-all; }

/* "Updated N min ago" + a refresh, under the search field. */
.syncline {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  padding: var(--s2) var(--s1) 0;
  font-size: var(--t-sm);
  color: var(--dim);
}
.linkish {
  background: none;
  border: 0;
  font-family: inherit;
  font-size: var(--t-sm);
  font-weight: 600;
  color: var(--ink);
  cursor: pointer;
  padding: var(--s2) 0 var(--s2) var(--s3);
  min-height: var(--h-sm);
}
.linkish:disabled { opacity: .4; }

/* An empty body must take no room, or the dialog keeps a gap where a sentence
   used to be. */
.sheet__body:empty { display: none; }

/* Health warnings on /admin. Shown only when something is wrong - a permanent
   "all good" panel is one people stop reading within a week. */
.alert {
  display: flex;
  flex-direction: column;
  gap: var(--s1);
  padding: var(--s3) var(--s4);
  border-radius: var(--r-md);
  border: 1px solid var(--line-strong);
  margin-bottom: var(--s3);
  font-size: var(--t-body);
  line-height: 1.45;
}
.alert strong { font-size: var(--t-input); }
.alert--bad  { background: #fbe3e1; border-color: #f0c7c3; color: #93231c; }
.alert--warn { background: #fdf1d8; border-color: #f0dcae; color: #8a5a00; }
