/*
 * BNESIM Admin — UI utility components (P2)
 *
 * Small set of token-powered building blocks. Namespaced `ui-*` to avoid
 * colliding with Bootstrap 3 classes and with the page-level flex/gap
 * utilities that many Alpine pages ship in their own inline <style> blocks.
 *
 * Use these for new UI. Legacy markup (`.white-box`, `.badge`, `.panel`,
 * Bootstrap alerts) keeps working — the migration to these utilities happens
 * as we touch each page.
 */

/* ─── Card ──────────────────────────────────────────────────────────── */

.ui-card {
    background-color: var(--bg-surface);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
    padding: 20px;
    transition:
        border-color var(--motion-fast) var(--ease-out),
        box-shadow var(--motion-fast) var(--ease-out);
}

.ui-card:hover {
    border-color: var(--border-default);
}

.ui-card--flush {
    padding: 0;
}

.ui-card__header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 16px 20px;
    border-bottom: 1px solid var(--border-subtle);
}

.ui-card__body {
    padding: 20px;
}

.ui-card__title {
    font-size: 15px;
    font-weight: 600;
    color: var(--text-primary);
    margin: 0;
}

.ui-card__subtitle {
    font-size: 13px;
    color: var(--text-secondary);
    margin: 2px 0 0;
}

.ui-card__overline {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-tertiary);
    margin: 0 0 6px;
}

/* ─── Badge (pill) ──────────────────────────────────────────────────── */

.ui-badge {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 3px 10px;
    font-size: 11px;
    font-weight: 600;
    line-height: 1.4;
    letter-spacing: 0.02em;
    border-radius: var(--radius-pill);
    border: 1px solid transparent;
    white-space: nowrap;
}

.ui-badge i {
    font-size: 10px;
}

.ui-badge--success {
    color: var(--success);
    background-color: var(--success-muted);
    border-color: var(--success);
}

.ui-badge--warning {
    color: var(--warning);
    background-color: var(--warning-muted);
    border-color: var(--warning);
}

.ui-badge--danger {
    color: var(--danger);
    background-color: var(--danger-muted);
    border-color: var(--danger);
}

.ui-badge--info {
    color: var(--info);
    background-color: var(--info-muted);
    border-color: var(--info);
}

.ui-badge--accent {
    color: var(--accent);
    background-color: var(--accent-muted);
    border-color: var(--accent);
}

.ui-badge--neutral {
    color: var(--text-secondary);
    background-color: var(--bg-subtle);
    border-color: var(--border-subtle);
}

/* ─── Status dot ────────────────────────────────────────────────────── */
/* Leading indicator for status labels so they read without color alone. */

.ui-status-dot {
    display: inline-block;
    width: 8px;
    height: 8px;
    border-radius: var(--radius-pill);
    margin-right: 6px;
    vertical-align: middle;
    background-color: currentColor;
}

.ui-status--success {
    color: var(--success);
}

.ui-status--warning {
    color: var(--warning);
}

.ui-status--danger {
    color: var(--danger);
}

.ui-status--info {
    color: var(--info);
}

.ui-status--accent {
    color: var(--accent);
}

.ui-status--muted {
    color: var(--text-tertiary);
}

/* ─── Empty state ───────────────────────────────────────────────────── */

.ui-empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 10px;
    padding: 48px 24px;
    text-align: center;
    color: var(--text-secondary);
}

.ui-empty-state__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 56px;
    height: 56px;
    border-radius: var(--radius-pill);
    background-color: var(--accent-muted);
    color: var(--accent);
    font-size: 22px;
}

.ui-empty-state__title {
    font-size: 16px;
    font-weight: 600;
    color: var(--text-primary);
    margin: 6px 0 0;
}

.ui-empty-state__desc {
    font-size: 13px;
    color: var(--text-secondary);
    margin: 0;
    max-width: 360px;
}

.ui-empty-state__cta {
    margin-top: 12px;
}

/* ─── Divider ───────────────────────────────────────────────────────── */

.ui-divider {
    border: 0;
    border-top: 1px solid var(--border-subtle);
    margin: 16px 0;
}

/* ═════════════════════════════════════════════════════════════════════
   Legacy pattern refresh (P4)
   ─────────────────────────────────────────────────────────────────────
   Promotes the patterns I used on the dashboard + customers pilot pages
   to the global CSS, so every page consuming them is refreshed for free:

     - `.white-box`                         — card surface with border + shadow
     - `.white-box > .box-title.*-background` — overline + accent-bar header
     - `.pagination-button`                 — Alpine-driven pagination pills
     - `.sorting` / `.sorting-asc` / `.sorting-desc` — sortable column headers
     - `.loading-container`                 — centered spinner overlay
     - `.skeleton` / `.chart-skeleton`      — loading placeholders

   The existing `style.css` + `custom.css` rules for these classes are
   overridden here with `!important` where needed so this layer wins.
   ═════════════════════════════════════════════════════════════════════ */

/* ─── White-box card ───────────────────────────────────────────────── */

.white-box {
    background-color: var(--bg-surface);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
    padding: 24px;
    color: var(--text-primary);
    transition:
        border-color var(--motion-fast) var(--ease-out),
        box-shadow var(--motion-fast) var(--ease-out);
}

/* ─── Box-title — overline + accent bar (kills the legacy ribbon) ──── */
/* The `.service-N-background` + `.standard-background` classes used to fill
   the h3 with a colored background. We re-read them as a *category color*
   for the left-side accent bar, preserving visual differentiation without
   the heavy ribbon look. */

.white-box > .box-title.service-1-background {
    --title-accent: #5dc1cc;
}

