/* ===========================================================================
   IDHOME features.css — cross-app + un-migrated app widgets

   This file now holds ONLY two categories of style:

     A) Cross-app shared patterns — used (or could be used) by more than
        one Django app. Adding a new app should be able to consume these
        without copying.
        · Photo lightbox       (any audit-review surface)
        · Custom date range row (queue / history / reports filter rows)
        · Offline indicator    (PWA infra)
        · Mobile filter collapse + collapsible banner (any filter card)

     B) Sections for apps not yet migrated to the per-app `static/` layout.
        Each of these should eventually move to its app's own folder:
        · documents app → file icon, dropzone, visibility radio cards,
          document detail layout
        · notifications app → activity feed
        · employees app    → profile hero
        · attendance app   → clock hero + state badge, weekly summary strip,
                              leave type cards + status

   Already migrated out (see `apps/<app>/static/<app>/css/<app>.css`):
     · goods_release → apps/goods_release/static/goods_release/css/release.css
       (photo slot, signature pad, completed-release photos, photo thumbnail,
        onboarding panel, barcode scanner overlay, reports dashboard,
        audit comparison view)

   All classes use the `ui-*` namespace + consume tokens from `tokens.css`.

   Migration rule: when an app uses 2+ sections from here and they are not
   used elsewhere, those sections move into the app's own stylesheet and
   the app template loads it via `{% block extra_css %}`.

   Notes:
     - `:has(input:checked)` is used for radio-card highlighting. ~96% support.
       Provide explicit `.is-checked` modifier if older browser fallback needed.
     - Animations respect `prefers-reduced-motion` via tokens.css media query.
   =========================================================================== */


/* =============================================================================
   1. Toolbar / spacer / chip / filter-chips
   ============================================================================= */

.ui-toolbar {
  display: flex;
  align-items: center;
  gap: var(--gap);
  margin-bottom: var(--gap);
  flex-wrap: wrap;
}
.ui-toolbar .ui-filter-chips { display: flex; gap: 6px; flex-wrap: wrap; }
.ui-toolbar .ui-spacer { flex: 1; }

.ui-filter-chips { display: flex; gap: 6px; flex-wrap: wrap; }
.ui-spacer { flex: 1; }

.ui-chip {
  padding: 6px 12px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface);
  font-size: 12.5px;
  cursor: pointer;
  color: var(--text-secondary);
  display: inline-flex;
  align-items: center;
  gap: 6px;
  text-decoration: none;
  font-weight: 500;
  transition: background 0.12s, color 0.12s, border-color 0.12s;
}
.ui-chip:hover { background: var(--surface-2); color: var(--text); text-decoration: none; }
.ui-chip.active {
  background: var(--accent-soft);
  color: var(--accent);
  border-color: transparent;
  font-weight: 600;
}
.ui-chip:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px var(--accent-ring);
}


/* =============================================================================
   2. File icon (documents + downloads)
   ============================================================================= */

.ui-file-icon {
  width: 40px;
  height: 40px;
  border-radius: var(--r-md);
  background: var(--surface-2);
  border: 1px solid var(--border);
  display: grid;
  place-items: center;
  flex-shrink: 0;
  font-size: 18px;
  color: var(--text-secondary);
  position: relative;
}
.ui-file-icon.sm { width: 28px; height: 28px; font-size: 13px; border-radius: var(--r-sm); }
.ui-file-icon.lg { width: 56px; height: 56px; font-size: 24px; border-radius: var(--r-lg); }
/* Per-type accent bar on left edge — keeps grid calm but scannable */
.ui-file-icon::after {
  content: "";
  position: absolute;
  left: 0; top: 6px; bottom: 6px;
  width: 3px;
  background: currentColor;
  border-radius: 0 2px 2px 0;
  opacity: 0.5;
}
.ui-file-icon.pdf   { color: oklch(58% 0.20 25); }   /* red-orange  */
.ui-file-icon.doc   { color: oklch(55% 0.16 240); }  /* blue        */
.ui-file-icon.xls   { color: oklch(55% 0.16 155); }  /* green       */
.ui-file-icon.ppt   { color: oklch(60% 0.18 60); }   /* orange      */
.ui-file-icon.img   { color: oklch(58% 0.16 305); }  /* violet      */
.ui-file-icon.txt   { color: oklch(50% 0.012 250); } /* slate       */
.ui-file-icon.zip   { color: oklch(55% 0.10 90); }   /* amber-brown */
.ui-file-icon.other { color: var(--text-muted); }


/* =============================================================================
   3. Activity feed (notifications list)
   ============================================================================= */

.ui-activity-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.ui-activity-item {
  display: flex;
  gap: 14px;
  align-items: flex-start;
  padding: 14px 16px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 3px solid var(--border);
  border-radius: var(--r-md);
  transition: border-left-color 0.18s, background 0.18s;
}
.ui-activity-item:hover { border-left-color: var(--accent); }
.ui-activity-item.unread {
  background: color-mix(in oklch, var(--accent-soft) 30%, var(--surface));
  border-left-color: var(--accent);
}
.ui-activity-icon {
  width: 36px;
  height: 36px;
  border-radius: var(--r-md);
  display: grid;
  place-items: center;
  flex-shrink: 0;
  font-size: 16px;
  background: var(--accent-soft);
  color: var(--accent);
}
.ui-activity-icon.success { background: var(--success-soft); color: var(--success); }
.ui-activity-icon.info    { background: var(--info-soft);    color: var(--info); }
.ui-activity-icon.warning { background: var(--warning-soft); color: var(--warning-strong); }
.ui-activity-icon.danger  { background: var(--danger-soft);  color: var(--danger); }
.ui-activity-body { flex: 1; min-width: 0; }
.ui-activity-title { font-weight: 600; font-size: 14px; }
.ui-activity-text {
  font-size: 13px;
  color: var(--text-secondary);
  margin-top: 2px;
  line-height: 1.5;
  -webkit-user-select: text;
  user-select: text;
}
.ui-activity-meta { font-size: 11.5px; color: var(--text-muted); margin-top: 6px; display: flex; gap: 8px; flex-wrap: wrap; }
.ui-activity-actions { display: flex; gap: 4px; align-items: center; flex-shrink: 0; }


/* =============================================================================
   4. Profile hero (employees + users detail)
   ============================================================================= */

.ui-profile-hero {
  display: flex;
  gap: 24px;
  align-items: center;
  padding: var(--pad-lg);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  margin-bottom: var(--gap-lg);
  flex-wrap: wrap;
}
.ui-profile-avatar {
  width: 84px;
  height: 84px;
  border-radius: 50%;
  background: linear-gradient(135deg, var(--accent), var(--accent-hover));
  color: #fff;
  display: grid;
  place-items: center;
  font-size: 30px;
  font-weight: 700;
  flex-shrink: 0;
  box-shadow: 0 8px 20px color-mix(in oklch, var(--accent) 25%, transparent);
}
.ui-profile-meta { flex: 1; min-width: 240px; }
.ui-profile-meta .name {
  font-size: 22px;
  font-weight: 700;
  letter-spacing: -0.01em;
  margin: 0 0 4px;
}
.ui-profile-meta .sub {
  color: var(--text-muted);
  font-size: 13.5px;
  margin: 0 0 10px;
}
.ui-profile-meta .chips { display: flex; gap: 6px; flex-wrap: wrap; }


/* =============================================================================
   5. Dropzone (documents upload)
   ============================================================================= */

.ui-upload-form {
  display: flex;
  flex-direction: column;
  gap: var(--gap);
}

.ui-dropzone {
  position: relative;
  display: grid;
  place-items: center;
  min-height: 180px;
  padding: 28px 20px;
  border: 2px dashed var(--border-strong);
  border-radius: var(--r-lg);
  background: var(--surface-2);
  color: var(--text-muted);
  text-align: center;
  cursor: pointer;
  transition: border-color 0.15s, background 0.15s, color 0.15s;
}
.ui-dropzone:hover,
.ui-dropzone.dragover {
  border-color: var(--accent);
  background: var(--accent-soft);
  color: var(--accent);
}
.ui-dropzone [data-lucide] {
  width: 32px;
  height: 32px;
  margin-bottom: 10px;
}
.ui-dropzone .title {
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
  margin-bottom: 4px;
}
.ui-dropzone .hint {
  font-size: 11.5px;
  color: var(--text-muted);
}
.ui-dropzone.dragover .title,
.ui-dropzone.dragover .hint { color: var(--accent); }
.ui-dropzone input[type="file"] {
  position: absolute;
  inset: 0;
  opacity: 0;
  cursor: pointer;
}
.ui-dropzone-filename {
  margin-top: 10px;
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  padding: 4px 10px;
  border-radius: var(--r-sm);
  display: inline-flex;
  align-items: center;
  gap: 6px;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}


/* =============================================================================
   6. Visibility radio cards (documents upload)
   ============================================================================= */

.ui-vis-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 10px;
}
.ui-vis-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 14px;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface);
  cursor: pointer;
  transition: border-color 0.15s, background 0.15s, transform 0.08s;
}
.ui-vis-card:hover { border-color: var(--border-strong); }
.ui-vis-card input[type="radio"] {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}
.ui-vis-card .ico {
  width: 32px;
  height: 32px;
  border-radius: var(--r-sm);
  background: var(--surface-2);
  color: var(--text-secondary);
  display: grid;
  place-items: center;
  font-size: 16px;
  margin-bottom: 4px;
  transition: background 0.15s, color 0.15s;
}
.ui-vis-card .title {
  font-size: 13.5px;
  font-weight: 600;
  color: var(--text);
}
.ui-vis-card .sub {
  font-size: 11.5px;
  color: var(--text-muted);
  line-height: 1.4;
}
.ui-vis-card:has(input:checked) {
  border-color: var(--accent);
  background: color-mix(in oklch, var(--accent-soft) 40%, var(--surface));
}
.ui-vis-card:has(input:checked) .ico {
  background: var(--accent-soft);
  color: var(--accent);
}
.ui-vis-card:has(input:checked) .title { color: var(--accent); }

/* Conditional field (department / branch) toggled by JS via data-shown */
.ui-vis-conditional[data-shown="false"] { display: none; }



/* =============================================================================
   7. Document detail layout
   ============================================================================= */

