/* LinX v3 dashboard styles — captured from lxbx.duckdns.org/v3/admin/ and scoped under .v3-body
   for the marketing-site demo route. Do not edit by hand; regenerate from the source app. */



/* ============ css/base.css (22538 chars) ============ */
/* app_main_v3 base.css — design tokens ported from the Lovable "linx-v3-ui-ux-update"
   mockup's src/styles.css (design reference only — colors/radius/shadows/typography
   scale copied as visual tokens, no code ported). .v3- namespace only, fully
   independent from legacy CSS (commun/, loxbox/, ...). */

.v3-body {
  /* Colors — light theme, ported from Lovable's :root (OKLCH source of truth) */
  --v3-color-bg:            oklch(0.985 0.005 250);
  --v3-color-fg:             oklch(0.2 0.03 260);
  --v3-color-card:          oklch(1 0 0);
  --v3-color-card-fg:        oklch(0.2 0.03 260);

  --v3-color-primary:        oklch(0.72 0.17 55);
  --v3-color-primary-fg:      oklch(0.99 0.01 90);
  --v3-color-secondary:      oklch(0.94 0.01 260);
  --v3-color-secondary-fg:    oklch(0.25 0.03 260);
  --v3-color-muted:          oklch(0.95 0.01 260);
  --v3-color-muted-fg:        oklch(0.5 0.02 260);
  --v3-color-accent:         oklch(0.62 0.15 210);
  --v3-color-accent-fg:       oklch(0.99 0.01 90);

  --v3-color-destructive:     oklch(0.6 0.22 25);
  --v3-color-destructive-fg:   oklch(0.99 0.01 60);
  --v3-color-success:        oklch(0.65 0.17 155);
  --v3-color-success-fg:      oklch(0.99 0.01 160);
  --v3-color-warning:        oklch(0.78 0.17 85);
  --v3-color-warning-fg:      oklch(0.2 0.03 60);
  /* User request: "la couleur des colis préparés est le bleu (couleur fixe) même dans les donuts
     de toutes les dashboards ça doit être pareil" — the Admin/SC/Vendor dashboards' "Préparé"
     pipeline tile + donut segment used var(--v3-color-primary), which is NOT fixed: it's exactly
     what the topbar's "Couleur principale" picker overrides per data-accent (topbar-menus.css)
     — so "Préparé" changed color along with the tenant's branding pick instead of staying a
     stable status color, and (depending on that pick) could even collide with --v3-color-accent,
     already reused by several OTHER distinct statuses (Collecté/En Route/...) in those same
     donuts. A dedicated, never-overridden token, same blue OKLCH values as the picker's own
     "blue" preset (topbar-menus.css's data-accent="blue") so it reads as an intentional blue. */
  --v3-color-info:           oklch(0.6 0.18 250);
  --v3-color-info-fg:         oklch(0.99 0.01 90);

  --v3-color-border:         oklch(0.9 0.01 260);
  --v3-color-input:          oklch(0.92 0.01 260);
  --v3-color-ring:           var(--v3-color-primary);

  --v3-color-sidebar:        oklch(0.97 0.005 260);
  --v3-color-sidebar-fg:      oklch(0.25 0.02 260);
  --v3-color-sidebar-accent:  oklch(0.93 0.01 260);
  --v3-color-sidebar-border:  oklch(0.88 0.01 260 / 70%);

  --v3-gradient-primary: linear-gradient(135deg, oklch(0.75 0.17 65), oklch(0.68 0.2 40));
  --v3-gradient-accent:  linear-gradient(135deg, oklch(0.65 0.15 210), oklch(0.6 0.17 250));
  --v3-gradient-surface: linear-gradient(180deg, oklch(1 0 0), oklch(0.97 0.005 260));

  --v3-shadow-elevated: 0 20px 40px -20px oklch(0.2 0.03 260 / 18%), 0 0 0 1px oklch(0.2 0.03 260 / 4%);
  --v3-shadow-glow:     0 0 40px -8px oklch(0.75 0.17 55 / 40%);
  --v3-shadow-sm:       0 1px 2px oklch(0.2 0.03 260 / 6%);

  /* Radius scale — ported from Lovable's --radius: 0.875rem base */
  --v3-radius-sm: 0.625rem;  /* 10px */
  --v3-radius-md: 0.75rem;   /* 12px */
  --v3-radius-lg: 0.875rem;  /* 14px */
  --v3-radius-xl: 1.125rem;  /* 18px */
  --v3-radius-2xl: 1.375rem; /* 22px */

  /* Spacing scale (Tailwind-equivalent px values used in the mockup) */
  --v3-space-1: 0.25rem;  /* 4px */
  --v3-space-2: 0.5rem;   /* 8px */
  --v3-space-3: 0.75rem;  /* 12px */
  --v3-space-4: 1rem;     /* 16px */
  --v3-space-5: 1.25rem;  /* 20px */
  --v3-space-6: 1.5rem;   /* 24px */
  --v3-space-8: 2rem;     /* 32px */
  --v3-space-10: 2.5rem;  /* 40px */
  --v3-space-12: 3rem;    /* 48px */

  /* Typography — font family tokens named to match the mockup; system-font
     fallback since we don't self-host webfonts for this phase (see plan §8:
     avoid CDN dependencies). */
  --v3-font-display: "Space Grotesk", ui-sans-serif, system-ui, sans-serif;
  --v3-font-sans:    "Inter", ui-sans-serif, system-ui, sans-serif;
  --v3-font-mono:    "JetBrains Mono", ui-monospace, monospace;

  /* "Soft" tint tokens (flagged as missing in the mockup's own design-system audit
     — §8.2 — so defined explicitly here rather than relying on relative-color
     syntax for broader browser support). */
  --v3-color-primary-soft:      oklch(0.72 0.17 55 / 15%);
  --v3-color-accent-soft:       oklch(0.62 0.15 210 / 15%);
  --v3-color-success-soft:      oklch(0.65 0.17 155 / 15%);
  --v3-color-warning-soft:      oklch(0.78 0.17 85 / 15%);
  --v3-color-destructive-soft:  oklch(0.6 0.22 25 / 15%);
  --v3-color-muted-soft:        oklch(0.5 0.02 260 / 10%);
  --v3-color-info-soft:         oklch(0.6 0.18 250 / 15%);

  /* Backwards-compat aliases for the Phase-0 token names already used elsewhere */
  --v3-color-text: var(--v3-color-fg);
  --v3-color-surface: var(--v3-color-card);
  --v3-color-danger: var(--v3-color-destructive);
}

@media (prefers-color-scheme: dark) {
  .v3-body {
    --v3-color-bg:            oklch(0.16 0.02 260);
    --v3-color-fg:             oklch(0.97 0.01 250);
    --v3-color-card:          oklch(0.21 0.025 262);
    --v3-color-card-fg:        oklch(0.97 0.01 250);

    --v3-color-primary:        oklch(0.78 0.17 65);
    --v3-color-primary-fg:      oklch(0.99 0.01 90);
    --v3-color-secondary:      oklch(0.28 0.03 262);
    --v3-color-secondary-fg:    oklch(0.9 0.01 260);
    --v3-color-muted:          oklch(0.24 0.02 262);
    --v3-color-muted-fg:        oklch(0.68 0.02 260);
    --v3-color-accent:         oklch(0.68 0.16 200);
    --v3-color-accent-fg:       oklch(0.99 0.01 90);

    --v3-color-destructive:     oklch(0.65 0.22 25);
    --v3-color-success:        oklch(0.75 0.18 155);
    --v3-color-warning:        oklch(0.82 0.17 85);
    /* Fixed status blue, same reasoning as the light block above — dark-theme value matches the
       accent picker's own "blue" dark-theme override (topbar-menus.css). */
    --v3-color-info:           oklch(0.68 0.16 250);

    --v3-color-border:         oklch(0.3 0.02 262 / 60%);
    --v3-color-input:          oklch(0.28 0.02 262);

    --v3-color-sidebar:        oklch(0.13 0.02 262);
    --v3-color-sidebar-fg:      oklch(0.9 0.01 260);
    --v3-color-sidebar-accent:  oklch(0.22 0.02 262);
    --v3-color-sidebar-border:  oklch(0.3 0.02 262 / 60%);

    --v3-shadow-elevated: 0 20px 40px -20px oklch(0 0 0 / 60%), 0 0 0 1px oklch(1 0 0 / 4%);

    --v3-color-primary-soft:      oklch(0.78 0.17 65 / 20%);
    --v3-color-accent-soft:       oklch(0.68 0.16 200 / 20%);
    --v3-color-success-soft:      oklch(0.75 0.18 155 / 20%);
    --v3-color-warning-soft:      oklch(0.82 0.17 85 / 20%);
    --v3-color-destructive-soft:  oklch(0.65 0.22 25 / 20%);
    --v3-color-muted-soft:        oklch(0.68 0.02 260 / 12%);
  }
}

html:has(.v3-body) {
  scroll-behavior: smooth;
}

.v3-body, .v3-body * {
  box-sizing: border-box;
}

