/* ---------------------------------------------------------------------------
   style.css — deliberately plain. Everything themeable lives in the
   :root block below (colors, fonts, radius) so swapping in a real logo
   and brand palette later is a find-and-replace, not a rewrite.
--------------------------------------------------------------------------- */

:root{
  --bg: #faf8f3;
  --bg-raised: #ffffff;
  --ink: #17140f;
  --ink-soft: #55503f;
  --ink-faint: #8c8574;
  --line: #e3dccb;
  --brand-tan: #d3b191;      /* sampled straight from the PAWNALOG logo */
  --brand-tan-bg: #f4e9db;
  --accent: #74614f;         /* darkened logo tan — used for buttons/links (contrast) */
  --accent-bg: #f4e9db;
  --good: #3c7a52;
  --good-bg: #e4efe4;
  --bad: #a8432b;
  --bad-bg: #f6e4de;
  --radius: 10px;
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --font-display: Georgia, "Liberation Serif", "Times New Roman", serif;
  --font-mono: ui-monospace, "SFMono-Regular", Menlo, monospace;
  /* Desktop/wide-screen size — unchanged from the app's original values.
     The bigger topbar/bottom-nav treatment (1.75x / 1.7x) is phone-only
     now, in the max-width:480px media query below, which overrides both
     of these; everything that reads either variable (.container's top/
     bottom padding, .topbar's and .bottom-nav's own height, .docked-
     panel's bottom offset) picks up whichever size is active automatically. */
  --header-h: 64px;
  --nav-h: 72px;
}

@media (prefers-color-scheme: dark){
  :root:not([data-theme="light"]){
    --bg: #181613;
    --bg-raised: #221f1a;
    --ink: #f1ece0;
    --ink-soft: #c4bcab;
    --ink-faint: #8c8574;
    --line: #3a352a;
    --brand-tan: #d3b191;
    --brand-tan-bg: #332a1e;
    --accent: #d3b191;
    --accent-bg: #332a1e;
    --good: #7ec293;
    --good-bg: #1f2c22;
    --bad: #e18567;
    --bad-bg: #2f1f1a;
  }
}

/* Explicit "Dark" choice from the Appearance setting on the Store page —
   applies regardless of the device's own OS setting, unlike the
   prefers-color-scheme block above which only kicks in when the OS is
   already set to dark. Same palette either way. */
:root[data-theme="dark"]{
  --bg: #181613;
  --bg-raised: #221f1a;
  --ink: #f1ece0;
  --ink-soft: #c4bcab;
  --ink-faint: #8c8574;
  --line: #3a352a;
  --brand-tan: #d3b191;
  --brand-tan-bg: #332a1e;
  --accent: #d3b191;
  --accent-bg: #332a1e;
  --good: #7ec293;
  --good-bg: #1f2c22;
  --bad: #e18567;
  --bad-bg: #2f1f1a;
}

*{ box-sizing: border-box; }
html, body{ height: 100%; }
/* Stops the browser's own "rubber-band" bounce past the top/bottom of the
   page (most noticeable on iPhone) — without this, scrolling past either
   edge briefly slides the whole page (topbar included) instead of just
   stopping, since sticky elements can't hold still through it. */
html{ overscroll-behavior-y: none; }
body{
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font);
  font-size: 16px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  overscroll-behavior-y: none;
}
/* Applied to <html>/<body> only while the Capture screen is mounted (see
   capture.js) — added because the Capture page's own "moving up and
   down" report turned out NOT to be the browser's address bar resizing
   the viewport (a live on-screen readout there proved the reported
   viewport height genuinely never changes) and not page overflow either
   (independently verified — the page's content exactly fills one screen,
   nothing to scroll). That leaves iOS Safari's elastic "rubber-band"
   bounce itself: a touch-drag can still visibly shift the WHOLE page a
   few pixels and spring back even when there's truly nothing to scroll,
   which is a lower-level touch behavior overscroll-behavior-y:none above
   doesn't fully suppress on its own — that property mainly stops bounce
   CHAINING past an actual scrollable edge, not this. position:fixed here
   removes html/body from being draggable at all, which is the actual
   fix for that. Scoped to Capture only (added/removed by that view, not
   left on globally) because every other screen genuinely needs normal
   scrolling — a ticket list longer than one screen, a long form, etc. —
   and this would break all of that if it applied everywhere.

   Also applied (as html.lightbox-lock — see lightbox.js) while the photo
   lightbox overlay is open: same root cause, different screen. The
   lightbox is a position:fixed overlay covering the whole viewport, but
   fixed positioning alone doesn't stop a touch-drag on it from still
   scrolling whatever's underneath — the overlay visually stays put while
   the real page behind it silently scrolls, so closing the lightbox lands
   back on a different scroll position than where it was opened, or (worse)
   the page is visibly draggable up and down while a photo's supposedly
   "full screen" on top of it. Locking html/body the exact same way Capture
   already does fixes both. */
html.capture-lock, html.capture-lock body,
html.lightbox-lock, html.lightbox-lock body{
  position: fixed; inset: 0; width: 100%; height: 100%; overflow: hidden;
}
button, input, select, textarea{ font-family: inherit; font-size: 1rem; }
textarea{ overflow-wrap: break-word; word-break: break-word; }
a{ color: var(--accent); }

/* This is an app, not a page of text to read — nothing in the app shell
   should ever be selectable, so fast/repeated taps (mashing the Capture
   screen's shutter button is the case that actually got reported: rapid
   taps there were sometimes read as iOS's native text-selection gesture
   instead of a series of taps, leaving the blue selection handles/callout
   stuck over the whole screen) can never trigger it. -webkit-touch-callout
   is the separate iOS rule for the long-press "Copy/Look Up/Share" menu —
   turned off here too, same reasoning. Scoped to #app as a whole rather
   than just buttons, since the stray selection in the report wasn't
   confined to whatever was actually tapped — it grabbed surrounding text
   too. input/textarea are explicitly excluded right below so typing,
   selecting, and copying in an actual field (store name, notes, search
   boxes, etc.) still works exactly like normal. */
#app{
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}
input, textarea{
  -webkit-user-select: text;
  user-select: text;
  -webkit-touch-callout: default;
}