.white-box > .box-title.service-2-background {
    --title-accent: #df7158;
}

.white-box > .box-title.service-3-background {
    --title-accent: #ef994e;
}

.white-box > .box-title.service-4-background {
    --title-accent: #6abee9;
}

.white-box > .box-title.service-5-background {
    --title-accent: #efb950;
}

.white-box > .box-title.service-6-background {
    --title-accent: #244c7e;
}

.white-box > .box-title.service-7-background {
    --title-accent: #9723e9;
}

.white-box > .box-title.service-8-background {
    --title-accent: #d5537c;
}

.white-box > .box-title.service-9-background {
    --title-accent: #0099a8;
}

.white-box > .box-title.service-10-background {
    --title-accent: #40818c;
}

.white-box > .box-title.service-11-background {
    --title-accent: #990d36;
}

.white-box > .box-title.service-12-background {
    --title-accent: #c59f70;
}

.white-box > .box-title.service-help-background {
    --title-accent: #6b03ba;
}

.white-box > .box-title.service-halo-background {
    --title-accent: #676cd5;
}

.white-box > .box-title.standard-background {
    --title-accent: var(--accent-secondary);
}

.white-box > .box-title {
    background: transparent !important;
    color: var(--text-primary) !important;
    border-radius: 0 !important;
    padding: 0 !important;
    margin: 0 0 16px !important;
    font-size: 11px !important;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    display: flex;
    align-items: center;
    gap: 10px;
}

.white-box > .box-title::before {
    content: "";
    display: inline-block;
    width: 3px;
    height: 14px;
    flex: 0 0 auto;
    border-radius: var(--radius-pill);
    background: var(--title-accent, var(--accent));
}

/* Inline "UPDATE" or similar action buttons inside a box-title (dashboard,
   customer-feedbacks use this pattern with baked-in inline styles). */
.white-box > .box-title .btn {
    margin-left: auto !important;
    border-radius: var(--radius-pill) !important;
    padding: 2px 12px !important;
    font-size: 10px !important;
    min-width: 0 !important;
    letter-spacing: 0.06em;
    float: none !important;
}

/* ─── Pagination-button pattern (Alpine pages define these inline) ─── */
/* `!important` on colors so these rules beat per-page inline <style>
   redefinitions (which load later and would otherwise win by source order). */

.pagination-button {
    color: var(--text-primary) !important;
    background: var(--bg-elevated) !important;
    border: 1px solid var(--border-subtle) !important;
    padding: 4px 12px;
    font-size: 13px;
    font-weight: 600;
    border-radius: var(--radius-pill) !important;
    box-sizing: border-box;
    display: inline-block;
    min-width: 1.5em;
    text-align: center;
    cursor: pointer;
    transition:
        background-color var(--motion-fast) var(--ease-out),
        color var(--motion-fast) var(--ease-out),
        border-color var(--motion-fast) var(--ease-out);
}

.pagination-button:hover {
    background: var(--bg-subtle) !important;
    border-color: var(--border-default) !important;
}

.pagination-button.current,
.pagination-button.current:hover {
    cursor: default;
    color: var(--text-on-accent) !important;
    background: var(--accent) !important;
    border-color: var(--accent) !important;
}

.pagination-button:disabled {
    color: var(--text-tertiary) !important;
    border: 1px solid transparent !important;
    background: transparent !important;
    box-shadow: none;
    cursor: not-allowed;
}

.pagination-ellipsis {
    padding: 0 0.75rem;
    color: var(--text-tertiary) !important;
}

/* ─── Sortable column headers (custom Alpine-driven, not DataTables) ── */

.sorting,
.sorting-asc,
.sorting-desc {
    cursor: pointer;
    user-select: none;
    box-sizing: content-box;
}

.sorting::after,
.sorting-asc::after,
.sorting-desc::after {
    float: right;
    font-family: fontawesome;
    margin-left: 6px;
}

.sorting::after {
    content: "\f0dc";
    color: var(--text-tertiary) !important;
    opacity: 0.6;
}

.sorting-asc::after {
    content: "\f0de";
    color: var(--accent) !important;
}

.sorting-desc::after {
    content: "\f0dd";
    color: var(--accent) !important;
}

/* `.loading-container` isn't promoted globally — it means different things
   on different pages (absolute overlay vs. inline centered spinner). Each
   page keeps its own definition; we just ensure any absolute-positioned
   variant that uses a white backdrop picks up `--bg-overlay` via the
   per-page tokenization. */

/* ─── Skeleton / chart-skeleton placeholder ────────────────────────── */

.skeleton {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    z-index: 1;
    display: flex;
    justify-content: center;
    align-items: center;
    flex-direction: row;
    flex-wrap: wrap;
    gap: 1rem;
    overflow: hidden;
    padding: 1rem;
    background: var(--bg-overlay) !important;
    -webkit-backdrop-filter: blur(8px);
    backdrop-filter: blur(8px);
    border-radius: var(--radius-lg);
}

.chart-skeleton > i,
.chart-skeleton > svg {
    color: var(--text-tertiary) !important;
    opacity: 0.5;
    font-size: clamp(5rem, 12vw, 10rem);
    width: 1em;
    animation: tw-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}

.bg-skeleton {
    background-color: var(--bg-subtle) !important;
}

/* ─── Form-check / Form-switch (Bootstrap-4 polyfill used on many pages) ─ */
/* `profile.html`, `customer-activation.html`, `customer-details.html` each
   ship this same block of CSS inline with hardcoded blues. Promoting it
   here (tokenized) lets the per-page duplicates remain harmless while
   ensuring theme-aware styling on any page using the classes. */