.v3-body {
  margin: 0;
  background: var(--v3-color-bg);
  color: var(--v3-color-fg);
  font-family: var(--v3-font-sans);
  font-size: 0.875rem;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

.v3-body h1, .v3-body h2, .v3-body h3, .v3-body h4 {
  font-family: var(--v3-font-display);
  letter-spacing: -0.02em;
  margin: 0 0 var(--v3-space-2);
}

.v3-body h1 { font-size: 1.5rem; font-weight: 600; }
@media (min-width: 768px) {
  .v3-body h1 { font-size: 1.875rem; }
}
.v3-body h2 { font-size: 0.875rem; font-weight: 600; }

/* Shell layout — sidebar (fixed left column) + flex column (topbar + main),
   ported from the Lovable mockup's DashboardShell: `min-h-screen flex w-full`. */
.v3-shell {
  display: flex;
  min-height: 100vh;
  width: 100%;
}

.v3-shell__content {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.v3-shell__main {
  flex: 1;
  padding: var(--v3-space-4);
  min-width: 0;
  max-width: 100vw;
}
@media (min-width: 1024px) {
  .v3-shell__main { padding: var(--v3-space-8); }
}

/* .v3-topbar__menu-btn default-hidden / .v3-sidebar-backdrop display, and
   their mobile-visible overrides, live in responsive.css instead of here —
   that file loads after topbar-menus.css so its unconditional
   `.v3-topbar__icon-btn { display: grid }` (same specificity, later in the
   cascade) can't silently win back over a `display: none` defined earlier
   in this file. See responsive.css for the actual rules. */

/* Topbar — ported from DashboardShell's <header class="h-16 ... sticky top-0
   z-10 backdrop-blur bg-background/80"> */
.v3-topbar {
  height: 4rem;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: var(--v3-space-2);
  padding: 0 var(--v3-space-3);
  border-bottom: 1px solid var(--v3-color-border);
  position: sticky;
  top: 0;
  z-index: 10;
  backdrop-filter: blur(8px);
  background: color-mix(in oklch, var(--v3-color-bg) 80%, transparent);
}
@media (min-width: 1024px) {
  .v3-topbar { padding: 0 var(--v3-space-8); gap: var(--v3-space-3); }
}

.v3-topbar__breadcrumb {
  display: flex;
  align-items: center;
  gap: var(--v3-space-2);
  font-size: 0.75rem;
  color: var(--v3-color-muted-fg);
  min-width: 0;
}

.v3-topbar__breadcrumb a span,
.v3-topbar__breadcrumb > a {
  white-space: nowrap;
}

.v3-topbar__current {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

@media (max-width: 639px) {
  /* Keep only the current page name; "Tableau de bord ›" wastes width on phones. */
  .v3-topbar__breadcrumb a,
  .v3-topbar__breadcrumb > span:not(.v3-topbar__current) {
    display: none;
  }
  .v3-topbar__current {
    max-width: 38vw;
  }
}

.v3-topbar__breadcrumb a { color: inherit; text-decoration: none; }
.v3-topbar__breadcrumb a:hover { color: var(--v3-color-fg); }

.v3-topbar__current {
  color: var(--v3-color-fg);
  font-weight: 500;
}

.v3-topbar__spacer { flex: 1; }

.v3-topbar__user {
  display: flex;
  align-items: center;
  gap: var(--v3-space-2);
  padding-left: var(--v3-space-3);
  border-left: 1px solid var(--v3-color-border);
}

.v3-topbar__avatar {
  width: 2rem;
  height: 2rem;
  border-radius: 999px;
  display: grid;
  place-items: center;
  font-size: 0.75rem;
  font-weight: 600;
  background: var(--v3-gradient-primary);
  color: var(--v3-color-primary-fg);
}

.v3-topbar__user-meta {
  font-size: 0.75rem;
  line-height: 1.2;
  text-align: left;
}
.v3-topbar__user-name { font-weight: 500; }
.v3-topbar__user-role { color: var(--v3-color-muted-fg); }

@media (max-width: 767px) {
  /* Avatar stays (identity + tap target); name/role text is redundant once
     the user menu itself shows the same info, and it's the first thing
     squeezed by narrow phones. */
  .v3-topbar__user-meta { display: none; }
  .v3-topbar__user { padding-left: var(--v3-space-2); gap: var(--v3-space-1); }
}

@media (max-width: 479px) {
  /* LOX-1645: "la recherche rapide de la header n'est pas visible sur mobile" — quicksearch used
     to be dropped here along with the language button on the narrowest phones (rationale at the
     time: both reachable another way, and kept the topbar from overflowing/wrapping). Restored:
     quicksearch is a primary action users expect in the header, not a nice-to-have. Language
     stays hidden — rarely changed mid-session, and dropping it alone still keeps the row from
     overflowing. */
  .v3-topbar__lang-menu {
    display: none;
  }
}

/* Sidebar — ported from ShipperSidebar: w-72 (288px), brand header, search
   box, collapsible icon groups, bottom widget slot. */
.v3-sidebar {
  width: 288px;
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  background: var(--v3-color-sidebar);
  color: var(--v3-color-sidebar-fg);
  border-right: 1px solid var(--v3-color-sidebar-border);
  height: 100vh;
  position: sticky;
  top: 0;
}

.v3-sidebar__brand {
  display: flex;
  align-items: center;
  gap: var(--v3-space-3);
  padding: 0 var(--v3-space-6);
  height: 4rem;
  border-bottom: 1px solid var(--v3-color-sidebar-border);
  flex-shrink: 0;
}

.v3-sidebar__brand-icon {
  display: grid;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: var(--v3-radius-xl);
  background: var(--v3-gradient-primary);
  color: var(--v3-color-primary-fg);
  box-shadow: var(--v3-shadow-glow);
  flex-shrink: 0;
}

.v3-sidebar__brand-name {
  font-family: var(--v3-font-display);
  font-size: 1rem;
  font-weight: 600;
}

.v3-sidebar__search {
  padding: var(--v3-space-3);
  flex-shrink: 0;
}

.v3-sidebar__search-input-wrap {
  position: relative;
}

.v3-sidebar__search-icon {
  position: absolute;
  left: 0.75rem;
  top: 50%;
  transform: translateY(-50%);
  color: var(--v3-color-muted-fg);
  pointer-events: none;
}

.v3-sidebar__search input {
  width: 100%;
  background: var(--v3-color-sidebar-accent);
  border: 1px solid var(--v3-color-sidebar-border);
  border-radius: var(--v3-radius-md);
  padding: 0.5rem 0.75rem 0.5rem 2.25rem;
  height: 2.25rem;
  font-size: 0.8125rem;
  color: inherit;
}

.v3-sidebar__nav {
  flex: 1;
  overflow-y: auto;
  padding: 0 var(--v3-space-3) var(--v3-space-3);
  scrollbar-width: thin;
  scrollbar-color: color-mix(in oklch, var(--v3-color-sidebar-fg) 25%, transparent) transparent;
}

.v3-sidebar__nav::-webkit-scrollbar { width: 6px; }
.v3-sidebar__nav::-webkit-scrollbar-track { background: transparent; }
.v3-sidebar__nav::-webkit-scrollbar-thumb {
  background: color-mix(in oklch, var(--v3-color-sidebar-fg) 25%, transparent);
  border-radius: 999px;
}
.v3-sidebar__nav::-webkit-scrollbar-thumb:hover {
  background: color-mix(in oklch, var(--v3-color-sidebar-fg) 45%, transparent);
}

.v3-sidebar__group {
  margin-bottom: var(--v3-space-2);
}

.v3-sidebar__group-label {
  font-size: 0.625rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--v3-color-muted-fg);
  padding: var(--v3-space-3) var(--v3-space-3) var(--v3-space-2);
}

.v3-sidebar__link {
  display: flex;
  align-items: center;
  gap: var(--v3-space-3);
  padding: var(--v3-space-2) var(--v3-space-3);
  border-radius: var(--v3-radius-md);
  color: color-mix(in oklch, var(--v3-color-sidebar-fg) 75%, transparent);
  text-decoration: none;
  font-size: 0.875rem;
}

.v3-sidebar__link svg { flex-shrink: 0; width: 1rem; height: 1rem; }
.v3-sidebar__link i { flex-shrink: 0; width: 1rem; font-size: 0.875rem; text-align: center; }
.v3-sidebar__search-icon i { font-size: 0.75rem; }
.v3-sidebar__widget-title i { font-size: 0.8125rem; }

.v3-sidebar__link:hover {
  background: color-mix(in oklch, var(--v3-color-sidebar-accent) 50%, transparent);
  color: var(--v3-color-sidebar-fg);
}

.v3-sidebar__link--active {
  background: color-mix(in oklch, var(--v3-color-sidebar-accent) 60%, transparent);
  color: var(--v3-color-sidebar-fg);
  font-weight: 600;
}

.v3-sidebar__link--active svg,
.v3-sidebar__link--active i { color: var(--v3-color-primary); }

/* Parent of an open subgroup: color its icon too, so the accent shows even before/without a
   direct --active match on the parent itself (e.g. an active child page, subgroup expanded). */
.v3-sidebar__link--has-subgroup:has(+ .v3-sidebar__subgroup--open) i { color: var(--v3-color-primary); }

/* Accordion (2026-08-06) — a link followed by a .v3-sidebar__subgroup gets a chevron toggle
   injected by v3-topbar.js; .v3-sidebar__link--has-subgroup is added by that same script so this
   rule doesn't shift every other sidebar link's layout. */
.v3-sidebar__link--has-subgroup { justify-content: space-between; }
.v3-sidebar__chevron {
  margin-left: auto;
  transition: transform 0.15s ease;
  color: color-mix(in oklch, var(--v3-color-sidebar-fg) 50%, transparent);
}
.v3-sidebar__chevron--open { transform: rotate(180deg); }

/* "en déroulant une liste du sidebar, je veux que ça soit animé d'une façon ergonomique et
   belle" — was an instant display:none/block flip. `max-height` is the one dimension CSS can
   actually transition without knowing the content's real height up front (`height:auto` doesn't
   animate); v3-topbar.js's setV3SubgroupOpen() sets the inline max-height (measured via
   scrollHeight) that this transitions to/from. A soft opacity fade rides along so it reads as a
   gentle unfurl rather than a sidebar element mechanically stretching. */
.v3-sidebar__subgroup {
  margin: 0.125rem 0 0.25rem 1rem;
  padding-left: var(--v3-space-3);
  border-left: 1px solid var(--v3-color-sidebar-border);
  overflow: hidden;
  max-height: 0;
  opacity: 0;
  transition: max-height 0.28s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.22s ease;
}
.v3-sidebar__subgroup--open { opacity: 1; }

.v3-sidebar__sublink {
  display: block;
  padding: 0.375rem var(--v3-space-3);
  border-radius: var(--v3-radius-sm);
  color: color-mix(in oklch, var(--v3-color-sidebar-fg) 65%, transparent);
  text-decoration: none;
  font-size: 0.8125rem;
}

.v3-sidebar__sublink:hover {
  background: color-mix(in oklch, var(--v3-color-sidebar-accent) 40%, transparent);
  color: var(--v3-color-sidebar-fg);
}

.v3-sidebar__widget {
  margin: var(--v3-space-3);
  padding: var(--v3-space-4);
  border-radius: var(--v3-radius-xl);
  border: 1px solid var(--v3-color-sidebar-border);
  background: color-mix(in oklch, var(--v3-color-sidebar-accent) 40%, transparent);
  flex-shrink: 0;
}

.v3-sidebar__widget-title {
  display: flex;
  align-items: center;
  gap: var(--v3-space-2);
  font-size: 0.75rem;
  font-weight: 500;
}

.v3-sidebar__widget-body {
  font-size: 0.75rem;
  color: var(--v3-color-muted-fg);
  margin: 0.375rem 0 0;
  line-height: 1.5;
}

.v3-sidebar__widget-link {
  display: block;
  margin-top: var(--v3-space-3);
  text-align: center;
  font-size: 0.75rem;
  font-weight: 500;
  height: 2rem;
  line-height: 2rem;
  border-radius: var(--v3-radius-md);
  background: var(--v3-color-primary);
  color: var(--v3-color-primary-fg);
  text-decoration: none;
}

.v3-body a {
  color: var(--v3-color-primary);
}

.v3-body .v3-btn {
  font-family: var(--v3-font-sans);
  font-size: 0.875rem;
  font-weight: 500;
  border-radius: var(--v3-radius-md);
  border: 1px solid transparent;
  padding: 0.5rem 1rem;
  cursor: pointer;
  background: var(--v3-gradient-primary);
  color: var(--v3-color-primary-fg);
  box-shadow: var(--v3-shadow-glow);
}

.v3-body .v3-btn:hover {
  filter: brightness(1.03);
}

/* 2026-08-23: a disabled button (e.g. "Filtrer" before any filter field has actually changed)
   must visibly read as non-clickable, not just be inert — grayed out, no pointer cursor, no hover
   brighten, no gradient/glow. */
.v3-body .v3-btn:disabled, .v3-body .v3-btn[disabled] {
  background: var(--v3-color-muted, #d0d5dd);
  color: var(--v3-color-muted-fg);
  box-shadow: none;
  cursor: not-allowed;
  opacity: 0.65;
}
.v3-body .v3-btn:disabled:hover {
  filter: none;
}

.v3-body .v3-btn--secondary {
  background: var(--v3-color-secondary);
  color: var(--v3-color-secondary-fg);
  box-shadow: none;
}

/* 2026-08-11: tone-colored action-button variants (Lovable "verifier-status" reference's
   ScanIdPanel: destructive for delivery cancellation, warning for return cancellation, success
   for a money update — see app_admin_v3's check_status/cancel_return/cancel_delivery/
   update_money pages). */
.v3-body .v3-btn--destructive {
  background: var(--v3-color-destructive);
  color: var(--v3-color-destructive-fg);
  box-shadow: none;
}
.v3-body .v3-btn--warning {
  background: var(--v3-color-warning);
  color: var(--v3-color-warning-fg);
  box-shadow: none;
}
.v3-body .v3-btn--success {
  background: var(--v3-color-success);
  color: var(--v3-color-success-fg);
  box-shadow: none;
}
/* Explicit user request: the status-check button reads as a neutral dark/grayish action, not a
   semantic status color — a fixed dark neutral (not theme-mapped) so it stays "noir grisâtre" in
   both light and dark mode rather than inverting. */
.v3-body .v3-btn--dark {
  background: #27272a;
  color: #fafafa;
  box-shadow: none;
}
.v3-body .v3-btn--dark:hover { background: #3f3f46; }

.v3-body input, .v3-body select, .v3-body textarea {
  font-family: var(--v3-font-sans);
  font-size: 0.875rem;
  border: 1px solid var(--v3-color-input);
  border-radius: var(--v3-radius-sm);
  padding: 0.45rem 0.65rem;
  background: var(--v3-color-card);
  color: var(--v3-color-fg);
}

.v3-body input:focus, .v3-body select:focus, .v3-body textarea:focus {
  outline: none;
  border-color: var(--v3-color-ring);
  box-shadow: 0 0 0 2px oklch(from var(--v3-color-ring) l c h / 30%);
}

/* Mobile shell overrides — below 1024px the sidebar becomes an off-canvas
   drawer (fixed, translated off-screen) toggled by the topbar hamburger
   button (see topbar-menus.css / v3-topbar.js), with a dimming backdrop.
   Placed at the end of the file (source order, not just specificity) so it
   wins over the unconditional `.v3-sidebar { position: sticky; ... }` rule
   defined earlier — an equal-specificity override earlier in the cascade
   would otherwise get clobbered by that later rule regardless of the media
   query matching. */
@media (max-width: 1023px) {
  .v3-shell {
    overflow-x: hidden;
  }

  /* .v3-topbar__menu-btn's default-hide/mobile-show toggle lives in
     responsive.css (loaded last), not here — see the comment near the old
     location of this rule, above the topbar section. */

  .v3-sidebar {
    position: fixed;
    left: 0;
    top: 0;
    z-index: 40;
    width: min(288px, 82vw);
    transform: translateX(-100%);
    transition: transform 0.22s ease;
    box-shadow: var(--v3-shadow-elevated);
  }

  .v3-sidebar--open {
    transform: translateX(0);
  }

  .v3-sidebar-backdrop {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 30;
    background: oklch(0.1 0.02 260 / 45%);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.22s ease;
  }

  .v3-sidebar-backdrop--visible {
    opacity: 1;
    pointer-events: auto;
  }
}


/* ============ css/components/kpi-card.css (5017 chars) ============ */
/* KPI card — ported from the Lovable mockup's ShipperDashboardPage KPI grid:
   rounded-2xl card w/ gradient-surface bg + elevated shadow, top icon "pastille"
   (w-10 h-10, rounded-xl, tinted bg/border), value below in display font. */

.v3-kpi-card {
  position: relative;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  padding: var(--v3-space-5);
  background: var(--v3-gradient-surface);
  color: var(--v3-color-card-fg);
  border: 1px solid var(--v3-color-border);
  border-radius: var(--v3-radius-2xl);
  box-shadow: var(--v3-shadow-elevated);
  min-width: 200px;
  flex: 1;
}

/* Interactive "bubble" feedback (2026-08-09) — ported from the Lovable reference's general feel
   (tiles/cards there react to hover/press even without a documented dedicated CSS rule for it).
   Scoped to kpi-cards that are ALSO clickable (a wrapper, a role=button, an onclick, or an
   explicit --clickable modifier) so plain informational cards (tables, static panels) stay inert. */
a.v3-kpi-card,
.v3-kpi-card[role="button"],
.v3-kpi-card[onclick],
.v3-kpi-card--clickable,
.v3-dashboard-bubble,
.v3-body a[href*="?status="] {
  cursor: pointer;
  transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
}
/* LOX-1620: "en mettant la souris sur la bulle, deux couleurs sont superposées" — this used to
   force border-color to the user's chosen accent (--v3-color-primary) via !important, regardless
   of a card's own semantic modifier (--success/--warning/--anomaly, which only tint the icon
   pastille, not the border). Hovering a colored card showed its own modifier color on the icon
   AND an unrelated accent-colored glowing border at once. Dropped the border override — the lift
   + shadow-glow (already accent-matched per theme, see topbar-menus.css) is enough hover feedback
   on its own, without introducing a second, clashing color. */
a.v3-kpi-card:hover,
.v3-kpi-card[role="button"]:hover,
.v3-kpi-card[onclick]:hover,
.v3-kpi-card--clickable:hover,
.v3-dashboard-bubble:hover,
.v3-body a[href*="?status="]:hover {
  transform: translateY(-2px);
  box-shadow: var(--v3-shadow-elevated), var(--v3-shadow-glow);
}
a.v3-kpi-card:active,
.v3-kpi-card[role="button"]:active,
.v3-kpi-card[onclick]:active,
.v3-kpi-card--clickable:active,
.v3-dashboard-bubble:active,
.v3-body a[href*="?status="]:active {
  transform: translateY(0) scale(0.98);
  box-shadow: var(--v3-shadow-sm);
}

.v3-kpi-card__top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
}

.v3-kpi-card__icon {
  display: grid;
  place-items: center;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: var(--v3-radius-xl);
  background: var(--v3-color-primary-soft);
  color: var(--v3-color-primary);
  border: 1px solid color-mix(in oklch, var(--v3-color-primary) 25%, transparent);
  flex-shrink: 0;
}

.v3-kpi-card__icon svg { width: 1.25rem; height: 1.25rem; }
.v3-kpi-card__icon i { font-size: 1.125rem; }

/* fa-building's glyph ink sits visibly right of its own advance-width box (narrower box than
   most icons in this set, e.g. fa-clock/fa-sack-dollar, with the ink not centered inside it) —
   `place-items:center` above centers the box correctly, but the icon still reads as off-center
   inside the circle. Reported on /v3/vendor/cod/'s "Disponible en agence" tile (mobile). */
.v3-kpi-card__icon .fa-building { transform: translateX(-2px); }

.v3-kpi-card--success .v3-kpi-card__icon { background: var(--v3-color-success-soft); color: var(--v3-color-success); border-color: color-mix(in oklch, var(--v3-color-success) 25%, transparent); }
.v3-kpi-card--accent .v3-kpi-card__icon { background: var(--v3-color-accent-soft); color: var(--v3-color-accent); border-color: color-mix(in oklch, var(--v3-color-accent) 25%, transparent); }
.v3-kpi-card--warning .v3-kpi-card__icon { background: var(--v3-color-warning-soft); color: var(--v3-color-warning); border-color: color-mix(in oklch, var(--v3-color-warning) 25%, transparent); }
.v3-kpi-card--anomaly .v3-kpi-card__icon { background: var(--v3-color-destructive-soft); color: var(--v3-color-destructive); border-color: color-mix(in oklch, var(--v3-color-destructive) 25%, transparent); }

.v3-kpi-card--anomaly {
  border-color: color-mix(in oklch, var(--v3-color-destructive) 60%, transparent);
  box-shadow: 0 0 0 2px color-mix(in oklch, var(--v3-color-destructive) 30%, transparent), var(--v3-shadow-elevated);
}

.v3-kpi-card__hint {
  font-size: 0.625rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--v3-color-muted-fg);
}

.v3-kpi-card__body {
  margin-top: var(--v3-space-6);
}

.v3-kpi-card__value {
  font-family: var(--v3-font-display);
  font-size: 1.75rem;
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.1;
}

.v3-kpi-card__label {
  font-size: 0.75rem;
  color: var(--v3-color-muted-fg);
  margin-top: var(--v3-space-1);
}

.v3-kpi-card--pulse {
  animation: v3-pulse-border 1.6s ease-in-out infinite;
}

@keyframes v3-pulse-border {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.7; }
}


/* ============ css/components/status-badge.css (1399 chars) ============ */
/* Status badge — ported from the Lovable mockup's ad-hoc badge classes
   (bg-success/15, bg-warning/15, bg-destructive/15, bg-primary/15, bg-muted),
   here tokenized as reusable BEM modifiers rather than recreated inline per page
   (the mockup's own audit flagged this duplication — §7 of DESIGN_SYSTEM.md). */

.v3-status-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--v3-space-1);
  padding: 0.2rem 0.6rem;
  border-radius: 999px;
  font-size: 0.75rem;
  font-weight: 600;
  background: var(--v3-color-muted-soft);
  color: var(--v3-color-muted-fg);
  white-space: nowrap;
}