.ui-doc-grid {
  display: grid;
  grid-template-columns: 1fr 320px;
  gap: var(--gap-lg);
}
@media (max-width: 1024px) {
  .ui-doc-grid { grid-template-columns: 1fr; }
}

.ui-download-card {
  background: linear-gradient(135deg, var(--surface) 0%, var(--surface-2) 100%);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: var(--pad-lg);
  display: flex;
  flex-direction: column;
  gap: 14px;
  align-items: center;
  text-align: center;
}
.ui-download-card .summary {
  font-size: 12px;
  color: var(--text-muted);
  font-family: var(--font-mono);
  word-break: break-all;
  line-height: 1.5;
  -webkit-user-select: text;
  user-select: text;
}
.ui-download-card .checksum {
  display: block;
  font-size: 10.5px;
  color: var(--text-disabled);
  margin-top: 4px;
  -webkit-user-select: text;
  user-select: text;
}


/* =============================================================================
   8. Clock hero + state badge (attendance)
   ============================================================================= */

.ui-clock-hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  padding: 40px 24px 32px;
  background:
    radial-gradient(600px 280px at 50% 0%, var(--accent-soft), transparent 70%),
    var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  text-align: center;
  position: relative;
  overflow: hidden;
}
.ui-clock-time {
  font-family: var(--font-mono);
  font-size: 56px;
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.04em;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.ui-clock-date {
  font-size: 14px;
  color: var(--text-secondary);
  font-weight: 500;
}
.ui-clock-hero .ui-clock-time {
  font-size: 72px;
  font-weight: 800;
  letter-spacing: -0.04em;
  line-height: 1;
}
.ui-clock-hero .ui-clock-date {
  font-size: 14px;
  color: var(--text-muted);
  font-weight: 500;
  letter-spacing: 0.02em;
}
.ui-clock-hero .open-since {
  font-size: 12.5px;
  color: var(--text-muted);
  margin-top: 4px;
  font-variant-numeric: tabular-nums;
}
.ui-clock-hero form {
  display: flex;
  flex-direction: column;
  gap: 10px;
  width: 100%;
  max-width: 360px;
}
.ui-clock-hero .ui-input { text-align: center; }

@media (max-width: 640px) {
  .ui-clock-hero { padding: 28px 16px 24px; }
  .ui-clock-hero .ui-clock-time { font-size: 52px; }
}

/* Status pill — idle vs on-the-clock (pulsing dot when live) */
.ui-clock-state-badge {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 14px;
  border-radius: 999px;
  font-size: 12.5px;
  font-weight: 600;
  background: var(--surface-2);
  color: var(--text-secondary);
  border: 1px solid var(--border);
  letter-spacing: 0.01em;
}
.ui-clock-state-badge .live-dot {
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--text-disabled);
  flex-shrink: 0;
}
.ui-clock-state-badge.on {
  background: var(--success-soft);
  color: var(--success);
  border-color: transparent;
}
.ui-clock-state-badge.on .live-dot {
  background: var(--success);
  box-shadow: 0 0 0 0 currentColor;
  animation: ui-pulse 1.8s ease-out infinite;
}
@keyframes ui-pulse {
  0%   { box-shadow: 0 0 0 0 color-mix(in oklch, var(--success) 60%, transparent); }
  70%  { box-shadow: 0 0 0 8px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}
@media (prefers-reduced-motion: reduce) {
  .ui-clock-state-badge.on .live-dot { animation: none; }
}


/* =============================================================================
   9. Weekly summary strip (attendance)
   ============================================================================= */

.ui-week-strip {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 8px;
}
.ui-week-tile {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: 10px 6px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  position: relative;
  min-height: 86px;
  transition: border-color 0.15s, background 0.15s;
}
.ui-week-tile .dow {
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
  font-weight: 600;
}
.ui-week-tile .day {
  font-size: 16px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}
.ui-week-tile .value {
  font-size: 11.5px;
  color: var(--text-secondary);
  font-family: var(--font-mono);
  margin-top: auto;
  font-variant-numeric: tabular-nums;
}
.ui-week-tile.worked .value { color: var(--success); font-weight: 600; }
.ui-week-tile.open { border-color: var(--accent); }
.ui-week-tile.open .value { color: var(--accent); font-weight: 700; }
.ui-week-tile.leave { background: var(--info-soft); border-color: transparent; }
.ui-week-tile.leave .value { color: var(--info); }
.ui-week-tile.off { opacity: 0.55; }
.ui-week-tile.off .value { color: var(--text-disabled); }
.ui-week-tile.today { border-top: 3px solid var(--accent); padding-top: 8px; }


/* =============================================================================
   10. Leave type cards + status (attendance)
   ============================================================================= */

.ui-leave-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 10px;
}
.ui-leave-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 14px;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface);
  cursor: pointer;
  transition: border-color 0.15s, background 0.15s;
}
.ui-leave-card:hover { border-color: var(--border-strong); }
.ui-leave-card input[type="radio"] {
  position: absolute; opacity: 0; pointer-events: none;
}
.ui-leave-card .head {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
}
.ui-leave-card .title { font-size: 13.5px; font-weight: 600; color: var(--text); }
.ui-leave-card .quota {
  font-size: 10.5px;
  font-family: var(--font-mono);
  color: var(--text-muted);
  background: var(--surface-2);
  padding: 1px 7px;
  border-radius: 999px;
}
.ui-leave-card .desc { font-size: 11.5px; color: var(--text-muted); line-height: 1.4; }
.ui-leave-card:has(input:checked) {
  border-color: var(--accent);
  background: color-mix(in oklch, var(--accent-soft) 40%, var(--surface));
}
.ui-leave-card:has(input:checked) .title { color: var(--accent); }
.ui-leave-card:has(input:checked) .quota { background: var(--accent-soft); color: var(--accent); }

/* Status badge variants — applies on top of .ui-badge base */
.ui-leave-status.pending   { background: var(--warning-soft); color: var(--warning-strong); border-color: transparent; }
.ui-leave-status.approved  { background: var(--success-soft); color: var(--success); border-color: transparent; }
.ui-leave-status.rejected  { background: var(--danger-soft);  color: var(--danger);  border-color: transparent; }
.ui-leave-status.cancelled { background: var(--surface-2);    color: var(--text-muted); border-color: var(--border); }




/* =============================================================================
   9. Photo lightbox (goods_release detail audit review)
   -----------------------------------------------------------------------------
   Native `<dialog>` opens via `.showModal()` so the browser owns the
   backdrop + focus trap + ESC handling. Inside, a full-bleed stage shows
   the image with transform-based zoom (1×–6×) and click-drag pan. Prev /
   next nav traverses thumbs sharing a `data-lightbox-group`. Driven by
   `static/js/ui-lightbox.js`; this file is layout + colour only.

   Layout:
       ┌──────────────────────────────────────────────────┐
       │ <          [        IMAGE STAGE        ]       > │
       │                                                  │
       │            [ caption / counter ]                 │
       │  [- 100% + ⤢]                            [×]    │
       └──────────────────────────────────────────────────┘
   ============================================================================= */

.ui-lightbox {
  /* `<dialog>.showModal()` puts us in the top layer and centres us,
     but the native `width: 100vw` calc on iOS Safari leaves the body
     scrollbar gutter uncovered — the gutter reveals whatever surface
     happens to sit at that horizontal slice of the page (a card edge,
     the sidebar, anything with `--surface-2`). That surface bleeds
     through as a coloured vertical strip on the right.

     Fix: force absolute coverage with `position: fixed; inset: 0`
     instead of viewport-relative sizing. Combined with the
     html-scroll-lock below, the dialog now paints across the entire
     visible viewport — no scrollbar gutter, no strip. */
  position: fixed;
  inset: 0;
  width: auto;
  height: auto;
  max-width: none;
  max-height: none;
  margin: 0;
  padding: 0;
  border: 0;
  background: #0a1018;
  color: #ffffff;
  overflow: hidden;
}
.ui-lightbox::backdrop {
  background: #0a1018;
}
/* Hide the body scrollbar while the lightbox is open so there's no
   gutter to reveal a colour strip. Uses `:has()` so no JS scroll-lock
   is needed — gracefully degrades on browsers without :has support
   (the dialog background is still opaque, just may leak a thin gutter
   on those old browsers). */
html:has(.ui-lightbox[open]) {
  overflow: hidden;
}
.ui-lightbox[open] {
  display: grid;
  grid-template-columns: auto 1fr auto;
  grid-template-rows: 1fr auto;
  grid-template-areas:
    "prev  stage   next"
    "tools tools   close";
}

.ui-lightbox-stage {
  grid-area: stage;
  position: relative;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Disable native pinch/scroll on the stage so pointer gestures route
     to our zoom/pan code without competing with the browser's. */
  touch-action: none;
}
.ui-lightbox-img {
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
  transform-origin: center center;
  transition: transform 80ms ease-out;
  user-select: none;
  -webkit-user-drag: none;
  cursor: zoom-in;
}
/* Tinted variant — used for transparent-PNG signature images so the
   black ink is readable against the dark backdrop. White card with a
   touch of padding; subtle shadow so it floats over the backdrop
   instead of bleeding into nothing. JS adds the class when the
   trigger has `data-lightbox-tint="white"`. */
.ui-lightbox-img.is-tinted-white {
  background: #fff;
  padding: var(--space-3);
  border-radius: var(--r);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
}