.form-check {
    display: block;
    min-height: 1.5rem;
    padding-left: 1.5em;
    margin-bottom: 0.125rem;
}

.form-check .form-check-input {
    float: left;
    margin-left: -1.5em;
}

.form-check .form-check-label {
    margin-bottom: 0;
    color: var(--text-primary);
}

.form-check-input {
    width: 18px;
    height: 18px;
    margin-top: 0;
    vertical-align: middle;
    background-color: var(--bg-surface) !important;
    background-repeat: no-repeat;
    background-position: center;
    background-size: 12px 12px;
    border: 1.5px solid var(--border-default) !important;
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
    -webkit-print-color-adjust: exact;
    color-adjust: exact;
    cursor: pointer;
    transition:
        background-color var(--motion-fast) var(--ease-out),
        border-color var(--motion-fast) var(--ease-out);
}

.form-check-input[type="checkbox"] {
    border-radius: 4px;
}

.form-check-input[type="radio"] {
    border-radius: 50%;
}

.form-check-input:active {
    filter: brightness(90%);
}

.form-check-input:focus {
    border-color: var(--accent) !important;
    outline: 0;
    box-shadow: var(--focus-ring) !important;
}

.form-check-input:checked {
    background-color: var(--accent) !important;
    border-color: var(--accent) !important;
}

.form-check-input:checked[type="checkbox"] {
    background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none'%3e%3cpath d='M3 8.5l3.5 3.5L13 5' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3e%3c/svg%3e") !important;
}

.form-check-input:checked[type="radio"] {
    background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e") !important;
}

.form-check-input[type="checkbox"]:indeterminate {
    background-color: var(--accent) !important;
    border-color: var(--accent) !important;
    background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none'%3e%3cpath d='M4 8h8' stroke='%23fff' stroke-width='2' stroke-linecap='round'/%3e%3c/svg%3e") !important;
}

/* Global checkbox style — mirrors the "Remember me" login checkbox.
   Targets every native <input type="checkbox"> so bare inputs (no class)
   adopt the design too. Plugin-rendered widgets (Treeselect, Tagify,
   daterangepicker) don't use real checkboxes, so no exclusion needed.
   .login-checkbox visually hides its own input (position: absolute, 1px,
   opacity: 0) and renders an SVG via a sibling <span>, so this rule's
   visuals don't surface there.
   .form-switch keeps its 2em pill shape because its own descendant
   selector (`.form-switch .form-check-input`) outranks ours on width /
   border-radius and uses `!important` for its thumb background image. */
input[type="checkbox"] {
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
    width: 18px;
    height: 18px;
    vertical-align: middle;
    background-color: var(--bg-surface);
    background-repeat: no-repeat;
    background-position: center;
    background-size: 12px 12px;
    border: 1.5px solid var(--border-default);
    border-radius: 4px;
    cursor: pointer;
    margin: 0;
    transition:
        background-color var(--motion-fast) var(--ease-out),
        border-color var(--motion-fast) var(--ease-out);
}

input[type="checkbox"]:hover {
    border-color: var(--accent);
}

input[type="checkbox"]:checked {
    background-color: var(--accent);
    border-color: var(--accent);
    background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none'%3e%3cpath d='M3 8.5l3.5 3.5L13 5' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3e%3c/svg%3e");
}

input[type="checkbox"]:indeterminate {
    background-color: var(--accent);
    border-color: var(--accent);
    background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none'%3e%3cpath d='M4 8h8' stroke='%23fff' stroke-width='2' stroke-linecap='round'/%3e%3c/svg%3e");
}

input[type="checkbox"]:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

input[type="checkbox"]:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

.form-check-input:disabled {
    pointer-events: none;
    filter: none;
    opacity: 0.5;
}

.form-check-input:disabled ~ .form-check-label,
.form-check-input[disabled] ~ .form-check-label {
    opacity: 0.5;
}

.form-switch {
    padding-left: 2.5em;
    display: inline-flex;
    align-items: center;
}

.form-switch .form-check-input {
    width: 2em;
    margin-left: -2.5em;
    background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28148, 163, 184, 0.6%29'/%3e%3c/svg%3e") !important;
    background-position: left center;
    border-radius: 2em;
    transition:
        background-position 0.15s ease-in-out,
        background-color var(--motion-fast) var(--ease-out),
        border-color var(--motion-fast) var(--ease-out);
}

@media (prefers-reduced-motion: reduce) {
    .form-switch .form-check-input {
        transition: none;
    }
}

.form-switch .form-check-input:checked {
    background-position: right center;
    background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e") !important;
}

/* The pill's negative margin pulls it flush against the label, leaving no
   breathing room. Add a small gap so the switch and its text don't touch. */
.form-switch .form-check-label {
    margin-left: 0.5em;
}

/* Any <label> wrapping a radio gets aligned input + text out of the box,
   so pages don't need to repeat `flex items-center gap-2 cursor-pointer
   m-0 font-normal` (or Bootstrap's legacy `.radio-inline`) on every
   radio. inline-flex (not flex) keeps multiple radios flowing side-by-side
   when they sit in normal block context. */
input[type="radio"] {
    margin: 0 !important;
}

label:has(> input[type="radio"]) {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    cursor: pointer;
    margin: 0;
    font-weight: 400;
}

/* ─── Textarea ─────────────────────────────────────────────────────────
   Global tokenized textarea theme. Bootstrap 3 `.form-control` ships flat
   greys + a hardcoded blue focus glow that doesn't track our accent / dark
   mode. This block re-skins every <textarea> on the admin (including the
   ~30 .form-control textareas across customer-details, faq, coupons,
   campaigns, promo-details, esim-compatibility, etc.) using design tokens.
   ───────────────────────────────────────────────────────────────────── */