.v3-status-badge--delivered,
.v3-status-badge--success {
  background: var(--v3-color-success-soft);
  color: var(--v3-color-success);
}

.v3-status-badge--in-transit,
.v3-status-badge--info {
  background: var(--v3-color-accent-soft);
  color: var(--v3-color-accent);
}

.v3-status-badge--anomaly,
.v3-status-badge--destructive {
  background: var(--v3-color-destructive-soft);
  color: var(--v3-color-destructive);
}

.v3-status-badge--pending,
.v3-status-badge--warning {
  background: var(--v3-color-warning-soft);
  color: var(--v3-color-warning);
}

.v3-status-badge--active,
.v3-status-badge--primary {
  background: var(--v3-color-primary-soft);
  color: var(--v3-color-primary);
}

.v3-status-badge--accent {
  background: var(--v3-color-accent-soft);
  color: var(--v3-color-accent);
}


/* ============ css/components/data-table.css (7917 chars) ============ */
/* Data table — ported from the Lovable mockup's TransactionsPage table pattern:
   muted header, subtle row hover, tabular numerics. */

.v3-data-table {
  /* No `overflow: hidden` here (removed 2026-08-08) — it used to self-clip this table's own
     rounded corners, but a table with non-visible overflow sits BETWEEN a sticky <td>/<th> and
     the actual scrolling ancestor (its wrapping .v3-kpi-card), which hijacks position:sticky's
     reference container the same way the card's own mismatched-axis overflow did (see the
     :has() rule at the bottom of this file) — confirmed via isolated reproduction. Every page
     using this table already sits inside a .v3-kpi-card that does its own corner-clipping (most
     pages additionally set `border:none; border-radius:0;` inline on the table itself for
     exactly this reason); a bare `.v3-data-table` with no such wrapper would show its header's
     square corners past its own border-radius, but no page in this codebase uses it that way. */
  width: 100%;
  /* `separate` (not `collapse`) on purpose: position:sticky on a <td>/<th> is silently inert in
     Chromium/WebKit when the table uses border-collapse:collapse (a known browser limitation of
     the collapsed-border layout model, not a bug in the sticky rule below) — needed for the
     sticky Actions column further down. border-spacing:0 keeps cells visually adjacent exactly
     like collapse did; every visible border here was already a per-cell border-bottom (no
     left/right cell borders anywhere), so there's no double-border artifact to compensate for. */
  border-collapse: separate;
  border-spacing: 0;
  background: var(--v3-color-card);
  border: 1px solid var(--v3-color-border);
  border-radius: var(--v3-radius-lg);
  font-size: 0.875rem;
}