.ui-lightbox-caption {
  position: absolute;
  left: 50%;
  bottom: 0;
  transform: translateX(-50%);
  padding: var(--space-2) var(--space-4);
  background: rgba(0, 0, 0, 0.55);
  color: #ffffff;
  border-radius: var(--r-pill);
  font-size: var(--font-sm);
  letter-spacing: var(--tracking-normal);
  pointer-events: none;
  max-width: 80%;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Edge nav buttons — grid-placed columns so they sit flush against the
   stage edges without overlap-fighting the image. */
.ui-lightbox-nav {
  width: 56px;
  height: 100%;
  background: transparent;
  border: 0;
  color: #ffffff;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background-color var(--dur-fast) var(--ease-out);
}
.ui-lightbox-nav.prev { grid-area: prev; }
.ui-lightbox-nav.next { grid-area: next; }
/* No `:hover` background on the nav buttons. The previous version
   tinted them `rgba(255,255,255,0.08)` on hover, but in practice this
   was perceived as the button "changing color" after a click — both
   desktop (cursor parks on the just-clicked button → sticky hover
   tint) and touch (`:hover` matches stay applied until the next tap
   somewhere else). The chevron icon + edge placement already make
   these buttons obvious affordances; an extra hover tint adds noise
   without information. Keyboard focus is preserved via the
   `:focus-visible` outline below. */
.ui-lightbox-nav:focus-visible {
  outline: 2px solid #ffffff;
  outline-offset: -4px;
}
.ui-lightbox-nav [data-lucide],
.ui-lightbox-nav svg.lucide { width: 28px; height: 28px; }
.ui-lightbox-nav[hidden] { display: none; }

/* Footer-row controls — zoom in/out + reset + close. Grouped so they
   sit in one tools row at the bottom of the grid. */
.ui-lightbox-tools {
  grid-area: tools;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4)
          calc(var(--space-3) + env(safe-area-inset-bottom));
}
.ui-lightbox-tools button {
  width: 40px; height: 40px;
  border-radius: var(--r-pill);
  background: rgba(255, 255, 255, 0.10);
  border: 1px solid rgba(255, 255, 255, 0.18);
  color: #ffffff;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition:
    background-color var(--dur-fast) var(--ease-out),
    transform        var(--dur-fast) var(--ease-out);
}
/* Same `:hover` desktop-gate as the nav buttons — see the comment
   above `.ui-lightbox-nav:hover`. Without this, a touch-device user
   tapping "zoom in" leaves the button visually highlighted until
   they tap another tool, which reads as "this control is selected /
   modal-active" instead of the intended hover affordance. */
@media (hover: hover) {
  .ui-lightbox-tools button:hover { background: rgba(255, 255, 255, 0.18); }
}
.ui-lightbox-tools button:active { transform: scale(0.95); }
.ui-lightbox-tools button [data-lucide],
.ui-lightbox-tools button svg.lucide { width: 18px; height: 18px; }
.ui-lightbox-zoom-label {
  font-variant-numeric: tabular-nums;
  font-size: var(--font-sm);
  font-weight: 500;
  min-width: 56px;
  text-align: center;
  color: rgba(255, 255, 255, 0.85);
}

.ui-lightbox-close {
  grid-area: close;
  align-self: end;
  justify-self: end;
  margin: var(--space-3) var(--space-4);
  width: 40px; height: 40px;
  border-radius: var(--r-pill);
  background: rgba(255, 255, 255, 0.10);
  border: 1px solid rgba(255, 255, 255, 0.18);
  color: #ffffff;
  display: none;  /* Close lives in tools-row on phone; keep this hidden by default */
}

@media (max-width: 540px) {
  /* Side nav columns shrink — keep tap target but reclaim image width. */
  .ui-lightbox-nav { width: 44px; }
  .ui-lightbox-tools { gap: var(--space-1); flex-wrap: wrap; }
}




/* =============================================================================
   12. Custom date range row (queue + history filter forms)
   -----------------------------------------------------------------------------
   Second filter row inside `.ui-filter-form` (queue) or as a standalone
   `<form>` (history). Lives below the preset chip row and lets the user
   pick an arbitrary from/to range. Visually quieter than the chips so
   the preset path stays the primary affordance — custom is for the
   "I need last week's Tuesday" cases.
   ============================================================================= */

.ui-daterange-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  width: 100%;
  margin-top: var(--space-2);
  padding-top: var(--space-2);
  border-top: 1px dashed var(--divider);
}
.ui-daterange-label {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--font-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
  color: var(--text-muted);
  margin-right: var(--space-1);
}
.ui-daterange-label [data-lucide],
.ui-daterange-label svg.lucide { width: 14px; height: 14px; }
.ui-daterange-input {
  /* Compact variant of `.ui-input` — `min-width` keeps the field big
     enough on mobile while not pushing the row across multiple lines on
     desktop. The browser's native date picker handles the calendar UI. */
  height: var(--h-input-sm);
  font-size: var(--font-sm);
  padding: 0 var(--space-2);
  min-width: 140px;
  max-width: 160px;
}
.ui-daterange-sep {
  font-size: var(--font-sm);
  color: var(--text-muted);
  font-weight: 500;
}

@media (max-width: 540px) {
  .ui-daterange-input { min-width: 0; flex: 1; max-width: none; }
  .ui-daterange-sep { display: none; }
}




/* =============================================================================
   14. Offline indicator (P3.1 PWA offline mode)
   -----------------------------------------------------------------------------
   Slim banner pinned to the very top of the viewport when the browser
   reports `navigator.onLine === false`. Sits above all other chrome
   (above sidebar, topbar, modal backdrop) so the user can't miss it.
   Pure warning amber so it doesn't compete with state badges for
   semantic meaning — this is "connectivity" not "error".

   `html[data-offline]` is also flipped by the inline script so other
   components can react (e.g. disable submit buttons globally).
   ============================================================================= */

.ui-offline-banner {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: var(--z-toast);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-4)
          calc(var(--space-2) + env(safe-area-inset-top, 0px));
  background: var(--warning-strong);
  color: #ffffff;
  font-size: var(--font-sm);
  font-weight: 500;
  letter-spacing: var(--tracking-normal);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
  animation: ui-offline-slide-in 220ms var(--ease-out);
}
.ui-offline-banner[hidden] { display: none; }
.ui-offline-banner [data-lucide],
.ui-offline-banner svg.lucide { width: 16px; height: 16px; flex-shrink: 0; }
.ui-offline-banner-text {
  max-width: 60ch;
  text-align: center;
}

@keyframes ui-offline-slide-in {
  from { transform: translateY(-100%); }
  to   { transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .ui-offline-banner { animation: none; }
}

/* Push the app shell down so the fixed banner doesn't cover the topbar.
   Only applied when the html element has `data-offline` — otherwise the
   shell uses its normal layout. */
html[data-offline] body { padding-top: 40px; }
@media (max-width: 540px) {
  html[data-offline] body { padding-top: 48px; }
}

/* Pending-submit pill (P3.1.1 offline queue) — floats at top-right of
   the viewport so it's visible no matter which page the user is on.
   Two states:
     - `pending` (default): brand-accent, count + "รอส่ง"
     - `stuck`: amber warning, set when any item has retried ≥4 times.
   Click → drain attempt (handler hooked in base.html). */
.ui-offline-pill {
  position: fixed;
  top: calc(var(--space-2) + env(safe-area-inset-top, 0px));
  right: calc(var(--space-3) + env(safe-area-inset-right, 0px));
  z-index: calc(var(--z-toast) - 1);
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-1) var(--space-3);
  background: var(--accent);
  color: var(--accent-on);
  border: 0;
  border-radius: var(--r-pill);
  font-size: var(--font-xs);
  font-weight: 600;
  letter-spacing: var(--tracking-normal);
  cursor: pointer;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
  transition: background-color var(--dur-fast) var(--ease-out);
}
.ui-offline-pill[hidden] { display: none; }
.ui-offline-pill:hover { background: var(--accent-hover); }
.ui-offline-pill [data-lucide],
.ui-offline-pill svg.lucide { width: 14px; height: 14px; }
.ui-offline-pill-count {
  font-variant-numeric: tabular-nums;
  font-weight: 700;
}
.ui-offline-pill[data-state="stuck"] {
  background: var(--warning-strong);
  color: #ffffff;
}
/* Bump down when the offline banner is active so they don't stack. */
html[data-offline] .ui-offline-pill {
  top: calc(48px + env(safe-area-inset-top, 0px));
}

/* Inline toast — emitted by the offline queue on enqueue / drain. Same
   bottom slot as a typical toast system, no JS dependency. */