textarea,
textarea.form-control {
    display: block;
    width: 100%;
    min-height: 80px;
    padding: 8px 12px;
    font-family: inherit;
    font-size: 14px;
    line-height: 1.6;
    color: var(--text-primary);
    background-color: var(--bg-surface);
    background-image: none;
    border: 1px solid var(--border-default);
    border-radius: var(--radius-md);
    box-shadow: none;
    resize: vertical;
    transition:
        border-color var(--motion-fast) var(--ease-out),
        box-shadow var(--motion-fast) var(--ease-out),
        background-color var(--motion-fast) var(--ease-out);
}

textarea::placeholder {
    color: var(--text-tertiary);
    opacity: 1;
}

textarea:hover:not(:disabled):not([readonly]):not(:focus) {
    border-color: var(--border-strong);
}

textarea:focus,
textarea.form-control:focus {
    outline: 0;
    border-color: var(--accent);
    box-shadow: var(--focus-ring);
    background-color: var(--bg-surface);
}

textarea:disabled,
textarea[readonly] {
    background-color: var(--bg-subtle);
    color: var(--text-secondary);
    cursor: not-allowed;
}

/* ═════════════════════════════════════════════════════════════════════
   Stat card — compact neutral-surface card with a colored accent rail,
   small uppercase label, large bold value, optional muted unit suffix.
   Used on customer-details, enterprise, and other summary pages.
   ═════════════════════════════════════════════════════════════════════ */

.stat-card {
    position: relative;
    background: var(--bg-surface);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-sm);
    padding: 18px 22px;
    margin-bottom: 16px;
    display: flex;
    flex-direction: column;
    gap: 6px;
    min-height: 96px;
    overflow: hidden;
    transition:
        transform var(--motion-fast) var(--ease-out),
        box-shadow var(--motion-fast) var(--ease-out),
        border-color var(--motion-fast) var(--ease-out);
}

.stat-card::before {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    width: 4px;
    border-radius: var(--radius-pill);
    background: var(--text-tertiary);
}

.stat-card[data-accent="coral"]::before {
    background: #f97c50;
}

.stat-card[data-accent="sage"]::before {
    background: #8dc6bf;
}

.stat-card[data-accent="teal"]::before {
    background: #5dc1cc;
}

.stat-card[data-accent="navy"]::before {
    background: #244c7e;
}

.stat-card[data-accent="sky"]::before {
    background: #6abee9;
}

.stat-card[data-accent="amber"]::before {
    background: #efb950;
}

.stat-card[data-accent="indigo"]::before {
    background: #676cd5;
}

.stat-card[data-accent="gold"]::before {
    background: #c59f70;
}

.stat-card[data-accent="rose"]::before {
    background: #d5537c;
}

.stat-card.is-clickable {
    cursor: pointer;
}

.stat-card.is-clickable:hover {
    transform: translateY(-1px);
    box-shadow: var(--shadow-md);
    border-color: var(--border-default);
}

.stat-label {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--text-tertiary);
}

.stat-value {
    margin: 0 !important;
    font-size: 22px;
    font-weight: 700;
    color: var(--text-primary) !important;
    letter-spacing: -0.01em;
    line-height: 1.2;
    display: flex;
    align-items: baseline;
    gap: 8px;
    flex-wrap: wrap;
}

.stat-unit {
    font-size: 12px;
    font-weight: 500;
    color: var(--text-tertiary);
    letter-spacing: 0.02em;
    text-transform: none;
}

/* ─── Status badges ─ */
/* Muted-bg + solid-token-color pills for state indicators on the
   agency activation / provisioning history pages. */
.badge-active {
    background: var(--success-muted);
    color: var(--success);
}

.badge-failed {
    background: var(--danger-muted);
    color: var(--danger);
}

.badge-processing {
    background: var(--warning-muted);
    color: var(--warning);
}

.badge-scheduled {
    background: var(--info-muted);
    color: var(--info);
}

/* ─── Label / value detail row ─ */
/* Compact "label above, value below" pair used on detail pages
   (sim-card-details, sim-card-statistics). Pair the classes with
   utility-class layout (e.g. `flex flex-col gap-1`) on the wrapper. */
.detail-label {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--text-tertiary);
}

.detail-value {
    font-size: 14px;
    color: var(--text-primary);
    font-weight: 500;
    word-break: break-word;
}

/* ─── Semantic text-color utilities ─ */
/* Override Bootstrap 3's hardcoded .text-* colors with the project's
   design tokens so these classes stay theme-aware. !important wins over
   Bootstrap's own declarations without needing a more specific selector. */
.text-success {
    color: var(--success) !important;
}

.text-warning {
    color: var(--warning) !important;
}

.text-danger {
    color: var(--danger) !important;
}

.text-muted {
    color: var(--text-tertiary) !important;
}

.text-accent {
    color: var(--accent) !important;
}

/* ─── Global scroll-to-top floating button ─
   Auto-injected into every page that loads `js/scroll-to-top.js` (via
   foot.html). Fades in once the viewport has scrolled more than 200px.
   The `.hidden-top-btn` class is toggled from JS — define it alongside
   the button so both states live in one place. */
#scroll-to-top-button {
    display: block;
    position: fixed;
    bottom: 16px;
    right: 16px;
    z-index: 99;
    padding: 10px;
    min-width: 40px;
    border-radius: var(--radius-pill);
    background-color: var(--bg-elevated);
    border: 1px solid var(--border-subtle);
    color: var(--text-primary);
    box-shadow: var(--shadow-md);
    transition:
        opacity var(--motion-base) var(--ease-out),
        background-color var(--motion-fast) var(--ease-out),
        transform var(--motion-fast) var(--ease-out);
}