.v3-data-table th,
.v3-data-table td {
  padding: var(--v3-space-2) var(--v3-space-3);
  border-bottom: 1px solid var(--v3-color-border);
  text-align: left;
}

/* 2026-08-14 user request: a long unbroken string (e.g. Contenu with no spaces) in a cell used
   to push past the column's width and overlap the next columns instead of wrapping. Cap every
   cell to a sane max-width and let long words break, so the row grows taller instead of
   bleeding into its neighbors — applies table-wide since any text column can receive free-form
   vendor input. */
.v3-data-table td {
  max-width: 22rem;
  /* `anywhere` (not `break-word`) on purpose: break-word doesn't reduce a flex/table cell's
     intrinsic min-content size per spec, so a single unbroken string (e.g. Contenu with no
     spaces) still forced the cell — and the whole row — to grow to fit it before wrapping
     kicked in. `anywhere` is counted in min-content sizing, so max-width above actually holds. */
  overflow-wrap: anywhere;
}

.v3-data-table thead th,
.v3-data-table__header-cell {
  position: sticky;
  top: 0;
  /* LOX-1817: "the sticky table header's text visually overlaps/ghosts into the first data
     row's text while scrolling" — NOT a WebKit compositing artifact (translateZ/backface-
     visibility/will-change on this cell, and even on its scroll container, changed nothing —
     verified by reproducing the exact overlap live and iterating through all three). The real
     cause is the same one already found and fixed for the sticky top+right corner cell just
     below in this file: `--v3-color-muted-soft` is a 10%-alpha tint, correct when it sits once
     over the card's flat background, but this header is OPAQUE-looking only by accident,
     because normally nothing scrolls underneath it. Used unlayered here, the row scrolling
     underneath a sticky header shows through it at 10% opacity — exactly the "ghosting" in the
     report. Same fix as the corner cell: layer the tint over an opaque `--v3-color-card` base
     instead of using it directly. */
  background: linear-gradient(var(--v3-color-muted-soft), var(--v3-color-muted-soft)), var(--v3-color-card);
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 600;
  color: var(--v3-color-muted-fg);
}