#app{ min-height: 100%; display: flex; flex-direction: column; }
.screen{ flex: 1; display: flex; flex-direction: column; }

/* Three real grid columns rather than a two-item flex row — that's what
   lets the logo sit in a column of its own, dead center in the bar, instead
   of "centered" only by coincidence when the left and right content happen
   to be the same width. minmax(0,1fr) on the outer two columns (rather than
   a bare 1fr) is what actually lets them shrink below their content's
   natural size — a plain 1fr/auto grid track refuses to shrink past its
   content's min size, which is exactly what defeats the title's ellipsis
   truncation below. Equal 1fr on BOTH sides (not just the left) is what
   keeps the center column visually centered even when the title text and
   the icon row are very different widths. */
.topbar{
  height: var(--header-h);
  display: grid;
  /* Both side columns are minmax(min-content, 1fr), not the plain
     minmax(0,1fr) this used to be. Plain minmax(0,1fr) lets a track shrink
     all the way to 0 whenever things get tight, trusting whatever's inside
     it to shrink gracefully too — true for the title (it wraps, and
     fitTitleToFit in ui.js shrinks its font as a last resort), but NOT
     true for the icon row on the right (bell/refresh/logout are rigid,
     non-wrapping buttons with no safe way to shrink) — so a plain 1fr
     track let that row get squeezed thinner than its own content and
     visually overflow past its column edge, overlapping the centered logo.
     The min-content floor here guarantees each side column at least enough
     room for whatever it actually holds (for the icon row, that's its full
     width, since none of it can wrap) before any leftover space gets
     shared out — so overlap can't happen — while both columns still carry
     the same 1fr weight, so that leftover space (when there IS room to
     spare) splits evenly between them and the logo in the middle stays
     genuinely centered, not shoved off to one side. */
  grid-template-columns: minmax(min-content, 1fr) auto minmax(min-content, 1fr);
  align-items: center;
  gap: 6px;
  padding: 0 14px;
  border-bottom: 1px solid var(--line);
  background: var(--bg-raised);
  /* Was `position: sticky; top: 0;` — correct per spec and worked fine in
     testing, but Firefox has a long-standing bug where a sticky element
     that's a DIRECT CHILD of a `display:flex` container (#app is exactly
     that) can fail to stick at all, silently falling back to normal
     in-flow positioning — so it scrolled away with everything else
     instead of staying put. `position: fixed` sidesteps the whole
     flex-item-sticky interaction: it's positioned against the viewport
     itself, not against #app's flex layout, so there's no ambiguity left
     for any browser to get wrong. See .container's padding-top below —
     taking the topbar out of normal flow means the content underneath
     needs its own reserved space instead of the topbar naturally pushing
     it down. */
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 5;
}
/* The left-hand group (back button/store pic + title block) is the one
   side allowed to shrink — min-width:0 overrides a flex/grid item's default
   "never shrink below my content's natural width," which is what lets the
   ellipsis rules below actually kick in instead of just overflowing (or,
   pre-grid, shoving the rest of the bar around). */
.topbar-left{ display: flex; align-items: center; min-width: 0; }
/* Just the store pic now — it's the current employee's own picture, shown
   on every page regardless of whether that page also has a back button, so
   it always sits centered on its own instead of being pushed off-center by
   whatever's going on in the title column next to it (which is exactly
   what having the back button share this stack used to do). */
.topbar-leading{
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0; margin-right: 8px;
}
/* Column now instead of a plain block — h1 on top, the back button (when
   there is one) underneath it. align-items stays the flex default
   (stretch) so h1 keeps stretching to the column's full width, same as
   before, which is what its own overflow/ellipsis rule needs to clip
   against; .topbar-back below opts itself out with align-self:flex-start
   so it doesn't also stretch full-width and hang the tap target off the
   title's right edge. */
.topbar-title{
  display: flex; flex-direction: column;
  min-width: 0; overflow: hidden;
}
/* No longer sharing this space with a second line for the store name (that
   moved to its own spot under the logo — see .topbar-center below), so the
   title can sit bigger without needing a taller bar to fit it. Wraps onto a
   second line instead of ellipsis-cutting when it doesn't fit on one — a
   ticket number ending in "…" is worse than useless (the missing digits
   are exactly the part that matters), so this never hides characters
   behind dots the way the old single-line + ellipsis rule could. The pic
   no longer shares a row with this (see .topbar-leading above), so a
   title wrapping to 2 lines just makes .topbar-title taller — it doesn't
   push the pic off-center or grow the bar itself out of shape. */
.topbar h1{
  /* Desktop/wide-screen default — the original, unscaled size. The phone
     media query below sets a bigger phone-only size; ui.js's
     fitTitleToFit() (run from wireChrome, after every render) shrinks that
     bigger size back down, but only as far as actually needed, whenever a
     given title (a long ticket number, say) wouldn't otherwise fit on two
     lines — so this rule can afford to be generous on phone without
     reopening the overlap/clipping bugs found while tuning it. */
  font-family: var(--font-display); font-size: 1.15rem; margin: 0; font-weight: 700;
  line-height: 1.15;
  max-width: 100%;
  /* Belt-and-suspenders alongside the width budget above: a single "word"
     (a long ticket number with no spaces, say) that still doesn't fit its
     column wraps character-by-character instead of overflowing past
     .topbar-title's overflow:hidden edge and silently losing whatever part
     stuck out — the visible risk being real information (part of a ticket
     number) disappearing rather than just an odd-looking line break. */
  overflow-wrap: anywhere;
}
/* The logo + store name column, centered in the bar — see topbar()'s own
   comment in ui.js for why the name lives here now instead of under the
   page title. flex-direction:column stacks them; align-items:center keeps
   the (usually narrower) name centered under the (usually wider) logo
   rather than left-hung under it. */