#scroll-to-top-button:hover {
    background-color: var(--bg-subtle);
    transform: translateY(-2px);
}

.hidden-top-btn {
    opacity: 0;
    cursor: default;
    pointer-events: none;
}

/* ─── Refresh-prompt banner stack ─
   Shared bottom-left container that holds any "refresh to apply" banners
   the watchdog scripts inject (access-watchdog, inactivity-watchdog, and
   any future siblings). Lets multiple prompts stack vertically with
   consistent spacing instead of overlapping. Pinned bottom-left so it
   doesn't fight the scroll-to-top FAB at bottom-right. Sits above the
   FAB (z-index 99) but below Bootstrap modals (z-index 1040+) so
   dialogs still take focus when open.

   `column-reverse` keeps the newest banner closest to the corner so the
   most recent alert is the most visually prominent — older ones get
   nudged upward as new ones arrive. The container itself ignores pointer
   events (so the page below stays clickable in the gap between banners)
   and each banner re-enables them. */
.refresh-banners-stack {
    position: fixed;
    bottom: 16px;
    left: 16px;
    z-index: 100;
    display: flex;
    flex-direction: column-reverse;
    gap: 12px;
    max-width: 420px;
    pointer-events: none;
}

.refresh-banners-stack > * {
    pointer-events: auto;
}

/* ─── Access-level watchdog banner ─
   Visual styling for an individual banner inside .refresh-banners-stack.
   The container handles positioning; the banner only owns its own look. */
.access-watchdog-banner {
    display: flex;
    align-items: center;
    gap: 16px;
    padding: 14px 16px;
    border-radius: var(--radius-md, 8px);
    background-color: var(--bg-elevated);
    border: 1px solid var(--border-subtle);
    color: var(--text-primary);
    box-shadow: var(--shadow-lg, var(--shadow-md));
    animation: access-watchdog-in 220ms var(--ease-out, ease-out);
}

.access-watchdog-content {
    display: flex;
    align-items: center;
    gap: 12px;
    flex: 1;
    min-width: 0;
}