.v3-data-table tbody tr:hover {
  background: var(--v3-color-muted-soft);
}

.v3-data-table__row--selected {
  background: var(--v3-color-primary-soft);
}

.v3-data-table__cell--numeric {
  text-align: right;
  font-variant-numeric: tabular-nums;
  /* A numeric value is one unbroken string, so the table-wide `overflow-wrap: anywhere`
     (set on .v3-data-table td, for free-form text columns) was breaking it character-by-character
     on narrow/mobile viewports instead of letting the table's own horizontal scroll handle it. */
  white-space: nowrap;
}

/* 2026-08-14 user request: the "#" row-number column has no own width, so on any list squeezed
   by its other (wider, free-form) columns, the table shrinks it toward min-content — and since
   the table-wide `overflow-wrap: anywhere` rule above permits breaking between digits, a
   two-digit row number (10, 11...) split onto separate lines instead of staying on one. This
   column's content is always a short fixed-format number, never the free-form text the
   anywhere-wrap rule exists for, so it's exempted and pinned to its own natural width instead of
   being squeezed. */
.v3-data-table__cell--rownum {
  width: 1%;
  white-space: nowrap;
  overflow-wrap: normal;
}

/* 2026-08-08 user request: on wide tables, the rightmost "Actions" column was buried inside the
   horizontally-scrolled area — reachable only by scrolling all the way right, easy to miss
   entirely on the last rows of a long list. Every list table in this codebase puts Actions as
   the final column, so a single :last-child rule fixes it everywhere at once (no per-template
   changes needed) by pinning that column to the right edge of the table's own scroll container
   while the rest of the row scrolls underneath it. Combines with the existing `thead th`
   top-sticky rule above to make the header's Actions cell sticky in both directions at once. */
.v3-data-table th:last-child,
.v3-data-table td:last-child {
  position: sticky;
  right: 0;
  background: var(--v3-color-card);
  box-shadow: -6px 0 8px -6px rgba(0, 0, 0, 0.15);
}

.v3-data-table thead th:last-child {
  /* `--v3-color-muted-soft` is a 10%-alpha tint (meant to sit over the card's own opaque
     background) — used unlayered here, the header row scrolling underneath this sticky corner
     cell showed through and garbled its text. Layering it as a background-image over a solid
     `--v3-color-card` base keeps the same tinted look while staying fully opaque. */
  background: linear-gradient(var(--v3-color-muted-soft), var(--v3-color-muted-soft)), var(--v3-color-card);
  z-index: 2;
}

.v3-data-table tbody td:last-child {
  z-index: 1;
}

.v3-data-table tbody tr:hover td:last-child {
  background: var(--v3-color-muted-soft);
}

.v3-data-table__row--selected td:last-child {
  background: var(--v3-color-primary-soft);
}

/* The other half of the sticky-column fix above: `.v3-kpi-card`'s own base rule sets
   `overflow: hidden` (both axes, for rounded-corner clipping). Every list page overrides just
   `overflow-x: auto` inline to let a wide table scroll, but that leaves `overflow-y: hidden`
   inherited from the class — and Chromium silently breaks position:sticky on an element whenever
   its nearest scrolling ancestor has MISMATCHED x/y overflow (one hidden, one auto/scroll),
   confirmed via isolated reproduction. Forcing overflow-y back to visible (browsers coerce this
   to `auto` per the CSS Overflow spec once overflow-x isn't visible, which is what actually
   un-mismatches the two axes) fixes every current page using the standard
   `<div class="v3-kpi-card" style="padding:0; overflow-x:auto;"><table class="v3-data-table">`
   markup, and any future one, without hunting down each template's inline style. */
.v3-kpi-card:has(.v3-data-table) {
  overflow-y: visible;
}


/* ============ css/components/anomaly-panel.css (692 chars) ============ */
/* Anomaly chip — net-new component (no legacy/mockup equivalent existed for this
   exact widget), styled consistently with the ported destructive/soft tokens. */

.v3-anomaly-chip {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--v3-space-2);
  padding: var(--v3-space-2) var(--v3-space-3);
  border: 1px solid var(--v3-color-border);
  border-left: 3px solid var(--v3-color-destructive);
  border-radius: var(--v3-radius-sm);
  background: var(--v3-color-card);
}

.v3-anomaly-chip__reason {
  font-weight: 600;
  font-size: 0.8125rem;
}

.v3-anomaly-chip__phone {
  color: var(--v3-color-primary);
  text-decoration: none;
  font-size: 0.8125rem;
}


/* ============ css/components/back-to-legacy-link.css (758 chars) ============ */
.v3-back-to-legacy-link {
  display: inline-flex;
  align-items: center;
  gap: var(--v3-space-1);
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--v3-color-muted-fg);
  text-decoration: none;
  padding: 0.35rem 0.75rem;
  border: 1px solid var(--v3-color-border);
  border-radius: var(--v3-radius-sm);
  background: var(--v3-color-card);
}

.v3-back-to-legacy-link:hover {
  color: var(--v3-color-fg);
  border-color: var(--v3-color-fg);
}

@media (max-width: 900px) {
  /* Collapse to icon-only once the topbar is tight (hamburger + several icon
     buttons + this link all compete for the same row below desktop widths). */
  .v3-back-to-legacy-link__text {
    display: none;
  }
  .v3-back-to-legacy-link {
    padding: 0.35rem 0.6rem;
  }
}


/* ============ css/components/topbar-menus.css (10903 chars) ============ */
/* Topbar icon buttons + dropdown menus — ported structurally from the legacy
   fixed.html header dropdowns (quick search, language, user menu), styled to
   match the Lovable topbar's icon-button language. */

.v3-topbar__icon-btn {
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  border-radius: var(--v3-radius-md);
  border: 1px solid var(--v3-color-border);
  background: var(--v3-color-card);
  color: var(--v3-color-muted-fg);
  box-shadow: none;
  padding: 0;
}

.v3-topbar__icon-btn:hover {
  color: var(--v3-color-fg);
  background: var(--v3-color-muted-soft);
  filter: none;
}

@media (max-width: 767px) {
  /* 32px is below the ~44px touch-target guideline; bump padding (not the
     icon itself) so taps are reliable without redrawing the icon grid. */
  .v3-topbar__icon-btn {
    width: 2.5rem;
    height: 2.5rem;
  }
}

.v3-menu {
  position: relative;
}

.v3-menu__panel {
  display: none;
  position: absolute;
  top: calc(100% + 0.5rem);
  right: 0;
  min-width: 220px;
  max-width: calc(100vw - 2rem);
  background: var(--v3-color-card);
  border: 1px solid var(--v3-color-border);
  border-radius: var(--v3-radius-lg);
  box-shadow: var(--v3-shadow-elevated);
  padding: var(--v3-space-2);
  z-index: 20;
}

@media (max-width: 767px) {
  /* Anchoring to `right: 0` on the triggering button can push a panel partly
     off-screen on narrow phones. Pin panels to the viewport instead. */
  .v3-menu__panel {
    position: fixed;
    top: calc(4rem + 0.5rem);
    left: 1rem;
    right: 1rem;
    width: auto;
    min-width: 0;
    max-width: none;
  }
}

.v3-menu__panel.v3-menu__panel--open {
  display: block;
}

.v3-menu__panel input {
  width: 100%;
}

.v3-menu__item {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  box-shadow: none;
  color: var(--v3-color-fg);
  padding: 0.5rem 0.75rem;
  border-radius: var(--v3-radius-sm);
  font-size: 0.8125rem;
  font-weight: 400;
  text-decoration: none;
}

.v3-menu__item:hover {
  background: var(--v3-color-muted-soft);
  filter: none;
}

.v3-menu__item--active {
  font-weight: 600;
  color: var(--v3-color-primary);
}

/* Theme override: JS sets data-theme on <html> and persists to localStorage,
   forcing light/dark regardless of prefers-color-scheme. */