.topbar-center{
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  /* Logo and name centered together as one small group, same as the rest
     of the bar's content — a previous version stretched this column to
     the bar's full height and pinned the name down near the bottom edge,
     which just read as a big awkward gap rather than "spaced out." This
     gap is the whole fix: a small, fixed bit of breathing room between the
     two instead of them touching, nothing more elaborate than that. */
  gap: 6px;
  min-width: 0;
}
.topbar-store-name{
  /* Desktop/wide-screen default — the original size, with its max-width
     kept close to the logo's own rendered width just below (unscaled here
     too) so the name still reads as sitting "under" it. The phone media
     query overrides both together whenever the logo's phone size changes,
     so they stay matched. */
  font-size: 0.7rem; color: var(--accent); font-weight: 600; line-height: 1.2;
  text-align: center;
  /* Roughly the logo's own rendered width at .topbar-logo's height below —
     without a cap here, a long store name would happily stretch this
     column (and so the whole center grid track) much wider than the logo
     sitting right above it, unbalancing the whole bar. Wrapping within
     that same budget instead keeps the name visually "under" the logo
     rather than spilling past its edges. -webkit-line-clamp caps it at 2
     lines with its own ellipsis if a name is ever long enough to still
     overflow that — belt and suspenders alongside the 40-character
     store-name limit itself. ui.js's fitStoreNameToTwoLines() (run from
     wireChrome, after every render) is what actually keeps most long names
     OFF that ellipsis in the first place: it nudges this font-size down,
     only as far as needed, until a name that would've needed a third line
     fits fully across these two — a normal, shorter name never gets
     touched and stays at the size set here. */
  max-width: 116px; /* matches .topbar-logo's own (unscaled) rendered width */
  white-space: normal;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* A name short enough to only ever need one line looked too close under
   the logo at the same gap that reads right once a name wraps to two —
   ui.js's fitStoreNameToTwoLines() adds this class only in the one-line
   case, so this extra nudge never touches the (already right) two-line
   spacing. */
.topbar-store-name--single{ margin-top: 5px; }
/* The center column's actual content — sized to its own column (grid's
   "auto" track just fits it), so it never competes for space with the
   title or the icon row on either side of it. */
.topbar-logo{ height: 28px; display: block; }
/* The icon row (bell/refresh/logout) gets the mirror-image column to the
   title's — same minmax(min-content,1fr) flexible track (see .topbar
   above), guaranteed at least its own content's width. justify-self:end
   keeps the buttons flush against the right edge (rather than stretching
   or centering) on the — common — occasion the track ends up wider than
   they need. */
.topbar-right{ display: flex; align-items: center; justify-self: end; min-width: 0; }
/* Sized up from 30px now that it isn't sharing the left side with a second
   line of store-name text underneath the title (see .topbar-center above)
   — there's real room to make it a bigger, easier-to-recognize part of the
   bar instead of a small circle squeezed next to two lines of text. */
.topbar .store-pic{
  /* Bumped up from the original 34px on its own — everything else in the
     desktop topbar (logo, title, store name, icons) stays at its original
     size; only the profile picture was asked to grow here. */
  width: 42px; height: 42px; min-width: 42px; border-radius: 50%;
  background: var(--accent-bg); object-fit: cover; flex-shrink: 0;
  display: inline-block; vertical-align: middle;
}
/* Sits under the page title now (inside .topbar-title, a column — see
   above) instead of under the store pic, so the pic stays in its own
   simple, always-centered slot regardless of whether the current page has
   a back button or not. Deliberately its own small footprint rather than a
   full icon-btn (~44px, sized for a comfortable tap target on its own row)
   — left-aligned under the title text it belongs to (flush with the "T" in
   "Ticket" above it, not floating off to one side), not centered under the
   whole title column. */
.topbar-back{
  align-self: flex-start;
  margin-top: -7px;
  font-size: 1rem; padding: 0; line-height: 1;
}
/* Both set by markTitleWrapState() in ui.js, and only on a screen that
   actually has a back button under its title — see that function's own
   comment for why. Single-line case: the pair (title + arrow) shifts down
   together as a unit, keeping the -7px gap between them exactly as it
   already is above. Two-line (wrapped) case: the title's already lower
   just from being taller, so this pulls the arrow in closer underneath it
   instead — tighter than the plain -7px default. */
.topbar-title--back-single{ margin-top: 4px; }
.topbar-title--back-wrapped .topbar-back{ margin-top: -10px; }
.icon-btn{
  background: none; border: none; color: var(--ink);
  font-size: 1.4rem; padding: 10px; cursor: pointer; line-height: 1;
}
/* The logout button's icon is a drawn SVG now, not a Unicode character (see
   ui.js's POWER_ICON_SVG for why) — font-size doesn't size an <svg>, so
   this gives it an explicit box that matches how big the old character
   glyph read at this button's own font-size, and currentColor (set on the
   <svg> itself) is what makes it pick up .icon-btn's color automatically,
   same as the text icons beside it. */
.icon-btn svg{ width: 1.4em; height: 1.4em; display: block; }
/* Refresh/logout are plain .icon-btn-sized on desktop — the bigger,
   more-spaced-out sizing for them (added so a reach for one on a small
   phone screen doesn't land on the other) is phone-only, in the media
   query below. */

/* This migration went through several rounds of enlarging the topbar for
   phones (logo/store-pic/store-name/icons all bigger, header-h itself up
   to 164.5px at the worst point) before landing back here, at the store
   owner's own explicit request, on the exact same topbar as desktop —
   same logo, store-pic, store-name, title, and icon sizes, none of them
   scoped to phone at all anymore. header-h below (64px) is the same
   number .topbar already uses on desktop/wide screens (see the plain
   :root block near the top of this file) for exactly that reason: this
   is intentionally a no-op for every topbar CHILD element now, kept only
   as documentation of that decision rather than deleted outright, so a
   future "the topbar looks small on phone" report doesn't read as an
   unnoticed regression.
   --nav-h is the one real phone-only size left: 1.3x its desktop default
   (72px) is 94px, picked directly by the store owner from a set of live
   on-device size options (a one-off "phone size picker" HTML tool, not
   part of this app) after an earlier, much larger 1.75x measured out as
   too big in practice. Everything that reads --nav-h (.container's
   bottom padding, .bottom-nav's own height, .docked-panel's bottom
   offset) picks it up automatically, since none of those hard-code a
   pixel number of their own. */
@media (max-width: 480px){
  :root{ --header-h: 64px; --nav-h: 94px; }
}

/* Top/bottom padding now has to reserve real space for the topbar and
   bottom-nav (see their comments above) — now that both are `position:
   fixed` instead of `sticky`, they're out of the normal document flow
   entirely, so nothing pushes this content down from the topbar or stops
   it from sliding under the bottom-nav on its own anymore; this padding is
   what does that job instead. Applied unconditionally (even on the rare
   screen that skips bottomNav(), like Capture) rather than conditionally —
   worst case there's a bit of extra blank space at the bottom of one of
   those, which is harmless, versus the alternative of content actually
   hiding behind a bar that IS present. */
.container{ max-width: 480px; margin: 0 auto; width: 100%; padding: calc(var(--header-h) + 18px) 16px calc(var(--nav-h) + 32px); flex: 1; }
.container.wide{ max-width: 720px; }

/* min-height is set twice on purpose: 100vh first as a fallback for
   browsers that don't understand 100dvh (dvh silently no-ops there,
   leaving the vh value in effect), then 100dvh to override it everywhere
   else. Plain 100vh on mobile Safari measures the tallest the page COULD
   be with the address bar hidden, not the actually-visible area on load
   — so a card centered with align-items:center sits noticeably lower/
   smaller-looking than the visible viewport suggests, exactly the "looks
   small on my iPhone" effect. 100dvh tracks the real visible viewport
   instead, address bar and all. */
.center-screen{ display: flex; align-items: center; justify-content: center; min-height: 100vh; min-height: 100dvh; padding: 24px 16px; }
.card{
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 28px 26px;
  width: 100%;
  max-width: 440px;
}
.card h1{ font-family: var(--font-display); font-size: 1.5rem; margin: 0 0 6px; }
.card .sub{ color: var(--ink-soft); margin: 0 0 20px; font-size: 0.96rem; }

/* Wordmark above the auth-card heading (login/signup/install) — one class
   with size modifiers instead of a one-off inline style per view, so the
   mobile breakpoint below can bump every size of it at once instead of
   needing to know each view's own inline px value. Base sizes here match
   what each view used inline before this was pulled out (44/36/32px). */
.auth-logo{ display: block; margin: 0 auto 16px; height: 44px; }
.auth-logo.sub-logo{ height: 36px; margin-bottom: 14px; }
.auth-logo.mini-logo{ height: 32px; margin-bottom: 14px; }

/* Real mobile pass for the auth screens (login/signup/forgot/accept/
   install's pre-login card, and blockedHours' access-request card —
   anything using .center-screen). Two attempts before this one just made
   the card's own numbers bigger (padding, font-size) and it kept reading
   as "still small" — because the actual problem was never the card's
   internal sizing, it was that a card with visible edges, sitting in the
   middle of a MUCH taller phone screen, always reads as a small floating
   box no matter how much its own padding/fonts grow — see the screenshots:
   the card got bigger each time and the empty space around it just grew
   to match. The fix that actually addresses that: on a phone-width
   screen, the "card" stops being an inset box altogether and becomes the
   full screen itself (edge to edge, no border, no radius, no gap of the
   page's darker --bg showing around it) — its content is then centered
   within that full height via flexbox. There's no separate box left to
   look small; the screen itself is the login form, the way an actual
   native app screen looks. Scoped to ".center-screen .card" (not a bare
   ".card" or ".field") on purpose — .card and .field are both used
   elsewhere (store.js's plan cards, ordinary in-app forms) that should
   NOT change here. */
@media (max-width: 640px){
  .center-screen{
    padding: 0;
    align-items: stretch;
  }
  .center-screen .card{
    max-width: none;
    min-height: 100vh;
    min-height: 100dvh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    border: none;
    border-radius: 0;
    padding: 56px 28px;
  }
  .center-screen .card h1{ font-size: 1.85rem; }
  .center-screen .card .sub{ font-size: 1.05rem; margin-bottom: 26px; }
  .center-screen .card .field{ margin-bottom: 18px; }
  .center-screen .card .field label{ font-size: 0.92rem; margin-bottom: 7px; }
  .center-screen .card .field input,
  .center-screen .card .field select,
  .center-screen .card .field textarea{
    padding: 14px 14px; font-size: 1rem; border-radius: 10px;
  }
  .center-screen .card .btn{ padding: 16px; font-size: 1.05rem; border-radius: 10px; }
  .center-screen .card .hint-text{ font-size: 0.95rem; }
  .auth-logo{ height: 64px; }
  .auth-logo.sub-logo{ height: 52px; }
  .auth-logo.mini-logo{ height: 46px; }
}

.field{ margin-bottom: 14px; }
.field label{ display: block; font-size: 0.82rem; font-weight: 600; color: var(--ink-soft); margin-bottom: 5px; }
.field input, .field select, .field textarea{
  width: 100%; padding: 10px 12px; border: 1px solid var(--line);
  border-radius: 8px; background: var(--bg); color: var(--ink);
}
.field textarea{ resize: vertical; min-height: 70px; overflow-wrap: break-word; word-break: break-word; }
.honeypot{ position: absolute; left: -9999px; width: 1px; height: 1px; opacity: 0; }

/* Dual-handle range slider (Store hours) — two native <input type=range>
   elements stacked on the same track via absolute positioning, each with
   its own visible track hidden (pointer-events:none on the input itself,
   re-enabled only on the round thumb) so both handles stay independently
   draggable without one stealing clicks meant for the other. */
.dual-range{ position: relative; height: 26px; margin: 2px 0 4px; }
.dual-range-track{
  position: absolute; top: 50%; left: 0; right: 0; height: 4px;
  background: var(--line); border-radius: 2px; transform: translateY(-50%);
}
.dual-range-fill{
  position: absolute; top: 50%; height: 4px; background: var(--accent);
  border-radius: 2px; transform: translateY(-50%);
}
.dual-range input[type="range"]{
  position: absolute; top: 0; left: 0; width: 100%; height: 26px; margin: 0;
  -webkit-appearance: none; appearance: none; background: transparent; pointer-events: none;
}
.dual-range input[type="range"]::-webkit-slider-runnable-track{ background: transparent; height: 26px; }
.dual-range input[type="range"]::-moz-range-track{ background: transparent; height: 26px; border: none; }
.dual-range input[type="range"]::-webkit-slider-thumb{
  -webkit-appearance: none; pointer-events: auto; width: 18px; height: 18px; margin-top: 4px;
  border-radius: 50%; background: var(--accent); border: 2px solid var(--bg-raised);
  box-shadow: 0 1px 3px rgba(0,0,0,0.35); cursor: pointer;
}
.dual-range input[type="range"]::-moz-range-thumb{
  pointer-events: auto; width: 18px; height: 18px; border-radius: 50%;
  background: var(--accent); border: 2px solid var(--bg-raised);
  box-shadow: 0 1px 3px rgba(0,0,0,0.35); cursor: pointer;
}

.btn{
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  padding: 11px 16px; border-radius: 8px; border: 1px solid var(--line);
  background: var(--bg-raised); color: var(--ink); font-weight: 600;
  cursor: pointer; width: 100%; text-align: center; text-decoration: none;
}
.btn:disabled{ opacity: 0.5; cursor: not-allowed; }
.btn.primary{ background: var(--accent); border-color: var(--accent); color: #fff; }
.btn.danger{ background: var(--bad-bg); border-color: var(--bad); color: var(--bad); }
.btn.ghost{ background: transparent; border-color: transparent; }
.btn.small{ width: auto; padding: 7px 12px; font-size: 0.85rem; }
/* The Standard/Close-up toggle on Capture specifically — bigger than
   .btn.small (which stays as-is; it's used elsewhere for genuinely small
   buttons) but still sized to sit two-up in a row via .btn-row .btn's own
   flex:1 below, not width:auto like .btn.small. */
.quality-btn{ padding: 13px 10px; font-size: 0.98rem; }
.btn-row{ display: flex; gap: 10px; }
.btn-row .btn{ flex: 1; }

.error-text{ color: var(--bad); font-size: 0.85rem; margin: 6px 0 0; }
.hint-text{ color: var(--ink-faint); font-size: 0.82rem; margin: 6px 0 0; }
.link-btn{ background: none; border: none; color: var(--accent); font-weight: 600; cursor: pointer; padding: 0; }

.tag{
  display: inline-block; font-size: 0.72rem; font-weight: 700; letter-spacing: 0.03em;
  text-transform: uppercase; padding: 3px 8px; border-radius: 999px;
  background: var(--accent-bg); color: var(--accent);
}
.tag.good{ background: var(--good-bg); color: var(--good); }
.tag.bad{ background: var(--bad-bg); color: var(--bad); }
.tag.faint{ background: var(--line); color: var(--ink-soft); }

.list-row{
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 14px; border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--bg-raised); margin-bottom: 10px;
}
.list-row .meta{ font-size: 0.82rem; color: var(--ink-faint); margin-top: 3px; }
/* Activity Log rows: description text can run to two or three lines (a long
   role name, a long list of ticket numbers, ...) — align-items:flex-start
   (instead of .list-row's default center) keeps the timestamp pinned to the
   top-right corner instead of drifting to the vertical middle of the whole
   row, which used to land it awkwardly beside a wrapped middle line. */
.log-row{ align-items: flex-start; }

.section-title{
  font-size: 0.78rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--ink-faint); margin: 22px 0 10px;
}
.section-title:first-child{ margin-top: 0; }
/* The Export history dropdown's own chevron (backup.js) — was inheriting
   .section-title's 0.78rem straight from its parent, same as the tiny
   uppercase label text next to it, which read as too small to register as
   a tappable arrow. Bumped on its own via em (relative to that inherited
   0.78rem) rather than touching .section-title itself, so the "EXPORT
   HISTORY" label stays exactly the same size everywhere else it's used. */