.access-watchdog-icon {
    font-size: 22px;
    color: var(--warning, #f0a500);
    flex-shrink: 0;
}

.access-watchdog-text {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.access-watchdog-title {
    font-size: 14px;
    line-height: 1.3;
    color: var(--text-primary);
}

.access-watchdog-subtitle {
    font-size: 12px;
    line-height: 1.3;
    color: var(--text-secondary);
}

.access-watchdog-actions {
    display: flex;
    gap: 8px;
    flex-shrink: 0;
}

.access-watchdog-leaving {
    opacity: 0;
    transform: translateY(8px);
    transition:
        opacity 180ms var(--ease-out, ease-out),
        transform 180ms var(--ease-out, ease-out);
}

@keyframes access-watchdog-in {
    from {
        opacity: 0;
        transform: translateY(8px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

/* ─── System-health banner ─
   Reuses the .access-watchdog-banner shell (see js/system-health.js) and adds only
   what differs: a red icon when an integration is already broken rather than merely
   expiring, and a compact list of the affected apps. */
.system-health-banner-danger .access-watchdog-icon {
    color: var(--danger, #e74c3c);
}

.system-health-list {
    margin: 8px 0 0;
    padding-left: 18px;
    font-size: 12px;
    line-height: 1.5;
    color: var(--text-secondary);
}

.system-health-list strong {
    color: var(--text-primary);
}

.system-health-env {
    opacity: 0.65;
    font-family: ui-monospace, Menlo, Consolas, monospace;
}

@media (max-width: 600px) {
    .refresh-banners-stack {
        left: 12px;
        right: 12px;
        bottom: 12px;
        max-width: none;
    }
    .access-watchdog-banner {
        flex-direction: column;
        align-items: stretch;
    }
    .access-watchdog-actions {
        justify-content: flex-end;
    }
}

/* ─── Branded loading spinner ─
   A unique SVG-arc spinner scoped to one class so updating the look
   (stroke width, dash pattern, speed) only touches this rule.

   Uses CSS `mask` + inline SVG so the stroke follows `currentColor` —
   drops into buttons, alerts, labels without needing a color override.
   Spins at 600ms (≈2× faster than Tailwind's `.animate-spin`) for a
   snappier feedback feel.

   Usage:  <span class="spinner"></span>           (generic inline)
           <i class="spinner"></i>                 (inside a button or icon slot)
           <span class="spinner" style="font-size: 24px"></span>   (larger)

   To redesign the glyph: edit `--spinner-svg` to a different inline
   SVG — see https://yoksel.github.io/url-encoder/ for a URL-safe encoder.
   The SVG's `stroke` color must be any literal (black works) since the
   mask only uses the alpha channel; the visible color comes from
   `background-color: currentColor`. */

.spinner {
    --spinner-svg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='black'%3e%3cpath opacity='.2' fill-rule='evenodd' clip-rule='evenodd' d='M12 19C15.866 19 19 15.866 19 12C19 8.13401 15.866 5 12 5C8.13401 5 5 8.13401 5 12C5 15.866 8.13401 19 12 19ZM12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z'/%3e%3cpath d='M2 12C2 6.47715 6.47715 2 12 2V5C8.13401 5 5 8.13401 5 12H2Z'/%3e%3c/svg%3e");
    display: inline-block;
    width: 1em;
    height: 1em;
    vertical-align: -0.125em;
    background-color: currentColor;
    -webkit-mask: var(--spinner-svg) no-repeat center / contain;
    mask: var(--spinner-svg) no-repeat center / contain;
    animation: tw-spin 0.6s linear infinite;
}

/* ─── Alpine.js initial-load flash guard ─ */
/* Alpine resolves x-cloak attributes only after it boots. Without this
   rule, Alpine-driven elements flash visible for a frame before being
   hidden. Safe to include project-wide since no non-Alpine code sets
   x-cloak. */
[x-cloak] {
    display: none !important;
}

/* ─── Empty-state section ─ */
/* Centered placeholder shown in cards/tables when a query returns no
   rows. Pages that need a tighter or more spacious variant should add a
   modifier class locally rather than overriding globally. */
.empty-state {
    text-align: center;
    padding: 48px 16px;
    color: var(--text-tertiary);
}

.empty-state i {
    font-size: 42px;
    margin-bottom: 16px;
    display: block;
}

/* ─── Horizontally-scrollable small-table wrapper ─ */
/* Mirrors Bootstrap 4's .table-responsive-sm behavior for tables that
   overflow narrow viewports. Keep outside the Bootstrap breakpoint
   scope so it applies at every width. */
.table-responsive-sm {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
}

/* ─── Table row actions ─────────────────────────────────────────────── */
/* The action cell at the end of a table row. Replaces the seven different
   hand-written wrapper class strings (`flex flex-wrap gap-2`,
   `flex flex-row gap-2 flex-wrap`, …) and the inline `style="min-width:200px"`
   hint that stopped DataTables squeezing the Actions column down to the
   narrowest row. See ai_docs/SPECS/SPEC_ADMIN_table_row_actions.md.

   Buttons inside keep the standard shape from SPEC_ADMIN_button_style.md
   (`btn btn-sm btn-{variant} inline-flex items-center gap-2 justify-center`).

   Deliberately a flat wrap even for the densest cell (campaigns, 7 buttons):
   forcing the buttons into stacked sub-groups was measured and made things
   worse — a stacked group has a narrower max-content width, so the table layout
   hands the Actions column LESS space and the row wraps to more lines, not
   fewer. Order the buttons by category in the renderer instead, so the natural
   wrap falls on a category boundary. */
.row-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    min-width: 200px;
}

/* Campaigns is the densest action cell (5 buttons after the Cluster panel
   collapse: Info / Edit / Cancel / Send Now / Cluster). A wider floor keeps it
   on one line where the viewport allows; 300px below that still saves a wrap
   line vs the shared 200px and never triggers horizontal scroll (measured
   1024–1680px). The 560px floor starts at 1440px, not 1280px: at 1280px it
   fits the buttons on one line but squeezes the text columns so hard that
   EVERY row grows from wrapped Subject text (measured avg 104px vs 67px) — a
   net loss. Scoped to the campaigns table — a 560px floor on a shared
   2-button cell would be absurd. */
#campaigns-table .row-actions {
    min-width: 300px;
}

@media (min-width: 1440px) {
    #campaigns-table .row-actions {
        min-width: 560px;
    }
}

/* Third floor for the Test Send era: with 6 row buttons (Info / Edit / Cancel /
   Send Now / Test Send / Cluster) the single line needs ~680px, which only fits
   without squeezing the text columns from 1600px up. */
@media (min-width: 1600px) {
    #campaigns-table .row-actions {
        min-width: 680px;
    }
}

/* Floating panel for a row's secondary actions, opened by a trigger button
   in the same cell (campaigns: the "Cluster" button). The panel holds the
   SAME standard buttons (SPEC_ADMIN_button_style.md shape, full labels),
   just stacked vertically.

   position:fixed is load-bearing, not a styling choice: the ancestors
   (.table-responsive and .dt-hscroll) set overflow-y:hidden, which clips any
   absolutely-positioned dropdown. A fixed-position element escapes the clip —
   verified by hit-testing. The opener positions it next to the trigger and
   closes it on outside click / scroll / resize (see campaigns.js
   toggle_cluster_menu). */
.row-actions-menu {
    position: fixed;
    z-index: 1060; /* above the sticky header (1050 in chrome.css) */
    display: none;
    flex-direction: column;
    align-items: stretch;
    gap: 6px;
    padding: 8px;
    background: var(--bg-surface);
    border: 1px solid var(--border-subtle);
    border-radius: 8px;
    box-shadow: var(--shadow-lg);
}

.row-actions-menu.is-open {
    display: flex;
}

/* Buttons inside the panel left-align their icon+label so the stacked list
   reads as a menu; width comes from the widest label. */
.row-actions-menu > .btn {
    justify-content: flex-start;
}

/* ─── Image placeholder / upload preview frame ──────────────────────── */
/* Themed chrome around a preview <img> or an empty-state icon. Used by
   affiliate-edit and profile logo uploaders; set the size inline (width
   and height) — everything else is token-driven so it adapts to theme. */
.ui-image-placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-sm);
    background: var(--bg-subtle);
}

.ui-image-placeholder > img {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
}

.ui-image-placeholder > .fa {
    color: var(--text-tertiary);
}

/* ─── Box-title action button ───────────────────────────────────────── */
/* Compact circular icon-only action button (Update / Explorer / etc.) that
   floats on the right of a `.box-title` heading. Pair with `.btn .btn-info`
   for the base colour; this class handles size + position only.
   This is the one explicit exception to the project's icon+text rule —
   labels go in a tooltip (`data-toggle="tooltip" title="..."`) so the button
   stays flush in the header without competing with the title. */
.btn-box-action {
    float: right;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    min-width: 0;
    padding: 0;
    margin-top: 0;
    margin-right: -4px;
    font-size: 13px;
    line-height: 1;
    border-radius: 50%;
}