.ui-offline-toast {
  position: fixed;
  left: 50%;
  bottom: calc(var(--space-4) + env(safe-area-inset-bottom, 0px));
  transform: translate(-50%, 0);
  z-index: var(--z-toast);
  max-width: 92vw;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--r-md);
  background: var(--text);
  color: var(--bg-elev);
  font-size: var(--font-sm);
  line-height: 1.5;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
  animation: ui-offline-toast-in 240ms var(--ease-out);
}
.ui-offline-toast[data-kind="info"]    { background: var(--info);    color: #ffffff; }
.ui-offline-toast[data-kind="success"] { background: var(--success); color: #ffffff; }
.ui-offline-toast[data-kind="danger"]  { background: var(--danger);  color: #ffffff; }
.ui-offline-toast.is-leaving { opacity: 0; transition: opacity 220ms var(--ease-out); }

@keyframes ui-offline-toast-in {
  from { transform: translate(-50%, 16px); opacity: 0; }
  to   { transform: translate(-50%, 0);    opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .ui-offline-toast { animation: none; }
}


/* =============================================================================
   15. Mobile filter collapse + collapsible banner (release/history)
   -----------------------------------------------------------------------------
   The history page's filter card has 4+ chip rows (audience, scope,
   preset range, custom range, POS state). On a phone that's 50-70% of
   the viewport before any data shows up — fatal first impression.
   Solution: collapse the filter card behind a single "ตัวกรอง" toggle
   on mobile, with the currently applied filters rendered as read-only
   chips in a slim summary bar. Desktop is unchanged.

   Components:
     - `.ui-mobile-filter-bar`     summary bar + toggle button (mobile only)
     - `.ui-mobile-collapsible`    the filter card itself, collapses on mobile
     - `.ui-mobile-collapse-label` label text dropped when icon-only on phone
     - `.ui-banner-collapsible`    `<details>` banner — chevron rotates open

   These are deliberately scoped to the history page's structure; a
   more generic collapsible component can grow out of this once a
   second surface needs it.
   ============================================================================= */

/* The summary bar shows ONLY at mobile breakpoint — desktop keeps the
   full filter card visible always. */
.ui-mobile-filter-bar { display: none; }

@media (max-width: 720px) {
  /* Single-button toggle row — right-aligned so it sits at the user's
     thumb side. No summary chips: the open filter panel already shows
     the active state, and duplicating it above wastes vertical space. */
  .ui-mobile-filter-bar {
    display: flex;
    justify-content: flex-end;
    margin-bottom: var(--space-2);
    padding: var(--space-1) 0;
  }

  /* Filter card collapses by default; the toggle expands. The
     `is-open` class is set by JS — no transition on the property since
     collapsed→expanded with arbitrary intrinsic height is awkward; an
     instant reveal keeps the interaction snappy. */
  .ui-mobile-collapsible {
    display: none;
  }
  .ui-mobile-collapsible.is-open {
    display: block;
    animation: ui-mobile-collapse-in 180ms var(--ease-out);
  }

  /* Restore wrap on chip rows. Earlier the global rule turned chip
     rows into a horizontal scroll strip — fine for the queue page
     where there are only date chips, but on history the range row
     gains a custom-range chip and the state row ALL FOUR state chips
     that overflow uncomfortably mid-glyph. Inside the filter panel
     we let them wrap to a second line so nothing gets clipped. */
  .ui-mobile-collapsible .ui-chip-row {
    flex-wrap: wrap;
    overflow-x: visible;
    padding: 0;
    margin: 0;
  }
  .ui-mobile-collapsible .ui-chip-row > * { scroll-snap-align: none; }

  /* Drop labels on icon+label buttons in the page header so the page
     title gets full width. `.ui-mobile-collapse-label` is the opt-in
     wrapper — buttons that want to keep text don't add it. */
  .ui-page-header-actions .ui-mobile-collapse-label { display: none; }
  .ui-page-header-actions .ui-btn { padding: 0 var(--space-3); }
}

@keyframes ui-mobile-collapse-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .ui-mobile-collapsible.is-open { animation: none; }
}

/* Collapsible banner — `<details>` element styled to match `.ui-banner`.
   On mobile the chevron is visible and tapping the summary toggles the
   detail body; on desktop the whole banner stays open with no chevron. */
.ui-banner-collapsible {
  /* Match ui-banner padding/colors via the parent class chain; just
     override the `<details>` defaults so it lays out cleanly. */
  display: block;
}
.ui-banner-collapsible > summary {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  cursor: pointer;
  list-style: none;
  /* Reset the disclosure triangle in WebKit / Firefox. */
}
.ui-banner-collapsible > summary::-webkit-details-marker { display: none; }
.ui-banner-collapsible > summary::marker { content: ''; }
.ui-banner-collapsible > summary > strong { flex: 1; min-width: 0; }
.ui-banner-collapsible .ui-banner-chevron {
  width: 18px; height: 18px;
  flex-shrink: 0;
  transition: transform var(--dur-fast) var(--ease-out);
  opacity: 0.7;
}
.ui-banner-collapsible[open] .ui-banner-chevron { transform: rotate(180deg); }
.ui-banner-collapsible .ui-banner-body {
  margin-top: var(--space-2);
  padding-left: calc(18px + var(--space-3));
}
@media (min-width: 721px) {
  /* Desktop: hide chevron + force the body visible regardless of the
     `<details>` open state. Click on summary still fires the native
     toggle but is visually a no-op because the body keeps `display:
     block`. Trade-off accepted because forcing `open` via JS would
     race with the page render. */
  .ui-banner-collapsible .ui-banner-chevron { display: none; }
  .ui-banner-collapsible > summary { cursor: default; }
  .ui-banner-collapsible .ui-banner-body { display: block !important; }
}


/* =============================================================================
   Confirm dialog — promise-based replacement for window.confirm()
   -----------------------------------------------------------------------------
   Wired by `static/js/ui-confirm.js`. Single instance per page, mounted
   lazily on first `window.uiConfirm({...})` call. Rendered as a native
   `<dialog>` so the focus trap + ESC + backdrop + top-layer stacking
   are browser-provided.

   Visual direction: editorial minimal. Centred small card, icon circle
   in tinted bg signals kind (danger vs default), title in IBM Plex
   Sans Thai 600, supporting line in muted text, two equal-width
   action buttons. Brand red (IDHOME --accent / --danger same hue) does
   the destruction signalling — no extra coloured stripes, no shadows
   pretending to be paper. The dialog should feel like a calm,
   decisive checkpoint, not a system alert.
   ============================================================================= */

.ui-confirm {
  /* Reset native <dialog> chrome — margins, borders, padding all from
     the browser default look 2008. We rebuild from the IDHOME tokens. */
  margin: auto;
  border: 0;
  padding: 0;
  background: var(--surface);
  color: var(--text);
  width: min(92vw, 380px);
  max-width: 380px;
  border-radius: var(--r-lg);
  box-shadow:
    0 20px 50px -8px rgba(0, 0, 0, 0.30),
    0 8px 20px -4px rgba(0, 0, 0, 0.15);
  overflow: hidden;  /* keep border-radius corners crisp */
}

.ui-confirm::backdrop {
  background: rgba(8, 12, 20, 0.55);
  backdrop-filter: blur(4px) saturate(1.1);
  -webkit-backdrop-filter: blur(4px) saturate(1.1);
}

/* Body — generous vertical breathing room so the title isn't sandwiched
   between the icon and the message. Centred text reads as "ask",
   left-align would read as "list". */
.ui-confirm-body {
  padding: var(--space-6) var(--space-5) var(--space-5);
  text-align: center;
}

/* Icon circle. Default kind uses `--accent-soft` (brand red soft) for a
   "neutral confirm" — same hue family, just lower-stakes visual weight.
   Danger kind uses `--danger-soft` which in the light theme renders
   nearly identical to accent-soft (IDHOME brand red doubles as danger),
   so the icon glyph itself (alert-triangle vs help-circle) carries the
   semantic distinction.

   For uiAlert with `kind="success"` the circle bumps to 72px and lives
   on a `--success-soft` wash — this is the peak moment of the flow
   ("release saved"), so the icon should read as a deliberate marker,
   not the same checkpoint icon as a confirm question. The checkmark
   itself is a custom SVG below that draws itself in. */
.ui-confirm-icon {
  display: inline-flex;
  width: 56px;
  height: 56px;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-pill);
  background: var(--accent-soft);
  color: var(--accent);
  margin: 0 0 var(--space-4);
  position: relative;
}
.ui-confirm[data-kind="danger"] .ui-confirm-icon {
  background: var(--danger-soft);
  color: var(--danger);
}
.ui-confirm[data-kind="warning"] .ui-confirm-icon {
  background: var(--warning-soft);
  color: var(--warning-strong);
}
.ui-confirm[data-kind="info"] .ui-confirm-icon {
  background: var(--info-soft);
  color: var(--info);
}
.ui-confirm[data-kind="success"] .ui-confirm-icon {
  background: var(--success-soft);
  color: var(--success);
  width: 72px;
  height: 72px;
  margin-bottom: var(--space-5);
}
.ui-confirm-icon [data-lucide],
.ui-confirm-icon svg.lucide { width: 26px; height: 26px; }
.ui-confirm[data-kind="success"] .ui-confirm-icon [data-lucide],
.ui-confirm[data-kind="success"] .ui-confirm-icon svg.lucide {
  width: 36px;
  height: 36px;
}

/* Custom checkmark SVG — the path's total length is ~22 units; setting
   stroke-dasharray + stroke-dashoffset to that length renders it
   invisible at rest, then animates the offset to 0 so the tick draws
   itself stroke-by-stroke. 200ms delay gives the modal entrance + icon
   circle fade-in a moment first so the eye finds the icon BEFORE the
   check appears, making the draw register as a discrete event. */
.ui-confirm-icon-check {
  width: 36px;
  height: 36px;
  display: block;
}
.ui-confirm-icon-check path {
  stroke-dasharray: 22;
  stroke-dashoffset: 22;
  animation: ui-confirm-check-draw 480ms cubic-bezier(0.65, 0, 0.45, 1) 200ms forwards;
}
@keyframes ui-confirm-check-draw {
  to { stroke-dashoffset: 0; }
}

/* Subtle radial pulse behind the success icon — one ripple, fades out
   while expanding. Reads as quiet "ta-da" without confetti. */
.ui-confirm[data-kind="success"] .ui-confirm-icon::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: var(--success);
  opacity: 0;
  animation: ui-confirm-pulse 900ms ease-out 100ms 1 forwards;
}
@keyframes ui-confirm-pulse {
  0%   { opacity: 0.25; transform: scale(1); }
  100% { opacity: 0;    transform: scale(1.5); }
}

.ui-confirm-title {
  font-size: var(--font-lg);
  font-weight: 600;
  color: var(--text);
  letter-spacing: var(--tracking-tight);
  margin: 0 0 var(--space-2);
  line-height: 1.3;
  text-wrap: balance;
}
/* Alert mode title — boost to font-xl. This is the peak moment of the
   workflow (an announcement), so the heading should read louder than
   the question-mark "title" of a uiConfirm checkpoint. */
.ui-confirm[data-mode="alert"] .ui-confirm-title {
  font-size: var(--font-xl);
}

.ui-confirm-message {
  font-size: var(--font-base);
  color: var(--text-secondary);
  line-height: 1.55;
  margin: 0 auto var(--space-5);
  max-width: 30ch;
  text-wrap: pretty;
  /* Preserve `\n` line breaks coming from multi-line Django messages
     (e.g. save-progress popup: "ติ๊กเพิ่ม X / เอาออก Y" stacked on
     separate lines). textContent assigns the literal `\n` chars; this
     CSS rule turns them into rendered breaks while still letting
     single-line messages wrap naturally. */
  white-space: pre-line;
}
.ui-confirm-message[hidden] { display: none; }

/* DOCNO / reference chip — mono typeface, surface-2 wash, subtle
   border. Reads like a receipt stamp / shift-handover reference tag.
   Spaced as its own row above the actions so the eye lands on the
   reference before the action button. */
.ui-confirm-docno {
  display: inline-flex;
  align-items: center;
  font-family: var(--font-mono);
  font-size: var(--font-sm);
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  color: var(--text);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r);
  padding: 6px var(--space-3);
  margin: 0 auto var(--space-5);
  white-space: nowrap;
}
.ui-confirm-docno[hidden] { display: none; }

/* Actions — two equal-width buttons by default. Always side-by-side
   (never stack) because a 2-choice decision belongs on one row;
   stacking implies priority, but cancel and confirm should look
   equally available.

   The `--single` modifier (alert mode) collapses to one full-width
   button — there's no second choice to balance against, so the
   button takes the whole row and a slightly larger height for a
   thumb-friendly tap zone on phones. */