.toggle-arrow{ font-size: 2.2em; }

.toolbar{ display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 14px; }
/* font-size is 1rem (16px), not smaller — iOS Safari auto-zooms the whole
   page in on focus whenever a text input/select's font-size is under 16px,
   then stays zoomed in after you're done (there's no matching auto-zoom-
   back-out). It's not a bug in the zoom itself, it's Safari's built-in
   "make sure you can read what you're typing" behavior kicking in because
   this text used to render below that 16px threshold — keeping every real
   form control at 16px+ is what keeps Safari from ever deciding it needs to
   step in. */
.toolbar select, .toolbar input{
  padding: 8px 10px; border: 1px solid var(--line); border-radius: 8px;
  background: var(--bg-raised); color: var(--ink); font-size: 1rem;
}

.empty-state{ text-align: center; color: var(--ink-faint); padding: 40px 20px; }

.photo-grid{ display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: 8px; margin: 12px 0; }
.photo-grid img{ width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: 8px; border: 1px solid var(--line); }
.photo-cell{ position: relative; }
/* Was scoped as ".photo-cell .remove-x" (only the pending-photos grid used
   it) — widened to a plain, standalone class so ticketDetail.js's per-photo
   row (a bordered .list-row card, not a .photo-cell grid tile) can reuse
   the exact same round × button instead of a second near-duplicate rule.
   No behavior change for the grid: .photo-cell still supplies the
   position:relative this positions against, same as before. Any OTHER
   positioned ancestor works identically now too — this only ever
   positions relative to whichever one is nearest. */