/* ─── Inline SVG icon ───────────────────────────────────────────────── */
/* Sits in the same visual cell as a Font Awesome glyph: ~0.9em square,
   baseline-aligned to match FA's font metrics so SVG icons align with FA
   siblings on the same line (dropdown rows, nav links). Inherits text
   colour via `currentColor`. Combine with `.fa-fw` when you need FA's
   fixed-width column behaviour (e.g. dropdown-menu rows that mix FA +
   SVG icons). */
.svg-icon {
    display: inline-block;
    width: 0.9em;
    height: 0.9em;
    vertical-align: -0.15em;
    fill: currentColor;
}

/* When combined with `.fa-fw`, expand to FA's fixed-width column (1.28em)
   so the icon cell aligns with FA siblings — the SVG content stays its
   natural ~0.9em via viewBox aspect-ratio preservation, centred in the
   wider cell exactly like an FA glyph. Compound selector beats the plain
   `.svg-icon` width above. */
.svg-icon.fa-fw {
    width: 1.28571429em;
}

/* ─── <bne-icon> custom-element host ────────────────────────────────
   Defined in /js/icons.js, renders an inner <svg.svg-icon>. The host
   itself acts as a flex centring slot so the SVG can use its natural
   aspect ratio (`width: auto`, height driven by font-size) without
   getting squeezed inside square tile containers. */
bne-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    line-height: 1;
    color: inherit;
}

bne-icon > svg {
    width: auto;
    height: 1em;
    fill: currentColor;
}

/* Larger glyph inside coloured icon tiles (cmdk results, search-result
   list). Tile contexts scale font-size up so the SVG fills more of the
   cell and matches FA siblings visually. */
.cmdk-item-icon bne-icon > svg,
.search-page-item-icon bne-icon > svg {
    height: 1.4em;
}

/* ─── App logo chip ─────────────────────────────────────────────────
   32px square that frames an app icon — used in promo/customer rows
   and in the targeted-apps multi-select. Accent-tinted background so
   the chip reads as a brand surface rather than a plain card. */
.custom-select-logo-container {
    width: 32px;
    height: 32px;
    border-radius: var(--radius-sm);
    border: 2px solid var(--accent-muted);
    box-shadow: var(--shadow-sm);
    overflow: hidden;
    transition:
        border-color var(--motion-fast) var(--ease-out),
        box-shadow var(--motion-fast) var(--ease-out);
}

.custom-select-logo {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* ─── Custom multi-select dropdown ──────────────────────────────────
   Popup container used by Alpine-driven multi-select pickers (target
   apps in promo-details, target-apps filter in promos advanced search,
   …). The trigger is a plain <button> styled with `.form-control`;
   this popup is an absolutely-positioned listbox rendered relative to
   the trigger's positioned ancestor. Width is intentionally driven by
   content + `min-w-[…]` utility on the markup, so callers can size it
   to fit their longest option without changing this base.

   Pair `.custom-select-container` (wraps the option list) with
   `.custom-select-item` (each `<button role="option">`). Highlight
   state (keyboard navigation) is toggled by the caller via the
   `.option-highlighted` class. */
.custom-select-container {
    border-radius: var(--radius-sm);
    top: calc(100% + 4px);
    /* High enough to overlap sibling form content (e.g. the promos language
       tab strip rendered just below the advanced search panel) while still
       staying under the global scroll-to-top FAB (z-index 99) and Bootstrap
       modals (z-index 1040+). */
    z-index: 50;
    display: flex;
    flex-direction: column;
    max-height: 300px;
    overflow-y: auto;
    box-shadow: var(--shadow-lg);
    padding: 0 8px;
    background-color: var(--bg-elevated);
    border: 1px solid var(--border-subtle);
}

.custom-select-item {
    padding: 8px 12px;
    background-color: transparent;
    color: var(--text-primary);
    border-bottom: 1px solid var(--border-subtle);
    cursor: pointer;
    border-radius: 0;
    transition: background-color var(--motion-fast) var(--ease-out);
    box-shadow: none;
}

.custom-select-item:hover,
.custom-select-item.option-highlighted {
    background-color: var(--accent);
    color: var(--text-on-accent);
}

.custom-select-item:last-child {
    border-bottom: none;
}

/* ─── Input group ───────────────────────────────────────────────────
   Bootstrap 3's `.input-group` was `display: table` with child cells.
   The design refresh added `.form-control { border-radius: var(--radius-md)
   !important }` in chrome.css, which broke the visual continuation —
   children rounded on all four corners instead of meeting flush. We
   override here, switching the layout to flex so the input + addon/
   button reads as one unified control. components.css loads after
   chrome.css, so these rules win the cascade. */
.input-group {
    display: flex !important;
    flex-wrap: nowrap;
    align-items: stretch;
    width: 100%;
    max-width: 100%;
    box-sizing: border-box;
}

/* Default: input fills the remaining row space.
   `flex: 1 1 auto` makes flex-basis follow the `width` property —
   so an inline `style="width: calc(100% - 150px)"` (coupons reward
   value field) or a utility `w-4/5` (coupons discount field) becomes
   the basis directly. For the plain search case where no width is
   declared, our `width: auto` (same specificity as Bootstrap's
   `.input-group .form-control { width: 100% }` but later in source
   order, so it wins) keeps basis intrinsic and `flex-grow: 1` then
   fills the remaining space beside the button. `min-width: 0` lets
   it shrink below intrinsic when the row gets tight. */
.input-group > .form-control {
    width: auto;
    flex: 1 1 auto;
    min-width: 0;
}

/* Page-level width-utility classes on a form-control inside `.input-group`
   need (0,3,0) specificity to win over Bootstrap's `.input-group
   .form-control { width: 100% }` (0,2,0) and our `width: auto` above.
   Only fractions actually used on `.form-control` inside an input-group
   today are promoted; add the matching pair if a new split is needed. */
.input-group > .form-control.w-1\/5 {
    width: 20%;
    flex: 0 0 auto;
}
.input-group > .form-control.w-4\/5 {
    width: 80%;
    flex: 0 0 auto;
}

/* Inline `style="width: ..."` (e.g. coupons.html extra-reward
   `calc(100% - 150px)` + `width: 150px`) is already (1,0,0,0)
   specificity, but our default rule above keeps `flex-grow: 1` which
   would stretch it past the declared width. Cap grow when an inline
   width attribute is present. */
.input-group > .form-control[style*="width"] {
    flex: 0 0 auto;
}

/* Flatten every child's radius, then re-round the outer corners on
   the first/last child. `!important` is required to beat
   `.form-control { border-radius … !important }` from chrome.css. */
.input-group > *,
.input-group > .input-group-btn > .btn {
    border-radius: 0 !important;
}

.input-group > :first-child,
.input-group > .input-group-btn:first-child > .btn {
    border-top-left-radius: var(--radius-md) !important;
    border-bottom-left-radius: var(--radius-md) !important;
}

.input-group > :last-child,
.input-group > .input-group-btn:last-child > .btn {
    border-top-right-radius: var(--radius-md) !important;
    border-bottom-right-radius: var(--radius-md) !important;
}

/* Addon / prefix label (e.g. `€`, currency selector). */
.input-group-text,
.input-group-addon {
    display: flex;
    align-items: center;
    padding: 6px 12px;
    margin-bottom: 0;
    white-space: nowrap;
    background-color: var(--bg-subtle);
    color: var(--text-secondary);
    border: 1px solid var(--border-subtle);
}

/* Icon-only button slot — search-style input-groups put a compact
   square button next to the input. The button keeps its colour class
   (`.btn-info`, etc.) and gets a tooltip/aria-label for the action.
   `.input-group-btn` is a `<span>`; force flex so the inner button
   stretches to the row height instead of inheriting the span's
   inline natural height. */
.input-group > .input-group-btn {
    display: flex;
    align-items: stretch;
    flex: 0 0 auto;
    width: auto;
}

.input-group-btn > .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0 14px;
    min-width: 42px;
    height: auto;
    line-height: 1;
}