.ui-confirm-actions {
  display: flex;
  gap: var(--space-3);
}
.ui-confirm-actions > .ui-btn {
  flex: 1 1 0;
  min-width: 0;
  justify-content: center;
}
.ui-confirm-actions--single {
  flex-direction: column;
}
.ui-confirm-actions--single > .ui-btn {
  width: 100%;
  flex: 0 0 auto;
}

/* Mobile bump — slightly bigger tap target on phones. The default
   `.ui-btn` height token already grows on mobile via `tokens.css`, so
   this just ensures the dialog buttons inherit that bump even though
   they're inside an overlay (no surrounding `.ui-content` margin). */
@media (max-width: 720px) {
  .ui-confirm-body {
    padding: var(--space-5) var(--space-4) var(--space-4);
  }
  .ui-confirm-actions > .ui-btn {
    font-size: var(--font-md);
  }
}

/* Entrance animation — a soft rise + fade. Subtle enough to read as
   "appearing", not "popping". Pair with backdrop fade so the page
   behind dims at the same rhythm. `prefers-reduced-motion` opts out
   entirely for vestibular users. */
@keyframes ui-confirm-rise {
  from { opacity: 0; transform: translateY(12px) scale(0.96); }
  to   { opacity: 1; transform: translateY(0)    scale(1);    }
}
@keyframes ui-confirm-backdrop-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
.ui-confirm[open] {
  animation: ui-confirm-rise 220ms cubic-bezier(0.16, 1, 0.3, 1);
}
.ui-confirm[open]::backdrop {
  animation: ui-confirm-backdrop-in 220ms ease-out;
}
@media (prefers-reduced-motion: reduce) {
  .ui-confirm[open],
  .ui-confirm[open]::backdrop { animation: none; }
}


/* =============================================================================
   HTTP error pages — 403 (forbidden) + 404 (not found)
   -----------------------------------------------------------------------------
   Used by `templates/403.html` + `templates/404.html`, wired via
   `handler403` / `handler404` in `config/urls.py`. Single layout shared
   by both — the icon + heading change, but the card chrome + action row
   are identical so the operator's eye learns "this is a bounce page"
   regardless of which one fired.

   Layout: centered card inside `.ui-content`. The user is authenticated
   in both cases (unauth on a 403 is intercepted and redirected to login
   by the handler), so the sidebar + topbar of `base.html` render around
   this content — operator can still navigate to another module.
   ============================================================================= */

/* ============================================================
   404 — "Caveman" full-page art (pure CSS).
   ------------------------------------------------------------
   Adapted 1:1 from the user-supplied Caveman 404 Page snippet:
   two mirrored caveman figures swinging clubs on either side of
   a giant faded "404". Every generic class name from the original
   (`.head`, `.eye`, `.leg`, `.shape`, `.circle`, `.container`,
   `.text`, `.arm-right`, `.club`, `.foot`, `.fingers`, `.nose`,
   `.mouth`) is scoped under `.ui-404-caveman-page` so nothing
   bleeds into the rest of the app's stylesheet.
   ============================================================ */
.ui-404-caveman-page {
  /* Full-bleed orange canvas. `{% block layout %}` in 404.html
     bypasses the app shell, and `position: fixed` covers the
     viewport so body chrome (offline banner etc.) sits beneath. */
  position: fixed;
  inset: 0;
  background-color: #FF7F2E;
  font-family: "Concert One", cursive;
  overflow: hidden;
  /* The whole page is an <a> link — keep cursor + focus ring clean. */
  display: block;
  text-decoration: none;
  color: inherit;
  z-index: 1;
}
.ui-404-caveman-page:focus-visible {
  outline: 3px solid rgba(255, 255, 255, 0.6);
  outline-offset: -6px;
}

.ui-404-caveman-page .text {
  left: 50%;
  position: absolute;
  top: 40%;
  transform: translate(-50%, -50%);
  color: rgba(19, 31, 36, 0.183);
  font-size: 30em;
  text-align: center;
  line-height: 1;
  margin: 0;
}
.ui-404-caveman-page .text p { margin: 0; }

.ui-404-caveman-page .container {
  left: 50%;
  position: absolute;
  top: 50%;
  transform: translate(-50%, -50%);
  height: 300px;
  width: 500px;
}
.ui-404-caveman-page .container:after {
  position: absolute;
  content: "";
  background-color: rgba(19, 36, 44, 0.1);
  border-radius: 12px;
  bottom: 40px;
  height: 12px;
  left: 80px;
  width: 350px;
  z-index: -1;
  animation: ui-404-caveman-shadow 1.2s infinite cubic-bezier(0.55, 0.01, 0.16, 1.34);
  animation-delay: 0.1s;
}

/* Caveman — base figure. */
.ui-404-caveman-page .caveman {
  height: 300px;
  position: absolute;
  width: 250px;
}
.ui-404-caveman-page .caveman:nth-child(1) { right: 20px; }
.ui-404-caveman-page .caveman:nth-child(2) {
  left: 20px;
  transform: rotateY(180deg);
}

/* Head — black silhouette with two little ear/tuft details. */
.ui-404-caveman-page .head {
  position: absolute;
  background-color: #13242C;
  border-radius: 50px;
  height: 140px;
  left: 60px;
  top: 25px;
  width: 65px;
}
.ui-404-caveman-page .head:after,
.ui-404-caveman-page .head:before {
  content: "";
  position: absolute;
  background-color: #13242C;
  border-radius: 10px;
  height: 20px;
  width: 7px;
}
.ui-404-caveman-page .head:after  { left: 35px; top: -8px; transform: rotate(20deg);  }
.ui-404-caveman-page .head:before { left: 30px; top: -8px; transform: rotate(-20deg); }

/* Eye band — skin-coloured strip with two pupils + nose. */
.ui-404-caveman-page .head .eye {
  left: 45%;
  position: absolute;
  transform: translateX(-50%);
  background-color: #EAB08C;
  border-radius: 50px;
  height: 16px;
  top: 40px;
  width: 48px;
}
.ui-404-caveman-page .head .eye:after,
.ui-404-caveman-page .head .eye:before {
  content: "";
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  background-color: #13242C;
  border-radius: 50%;
  height: 5px;
  width: 5px;
}
.ui-404-caveman-page .head .eye:after  { left: 5px; }
.ui-404-caveman-page .head .eye:before { right: 9px; }

.ui-404-caveman-page .head .eye .nose {
  left: 45%;
  position: absolute;
  top: 12px;
  transform: translate(-50%, -50%);
  background-color: #D9766C;
  border-left: 8px solid rgba(19, 36, 44, 0.1);
  border-radius: 10px;
  box-sizing: border-box;
  height: 35px;
  width: 15px;
}

/* "Shape" — round body torso with two darker circles painted on
   to suggest fur/spots. Container colours swap between the two
   cavemen so they read as different "people". */
.ui-404-caveman-page .shape {
  left: 50%;
  position: absolute;
  transform: translateX(-50%);
  border-radius: 50%;
  height: 140px;
  overflow: hidden;
  top: 70px;
  width: 140px;
}
.ui-404-caveman-page .shape .circle {
  position: absolute;
  border-radius: 50%;
  height: 60px;
  width: 60px;
}
.ui-404-caveman-page .shape .circle:after,
.ui-404-caveman-page .shape .circle:before {
  content: "";
  position: absolute;
  border-radius: 50%;
  height: 20px;
  width: 20px;
}
.ui-404-caveman-page .shape .circle:after  { left: 50px; top: 10px; }
.ui-404-caveman-page .shape .circle:before { left: 60px; top: 45px; }
.ui-404-caveman-page .shape .circle:nth-child(1) { left: -12px; top: 80px; }
.ui-404-caveman-page .shape .circle:nth-child(2) {
  right: 10px;
  top: 0;
  transform: rotate(90deg);
}
.ui-404-caveman-page .shape .circle:nth-child(2):after  { left: 65px; top: 10px; }
.ui-404-caveman-page .shape .circle:nth-child(2):before { display: none; }