.remove-x{
  position: absolute; top: 4px; right: 4px; background: rgba(20,19,17,0.7); color: #fff;
  border: none; border-radius: 50%; width: 26px; height: 26px; font-size: 1rem; cursor: pointer;
  /* Without these, the × glyph sat off-center inside the round button —
     a plain <button> lays its text out like inline content (subject to the
     browser's default line-height and the glyph's own vertical metrics),
     not centered in the box the way flex centering guarantees on both
     axes regardless of font quirks. padding:0 + line-height:1 stop those
     defaults from nudging it off-center in the first place. */
  display: flex; align-items: center; justify-content: center; padding: 0; line-height: 1;
}

/* Fixed height (not "max-height" derived from the stream's own dimensions)
   is what actually keeps this screen stationary. Before this, the wrap's
   height came from the <video> element's own intrinsic size — a <video>
   with no stream yet renders at the browser's placeholder aspect ratio
   (a wide ~2:1 box), then jumps to the real camera's aspect ratio (often
   tall/portrait on a phone) the instant startCamera() gets its first frame
   a beat later. That jump is what read as the camera "moving" — nothing
   below it (shutter, counter, Cancel/Done) was pushed by a *growing list*,
   it was pushed by the frame itself changing height out from under it.
   Giving the wrap a height that never depends on the stream, and having
   the video fill it with object-fit:contain (full frame always visible,
   letterboxed with black bars rather than cropped) instead of stretching
   or cropping, means this box is exactly the same height before, during,
   and after the camera connects — nothing to shift. contain (not cover) is
   deliberate: captureStampedFrame() below always captures the video's
   real, full videoWidth/videoHeight, so cropping the PREVIEW with
   object-fit:cover would show less than what actually gets saved — a
   framing mismatch. contain keeps preview and capture showing/saving the
   same full frame. */
