/* ── CSS variables ── */
    :root {
      --bg:                #f5f5f5;
      --surface:           #ffffff;
      --border:            #e5e5e5;
      --border-light:      #f3f4f6;
      --text:              #1a1a1a;
      --text-secondary:    #6b7280;
      --text-muted:        #9ca3af;
      --navbar-bg:         #1a1a1a;
      --navbar-text:       #ffffff;
      --navbar-link:       #cccccc;
      --input-bg:          #ffffff;
      --input-border:      #d1d5db;
      --table-header-bg:   #f9fafb;
      --btn-primary-bg:    #1a1a1a;
      --btn-primary-text:  #ffffff;
      --btn-secondary-bg:  #ffffff;
      --btn-secondary-text:#1a1a1a;
      --btn-secondary-border:#dddddd;
      --alert-success-bg:  #f0fdf4;
      --alert-success-text:#166534;
      --alert-success-border:#bbf7d0;
      --alert-error-bg:    #fef2f2;
      --alert-error-text:  #991b1b;
      --alert-error-border:#fecaca;
      --btn-danger-bg:     #dc2626;
      --btn-danger-text:   #ffffff;

      /* Status-badge palette. Consolidates what were previously ad-hoc
         hardcoded hex pairs repeated (and drifting — two different
         "success green"s existed) across manage.html, users.html,
         lookup.html, dashboard.html, print_history.html and my_devices.html.
         Every badge usage site now points at one of these six semantic
         pairs instead of a literal color, and every pair gets a real dark-
         mode value below (badges previously had none at all). */
      --badge-success-bg:    #dcfce7;
      --badge-success-text:  #166534;
      --badge-success-border:#bbf7d0;
      --badge-warning-bg:    #fef3c7;
      --badge-warning-text:  #92400e;
      --badge-warning-border:#fde68a;
      --badge-danger-bg:     #fef2f2;
      --badge-danger-text:   #dc2626;
      --badge-danger-border: #fecaca;
      --badge-neutral-bg:    #f3f4f6;
      --badge-neutral-text:  #6b7280;
      --badge-neutral-border:#e5e7eb;
      --badge-info-bg:       #dbeafe;
      --badge-info-text:     #1d4ed8;
      --badge-info-border:   #bfdbfe;
      --badge-accent-bg:     #fef9c3;
      --badge-accent-text:   #854d0e;
      --badge-accent-border: #fde047;
      --badge-dark-bg:       #1a1a1a;
      --badge-dark-text:     #ffffff;
      --badge-dark-border:   #1a1a1a;
    }

    html.dark {
      --bg:                #111111;
      --surface:           #1e1e1e;
      --border:            #2d2d2d;
      --border-light:      #252525;
      --text:              #e5e5e5;
      --text-secondary:    #9ca3af;
      --text-muted:        #6b7280;
      --navbar-bg:         #0d0d0d;
      --navbar-text:       #f5f5f5;
      --navbar-link:       #9ca3af;
      --input-bg:          #252525;
      --input-border:      #3d3d3d;
      --table-header-bg:   #252525;
      --btn-primary-bg:    #f5f5f5;
      --btn-primary-text:  #1a1a1a;
      --btn-secondary-bg:  #2a2a2a;
      --btn-secondary-text:#e5e5e5;
      --btn-secondary-border:#3d3d3d;
      --alert-success-bg:  #052e16;
      --alert-success-text:#86efac;
      --alert-success-border:#166534;
      --alert-error-bg:    #450a0a;
      --alert-error-text:  #fca5a5;
      --alert-error-border:#991b1b;
      --btn-danger-bg:     #dc2626;
      --btn-danger-text:   #ffffff;

      /* Same dark, saturated-bg-plus-bright-pastel-text treatment already
         used for --alert-*-bg/text above, applied to every badge pair. */
      --badge-success-bg:    #052e16;
      --badge-success-text:  #86efac;
      --badge-success-border:#166534;
      --badge-warning-bg:    #451a03;
      --badge-warning-text:  #fcd34d;
      --badge-warning-border:#92400e;
      --badge-danger-bg:     #450a0a;
      --badge-danger-text:   #fca5a5;
      --badge-danger-border: #991b1b;
      --badge-neutral-bg:    #262626;
      --badge-neutral-text:  #a3a3a3;
      --badge-neutral-border:#404040;
      --badge-info-bg:       #1e3a5f;
      --badge-info-text:     #93c5fd;
      --badge-info-border:   #1d4ed8;
      --badge-accent-bg:     #422006;
      --badge-accent-text:   #fde047;
      --badge-accent-border: #854d0e;
      --badge-dark-bg:       #f5f5f5;
      --badge-dark-text:     #1a1a1a;
      --badge-dark-border:   #f5f5f5;
    }

    /* ── Reset ── */
    * { box-sizing: border-box; margin: 0; padding: 0; }

    body {
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
      background: var(--bg);
      color: var(--text);
      transition: background 0.2s, color 0.2s;
    }

    /* ── Navbar ── */
    .navbar {
      background: var(--navbar-bg);
      color: var(--navbar-text);
      padding: 14px 24px;
      display: flex;
      justify-content: space-between;
      align-items: center;
      position: relative;
    }
    .navbar .brand {
      font-size: 18px;
      font-weight: 700;
      letter-spacing: 1px;
      color: var(--navbar-text);
      text-decoration: none;
      white-space: nowrap;
    }
    .nav-links {
      display: flex;
      align-items: center;
      gap: 16px;
    }
    .nav-user {
      color: var(--navbar-link);
      font-size: 13px;
    }
    .nav-links a {
      color: var(--navbar-link);
      text-decoration: none;
      font-size: 13px;
    }
    .nav-links a:hover { color: var(--navbar-text); }

    .dark-toggle {
      background: none;
      border: 1px solid var(--navbar-link);
      color: var(--navbar-link);
      border-radius: 20px;
      padding: 3px 10px;
      font-size: 13px;
      cursor: pointer;
      text-decoration: none;
      display: inline-flex;
      align-items: center;
      gap: 4px;
    }
    .dark-toggle:hover {
      border-color: var(--navbar-text);
      color: var(--navbar-text);
    }

    /* ── Icons ──
       Shared icon sprite is inlined once in base.html; every usage site is
       just `<svg class="icon"><use href="#icon-NAME"/></svg>`. Sized in em
       so it scales with the surrounding text and always inherits the text
       color (currentColor) automatically — no per-icon color rules needed,
       light or dark mode. vertical-align nudges it onto the text baseline
       instead of sitting high like a raw inline SVG normally does. */
    .icon {
      width: 1em;
      height: 1em;
      display: inline-block;
      vertical-align: -0.125em;
      flex-shrink: 0;
    }

    /* ── Layout ── */
    .container { max-width: 1000px; margin: 32px auto; padding: 0 24px; }

    /* ── Card ── */
    .card {
      background: var(--surface);
      border-radius: 10px;
      border: 1px solid var(--border);
      padding: 24px;
      margin-bottom: 20px;
    }
    .card h2 { font-size: 16px; font-weight: 600; margin-bottom: 16px; color: var(--text); }

    /* Horizontal-scroll wrapper for wide tables — see the mobile-responsive
       block below for why this can't just be `overflow-x:auto` on .card
       itself (forces overflow-y:auto too, which clips anything meant to
       escape the card, like a Tom Select dropdown). Originally only turned
       on inside the mobile media query, on the assumption a ~1000px-wide
       desktop container always has room — that assumption broke for
       print_history.html's PDF-parts row on a large batch (many "PDF (i/N)"
       links in one cell), which overflowed the card and grew the whole page
       instead of scrolling, leaving the card's background box short of the
       buttons that spilled past it. Defined here unconditionally so it's a
       safety net on every screen size, not just narrow ones — `overflow:
       auto` only shows a scrollbar when content actually doesn't fit, so
       this can't affect any table that already fits its card. */
    .table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

    /* ── Buttons ──
       Replaces the old single blanket `.btn:hover { opacity: 0.85 }` (same
       flat treatment for every variant, no active/focus state, no
       elevation) with real per-variant hover/active states and a subtle
       resting shadow on the two solid-fill variants, plus a keyboard
       focus-visible ring that didn't exist before at all. See mobile UI
       plan, Group C — button & status-badge system. */
    .btn {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      gap: 6px;
      padding: 10px 20px;
      border-radius: 8px;
      font-size: 14px;
      font-weight: 600;
      cursor: pointer;
      border: none;
      text-decoration: none;
      transition: background 0.15s, border-color 0.15s, box-shadow 0.15s,
                  transform 0.05s, filter 0.15s;
      min-height: 44px;
      line-height: 1.2;
    }
    .btn:focus-visible {
      outline: 2px solid var(--text-secondary);
      outline-offset: 2px;
    }
    .btn:disabled, .btn[disabled] {
      opacity: 0.5;
      cursor: not-allowed;
      box-shadow: none;
      transform: none;
      filter: none;
    }
    .btn-primary {
      background: var(--btn-primary-bg);
      color: var(--btn-primary-text);
      box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
    }
    .btn-primary:hover { filter: brightness(1.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.16); }
    .btn-primary:active { filter: brightness(0.95); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); transform: translateY(1px); }
    .btn-secondary {
      background: var(--btn-secondary-bg);
      color: var(--btn-secondary-text);
      border: 1px solid var(--btn-secondary-border);
    }
    .btn-secondary:hover { background: var(--table-header-bg); border-color: var(--text-secondary); }
    .btn-secondary:active { transform: translateY(1px); }
    .btn-danger {
      background: var(--btn-danger-bg);
      color: var(--btn-danger-text);
      box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
    }
    .btn-danger:hover { filter: brightness(1.1); box-shadow: 0 2px 8px rgba(220, 38, 38, 0.25); }
    .btn-danger:active { filter: brightness(0.92); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); transform: translateY(1px); }

    /* Compact size variant — replaces the same `style="padding:6px 14px;
       font-size:13px;"` override that was being repeated inline on ~17
       button/link instances across manage.html, print_history.html and
       scan_active.html (pagination controls, table row actions). */
    .btn-sm {
      padding: 6px 14px;
      font-size: 13px;
      min-height: 34px;
      border-radius: 6px;
    }

    /* Icon-only square button — replaces the fully hand-rolled inline
       style previously duplicated on the assign(+)/unassign(×) buttons in
       assignments.html and the session-log close button in
       scan_active.html. Default is a neutral outline; the -success/-danger
       modifiers give it a colored border+icon at rest and a tinted badge
       background on hover, reusing the badge palette above so the two
       systems read as one language instead of two. */
    .btn-icon {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      flex-shrink: 0;
      width: 30px;
      height: 30px;
      padding: 0;
      border-radius: 6px;
      background: none;
      border: 1px solid var(--border);
      color: var(--text-secondary);
      cursor: pointer;
      font-size: 16px;
      transition: background 0.15s, border-color 0.15s, color 0.15s;
    }
    .btn-icon:hover { background: var(--table-header-bg); color: var(--text); }
    .btn-icon:disabled, .btn-icon[disabled] { opacity: 0.5; cursor: not-allowed; }
    .btn-icon-sm { width: 26px; height: 26px; font-size: 14px; }
    .btn-icon-success {
      border-color: var(--badge-success-border);
      color: var(--badge-success-text);
    }
    .btn-icon-success:hover { background: var(--badge-success-bg); }
    .btn-icon-danger {
      border-color: var(--badge-danger-border);
      color: var(--badge-danger-text);
    }
    .btn-icon-danger:hover { background: var(--badge-danger-bg); }

    /* ── Loading state for network-bound actions ──
       .btn-spinner uses currentColor for its ring so it automatically
       matches whatever text color it's placed in — a white ring inside
       .btn-primary/.btn-danger, the muted gray of a plain status line
       (e.g. "Loading…" text), all with zero per-context overrides needed.
       .is-loading is what ztLoadingStart()/ztLoadingEnd() (base.html) toggle:
       real :disabled styling already covers <button>, but an <a> has no
       disabled attribute, so it needs its own rule here. */
    .btn-spinner {
      display: inline-block;
      width: 14px;
      height: 14px;
      border: 2px solid currentColor;
      border-top-color: transparent;
      border-radius: 50%;
      opacity: 0.65;
      animation: zt-spin 0.6s linear infinite;
      vertical-align: -2px;
      flex-shrink: 0;
    }
    @keyframes zt-spin { to { transform: rotate(360deg); } }
    a.is-loading {
      pointer-events: none;
      opacity: 0.7;
    }

    /* ── Status badges ──
       One shared pill shape + six semantic color pairs (vars defined
       above), replacing the per-template hardcoded hex pills that had
       drifted (two different "success green"s existed) and had zero
       dark-mode handling at all. -info/-accent/-dark are the three role
       badges on users.html; the rest are status words used across
       manage.html, lookup.html, dashboard.html, print_history.html and
       my_devices.html. */
    .badge {
      display: inline-flex;
      align-items: center;
      gap: 4px;
      padding: 3px 10px;
      border-radius: 20px;
      font-size: 12px;
      font-weight: 600;
      border: 1px solid transparent;
      white-space: nowrap;
    }
    .badge-success { background: var(--badge-success-bg); color: var(--badge-success-text); border-color: var(--badge-success-border); }
    .badge-warning { background: var(--badge-warning-bg); color: var(--badge-warning-text); border-color: var(--badge-warning-border); }
    .badge-danger  { background: var(--badge-danger-bg);  color: var(--badge-danger-text);  border-color: var(--badge-danger-border); }
    .badge-neutral { background: var(--badge-neutral-bg); color: var(--badge-neutral-text); border-color: var(--badge-neutral-border); }
    .badge-info    { background: var(--badge-info-bg);    color: var(--badge-info-text);    border-color: var(--badge-info-border); }
    .badge-accent  { background: var(--badge-accent-bg);  color: var(--badge-accent-text);  border-color: var(--badge-accent-border); }
    .badge-dark    { background: var(--badge-dark-bg);    color: var(--badge-dark-text);    border-color: var(--badge-dark-border); }

    /* ── Alerts ── */
    .alert {
      padding: 12px 16px;
      border-radius: 7px;
      margin-bottom: 16px;
      font-size: 14px;
    }
    .alert-success {
      background: var(--alert-success-bg);
      color: var(--alert-success-text);
      border: 1px solid var(--alert-success-border);
    }
    .alert-error {
      background: var(--alert-error-bg);
      color: var(--alert-error-text);
      border: 1px solid var(--alert-error-border);
    }

    /* ── Form elements ── */
    select, input[type=text], input[type=number], input[type=password], input[type=date] {
      width: 100%;
      padding: 10px 12px;
      border: 1px solid var(--input-border);
      border-radius: 7px;
      font-size: 14px;
      margin-bottom: 14px;
      background: var(--input-bg);
      color: var(--text);
      min-height: 44px;
    }
    textarea {
      background: var(--input-bg);
      color: var(--text);
      border: 1px solid var(--input-border);
      border-radius: 7px;
      font-size: 14px;
    }
    select:focus, input:focus, textarea:focus {
      outline: none;
      border-color: var(--text-secondary);
    }
    label {
      display: block;
      font-size: 13px;
      font-weight: 600;
      margin-bottom: 6px;
      color: var(--text-secondary);
    }

    /* ── Inline form validation ──
       Set by ztShowErrors() (base.html) in place of the native red-outline/
       tooltip popup — see class="zt-validate" on the handful of forms that
       opt in (login, generate labels, QC report, create account). Plain
       positive margins on .zt-field-error deliberately — the field it
       follows doesn't always carry the same margin-bottom (some pages
       override it inline), so a negative top margin to "pull up" into that
       gap would collapse differently per page; this way the spacing is the
       same everywhere regardless. */
    .is-invalid {
      border-color: var(--badge-danger-text) !important;
      box-shadow: 0 0 0 1px var(--badge-danger-text);
    }
    .zt-field-error {
      color: var(--badge-danger-text);
      font-size: 12px;
      line-height: 1.4;
      margin: 4px 0 14px;
    }
    /* Tom Select hides the real <select> and renders its own .ts-wrapper in
       its place — .is-invalid lands on the wrapper too (see ztShowErrors),
       so the visible control gets the same red ring as every other field. */
    .ts-wrapper.is-invalid .ts-control {
      border-color: var(--badge-danger-text) !important;
      box-shadow: 0 0 0 1px var(--badge-danger-text);
    }

    /* ── Table ── */
    table { width: 100%; border-collapse: collapse; font-size: 14px; }
    th {
      text-align: left;
      padding: 10px 12px;
      background: var(--table-header-bg);
      border-bottom: 1px solid var(--border);
      font-weight: 600;
      font-size: 12px;
      color: var(--text-secondary);
      text-transform: uppercase;
    }
    td {
      padding: 10px 12px;
      border-bottom: 1px solid var(--border-light);
      color: var(--text);
    }
    tr:last-child td { border-bottom: none; }

    /* ── Tom Select sizing & spacing (both themes) ──
       Tom Select's own stylesheet loads after this one (it's linked from
       inside each page's content block, so it comes later in document
       order), so these need !important to reliably win — including over
       scan_active.html/scan_select.html's own local <style> overrides,
       which is intentional: those set max-height:320px, and clamp() below
       matches or beats that on every real device rather than shrinking it.
       Two fixes here:
       1. The dropdown's scrollable option list is capped at a flat 200px
          by default regardless of screen size — far too short on a tall
          phone screen, which is why it looked like it was barely
          "expanding". clamp() scales with the viewport (45vh) but never
          drops below 250px (even on a short landscape screen, still an
          improvement on the 200px default) or grows past 500px.
       2. Selected item chips only get 3px of bottom margin before the next
          row (and the search input sits right after with no more space),
          which reads as visually cramped — especially with a colored chip
          background right up against the input line. Only touches the
          chip's own margin, not .ts-control's padding, so it doesn't
          fight with the per-page padding already set on scan_active.html/
          scan_select.html. See mobile UI plan, Group A items 1 & 4. */
    .ts-dropdown-content {
      max-height: clamp(250px, 45vh, 500px) !important;
    }
    .ts-control > div {
      margin: 0 4px 8px 0 !important;
    }

    /* 3. Tom Select's own shipped CSS gives its search <input> a fixed
       min-width: 7rem !important (~112px). Every page's .ts-control uses
       display:flex with no flex-wrap set, so once a selected item plus that
       112px input is wider than the control, the input wraps onto its own
       line below the selection — reading as a stray blinking cursor
       floating under the picked option. qc_report.html's narrower 480px
       form surfaces it first, but the same gap exists on generate_qr.html
       and scan_select.html too, so this is fixed here once for all three
       rather than per-template. Paired with the onChange `this.blur()`
       call on each single-select instance, which hides the search input
       immediately on pick instead of waiting for an actual blur. See
       mobile UI plan, Group C (Tom Select cursor fix).

       Scoped to .ts-wrapper.single only — Tom Select adds a `single` or
       `multi` class to the wrapper based on settings.mode. Applying nowrap
       unscoped also hit scan_select.html's SKU multi-select, forcing every
       selected SKU chip onto one cramped horizontal row instead of letting
       them wrap onto multiple lines, which is what cut off chip text like
       "Geooptics Baguette" on the right edge. The multi-select doesn't have
       this cursor bug in the first place: its search input sits after the
       last chip and simply wraps with the chips, it doesn't need nowrap. */
    .ts-wrapper.single .ts-control {
      flex-wrap: nowrap !important;
    }

    /* ── Tom Select dark mode ── */
    html.dark .ts-control {
      background: var(--input-bg) !important;
      border-color: var(--input-border) !important;
      color: var(--text) !important;
    }
    html.dark .ts-control input { color: var(--text) !important; }
    html.dark .ts-dropdown {
      background: var(--surface) !important;
      border-color: var(--border) !important;
      color: var(--text) !important;
    }
    html.dark .ts-dropdown .option { color: var(--text) !important; }
    html.dark .ts-dropdown .option:hover,
    html.dark .ts-dropdown .option.active {
      background: var(--table-header-bg) !important;
    }

    /* ── Confirmation modal (replaces native confirm()) ──
       [hidden] is repeated in a compound selector below deliberately: a
       bare attribute selector and a bare class selector both have the same
       specificity, so `.zt-confirm-overlay { display:flex }` could tie
       with (and beat, by source order) the UA stylesheet's plain
       `[hidden]{display:none}` rule. `.zt-confirm-overlay[hidden]` is more
       specific than either, so hiding always wins regardless of order. */
    .zt-confirm-overlay {
      position: fixed;
      inset: 0;
      background: rgba(0, 0, 0, 0.5);
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 20px;
      z-index: 10000;
    }
    .zt-confirm-overlay[hidden] { display: none; }
    .zt-confirm-box {
      background: var(--surface);
      border: 1px solid var(--border);
      border-radius: 10px;
      padding: 24px;
      max-width: 380px;
      width: 100%;
      box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
    }
    .zt-confirm-message {
      font-size: 14px;
      color: var(--text);
      line-height: 1.5;
      white-space: pre-line;
      margin-bottom: 20px;
    }
    .zt-confirm-actions {
      display: flex;
      justify-content: flex-end;
      gap: 10px;
    }
    @media (max-width: 480px) {
      /* column, not column-reverse: Cancel stays on top and Confirm on the
         bottom (DOM order), so the destructive/consequential action isn't
         the easiest one to land a thumb on first. */
      .zt-confirm-actions { flex-direction: column; }
      .zt-confirm-actions .btn { width: 100%; text-align: center; }
    }

    /* ── PDF parts modal (print_history.html) ──
       Same fixed-overlay pattern as the confirm modal above (position:fixed
       escapes .table-scroll's overflow clipping entirely, so this is safe
       even now that .table-scroll has overflow-x:auto on every screen
       size — see the comment on .table-scroll). Wider than the confirm box
       and with an internally-scrolling part list, since a large batch can
       split into dozens of parts and the modal itself must never grow
       past the viewport. */
    .pdf-parts-overlay {
      position: fixed;
      inset: 0;
      background: rgba(0, 0, 0, 0.5);
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 20px;
      z-index: 10000;
    }
    .pdf-parts-overlay[hidden] { display: none; }
    .pdf-parts-box {
      background: var(--surface);
      border: 1px solid var(--border);
      border-radius: 10px;
      padding: 24px;
      max-width: 440px;
      width: 100%;
      max-height: min(560px, 85vh);
      display: flex;
      flex-direction: column;
      box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
    }
    .pdf-parts-title {
      font-size: 15px;
      font-weight: 700;
      color: var(--text);
      margin-bottom: 4px;
    }
    .pdf-parts-subtitle {
      font-size: 12px;
      color: var(--text-muted);
      line-height: 1.5;
      margin-bottom: 16px;
    }
    .pdf-parts-list {
      flex: 1;
      min-height: 0;
      overflow-y: auto;
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
      align-content: start;
      gap: 8px;
      margin-bottom: 14px;
    }
    .pdf-parts-list a.btn { width: 100%; }
    .pdf-parts-status {
      font-size: 12px;
      color: var(--text-muted);
      line-height: 1.4;
      min-height: 16px;
      margin-bottom: 12px;
    }
    .pdf-parts-actions {
      display: flex;
      justify-content: space-between;
      align-items: center;
      gap: 10px;
      border-top: 1px solid var(--border-light);
      padding-top: 16px;
    }
    @media (max-width: 480px) {
      .pdf-parts-actions { flex-direction: column-reverse; }
      .pdf-parts-actions .btn { width: 100%; text-align: center; }
    }

    /* ── Camera viewfinder (scan_active.html + lookup.html share #scan-video,
       one instance loaded per page — no ID collision risk) ──
       Neither page's own <video style="width:100%..."> inline style sets a
       height, so without this the box sizes itself off the raw camera
       stream's native aspect ratio — the rear/environment camera on many
       phones reports back portrait-shaped (taller than wide) dimensions, so
       at width:100% the video balloons tall enough to push bag count, the
       SKU switcher, "+ Add SKU", last scan, and Session log entirely below
       the fold, forcing constant scrolling on scan_active.html to reach any
       of them (Sabari, 2026-09-01). Capping the CSS box height with
       object-fit:cover gives a normal, viewfinder-sized box regardless of
       what the stream reports — purely a display-size change: scanLoop()/
       tick() in scan_active.html (and the equivalent in lookup.html) still
       read video.videoWidth/videoHeight, the real unscaled stream, for
       decoding, so scan detection accuracy is unaffected. These two
       properties aren't set in either page's own inline style, so this
       applies cleanly with no specificity fight. */
    #scan-video {
      height: clamp(200px, 40vh, 320px);
      object-fit: cover;
    }

    /* ── Scan aiming box ──
       Only the centre of the camera frame is decoded (see the ROI_FRAC
       comment in scan_active.html / lookup.html). This draws that exact
       region so staff can see where to hold the label instead of hunting
       for the distance that happens to work. Its pixel size is set from JS
       because it has to track the real video dimensions and the
       object-fit:cover scale; everything here is static styling only.
       The huge spread box-shadow dims everything outside the box — it is a
       paint-only effect, no extra element and no layout cost. */
    #scan-reticle {
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      border: 2px solid rgba(255, 255, 255, 0.92);
      border-radius: 6px;
      box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.28);
      pointer-events: none;
      display: none;
    }
    #scan-reticle.ready { display: block; }

    /* Scan diagnostics — only ever rendered when the page is opened with
       ?debug=1 (see scan_active.html / lookup.html). QC staff never see it. */
    #scan-debug {
      position: absolute;
      left: 6px;
      bottom: 6px;
      margin: 0;
      padding: 6px 8px;
      font: 11px/1.45 ui-monospace, Menlo, Consolas, monospace;
      color: #d7ffd7;
      background: rgba(0, 0, 0, 0.72);
      border-radius: 6px;
      white-space: pre;
      pointer-events: none;
    }

    /* ── Utility ── */
    .subtle-box {
      background: var(--table-header-bg);
      color: var(--text);
      border-radius: 8px;
      padding: 14px;
      font-size: 14px;
    }
    .subtle-box .label   { color: var(--text-secondary); font-size: 12px; margin-bottom: 6px; }
    .subtle-box .secondary { color: var(--text-secondary); }

    /* ── Mobile responsive ── */
    @media (max-width: 768px) {
      .container { padding: 0 14px; margin: 16px auto; }
      .card { padding: 16px; border-radius: 8px; }
      .navbar { padding: 12px 16px; }
      .navbar .brand { font-size: 15px; }
      .nav-user { display: none; }
      table { min-width: 480px; }
      .mobile-stack { grid-template-columns: 1fr !important; }
      .btn { font-size: 13px; }
    }