.v3-body[data-theme="dark"] {
  --v3-color-bg:            oklch(0.16 0.02 260);
  --v3-color-fg:             oklch(0.97 0.01 250);
  --v3-color-card:          oklch(0.21 0.025 262);
  --v3-color-card-fg:        oklch(0.97 0.01 250);
  --v3-color-primary:        oklch(0.78 0.17 65);
  --v3-color-primary-fg:      oklch(0.99 0.01 90);
  --v3-color-secondary:      oklch(0.28 0.03 262);
  --v3-color-secondary-fg:    oklch(0.9 0.01 260);
  --v3-color-muted:          oklch(0.24 0.02 262);
  --v3-color-muted-fg:        oklch(0.68 0.02 260);
  --v3-color-accent:         oklch(0.68 0.16 200);
  --v3-color-accent-fg:       oklch(0.99 0.01 90);
  --v3-color-destructive:     oklch(0.65 0.22 25);
  --v3-color-destructive-fg:   oklch(0.99 0.01 60);
  --v3-color-success:        oklch(0.75 0.18 155);
  --v3-color-success-fg:      oklch(0.99 0.01 160);
  --v3-color-warning:        oklch(0.82 0.17 85);
  --v3-color-warning-fg:      oklch(0.2 0.03 60);
  --v3-color-border:         oklch(0.3 0.02 262 / 60%);
  --v3-color-input:          oklch(0.28 0.02 262);
  --v3-color-ring:           var(--v3-color-primary);
  --v3-color-sidebar:        oklch(0.13 0.02 262);
  --v3-color-sidebar-fg:      oklch(0.9 0.01 260);
  --v3-color-sidebar-accent:  oklch(0.22 0.02 262);
  --v3-color-sidebar-border:  oklch(0.3 0.02 262 / 60%);
  --v3-gradient-primary: linear-gradient(135deg, oklch(0.78 0.17 65), oklch(0.7 0.19 40));
  --v3-gradient-accent:  linear-gradient(135deg, oklch(0.68 0.16 200), oklch(0.62 0.18 250));
  --v3-gradient-surface: linear-gradient(180deg, oklch(0.23 0.025 262), oklch(0.19 0.02 260));
  --v3-shadow-elevated: 0 20px 40px -20px oklch(0 0 0 / 60%), 0 0 0 1px oklch(1 0 0 / 4%);
  --v3-shadow-glow:     0 0 40px -8px oklch(0.78 0.17 65 / 45%);
  --v3-shadow-sm:       0 1px 2px oklch(0 0 0 / 30%);
  --v3-color-primary-soft:      oklch(0.78 0.17 65 / 20%);
  --v3-color-accent-soft:       oklch(0.68 0.16 200 / 20%);
  --v3-color-success-soft:      oklch(0.75 0.18 155 / 20%);
  --v3-color-warning-soft:      oklch(0.82 0.17 85 / 20%);
  --v3-color-destructive-soft:  oklch(0.65 0.22 25 / 20%);
  --v3-color-muted-soft:        oklch(0.68 0.02 260 / 12%);
}

.v3-body[data-theme="light"] {
  --v3-color-bg:            oklch(0.985 0.005 250);
  --v3-color-fg:             oklch(0.2 0.03 260);
  --v3-color-card:          oklch(1 0 0);
  --v3-color-card-fg:        oklch(0.2 0.03 260);
  --v3-color-primary:        oklch(0.72 0.17 55);
  --v3-color-primary-fg:      oklch(0.99 0.01 90);
  --v3-color-secondary:      oklch(0.94 0.01 260);
  --v3-color-secondary-fg:    oklch(0.25 0.03 260);
  --v3-color-muted:          oklch(0.95 0.01 260);
  --v3-color-muted-fg:        oklch(0.5 0.02 260);
  --v3-color-accent:         oklch(0.62 0.15 210);
  --v3-color-accent-fg:       oklch(0.99 0.01 90);
  --v3-color-destructive:     oklch(0.6 0.22 25);
  --v3-color-destructive-fg:   oklch(0.99 0.01 60);
  --v3-color-success:        oklch(0.65 0.17 155);
  --v3-color-success-fg:      oklch(0.99 0.01 160);
  --v3-color-warning:        oklch(0.78 0.17 85);
  --v3-color-warning-fg:      oklch(0.2 0.03 60);
  --v3-color-border:         oklch(0.9 0.01 260);
  --v3-color-input:          oklch(0.92 0.01 260);
  --v3-color-ring:           var(--v3-color-primary);
  --v3-color-sidebar:        oklch(0.97 0.005 260);
  --v3-color-sidebar-fg:      oklch(0.25 0.02 260);
  --v3-color-sidebar-accent:  oklch(0.93 0.01 260);
  --v3-color-sidebar-border:  oklch(0.88 0.01 260 / 70%);
  --v3-gradient-primary: linear-gradient(135deg, oklch(0.75 0.17 65), oklch(0.68 0.2 40));
  --v3-gradient-accent:  linear-gradient(135deg, oklch(0.65 0.15 210), oklch(0.6 0.17 250));
  --v3-gradient-surface: linear-gradient(180deg, oklch(1 0 0), oklch(0.97 0.005 260));
  --v3-shadow-elevated: 0 20px 40px -20px oklch(0.2 0.03 260 / 18%), 0 0 0 1px oklch(0.2 0.03 260 / 4%);
  --v3-shadow-glow:     0 0 40px -8px oklch(0.75 0.17 55 / 40%);
  --v3-shadow-sm:       0 1px 2px oklch(0.2 0.03 260 / 6%);
  --v3-color-primary-soft:      oklch(0.72 0.17 55 / 15%);
  --v3-color-accent-soft:       oklch(0.62 0.15 210 / 15%);
  --v3-color-success-soft:      oklch(0.65 0.17 155 / 15%);
  --v3-color-warning-soft:      oklch(0.78 0.17 85 / 15%);
  --v3-color-destructive-soft:  oklch(0.6 0.22 25 / 15%);
  --v3-color-muted-soft:        oklch(0.5 0.02 260 / 10%);
}

/* Accent override: data-accent on <html> is set server-side from Tenant.primary_color
   (base_v3.html's inline pre-paint script; see context_builders/settings.py for the
   Paramétrage page that sets it). Only the primary/brand tokens are swapped —
   everything else (bg, card, muted, destructive, ...) stays as the active theme
   defines it. 2026-09-03: the client-side picker UI that used to drive this (localStorage
   'v3-accent', topbar button) was removed — it could clobber the tenant's real color
   with a hardcoded default on pages where no override had ever been set. Values match
   the original Lovable reference palette 1:1. */
.v3-body[data-accent="amber"] {
  --v3-color-primary: oklch(0.75 0.17 65);
  --v3-color-primary-fg: oklch(0.99 0.01 90);
  --v3-gradient-primary: linear-gradient(135deg, oklch(0.75 0.17 65), oklch(0.68 0.2 40));
  --v3-shadow-glow: 0 0 40px -8px oklch(0.75 0.17 65 / 45%);
  --v3-color-primary-soft: oklch(0.75 0.17 65 / 18%);
}
.v3-body[data-accent="red"] {
  --v3-color-primary: oklch(0.62 0.22 25);
  --v3-color-primary-fg: oklch(0.99 0.01 60);
  --v3-gradient-primary: linear-gradient(135deg, oklch(0.62 0.22 25), oklch(0.55 0.22 15));
  --v3-shadow-glow: 0 0 40px -8px oklch(0.62 0.22 25 / 45%);
  --v3-color-primary-soft: oklch(0.62 0.22 25 / 18%);
}
.v3-body[data-accent="blue"] {
  --v3-color-primary: oklch(0.6 0.18 250);
  --v3-color-primary-fg: oklch(0.99 0.01 90);
  --v3-gradient-primary: linear-gradient(135deg, oklch(0.6 0.18 250), oklch(0.52 0.19 265));
  --v3-shadow-glow: 0 0 40px -8px oklch(0.6 0.18 250 / 45%);
  --v3-color-primary-soft: oklch(0.6 0.18 250 / 18%);
}
.v3-body[data-accent="green"] {
  --v3-color-primary: oklch(0.65 0.17 150);
  --v3-color-primary-fg: oklch(0.99 0.01 160);
  --v3-gradient-primary: linear-gradient(135deg, oklch(0.65 0.17 150), oklch(0.56 0.18 155));
  --v3-shadow-glow: 0 0 40px -8px oklch(0.65 0.17 150 / 45%);
  --v3-color-primary-soft: oklch(0.65 0.17 150 / 18%);
}
.v3-body[data-accent="yellow"] {
  --v3-color-primary: oklch(0.82 0.14 95);
  --v3-color-primary-fg: oklch(0.2 0.03 90);
  --v3-gradient-primary: linear-gradient(135deg, oklch(0.82 0.14 95), oklch(0.74 0.16 75));
  --v3-shadow-glow: 0 0 40px -8px oklch(0.82 0.14 95 / 45%);
  --v3-color-primary-soft: oklch(0.82 0.14 95 / 18%);
}
.v3-body[data-accent="black"] {
  --v3-color-primary: oklch(0.2 0.01 260);
  --v3-color-primary-fg: oklch(0.97 0.01 250);
  --v3-gradient-primary: linear-gradient(135deg, oklch(0.2 0.01 260), oklch(0.32 0.01 260));
  --v3-shadow-glow: 0 0 40px -8px oklch(0.2 0.01 260 / 45%);
  --v3-color-primary-soft: oklch(0.2 0.01 260 / 18%);
}

/* "Installer l'application" (2026-08-11) — a soft looping glow so the button reads as an
   inviting call-to-action instead of blending into the sidebar footer. */
.v3-install-btn {
  animation: v3InstallPulse 2.4s ease-in-out infinite;
}
.v3-install-btn:hover {
  animation-play-state: paused;
  filter: brightness(1.08);
}
@keyframes v3InstallPulse {
  0%, 100% { box-shadow: var(--v3-shadow-glow); }
  50%      { box-shadow: 0 0 0 6px color-mix(in oklch, var(--v3-color-primary) 20%, transparent), var(--v3-shadow-glow); }
}