/* Plain "vh" itself isn't fully fixed on mobile Safari — it's recalculated
   as the address bar/toolbar collapses or reappears while scrolling, which
   was the source of the small residual shift left after the fix above.
   "svh" (small viewport height) always assumes the toolbar is showing, so
   it never changes as that bar comes and goes — genuinely constant instead
   of just usually-constant. The plain vh line stays first as a fallback for
   any browser that doesn't understand svh yet (which then simply ignores
   the second, more specific line and keeps using the first). */
.camera-wrap{ position: relative; background: #000; border-radius: var(--radius); overflow: hidden; height: 58vh; height: 58svh; }
/* position:absolute + inset:0 (added after a real Android report: the box
   was correctly sized right up until the camera permission was actually
   granted and the live stream started playing, then "expanded" back to
   cutting off Close/Done — the exact symptom of a flex item's automatic
   sizing picking up a replaced element's real content size the moment
   that size becomes known). Before this, the <video> was a normal in-flow
   child sized with plain width/height:100% — which depends on its
   containing block (camera-wrap) already having a settled height to be
   100% OF. camera-wrap's own height comes from flex (see
   .capture-view .camera-wrap below) and, on at least some Android Chrome
   builds, a <video> that only just received real dimensions (from
   getUserMedia negotiating an actual resolution, which doesn't happen
   until permission is granted and the stream attaches) can feed that
   intrinsic size back into the surrounding flex layout as a new content-
   based minimum — even with an explicit min-height already set — growing
   camera-wrap past its intended flex-computed box instead of just filling
   it. Taking the video fully out of normal flow with position:absolute
   makes its outer box 100% determined by camera-wrap's containing block
   ALONE, from inset:0 — its native videoWidth/videoHeight can no longer
   feed back into anything above it, on any browser, whether or not this
   specific bug reproduces there. object-fit:contain still does exactly
   the same job of letting the actual video frame's aspect ratio show
   in full without cropping, entirely unrelated to this. */
.camera-wrap video{ position: absolute; inset: 0; width: 100%; height: 100%; display: block; object-fit: contain; background: #000; }
/* Live-preview mirror of Close-up mode's fixed, constant 2.5x capture crop
   (see camera.js's captureStampedFrame `zoom` param and capture.js's
   QUALITY_MODES) — toggled on/off the .camera-wrap element itself in
   capture.js the instant Close-up/Standard is picked, never left to any
   user gesture (no pinch, no slider). A plain CSS transform on the video
   element, nothing more: the underlying camera stream is completely
   untouched, so this has zero effect on what Standard mode requests from
   the camera or on the actual captured resolution either mode produces —
   it only changes what's drawn on screen while framing a shot. Scaling the
   video (which already exactly fills camera-wrap via inset:0 above) by 2.5x
   around its own center is optically the same as viewing a centered 1/2.5
   crop blown back up to fill the box — the same region captureStampedFrame
   actually crops from the native frame at capture time — and camera-wrap's
   own overflow:hidden (above) clips the scaled-up video to the box exactly
   the same way it already clips everything else here. */
.camera-wrap.zoomed video{ transform: scale(2.5); transform-origin: 50% 50%; }
/* Capture's own .container only — every other page's .container is a
   normal, scrollable block (a growing list, a long form) and should stay
   exactly that. This one is meant to behave like a fixed camera
   viewfinder that never scrolls: a flat 58vh/58svh camera-wrap (above)
   PLUS the quality toggle, shot counter and Cancel/Done underneath it
   could add up to slightly MORE than one real screen's height once real
   (correctly-zoomed) phone sizing was actually being applied — and iOS
   Safari hides/shows its address bar the instant a page becomes even a
   little bit scrollable, which is what read as the whole page "moving up
   and down" on its own. The shutter button itself doesn't compete for
   this space at all anymore either — it now floats directly over the
   bottom of camera-wrap instead of sitting in its own row below it (see
   .camera-controls further down), one whole element's worth of height
   this no longer has to find room for.
   A first version of this fix used min-height here instead of height —
   that turned out not to actually fix the overflow: min-height is only a
   FLOOR, so once the flat 58svh camera-wrap plus everything else added up
   to MORE than one screen, the container simply grew taller to fit
   everything at its natural size instead — nothing ever told camera-wrap
   to shrink, because flex-shrink only kicks in against a firm, definite
   size, not a floor. A real (not min-) height here is what gives the flex
   algorithm that firm size to shrink camera-wrap against, down to its own
   min-height floor below — so it always exactly fills whatever's left
   over after everything else, on any phone. The one deliberate exception:
   an unusually tall banner (a long usage-limit warning, say) can still
   push the whole page past one screen even with camera-wrap already at
   its floor — overflow stays visible (never hidden/clipped) specifically
   so that case still scrolls normally instead of losing content. */
.capture-view{ display: flex; flex-direction: column; height: 100vh; height: 100dvh; }
/* Capture never renders bottomNav() (see capture.js's own render()) — but
   .container's own padding-bottom (above) unconditionally reserves
   calc(var(--nav-h) + 32px) of space anyway, on the theory that a little
   dead space at the bottom is harmless when there's no bar to protect
   against. That held on every iPhone tested, but a report from an Android
   phone showed the Cancel/Done row pushed below the visible viewport with
   no way to reach it (html.capture-lock disables scrolling for exactly
   this screen — see its own comment near the top of this file — so
   there's no scrolling down to it either). --nav-h alone is 94px on
   phone, so that reserved-but-unused strip was ~126px of vertical budget
   this screen never actually needed, and on a shorter/narrower Android
   viewport (a smaller phone, or Chrome's on-screen chrome eating more of
   100dvh than iOS Safari's does) that was apparently enough to tip
   camera-wrap's own min-height floor (below) past what was actually left.
   Reclaiming it here — bottom padding only, top padding (topbar
   clearance) is untouched since Capture's topbar is real — costs nothing
   on any device (there was never a bottom-nav bar here to collide with)
   and buys back real headroom on the tightest screens. Higher specificity
   than plain .capture-view (two classes vs. one) so this can't quietly
   lose to .container's own shorthand regardless of source order. */
.container.capture-view{ padding-bottom: 18px; }
/* min-height was 220px — lowered as a safety margin, not a target: this is
   only ever a FLOOR (see the big comment above .capture-view for why a
   floor, not a fixed size, is what makes the flex-shrink math work at
   all), so on every device with enough room, camera-wrap still gets
   exactly the same flex:1-computed height it always did — this floor is
   invisible unless something is tight enough to actually hit it. On an
   iPhone (or any screen with real headroom) nothing here changes. The
   Android report above is the case that WAS hitting the 220px floor and
   overflowing past it; 160px gives that case more room to keep shrinking
   before it has to overflow instead. This is a change to the on-screen
   PREVIEW box only — capture.js's actual photo resolution
   (QUALITY_MODES' targetLongEdge, drawn from the video element onto a
   canvas) never reads this CSS at all, so nothing about captured photo
   size/quality is affected on any device. */
.capture-view .camera-wrap{ flex: 1 1 auto; min-height: 160px; height: auto; }
/* Shown only until startCamera() resolves (or fails) — see capture.js's own
   comment on cameraReady/loadingEl. Without this, the wrap is just a flat
   black box for however long getUserMedia() takes to actually deliver a
   frame (can be a real, noticeable beat on some phones/first opens), which
   reads as the app hanging or being buggy rather than simply starting up. */
.camera-loading{
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  color: #f4f1ea; font-size: 0.9rem; background: rgba(0,0,0,0.35);
}
/* Floats over the bottom of the live camera preview now, instead of
   sitting in its own row underneath it (see the comment on this element
   in capture.js) — anchored to .camera-wrap's position:relative above.
   The dark-to-transparent gradient (rather than a flat translucent panel)
   is what keeps the shutter button readable against a bright or busy
   background behind it while still clearly showing the live feed through
   it, same idea as a phone's own built-in camera app. Padding includes
   env(safe-area-inset-bottom) so this never sits under the home-
   indicator area on an iPhone with no physical home button. */
.camera-controls{
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 1;
  padding: 28px 12px calc(12px + env(safe-area-inset-bottom, 0px));
  background: linear-gradient(to top, rgba(0,0,0,0.6), rgba(0,0,0,0) 100%);
  pointer-events: none; /* only the button itself should be tappable — see below */
}
.camera-controls .shutter-btn{ pointer-events: auto; }
/* A wide bar spanning the same width as the camera frame, instead of a
   small circle off on its own — the whole bar is one big, easy-to-hit tap
   target; the circular dot inside is just the familiar "shutter"
   affordance, not a smaller hit area of its own (the <button> itself is
   what's clickable, edge to edge). Translucent instead of the old solid
   --bg-raised fill — background/border are both semi-transparent now so
   the camera feed underneath still reads through the bar, per the store
   owner's own request, rather than hiding a strip of the shot behind a
   solid panel. */
.shutter-btn{
  width: 100%; height: 62px; border-radius: 16px; border: 2px solid rgba(244,241,234,0.85);
  background: rgba(20,18,14,0.32); cursor: pointer;
  /* manipulation drops the browser's built-in double-tap-to-zoom delay/
     gesture on this button specifically — the exact thing a fast series of
     shutter taps could otherwise be misread as, on top of the app-wide
     user-select/touch-callout fix above. */
  touch-action: manipulation;
  display: flex; align-items: center; justify-content: center;
}
.shutter-btn-dot{
  width: 44px; height: 44px; border-radius: 50%;
  background: var(--accent); border: 3px solid rgba(244,241,234,0.9); box-shadow: 0 0 0 2px rgba(0,0,0,0.45);
}
.shutter-btn:disabled .shutter-btn-dot{ opacity: 0.4; }

.note-row{ padding: 10px 0; border-bottom: 1px solid var(--line); }
.note-row:last-child{ border-bottom: none; }
.note-row .author{ font-weight: 700; font-size: 0.85rem; }
.note-row .ts{ color: var(--ink-faint); font-size: 0.75rem; margin-left: 6px; }
.note-row p{ margin: 4px 0 0; overflow-wrap: break-word; word-break: break-word; white-space: pre-wrap; }

.bottom-nav{
  /* Same fixed-instead-of-sticky fix as .topbar above, and for the same
     reason (a direct flex child of #app, which is where Firefox's sticky
     bug bites) — see that comment for the full explanation. */
  position: fixed; bottom: 0; left: 0; right: 0; z-index: 5; display: flex; border-top: 1px solid var(--line);
  background: var(--bg-raised); height: var(--nav-h);
}
.bottom-nav a{
  flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-decoration: none; color: var(--ink-faint); font-size: 0.78rem; gap: 3px;
  /* Each tab's own <a> already spans the full height of --nav-h (align-
     items/justify-content: center above) and 1/6th of the bar's width
     (flex:1) — so the real "make tabs easier to hit" fix is --nav-h itself
     being taller (see :root above), not padding piled on top of that,
     which would just shrink the icon/label further to make room. */
}
.bottom-nav a.active{ color: var(--accent); font-weight: 700; }
.bottom-nav .nav-icon{ font-size: 1.3rem; }

/* This HAS to live down here, after .bottom-nav a/.nav-icon's own base
   rules right above, instead of up with the rest of the phone overrides
   near the top of this file (where every other phone-only rule for the
   topbar/bottom-nav lives). Reason: a media query gets NO specificity
   boost over an unscoped rule — when two rules have equal specificity,
   whichever one is LATER in the file wins, media query or not. The base
   .bottom-nav a/.nav-icon rules above happen to sit further down in this
   file than that top block, so a copy of this override placed up there
   was silently losing the cascade on every phone, every time, no matter
   what size it was set to — the bar's own height (--nav-h, set up top)
   grew as expected since nothing else touches that, but the icon/label
   inside it stayed pinned at the original tiny size, which is exactly why
   this kept looking like "nothing changed." Keeping the override here,
   after its base rule, is what actually guarantees it wins.
   Settled at 1.3x the original 0.78rem/3px/1.3rem, matching --nav-h's own
   1.3x above — picked directly by the store owner from a set of live
   on-device size options (A 1.0x through E 1.6x), after 1.75x (the
   previous setting) measured out as noticeably too big in practice. The
   real blocker to ever SEEING this rule apply at all was never this rule
   itself — see that comment for the actual cause (Safari's per-site
   "Request Desktop Website" and page-zoom settings). */
@media (max-width: 480px){
  .bottom-nav a{ font-size: 1.014rem; gap: 3.9px; }
  .bottom-nav .nav-icon{ font-size: 1.69rem; }
}

/* A small floating panel docked just above bottom-nav, for an action that
   needs to stay reachable without scrolling past a long, growing list above
   it (today: activity.js's Owner-only "Clean up old log history" — see its
   own comment). Anchored with `bottom` only, no `top`, so opening the
   <details> inside grows the panel upward from the tab bar rather than the
   tab bar (or anything below this panel) ever moving. z-index sits just
   under .bottom-nav's so the tab bar always stays on top of it, never the
   other way around. */
.docked-panel{
  position: fixed; left: 0; right: 0; bottom: var(--nav-h); z-index: 4;
  background: var(--bg-raised); border-top: 1px solid var(--line);
  box-shadow: 0 -2px 10px rgba(20,18,14,0.08);
}
.docked-panel-inner{ max-width: 480px; margin: 0 auto; padding: 10px 16px; }

.role-perms{ display: grid; gap: 8px; margin: 10px 0; }
.role-perms label{ display: flex; align-items: center; gap: 8px; font-size: 0.9rem; }

.role-card{
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--bg-raised); margin-bottom: 10px; overflow: hidden;
}
.role-card summary{
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 14px; cursor: pointer; list-style: none; font-weight: 600;
}
.role-card summary::-webkit-details-marker{ display: none; }
.role-card summary::marker{ content: ""; }
.role-card summary::after{
  content: "\25BE"; color: var(--ink-faint); font-weight: 400; margin-left: 8px;
  transition: transform 0.15s ease; flex-shrink: 0;
}
.role-card[open] summary::after{ transform: rotate(180deg); }
.role-card .role-card-body{ padding: 0 14px 14px; }

.avatar-upload{
  width: 72px; height: 72px; min-width: 72px; flex-shrink: 0; border-radius: 50%; background: var(--accent-bg);
  display: flex; align-items: center; justify-content: center; cursor: pointer;
  border: 1px dashed var(--line); overflow: hidden; margin-bottom: 14px;
  /* "Add photo" wraps onto two lines inside this small a circle — without
     these, each wrapped line stayed left-aligned even though the circle
     itself centers its content as a block, so it read as hugging the left
     edge instead of sitting centered and readable. No padding here on
     purpose — an actual photo needs to fill this circle edge-to-edge (via
     the width:100%/height:100%/object-fit:cover rule below), the same way
     it does in the topbar; padding would inset it and leave a ring of
     background showing, which looked like the photo was cut off. */
  text-align: center; font-size: 0.78rem; line-height: 1.25; color: var(--accent);
  font-weight: 600; text-wrap: balance;
}
.avatar-upload img{ width: 100%; height: 100%; object-fit: cover; }

/* Avatar cropper (see js/cropper.js) — the circle here is a 1:1 preview of
   what gets saved, so the image is positioned with plain left/top + width/
   height (set from JS as the user drags/zooms) rather than object-fit,
   which wouldn't let us track an arbitrary pan/zoom offset. */
.cropper-box{ max-width: 320px; }
.cropper-viewport{
  width: 240px; height: 240px; margin: 4px auto 0; border-radius: 50%;
  overflow: hidden; position: relative; background: var(--line);
  touch-action: none; cursor: grab; border: 1px solid var(--line);
}
.cropper-viewport:active{ cursor: grabbing; }
.cropper-viewport img{
  position: absolute; max-width: none; max-height: none;
  user-select: none; -webkit-user-drag: none; pointer-events: none;
}

.banner{
  padding: 12px 14px; border-radius: var(--radius); margin-bottom: 16px;
  font-size: 0.88rem; border: 1px solid var(--line);
}
.banner.note{ background: var(--accent-bg); border-color: var(--accent); }
.banner.good{ background: var(--good-bg); border-color: var(--good); }

/* In-app confirm/alert/prompt — see the comment above openModal() in ui.js
   for why these replace window.confirm/alert/prompt everywhere. */
.modal-overlay{
  position: fixed; inset: 0; background: rgba(20,18,14,0.55);
  display: flex; align-items: center; justify-content: center;
  padding: 20px; z-index: 100;
}
.modal-box{
  background: var(--bg-raised); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 20px; width: 100%; max-width: 380px;
  box-shadow: 0 12px 32px rgba(0,0,0,0.25);
}
.modal-box h2{ font-family: var(--font-display); font-size: 1.1rem; margin: 0 0 10px; }

/* Small self-dismissing "Saved." confirmation — see showToast() in ui.js.
   Sits above the modal overlay's z-index (100) since a save action inside
   an open modal should still be able to show one. */
.toast{
  position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%) translateY(8px);
  background: var(--ink); color: var(--bg);
  padding: 10px 18px; border-radius: 999px; font-size: 0.88rem; font-weight: 600;
  box-shadow: 0 6px 20px rgba(0,0,0,0.25);
  opacity: 0; transition: opacity 0.2s ease, transform 0.2s ease;
  z-index: 200; pointer-events: none; max-width: calc(100vw - 32px); text-align: center;
}
.toast.show{ opacity: 1; transform: translateX(-50%) translateY(0); }