.ui-404-caveman-page .caveman:nth-child(1) .shape         { background-color: #D13433; }
.ui-404-caveman-page .caveman:nth-child(1) .shape .circle { background-color: #932422; }
.ui-404-caveman-page .caveman:nth-child(1) .shape .circle:after,
.ui-404-caveman-page .caveman:nth-child(1) .shape .circle:before { background-color: #932422; }
.ui-404-caveman-page .caveman:nth-child(2) .shape         { background-color: #932422; }
.ui-404-caveman-page .caveman:nth-child(2) .shape .circle { background-color: #D13433; }
.ui-404-caveman-page .caveman:nth-child(2) .shape .circle:after,
.ui-404-caveman-page .caveman:nth-child(2) .shape .circle:before { background-color: #D13433; }

/* Right arm + club — the swinging weapon. */
.ui-404-caveman-page .arm-right {
  position: absolute;
  background-color: #EAB08C;
  border-left: 8px solid rgba(19, 36, 44, 0.1);
  border-radius: 50px;
  box-sizing: border-box;
  height: 180px;
  left: 135px;
  top: 80px;
  transform-origin: 30px 30px;
  width: 60px;
  z-index: 1;
}
.ui-404-caveman-page .arm-right .club {
  position: absolute;
  border-bottom: 110px solid #601513;
  border-left: 10px solid transparent;
  border-right: 10px solid transparent;
  height: 0;
  left: -60px;
  top: 120px;
  transform: rotate(70deg);
  width: 20px;
}
.ui-404-caveman-page .arm-right .club:after,
.ui-404-caveman-page .arm-right .club:before {
  position: absolute;
  content: "";
  background-color: #601513;
  border-radius: 50%;
  left: 0;
}
.ui-404-caveman-page .arm-right .club:after  { height: 20px; width: 20px; top: -10px; }
.ui-404-caveman-page .arm-right .club:before { height: 40px; width: 40px; left: -10px; top: 90px; }

/* Legs + feet — two-tone striped pants made out of two `.leg` divs. */
.ui-404-caveman-page .leg {
  position: absolute;
  border-radius: 10px;
  height: 55px;
  top: 200px;
  width: 10px;
}
.ui-404-caveman-page .leg:after {
  position: absolute;
  content: "";
  border-radius: 50%;
  height: 10px;
  left: -5px;
  top: 15px;
  width: 10px;
}
.ui-404-caveman-page .leg .foot {
  position: absolute;
  border-radius: 25px 25px 0 0;
  height: 25px;
  left: -38px;
  top: 30px;
  width: 50px;
}
.ui-404-caveman-page .leg .foot:after,
.ui-404-caveman-page .leg .foot:before,
.ui-404-caveman-page .leg .foot .fingers,
.ui-404-caveman-page .leg .foot .fingers:after {
  position: absolute;
  background-color: #EAB08C;
  border-radius: 50%;
  bottom: 0;
  height: 15px;
  transform-origin: bottom;
  width: 15px;
}
.ui-404-caveman-page .leg .foot:after  { left: -6px; content: ""; }
.ui-404-caveman-page .leg .foot:before { left: 8px; transform: scale(0.6); content: ""; }
.ui-404-caveman-page .leg .foot .fingers { left: 15px; transform: scale(0.6); }
.ui-404-caveman-page .leg .foot .fingers:after { left: 11px; content: ""; }

.ui-404-caveman-page .leg:nth-child(1)              { background-color: #B2524D; left: 95px; }
.ui-404-caveman-page .leg:nth-child(1):after        { background-color: #B2524D; }
.ui-404-caveman-page .leg:nth-child(1) .foot        { background-color: #B2524D; }
.ui-404-caveman-page .leg:nth-child(1) .foot:after  { background-color: #B2524D; }
.ui-404-caveman-page .leg:nth-child(1) .foot:before { display: none; }
.ui-404-caveman-page .leg:nth-child(2)              { background-color: #D9766C; left: 115px; }
.ui-404-caveman-page .leg:nth-child(2):after        { background-color: #D9766C; }
.ui-404-caveman-page .leg:nth-child(2) .foot        { background-color: #D9766C; }

/* Animation timings — staggered between the two cavemen so the
   swing reads as alternating dance steps. */
.ui-404-caveman-page .caveman:nth-child(1) .arm-right {
  animation: ui-404-caveman-arm 1.2s infinite cubic-bezier(0.55, 0.01, 0.16, 1.34);
}
.ui-404-caveman-page .caveman:nth-child(2) .arm-right {
  animation: ui-404-caveman-arm 1.2s infinite cubic-bezier(0.55, 0.01, 0.16, 1.34);
  animation-delay: 0.6s;
}
@keyframes ui-404-caveman-arm {
  0%   { transform: rotate(0); }
  100% { transform: rotate(-360deg); }
}

.ui-404-caveman-page .caveman:nth-child(2) .head {
  animation: ui-404-caveman-head 1.2s infinite cubic-bezier(0.55, 0.01, 0.16, 1.34);
}
.ui-404-caveman-page .caveman:nth-child(1) .head {
  animation: ui-404-caveman-head 1.2s infinite cubic-bezier(0.55, 0.01, 0.16, 1.34);
  animation-delay: 0.6s;
}
@keyframes ui-404-caveman-head {
  0%   { top: 25px; }
  42%  { top: 25px; }
  45%  { top: 50px; }
  100% { top: 25px; }
}

.ui-404-caveman-page .caveman:nth-child(2) .eye:after,
.ui-404-caveman-page .caveman:nth-child(2) .eye:before {
  animation: ui-404-caveman-eye 1.2s infinite cubic-bezier(0.55, 0.01, 0.16, 1.34);
}
.ui-404-caveman-page .caveman:nth-child(1) .eye:after,
.ui-404-caveman-page .caveman:nth-child(1) .eye:before {
  animation: ui-404-caveman-eye 1.2s infinite cubic-bezier(0.55, 0.01, 0.16, 1.34);
  animation-delay: 0.6s;
}
@keyframes ui-404-caveman-eye {
  0%   { height: 5px; }
  42%  { height: 5px; }
  45%  { height: 1px; }
  100% { height: 5px; }
}

@keyframes ui-404-caveman-shadow {
  0%   { width: 350px; left: 80px; }
  25%  { width: 450px; left: 80px; }
  50%  { width: 350px; left: 80px; }
  75%  { width: 450px; left: 0; }
  100% { width: 350px; left: 80px; }
}

/* Mobile — the source art is sized in fixed px (container 500×300,
   each caveman 250×300, "404" text 30em). On narrow viewports the
   clubs swing past the screen edge and the giant 404 wraps. Scale
   the whole composition down rather than re-laying-out individual
   pieces, so proportions + animation timing stay identical. */
@media (max-width: 600px) {
  .ui-404-caveman-page .container {
    transform: translate(-50%, -50%) scale(0.6);
  }
  .ui-404-caveman-page .text { font-size: 16em; }
}
@media (max-width: 380px) {
  .ui-404-caveman-page .container {
    transform: translate(-50%, -50%) scale(0.48);
  }
  .ui-404-caveman-page .text { font-size: 12em; }
}

@media (prefers-reduced-motion: reduce) {
  .ui-404-caveman-page *,
  .ui-404-caveman-page *::before,
  .ui-404-caveman-page *::after {
    animation: none !important;
  }
}

.ui-403 {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 60vh;
  padding: var(--space-4);
}
.ui-403-card {
  width: 100%;
  max-width: 520px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: clamp(var(--space-5), 4vw, var(--space-8));
  box-shadow: var(--shadow-sm);
  text-align: center;
}
.ui-403-icon {
  display: inline-flex;
  width: 64px;
  height: 64px;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-pill);
  background: var(--danger-soft);
  color: var(--danger);
  margin: 0 auto var(--space-4);
}
.ui-403-icon [data-lucide],
.ui-403-icon svg.lucide { width: 32px; height: 32px; }
.ui-403-title {
  font-size: var(--font-xl);
  font-weight: 600;
  color: var(--text);
  margin: 0 0 var(--space-2);
  letter-spacing: var(--tracking-tight);
}
.ui-403-lead {
  font-size: var(--font-base);
  color: var(--text-secondary);
  margin: 0 auto var(--space-4);
  line-height: 1.6;
  max-width: 42ch;
  text-wrap: pretty;
}
.ui-403-lead strong {
  font-weight: 600;
  color: var(--text);
}

/* Missing-perm code chip — small monospace badge so the user can read
   it off to a supervisor or paste into a support ticket. */
.ui-403-perm {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r);
  padding: var(--space-2) var(--space-3);
  margin: 0 auto var(--space-4);
  font-size: var(--font-sm);
}
.ui-403-perm-label {
  color: var(--text-muted);
  font-size: var(--font-xs);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
  font-weight: 600;
}
.ui-403-perm code {
  color: var(--text);
  font-weight: 600;
}

.ui-403-hint {
  font-size: var(--font-sm);
  color: var(--text-muted);
  margin: 0 0 var(--space-5);
  line-height: 1.55;
}

.ui-403-actions {
  display: flex;
  gap: var(--space-3);
  justify-content: center;
  flex-wrap: wrap;
}
.ui-403-actions .ui-btn { min-width: 180px; }
.ui-403-logout-form { margin: 0; }
@media (max-width: 540px) {
  .ui-403-actions { flex-direction: column; align-items: stretch; }
  .ui-403-actions .ui-btn { width: 100%; min-width: 0; }
  .ui-403-logout-form .ui-btn { width: 100%; }
}

/* "Page that was attempted" line — small + subdued, useful when an
   operator needs to relay the failing URL to a supervisor approving
   their access request. */
.ui-403-meta {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  font-size: var(--font-xs);
  color: var(--text-muted);
  margin: var(--space-5) 0 0;
  flex-wrap: wrap;
}
.ui-403-meta [data-lucide] { width: 14px; height: 14px; }
.ui-403-meta code {
  background: var(--surface-2);
  padding: 2px 6px;
  border-radius: var(--r-xs);
  word-break: break-all;
}

/* ============================================================
   Auth — glass card login  (login.html only)
   ------------------------------------------------------------
   Glassmorphism is otherwise banned by this design system (see
   DESIGN.md "Don't") — this surface earns the exception because:
     1. login is the brand's first-impression moment, not a data
        screen, so identity-richness > Restrained;
     2. the effect is paired with a real, layered backdrop worth
        blurring — not glass-on-flat-color decoration;
     3. it ships on exactly one route. Glass never bleeds into
        product chrome (logged-in app stays Restrained).
   Always renders dark regardless of `data-theme` because this is
   an identity surface; the form inputs are inverted accordingly.
   ============================================================ */

.ui-auth-glass {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  overflow: hidden;
  /* Page background follows theme (light = white, dark = deep cool
     neutral). The pastel orb mist on top tints either tone. */
  background: var(--bg);
  color: var(--text);
  isolation: isolate;
}

/* ---- Backdrop: soft pastel mist on the white page. Heavy blur +
       low opacity makes the orbs read as diffuse colored fog rather
       than glowing balls — the "ขุ่นๆ เหมือน Glass มัวๆ" look. The
       cards on top are now solid white (no transparency), so this
       backdrop only paints the page around them. ---- */
.ui-auth-glass__backdrop {
  position: absolute;
  inset: -10%;
  z-index: 0;
  pointer-events: none;
}
.ui-auth-glass__backdrop .orb {
  position: absolute;
  border-radius: 50%;
  filter: blur(140px);
  will-change: transform;
  opacity: 0.28;
}
.ui-auth-glass__backdrop .orb-1 {
  width: 720px; height: 720px;
  top: -10%; left: -8%;
  background: radial-gradient(circle, oklch(70% 0.18 25) 0%, transparent 65%);
  animation: ui-auth-orb-1 56s ease-in-out infinite;
}
.ui-auth-glass__backdrop .orb-2 {
  width: 620px; height: 620px;
  bottom: -12%; right: -10%;
  background: radial-gradient(circle, oklch(72% 0.16 15) 0%, transparent 65%);
  opacity: 0.24;
  animation: ui-auth-orb-2 64s ease-in-out infinite -8s;
}
.ui-auth-glass__backdrop .orb-3 {
  width: 540px; height: 540px;
  top: 35%; left: 45%;
  background: radial-gradient(circle, oklch(74% 0.12 268) 0%, transparent 65%);
  opacity: 0.20;
  animation: ui-auth-orb-3 72s ease-in-out infinite -20s;
}
.ui-auth-glass__backdrop .grid {
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(to right, rgba(255, 255, 255, 0.025) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(255, 255, 255, 0.025) 1px, transparent 1px);
  background-size: 80px 80px;
  -webkit-mask-image: radial-gradient(ellipse 70% 70% at center, #000, transparent);
          mask-image: radial-gradient(ellipse 70% 70% at center, #000, transparent);
}

@keyframes ui-auth-orb-1 {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
  50%      { transform: translate3d(60px, 80px, 0) scale(1.12); }
}
@keyframes ui-auth-orb-2 {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
  50%      { transform: translate3d(-80px, -60px, 0) scale(1.08); }
}
@keyframes ui-auth-orb-3 {
  0%, 100% { transform: translate3d(-50%, -50%, 0) scale(1); }
  50%      { transform: translate3d(-40%, -55%, 0) scale(1.15); }
}

/* ---- Stage: 2-col grid sitting above the backdrop. ---- */
.ui-auth-glass__stage {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(380px, 460px);
  gap: var(--space-16);
  width: 100%;
  max-width: 1200px;
  padding: var(--space-12) var(--space-8);
  align-items: center;
}

/* ---- Hero (left): solid white card sized to its content. ---- */
.ui-auth-glass__hero {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
  color: var(--text);
  padding: var(--space-12);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 28px;
  box-shadow: 0 30px 80px -30px rgba(15, 23, 42, 0.18);
  /* Content-height, not stretched to grid row height. */
  align-self: center;
  animation: ui-auth-fade-up 600ms var(--ease-out) both;
}
.ui-auth-glass__hero .brand {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}
.ui-auth-glass__hero .brand .mark {
  width: 40px; height: 40px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--accent);
  color: var(--accent-on);
  border-radius: var(--r);
  font-weight: 700;
  font-size: 14px;
  letter-spacing: 0.04em;
  box-shadow: 0 8px 24px -6px oklch(56% 0.21 25 / 0.55);
}
.ui-auth-glass__hero .brand .label {
  font-size: var(--font-md);
  font-weight: 600;
  letter-spacing: -0.005em;
  color: var(--text);
}
.ui-auth-glass__hero .brand .sub {
  font-size: 11px;
  color: var(--text-muted);
  letter-spacing: 0.02em;
}
.ui-auth-glass__hero .body {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
.ui-auth-glass__hero .chip {
  align-self: flex-start;
  background: var(--accent-soft);
  border: 1px solid color-mix(in oklab, var(--accent) 25%, transparent);
  color: var(--accent);
  padding: 4px 12px;
  border-radius: var(--r-pill);
  font-size: var(--font-xs);
  font-weight: 500;
  letter-spacing: var(--tracking-wide);
}
.ui-auth-glass__hero h1 {
  font-size: clamp(32px, 4vw, 44px);
  font-weight: 600;
  line-height: 1.12;
  letter-spacing: -0.022em;
  color: var(--text);
  margin: 0;
  text-wrap: balance;
}
.ui-auth-glass__hero h1 .accent { color: var(--accent); }
.ui-auth-glass__hero .lead {
  font-size: var(--font-md);
  color: var(--text-secondary);
  max-width: 36ch;
  margin: 0;
}
.ui-auth-glass__hero .stats {
  display: flex;
  align-items: center;
  gap: var(--space-6);
  margin-top: var(--space-4);
}
.ui-auth-glass__hero .stats .stat-num {
  font-size: 28px;
  font-weight: 600;
  color: var(--text);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.012em;
}
.ui-auth-glass__hero .stats .stat-lbl {
  font-size: 11.5px;
  color: var(--text-muted);
}
.ui-auth-glass__hero .stats .divider {
  width: 1px; height: 32px;
  background: var(--border);
}
.ui-auth-glass__hero .copyright {
  font-size: 11px;
  color: var(--text-muted);
  margin-top: auto;
}

/* ---- Form card (right): solid white card. ---- */
.ui-auth-glass__panel {
  position: relative;
  padding: var(--space-10);
  border-radius: 28px;
  background: var(--surface);
  border: 1px solid var(--border);
  box-shadow: 0 30px 80px -30px rgba(15, 23, 42, 0.18);
  color: var(--text);
  animation: ui-auth-fade-up 700ms var(--ease-out) 80ms both;
}

/* ---- Form text + controls inside the white card.
   Light-theme overrides: dark text, light input fills, default
   border colors. ---- */
.ui-auth-glass__panel .ui-auth-form {
  max-width: none;
  gap: var(--space-4);
}
.ui-auth-glass__panel .ui-auth-form h1 {
  font-size: 24px;
  font-weight: 600;
  color: var(--text);
  letter-spacing: -0.014em;
  text-align: center;
}
.ui-auth-glass__panel .ui-auth-form .lead {
  color: var(--text-muted);
  text-align: center;
}
.ui-auth-glass__panel .ui-field-label { color: var(--text); }
.ui-auth-glass__panel a {
  color: var(--accent);
  text-decoration: none;
}
.ui-auth-glass__panel a:hover { text-decoration: underline; }

.ui-auth-glass__panel .ui-input { color: var(--text); }
.ui-auth-glass__panel .ui-input::placeholder { color: var(--text-muted); }
.ui-auth-glass__panel .ui-input-with-icon > [data-lucide],
.ui-auth-glass__panel .ui-input-with-icon > svg.lucide {
  color: var(--text-muted);
}
.ui-auth-glass__panel .ui-input-reveal { color: var(--text-muted); }
.ui-auth-glass__panel .ui-input-reveal:hover {
  color: var(--text);
  background: var(--surface-2);
}
.ui-auth-glass__panel .ui-check { color: var(--text); }

/* Pill-rounded inputs + submit button — match the heavily curved
   card corners (28px) with a friendly fully-rounded form. Scoped
   to the login card so the rest of the app keeps its default
   .ui-input / .ui-btn radius. */
.ui-auth-glass__panel .ui-input,
.ui-auth-glass__panel .ui-btn {
  border-radius: var(--r-pill);
}

/* Custom checkbox with rounded corners — native checkboxes can't
   round their corners reliably across engines, so we replace the
   look entirely. Stays only on the login card. */
.ui-auth-glass__panel .ui-check input[type="checkbox"] {
  appearance: none;
  -webkit-appearance: none;
  width: 18px;
  height: 18px;
  margin: 0;
  border: 1.5px solid var(--border-strong);
  border-radius: 6px;
  background: var(--surface);
  cursor: pointer;
  position: relative;
  flex-shrink: 0;
  transition: background 150ms var(--ease-out), border-color 150ms var(--ease-out);
}
.ui-auth-glass__panel .ui-check input[type="checkbox"]:hover {
  border-color: var(--accent);
}
.ui-auth-glass__panel .ui-check input[type="checkbox"]:checked {
  background: var(--accent);
  border-color: var(--accent);
}
.ui-auth-glass__panel .ui-check input[type="checkbox"]:checked::after {
  content: '';
  position: absolute;
  top: 1px;
  left: 4.5px;
  width: 5px;
  height: 9px;
  border: solid #fff;
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}
.ui-auth-glass__panel .ui-check input[type="checkbox"]:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px var(--accent-ring);
}

/* Entrance — gentle single-fire on initial render. */
@keyframes ui-auth-fade-up {
  from { opacity: 0; transform: translate3d(0, 16px, 0); }
  to   { opacity: 1; transform: translate3d(0, 0, 0); }
}

/* Mobile ≤880px — hero collapses, glass card fills the viewport. */
@media (max-width: 880px) {
  .ui-auth-glass__stage {
    grid-template-columns: 1fr;
    padding: var(--space-6);
    gap: var(--space-6);
    max-width: 480px;
  }
  .ui-auth-glass__hero { display: none; }
  .ui-auth-glass__panel { padding: var(--space-8) var(--space-6); }
}

/* ---- Theme toggle (sun/moon) — top-right of the login page. ---- */
.ui-auth-glass__theme-toggle {
  position: fixed;
  top: var(--space-6);
  right: var(--space-6);
  z-index: 5;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--text);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 4px 16px -4px rgba(15, 23, 42, 0.10);
  transition: background 150ms var(--ease-out), border-color 150ms var(--ease-out), transform 150ms var(--ease-out);
}
.ui-auth-glass__theme-toggle:hover {
  border-color: var(--border-strong);
  transform: scale(1.05);
}
.ui-auth-glass__theme-toggle:active { transform: scale(0.97); }
.ui-auth-glass__theme-toggle:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px var(--accent-ring);
}
.ui-auth-glass__theme-toggle [data-lucide],
.ui-auth-glass__theme-toggle svg.lucide {
  width: 20px;
  height: 20px;
}
/* Show the matching icon for the active theme — sun in dark mode,
   moon in light mode (the icon represents what you'd switch TO). */
.ui-auth-glass__theme-toggle .icon-sun { display: none; }
html[data-theme="dark"] .ui-auth-glass__theme-toggle .icon-sun { display: inline-flex; }
html[data-theme="dark"] .ui-auth-glass__theme-toggle .icon-moon { display: none; }

/* Reduced-motion — kill orb drift + entrance reveals. The static
   glass effect itself remains (it's a visual, not motion). */
@media (prefers-reduced-motion: reduce) {
  .ui-auth-glass__backdrop .orb,
  .ui-auth-glass__hero,
  .ui-auth-glass__panel {
    animation: none !important;
  }
}

/* ============================================================
   Auth — panda mascot clinging to the glass card top
   ------------------------------------------------------------
   A friendly delight moment for the login screen: a panda
   "perches" on the top edge of the glass card. Its eyes look
   up when the user is in the username field (peeking at what
   they're typing) and its paws cover its eyes when they enter
   the password field (not peeking — privacy). Implementation
   adapted from the Darin Senneff / panda-login pattern,
   restyled for our dark glass backdrop.

   Pure CSS shapes + transforms. JS only toggles a single
   `data-state` attribute on the wrapper; all positions and
   transitions live here.
   ============================================================ */

.ui-auth-panda {
  /* The em-cascade lets the whole panda scale by changing one
     font-size; bumping this token makes the bear bigger or smaller. */
  font-size: 15px;
  position: absolute;
  inset: 0;            /* span the panel — head peeks above, feet below */
  pointer-events: none;
  z-index: 2;
}

/* Head + feet groups — each is its own positioned cluster so we can
   anchor one to the top of the card and the other to the bottom
   without a tangle of per-piece negative offsets. */
.ui-auth-panda__head {
  position: absolute;
  top: 0;
  left: 50%;
  width: 8.4em;
  height: 11em;
  /* Lift almost entirely above the card top; ~1.6em overlaps so the
     bear visually clings to the upper edge instead of floating. */
  transform: translate(-50%, calc(-100% + 1.6em));
  filter: drop-shadow(0 6px 18px rgba(0, 0, 0, 0.45));
}
.ui-auth-panda__feet {
  position: absolute;
  bottom: 0;
  left: 50%;
  width: 8.4em;
  height: 3.12em;
  /* Drop the paws below the card bottom; ~0.8em stays inside the
     card so the paws look like they're gripping over the edge. */
  transform: translate(-50%, calc(100% - 0.8em));
  filter: drop-shadow(0 6px 18px rgba(0, 0, 0, 0.45));
}

/* Ears — tucked behind the face. Positions reference the head box. */
.ui-auth-panda .ear {
  position: absolute;
  top: 1.4em;
  width: 2.81em;
  height: 2.5em;
  background: #2a1f3a;
  border: 0.18em solid #1a1024;
  border-radius: 2.5em 2.5em 0 0;
}
.ui-auth-panda .ear-l { left: 0.5em;  transform: rotate(-38deg); }
.ui-auth-panda .ear-r { right: 0.5em; transform: rotate(38deg); }

/* Face — white capsule. */
.ui-auth-panda .face {
  position: absolute;
  top: 2em;
  left: 0;
  width: 8.4em;
  height: 7.5em;
  background: #ffffff;
  border: 0.18em solid #1a1024;
  border-radius: 7.5em 7.5em 5.62em 5.62em;
}
.ui-auth-panda .blush {
  position: absolute;
  top: 4em;
  width: 1.37em;
  height: 1em;
  background: #ff8bb1;
  border-radius: 50%;
}
.ui-auth-panda .blush-l { left: 1em;  transform: rotate(25deg); }
.ui-auth-panda .blush-r { right: 1em; transform: rotate(-25deg); }

/* Eyes (almonds). The eyeball inside moves to direct gaze. */
.ui-auth-panda .eye {
  position: absolute;
  top: 2.18em;
  width: 2em;
  height: 2.18em;
  background: #2a1f3a;
  border-radius: 2em;
}
.ui-auth-panda .eye-l { left: 1.37em;  transform: rotate(-20deg); }
.ui-auth-panda .eye-r { right: 1.37em; transform: rotate(20deg); }
.ui-auth-panda .eyeball {
  position: absolute;
  top: 0.6em;
  left: 0.6em;
  width: 0.6em;
  height: 0.6em;
  background: #ffffff;
  border-radius: 50%;
  transition: top 320ms var(--ease-out), left 320ms var(--ease-out);
}
.ui-auth-panda .eye-l .eyeball { transform: rotate(20deg); }
.ui-auth-panda .eye-r .eyeball { transform: rotate(-20deg); }

.ui-auth-panda .nose {
  position: absolute;
  top: 4.37em;
  left: 0;
  right: 0;
  margin: auto;
  width: 1em;
  height: 1em;
  background: #2a1f3a;
  border-radius: 1.2em 0 0 0.25em;
  transform: rotate(45deg);
}
.ui-auth-panda .nose::before {
  content: "";
  position: absolute;
  top: 0.75em;
  left: 1em;
  width: 0.1em;
  height: 0.6em;
  background: #2a1f3a;
  transform: rotate(-45deg);
}
.ui-auth-panda .mouth,
.ui-auth-panda .mouth::before {
  position: absolute;
  width: 0.93em;
  height: 0.75em;
  background: transparent;
  border-radius: 50%;
  box-shadow: 0 0.18em #2a1f3a;
}
.ui-auth-panda .mouth { top: 5.31em; left: 3.12em; }
.ui-auth-panda .mouth::before { content: ""; left: 0.87em; }

/* Hands — at rest, two small mittens grip the top edge of the card,
   flanking the face. On password focus they extend + rotate inward
   to form an X over the eyes (see [data-state="covered"] below).
   The face is the upper visual landmark, so the hand mittens sit
   below it at the card top line. */
.ui-auth-panda .hand {
  position: absolute;
  /* Sit at the bottom of the head wrapper, which lines up with the
     top edge of the glass card (the head wrapper overlaps the card
     top by 1.6em — see __head transform). */
  top: 8.3em;
  width: 3em;
  height: 3.45em;
  background: #2a1f3a;
  border: 0.18em solid #1a1024;
  /* Demo-faithful shape: squarish top (where the arm "attaches"),
     rounded fingertip bottom. The hand hangs naturally; when covered
     state rotates it ~150°, that rounded tip swings up to land on
     the opposite-side eye. */
  border-radius: 0.5em 0.5em 2em 2em;
  /* Stay above the face when they cross over in covered state. */
  z-index: 3;
  transition:
    transform 500ms var(--ease-out),
    top 500ms var(--ease-out),
    left 500ms var(--ease-out),
    right 500ms var(--ease-out),
    width 500ms var(--ease-out),
    height 500ms var(--ease-out);
}
/* Pulled well outside the face circle so they read as separate
   "mittens" gripping the card. Position computed so the hand CENTER
   aligns vertically with the paw CENTER below (paw width 3.12em,
   hand width 2.4em → 0.36em offset between left values keeps centers
   on the same x).

   At idle, each hand is rotated 90° so the rounded "fingers" side
   faces INWARD toward the face (gripping over the card edge from
   outside) while the flat "back-of-hand" faces outward. Without
   this rotation the mitten reads as a standing capsule, not a hand
   reaching over the card. Covered state overrides the rotation to
   swing each hand up over its same-side eye. */
.ui-auth-panda .hand-l { left: -3.3em;  transform: rotate(0deg); }
.ui-auth-panda .hand-r { right: -3.3em; transform: rotate(0deg); }

/* Paws (feet) — sit at the bottom of the card, gripping the lower
   edge. Adapted 1:1 from the reference: dark rounded square + large
   white palm pad + 3 white "toe" dots arranged in a triangle. */
.ui-auth-panda .paw {
  position: absolute;
  top: 0;
  width: 3.12em;
  height: 3.12em;
  background: #2a1f3a;
  border: 0.18em solid #1a1024;
  border-radius: 2.5em 2.5em 1.2em 1.2em;
}
/* Aligned vertically with the hand mittens above — paw is 3.12em
   wide, hand is 2.4em wide, so paw left is offset by 0.36em (half
   the width diff) to keep the centers on the same x. */
.ui-auth-panda .paw-l { left: -3.36em;  }
.ui-auth-panda .paw-r { right: -3.36em; }
/* Large white palm pad — fills the bottom 2/3 of the paw. */
.ui-auth-panda .paw::before {
  content: "";
  position: absolute;
  top: 1.12em;
  left: 0.55em;
  width: 1.75em;
  height: 1.37em;
  background: #ffffff;
  border-radius: 1.56em 1.56em 0.6em 0.6em;
}
/* 3 toe dots above the palm — one base dot + box-shadow trick
   produces a left + right twin without extra DOM. */
.ui-auth-panda .paw::after {
  content: "";
  position: absolute;
  top: 0.31em;
  left: 1.12em;
  width: 0.5em;
  height: 0.5em;
  background: #ffffff;
  border-radius: 50%;
  box-shadow:
     0.87em 0.37em #ffffff,
    -0.87em 0.37em #ffffff;
}

/* --- State machine ---
   Default: data-state absent OR "idle"
   "peek": username focused → eyeballs roll up
   "covered": password focused → hands swing up to cover the eyes
*/

/* Peek — eyes glance DOWN to read the field below the mascot. The
   form sits beneath the panda; the eyeballs converge slightly inward
   (mirroring the demo's "right:0.75em" on the right eye) to look at
   the centered input rather than off into space. */
.ui-auth-panda[data-state="peek"] .eye-l .eyeball {
  top: 1.2em;
  left: 0.75em;
}
.ui-auth-panda[data-state="peek"] .eye-r .eyeball {
  top: 1.2em;
  left: 0.45em;
}

/* True Λ shape (no crossing): each arm length is set EXACTLY to
   the eye-to-chin distance, and the rotation is tuned so the flat
   "wrist" ends of BOTH arms touch at the chin center (4.2em, 9.5em)
   without passing through.

   Geometry (per hand):
     - rounded end (palm) lands on same-side eye
       eye-l at (2.37em, 5.27em), eye-r mirrored
     - flat end (wrist) lands at chin center (4.2em, 9.5em) — both
       hands meet here, no overlap past
     - arm length √(1.83² + 4.23²) ≈ 4.6em
     - tilt from vertical atan(1.83/4.23) ≈ 23.4°
     - rotation = 180° (flip rounded to top) − 23.4° tilt = 156.6°,
       rounded to 157°
     - element center at midpoint (3.3em, 7.385em); from width 3em +
       height 4.6em → top 5em, left 1.8em (hand-l mirrored to right
       for hand-r)
   Result: hand-l covers eye-l, hand-r covers eye-r, wrists touch
   at chin center without crossing — clean Λ silhouette. */
.ui-auth-panda[data-state="covered"] .hand-l {
  width: 3em;
  height: 6.9em;
  top: 3em;
  left: -0.2em;
  transform: rotate(-143deg);
}
.ui-auth-panda[data-state="covered"] .hand-r {
  width: 3em;
  height: 6.9em;
  top: 3em;
  right: -0.2em;
  transform: rotate(143deg);
}

/* Mobile: keep the panda but scale it down so it still fits over the
   form card without crowding the inputs. The em-cascade means one
   font-size change resizes the whole mascot. */
@media (max-width: 480px) {
  .ui-auth-panda { font-size: 11px; }
}

/* Reduced-motion — keep the panda visible but make state changes
   instant. The mascot itself is delightful; the *motion* is what we
   suppress for users who asked for less of it. */
@media (prefers-reduced-motion: reduce) {
  .ui-auth-panda .eyeball,
  .ui-auth-panda .hand {
    transition: none !important;
  }
}