/* ─── Language tab (pane chooser) ───────────────────────────────────────
   Used by multi-language editors (campaigns, coupons, faq) where the
   left rail lists language codes and the right pane shows the editable
   translation. Three pages used to ship near-identical inline copies of
   these rules; this is the consolidated source. Variations are handled
   with utilities on the markup: `h-full` if the strip should grow to the
   sibling pane height, `text-start` for left-aligned codes. */
.language-item {
    position: relative;
    padding: 4px;
    text-align: center;
    color: var(--text-primary);
    border: 1px solid var(--border-subtle);
    border-bottom-color: transparent;
    cursor: pointer;
    overflow: hidden;
    box-sizing: border-box;
    transition: all 300ms ease-in-out;
}

.language-item:first-of-type {
    border-top-left-radius: 3px;
}

.language-item:last-of-type {
    border-bottom-left-radius: 3px;
    border-bottom-color: var(--border-subtle);
}

.language-item:hover {
    color: var(--accent-secondary);
}

.language-item.active {
    border-right-color: transparent;
    color: var(--accent);
}

.language-item-empty-box {
    position: absolute;
    top: 90%;
    left: 80%;
    width: 20px;
    height: 20px;
    font-size: 20px;
    line-height: 1;
    opacity: 0.2;
    transform: translate(-50%, -50%) rotate(-15deg);
    transition: all 300ms ease-in-out;
}

.language-item.active .language-item-empty-box {
    color: var(--text-primary);
}

.language-item:hover .language-item-empty-box {
    color: var(--accent-secondary);
}
/* ── Segmented tab-pill control (e.g. ALL / eSIM / SIM CARD) ────────────
   Reusable pill tab group shared across views (customers, dashboard stock).
   The active pill is highlighted by `.tab-pill-bg` — a single absolutely
   positioned element that animates its `left` + `width` to slide under the
   active pill (driven by `slide_tab_pill_bg()` in utility.js). This mirrors
   the animated language-tabs slider but adapts to variable-width labels. */
.tab-pills {
    position: relative;
    display: flex;
    flex-direction: row;
    width: max-content;
    gap: 2px;
    padding: 3px;
    border-radius: var(--radius-pill);
    background: var(--bg-subtle);
    border: 1px solid var(--border-subtle);
}

/* Sliding highlight — JS sets left/width to match the active pill. */
.tab-pills > .tab-pill-bg {
    position: absolute;
    top: 3px;
    left: 3px;
    width: 0;
    height: calc(100% - 6px);
    border-radius: var(--radius-pill);
    background: var(--accent);
    box-shadow: var(--shadow-sm);
    pointer-events: none;
    z-index: 0;
    transition:
        left var(--motion-slow) var(--ease-out),
        width var(--motion-slow) var(--ease-out);
}

.tab-pills > .tab-pill {
    position: relative;
    z-index: 1;
    padding: 6px 14px;
    border-radius: var(--radius-pill);
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-secondary);
    cursor: pointer;
    user-select: none;
    transition: color var(--motion-fast) var(--ease-out);
}

.tab-pills > .tab-pill:hover {
    color: var(--text-primary);
}

.tab-pills > .tab-pill.active {
    color: var(--text-on-accent);
}