/* "Visite guidée" sidebar entry — "un effect qui attire les yeux pour le tester" — same soft
   looping glow language as .v3-install-btn above, borrowed rather than reinvented, but on the
   icon only (not the whole row) so it stays a gentle nudge rather than a competing CTA next to
   real navigation items like Jeton API right below it. */
.v3-sidebar__link--tour-cta i:first-child {
  position: relative;
  border-radius: 50%;
  animation: v3TourCtaPulse 2.2s ease-in-out infinite;
}
.v3-sidebar__link--tour-cta:hover i:first-child {
  animation-play-state: paused;
}
@keyframes v3TourCtaPulse {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in oklch, var(--v3-color-primary) 55%, transparent); color: var(--v3-color-primary); }
  50%      { box-shadow: 0 0 0 5px color-mix(in oklch, var(--v3-color-primary) 0%, transparent); color: var(--v3-color-sidebar-fg); }
}


/* ============ css/components/responsive.css?v=20260825 (9195 chars) ============ */
/* Cross-page mobile-responsiveness fixes that don't belong to one specific
   component file. Targets specific structural widgets (built with inline
   styles per-page) via small marker classes added alongside those inline
   styles, so this file is the single place mobile breakpoints for them live. */

/* Topbar hamburger button — must be hidden by default and shown only below
   the sidebar's off-canvas breakpoint (base.css). This has to live in THIS
   file (loaded last of all the component stylesheets), not base.css: the
   button also carries `.v3-topbar__icon-btn` (topbar-menus.css), whose
   unconditional `display: grid` is defined in a file that loads AFTER
   base.css, so a `display: none` sitting in base.css gets silently
   overridden regardless of viewport width. Keeping both the default-hide and
   the mobile-show override together in the last-loaded file guarantees the
   final cascade order is what's written here, top to bottom. */
.v3-topbar__menu-btn {
  display: none;
}
@media (max-width: 1023px) {
  .v3-topbar__menu-btn {
    display: grid;
  }
}

/* Transaction detail stepper — a flex row of fixed-size step circles
   connected by lines, with nowrap step labels. Circles/lines shrink fine,
   but nowrap labels don't, so at phone widths the row either clips labels or
   forces the page to overflow horizontally. Turn the row into a horizontally
   scrollable strip instead of trying to squeeze every step into ~320px. */
@media (max-width: 639px) {
  .v3-tx-stepper-wrap {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    padding-bottom: 0.25rem;
    margin: 0 calc(var(--v3-space-4) * -1);
    padding-left: var(--v3-space-4);
    padding-right: var(--v3-space-4);
  }
  .v3-tx-stepper-row {
    flex-wrap: nowrap;
    gap: 0.75rem;
  }
  .v3-tx-stepper-item {
    flex: 0 0 auto !important;
    min-width: 4.75rem;
  }
  /* The connecting line between steps is a plain flex:1 child of
     .v3-tx-stepper-item (not its own class) — once the item itself stops
     stretching (flex:0 0 auto above), that inner flex:1 only has the small
     leftover space inside the item's own min-width to fill, so adjacent
     items' circles/labels end up touching with no visible gap. Force it to a
     fixed short connector instead of trying to fill remaining item space. */
  .v3-tx-stepper-item > div[style*="flex:1; height:2px"] {
    flex: 0 0 1.25rem !important;
  }
}

/* Ticket-type picker (3 icon cards) — 3-across is cramped below ~380px;
   stack to 1 column so the icon+label isn't squeezed unreadably narrow. */
@media (max-width: 479px) {
  .v3-ticket-type-grid {
    grid-template-columns: 1fr !important;
  }
}

/* Generic touch-target bump for small icon-only buttons that are otherwise
   under ~40px — applied via opt-in class rather than blanket-selecting every
   <button>, since several pages use bare `<button style="width:1.75rem...">`
   for dense table/row controls where a uniform 44px bump would break layout. */
@media (max-width: 767px) {
  .v3-touch-target {
    min-width: 2.5rem;
    min-height: 2.5rem;
  }
}

/* 2026-08-24 user request: bulk-action toolbars (Export Excel / Étiquette /
   Manifeste / Mark As Ready / ...) sit as a sibling row directly above a wide
   `<table>` inside the same `overflow-x:auto` .v3-kpi-card — so scrolling the
   table horizontally on mobile also drags the toolbar off-screen to the left,
   hiding the very buttons the user needs while browsing wide columns. Sticking
   the toolbar to the scroll container's left edge keeps it in view regardless
   of horizontal scroll position. Only kicks in on mobile: on desktop the whole
   table already fits without horizontal scroll, so sticky positioning would be
   a no-op there anyway, but scoping it to the same breakpoint as the rest of
   this file keeps intent explicit. */
@media (max-width: 1023px) {
  .v3-list-toolbar {
    position: sticky;
    left: 0;
    z-index: 2;
    background: var(--v3-color-card);
  }
}

/* 2026-08-24 user request: "/v3/admin/support/clients/12/ cette page doit être adaptée au
   mobile" — the ticket detail body (_vendor_ticket_detail_body.html, shared by admin and agency)
   used a fixed `grid-template-columns:280px 1fr` for its Informations/Conversation two-column
   layout. On a narrow viewport the outer shell (.v3-shell) sets `overflow-x:hidden`, so instead
   of a horizontal scrollbar warning something was wrong, the right column silently got clipped —
   confirmed live: the two tracks rendered at their full 280px+200px (480px total) inside a 343px
   container, invisibly cut off. Stack to a single column below the same phone breakpoint already
   used elsewhere in this file. */
@media (max-width: 639px) {
  .v3-ticket-detail-grid {
    grid-template-columns: 1fr !important;
  }
}

/* LOX-1608/LOX-1609: "adapte cette page au mobile" — check_status.html and cancel_return.html
   both tag their sender/receiver/package multi-column grids with `.v3-responsive-grid` (a
   marker class already used the same way across many other pages: create_transporter.html,
   create_staff.html, scan_and_edit.html, the saas dashboard...), but no rule anywhere in the
   codebase actually defines that selector — grep confirms it's referenced 8+ times platform-wide
   and styled zero times. Every inline `grid-template-columns:1fr 1fr`/`2fr 1fr`/`repeat(3, 1fr)`
   was staying multi-column at any width, squeezing 2-3 tracks into a ~343px mobile container.
   Fixing the missing rule here (rather than patching each page) fixes every one of those pages
   at once, which is the whole point of a shared marker class. */
@media (max-width: 639px) {
  .v3-responsive-grid {
    grid-template-columns: 1fr !important;
  }
}

/* LOX-1562: "il faut rendre l'affichage de la caméra 'Carré' sur le mobile" — the
   html5-qrcode library injects a <video> that fills its container's width while
   preserving the camera's native (non-square, usually 16:9 or taller) aspect ratio,
   so on narrow phones the preview stretches tall and covers most of the screen.
   Force every scan-reader container platform-wide to a 1:1 box and crop the video
   to fill it. The video's own size/position are set as inline styles by the
   library's JS, which otherwise wins over any stylesheet rule — hence !important.

   2026-08-27 (LOX-1687/1690): #v3-sc-reader-in/#v3-sc-reader-out (app_sc_v3/transporters/
   manage.html + manage_regional.html, the "collecter des colis depuis le transporteur" modal)
   removed from this rule — this is the EXACT same "constrain the reader box (aspect-ratio +
   overflow:hidden) and force the injected video to object-fit:cover !important" pattern that
   LOX-1687's own investigation spent 3 failed attempts on and concluded doesn't survive real
   mobile browsers, then reverted platform-wide from every page's own inline CSS — this global
   stylesheet rule was the one copy of that exact pattern the revert missed, because it lives
   here (component-level, id-selector) rather than inline in those templates. Symptom matched
   precisely: "la caméra s'affiche active (voyant caméra du téléphone allumé) mais n'est pas
   visible dans la popup" — a live stream with nothing rendered is the signature of the forced
   crop box fighting the library's own sizing inside a modal, not a getUserMedia failure.
   #v3-sc-scan-reader/#v3-trp-scan-reader/#v3-mfd-reader/#v3-scan-reader are left as-is: those
   are the pages LOX-1687 itself named as the confirmed-working references (never wrapped in a
   modal that starts hidden), so there's no reported bug there to fix — only the two ids actually
   reported broken are touched, per the same "don't guess blindly" lesson. */
@media (max-width: 639px) {
  #v3-sc-scan-reader,
  #v3-mfd-reader,
  #v3-trp-scan-reader,
  #v3-scan-reader {
    aspect-ratio: 1 / 1;
    overflow: hidden;
  }
  #v3-sc-scan-reader video,
  #v3-mfd-reader video,
  #v3-trp-scan-reader video,
  #v3-scan-reader video {
    width: 100% !important;
    height: 100% !important;
    object-fit: cover !important;
  }
}

/* LOX-1698: "taille de la caméra sur desktop, beaucoup trop grande ! il faut l'afficher en
   miniature ... sur toutes les pages DESKTOP qui affichent la caméra" — every reader container
   is just `width:100%`, so on a desktop-width card (500-700px+) the injected <video> — sized off
   the container's width, native aspect ratio preserved — renders correspondingly huge. This is a
   plain max-width cap, not the height/overflow + forced object-fit pattern LOX-1687 ruled out:
   it only limits how wide the container is allowed to grow, the video still sizes itself off
   that (now smaller) width exactly the way the library already expects, nothing to fight. Safe
   to include #v3-sc-reader-in/#v3-sc-reader-out here too (unlike the mobile square-crop rule
   above) for the same reason. #v3-scan-reader (the shared admin _scan_tool_panel.html) already
   ships its own `max-width:260px` inline and is left alone. */
@media (min-width: 640px) {
  #v3-sc-scan-reader,
  #v3-mfd-reader,
  #v3-trp-scan-reader,
  #v3-sc-reader-in,
  #v3-sc-reader-out {
    max-width: 280px;
    margin-left: auto;
    margin-right: auto;
  }
}


/* ============ css/components/v3-dialogs.css (3393 chars) ============ */
/* 2026-08-24 user request: "je ne veux pas du tout de popup du navigateur dans la plateforme
   entière" — v3Alert()/v3Confirm() (js/v3-dialogs.js) are the single in-app replacement for every
   native alert()/confirm() call across ALL V3 apps (admin/sc/vendor/trp/saas_admin), loaded once
   from base_v3.html so no page needs its own copy. Markup/visual language is a straight promotion
   of two patterns that already existed page-locally before this: app_trp_v3's v3-alert (2026-08-23)
   and app_vendor_v3's transactions/list.html #v3-confirm-modal (2026-08-14) — both are now built by
   JS instead of duplicated per-page HTML, but keep the exact same look. */
.v3-alert-overlay,
.v3-confirm-overlay {
  position: fixed;
  inset: 0;
  z-index: 1200;
  background: color-mix(in oklch, black 60%, transparent);
  display: none;
  align-items: center;
  justify-content: center;
  padding: var(--v3-space-4);
}
.v3-alert-overlay--visible,
.v3-confirm-overlay--visible { display: flex; }

.v3-alert-box {
  background: var(--v3-color-card);
  color: var(--v3-color-card-fg);
  border-radius: var(--v3-radius-xl);
  box-shadow: var(--v3-shadow-elevated);
  max-width: 22rem;
  width: 100%;
  padding: var(--v3-space-4);
  text-align: center;
}
.v3-alert-box__icon {
  width: 3rem;
  height: 3rem;
  border-radius: 999px;
  display: grid;
  place-items: center;
  margin: 0 auto var(--v3-space-3);
  font-size: 1.25rem;
}
.v3-alert-box--error .v3-alert-box__icon { background: var(--v3-color-destructive-soft); color: var(--v3-color-destructive); }
.v3-alert-box--success .v3-alert-box__icon { background: var(--v3-color-success-soft); color: var(--v3-color-success); }
.v3-alert-box--warning .v3-alert-box__icon { background: var(--v3-color-warning-soft); color: var(--v3-color-warning); }
.v3-alert-box--info .v3-alert-box__icon { background: var(--v3-color-primary-soft); color: var(--v3-color-primary); }

.v3-alert-box__message {
  font-size: 0.9375rem;
  line-height: 1.4;
  margin-bottom: var(--v3-space-4);
  white-space: pre-line;
}
.v3-alert-box__btn { width: 100%; }

/* v3Confirm — same card language as v3Alert, split OK/Cancel footer instead of a single button,
   ported verbatim from app_vendor_v3's original per-page #v3-confirm-modal. */
.v3-confirm-box {
  width: 100%;
  max-width: 21.25rem;
  background: var(--v3-color-card);
  color: var(--v3-color-card-fg);
  border-radius: var(--v3-radius-xl);
  overflow: hidden;
  box-shadow: var(--v3-shadow-elevated);
  text-align: center;
}
.v3-confirm-box__body { padding: var(--v3-space-5) var(--v3-space-4) var(--v3-space-4); }
.v3-confirm-box__icon {
  width: 3.25rem;
  height: 3.25rem;
  border-radius: 50%;
  background: var(--v3-color-destructive-soft);
  color: var(--v3-color-destructive);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.375rem;
  margin: 0 auto var(--v3-space-3);
}
.v3-confirm-box--info .v3-confirm-box__icon { background: var(--v3-color-primary-soft); color: var(--v3-color-primary); }
.v3-confirm-box__message { font-size: 0.9375rem; font-weight: 500; line-height: 1.4; white-space: pre-line; }
.v3-confirm-box__actions { display: flex; border-top: 1px solid var(--v3-color-border); }
.v3-confirm-box__btn {
  flex: 1;
  border-radius: 0;
  padding: var(--v3-space-3);
  border: none;
}
.v3-confirm-box__btn--ok { border-left: 1px solid var(--v3-color-border); }


/* ===== inline <style> #0 ===== */

  .v3-motd-banner--empty:hover { background: color-mix(in oklch, var(--v3-color-muted) 45%, transparent) !important; }


/* ===== inline <style> #1 ===== */

  .v3-ticket-pill {
    display: inline-flex; align-items: center; gap: 0.5rem;
    height: 2.5rem; padding: 0 0.875rem 0 0.75rem;
    border-radius: 999px; border: 1px solid var(--v3-color-border);
    background: color-mix(in oklch, var(--v3-color-muted) 40%, transparent);
    color: var(--v3-color-fg); text-decoration: none;
    font-size: 0.8125rem; font-weight: 500;
    transition: background 0.15s ease;
  }
  .v3-ticket-pill:hover { background: color-mix(in oklch, var(--v3-color-muted) 60%, transparent); }
  .v3-ticket-pill--new {
    border-color: color-mix(in oklch, var(--v3-color-destructive) 40%, transparent);
    background: var(--v3-color-destructive-soft);
    color: var(--v3-color-destructive);
    box-shadow: 0 0 16px -4px var(--v3-color-destructive);
    font-weight: 600;
  }
  .v3-ticket-pill--new:hover { background: color-mix(in oklch, var(--v3-color-destructive-soft) 150%, transparent); }
  .v3-ticket-pill__count {
    display: inline-flex; align-items: center; justify-content: center;
    width: 1.25rem; height: 1.25rem; border-radius: 999px;
    font-size: 0.625rem; font-weight: 700;
    background: var(--v3-color-destructive); color: var(--v3-color-destructive-fg);
  }
  .v3-ticket-pill--new .v3-ticket-pill__count { animation: v3-ticket-pulse 1.8s ease-in-out infinite; }
  .v3-ticket-pill__count--primary { background: var(--v3-color-primary-soft); color: var(--v3-color-primary); }
  .v3-ticket-pill__count--success { background: var(--v3-color-success-soft); color: var(--v3-color-success); }
  .v3-ticket-pill__count--muted { background: var(--v3-color-muted); color: var(--v3-color-muted-fg); }
  .v3-ticket-pill__dot {
    width: 0.375rem; height: 0.375rem; border-radius: 999px;
    background: var(--v3-color-destructive);
    animation: v3-ticket-pulse 1.8s ease-in-out infinite;
  }
  @keyframes v3-ticket-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.45; }
  }


/* ===== inline <style> #2 ===== */

    #v3-sub-refresh-close:disabled {
      background: var(--v3-color-muted);
      color: var(--v3-color-muted-fg);
      box-shadow: none;
      cursor: not-allowed;
      opacity: 0.7;
    }
  

/* ===== inline <style> #3 ===== */

  /* "Statut des colis" donut liveliness (2026-08-10, ported from app_sc_v3's dashboard) —
     load-in grow/spin, hover glow on the ring, and legend rows that highlight on hover. Pure
     CSS + a small count-up script below; no charting library. */
  @keyframes v3DonutGrowIn {
    from { opacity: 0; transform: scale(0.75) rotate(-90deg); }
    to   { opacity: 1; transform: scale(1) rotate(0deg); }
  }
  .v3-donut__ring {
    animation: v3DonutGrowIn 0.7s cubic-bezier(.34,1.56,.64,1) both;
    transition: transform 0.25s ease, filter 0.25s ease;
  }
  .v3-donut--live:hover .v3-donut__ring {
    transform: scale(1.05);
    filter: drop-shadow(0 4px 14px color-mix(in oklch, var(--v3-color-primary) 35%, transparent));
  }
  .v3-donut__legend-row {
    transition: background 0.15s ease, transform 0.15s ease;
  }
  .v3-donut__legend-row:hover {
    background: color-mix(in oklch, var(--v3-color-bg) 60%, transparent);
    transform: translateX(2px);
  }
  /* 2026-08-10 user request: hovering a wedge of the donut shows its exact percentage. */
  .v3-donut-tooltip {
    position: fixed;
    z-index: 60;
    display: none;
    pointer-events: none;
    background: var(--v3-color-card);
    border: 1px solid var(--v3-color-border);
    border-radius: var(--v3-radius-md);
    box-shadow: var(--v3-shadow-elevated);
    padding: 0.375rem 0.625rem;
    font-size: 0.75rem;
    line-height: 1.3;
    white-space: nowrap;
  }
  .v3-donut-tooltip strong { display:block; font-size:0.8125rem; }
