/* ============================================================================
 * components.css — shared component vocabulary
 * ============================================================================
 *
 * Sibling to theme.css. Architecture per FRONTEND_CONSTITUTION rules 1/6/9/10:
 *
 *   theme.css        — design tokens ONLY (colors, fonts, semantic aliases)
 *   components.css   — shared component classes (this file)
 *
 * Discipline (enforced by extraction-thread:Reports-as-template, 2026-06-09):
 *
 *   1. Total hoist or no hoist.
 *      When a class lives here, EVERY page-inline definition AND override of
 *      that class dies in the same commit. Before any class lands here, grep
 *      its name across all page <style> blocks; the count must be zero.
 *
 *   2. No speculative hoists.
 *      A class enters this file only with at least one live consumer. Shared
 *      CSS nobody uses is the old debt in a new location.
 *
 *   3. Version-string atomicity.
 *      Any change to either theme.css or this file bumps `?v=` on ALL pages
 *      in the same commit. Until the v1 build step lands with hashed
 *      filenames, code review is the hash.
 *
 *   4. Canonical-variant note.
 *      When page versions of a class diverged at extraction time, prefer
 *      the Reports/devices version (DESIGN_SYSTEM.md names devices the
 *      reference page) and record the decision in a comment below.
 *
 *   5. Shared files own the namespace prefix, not just the class list.
 *      When a family is hoisted here, components.css owns the entire
 *      `<family>-*` prefix on every page that loads it — current and
 *      future. Bespoke patterns on any consumer page MUST live under a
 *      page-local prefix (e.g. `.note-drawer-*`, `.svc-modal-*`), not
 *      under the shared namespace, even when their style intent is
 *      unrelated. Otherwise they're "squatters" — they'll silently
 *      inherit canonical properties their bespoke rules don't override,
 *      and a future canonical edit hits them too. Enforcement: before
 *      every hoist commit, grep the family prefix across EVERY page
 *      that loads this file (not just the matrix family pages); any
 *      hit outside the canonical class list is a squatter and must be
 *      page-prefix-renamed in the same commit.
 *
 *      Sweep ledger (most recent first):
 *        2026-06-10  drawer-*  three buckets confirmed clean:
 *                     family    = devices/assets/sites/entities/users
 *                     squatters = alert/notes/service (renamed under
 *                                  alert-drawer-*, note-drawer-*,
 *                                  svc-drawer-* respectively)
 *                     consumers = 9 pages (avl-inspector, backlog, ble,
 *                                  brainrot, index, integrations,
 *                                  landing, login, subscription)
 *        2026-06-10  modal-*   sweep + steps 1/2/3 complete.
 *                     family    = 13 pages: admin, alert, automations-
 *                                  canvas, automations-library, card-
 *                                  factory, channel-factory, entities,
 *                                  notes, reports, reports-builder,
 *                                  service, tag-library, users
 *                     squatters renamed = .admin-modal-btn*,
 *                                  .billing-modal-* (full set),
 *                                  .chan-modal-row*, .tl-modal-field,
 *                                  .svc-modal-{close-btn,header},
 *                                  .{al,rep,rb,tl}-modal-text-body
 *                     alias      = .modal-backdrop ≡ .modal-overlay
 *                                  (consumers: alert, notes)
 *                     consumers/clean = 10 pages
 *                     state class (step 3) = converged to single `.open`.
 *                       Pre-convergence inventory was over-broad: my
 *                       initial 8/7/1 count for .visible/.active/.show
 *                       counted ALL classList ops (toasts, ctx-menus,
 *                       dirty-dots) — settled-predicate sweep showed
 *                       .active and .show had ZERO modal consumers;
 *                       .visible had 5 (automations-canvas, card-
 *                       factory, channel-factory, entities, users)
 *                       renamed to .open in JS via precision rewrites
 *                       scoped to canonical modal IDs only (36 calls).
 *                       Non-modal .visible/.active/.show usages were
 *                       left untouched.
 *        2026-06-10  card-*    actionable-card family extraction complete.
 *                     family = 8 pages: admin (no card-defs, consumer only),
 *                              alert, assets, devices, entities, index
 *                              (fleet panel + sites panel), notes, sites,
 *                              users. + service (not in family — .plan-card
 *                              is display-only).
 *                     name change: cousins renamed onto a NEW canonical
 *                       `.entity-card` rather than canonicalizing any
 *                       existing name, because .device-card meant two
 *                       different things on index vs devices (fleet card
 *                       vs list row). One name = one component.
 *                     cousins renamed: .device-card (index, devices, assets),
 *                       .gf-card (index), .geofence-card + .history-event-card
 *                       (sites), .company-card (entities), .user-card (users),
 *                       .event-card (alert), .note-card (notes),
 *                       .session-row (assets — becomes .entity-card-row
 *                       grid modifier).
 *                     tag split (the Option-1 taxonomy ruling):
 *                       .card-tag was 3 different components under one
 *                       name. Split into .entity-card-tag (interactive
 *                       removable, β body) for assets/devices/entities/
 *                       users; .entity-card-label (static display, γ body)
 *                       for sites + index sites panel.
 *                     children: gf-card-{name,meta,actions,btn,labels}
 *                       converged to .entity-card-{name,meta,actions,
 *                       btn,labels} vocabulary (single consumer for
 *                       most — discipline #2 kept page-inline, names
 *                       converged so future pages can hoist).
 *                     lane-crossing: fleet-panel.js (6 token rewrites,
 *                       no logic), 11 template literals across the 5
 *                       tag pages. Constructed-name hunt: zero hits.
 *                     selected glow opacity = donor's 0.45 (NOT draft
 *                       0.18 — see entity-card section below).
 *        2026-06-10  card-*    Option-1 reversal: .entity-card-label
 *                     RETIRED. The β-vs-γ split (interactive vs
 *                     display-only) was the matrix reading drift as
 *                     intent — sites' tags were display-only because
 *                     handlers were never built, not by design.
 *                     Operator polish loop caught it ("shouldn't tags
 *                     be interactive anywhere?"). All tag surfaces
 *                     now use one canonical .entity-card-tag; pages
 *                     without write handlers (sites, index sites
 *                     panel) just render via the same class until
 *                     their interactive behavior ships. Rule
 *                     "two names for two roles is taxonomy" was
 *                     right; "display-only" was an absence, not a
 *                     role. Documented to prevent re-splitting.
 *
 * ----------------------------------------------------------------------------
 * Family index
 * ----------------------------------------------------------------------------
 *   Filter pills + filter bar    .filter-pills, .filter-pill, .filter-bar
 *   Drawer (asset/admin)         .drawer-overlay, .drawer-modal, .drawer-header,
 *                                .drawer-title*, .drawer-tags*, .drawer-body,
 *                                .drawer-footer, .drawer-btn (+ variants),
 *                                .drawer-close
 *   Modal (centered overlay)     .modal-overlay (alias .modal-backdrop),
 *                                .modal-card (+ .modal-card-wide),
 *                                .modal-header, .modal-title, .modal-body,
 *                                .modal-footer, .modal-actions, .modal-close,
 *                                .modal-field (+ label/input/select/small)
 *   Entity card (actionable)     .entity-card (+ .selected, .has-flag),
 *                                .entity-card::before (top swoosh),
 *                                .entity-card.status-* (4 modifiers),
 *                                .entity-card.flag-* (4 modifiers + 2 pulse
 *                                anims), .entity-card-row (grid variant),
 *                                .entity-card-tag (+ -remove, .overflow)
 *
 * Future hoists land below in their own labelled section. Update this index
 * when adding a new family.
 *
 * ----------------------------------------------------------------------------
 * z-index ladder (shared overlays — keep coherent across pages)
 * ----------------------------------------------------------------------------
 *      0–9    page background art, dot grids, watermark
 *     10–99   in-flow chrome (filter bars, sticky headers) — NOTE: nav.js
 *             injects its bottom nav pills + drop-up menus at 99990–99999
 *             (a pre-existing outlier to be re-banded at v1; the rest of
 *             this ladder is the authoritative model)
 *    100–199  hover tooltips
 *   1000      toast notifications — DECISION (2026-06-10): toasts render
 *             UNDER open modals (2100). If a future use case needs
 *             "toast visible over modal", revise this entry rather than
 *             hacking a per-page z-index.
 *   2000      .drawer-overlay (asset/admin drawer)
 *   2100      .modal-overlay (delete-confirm + content modals) —
 *             stacks ABOVE the drawer so a modal opened from inside an
 *             open drawer (e.g. delete-confirm from entities drawer)
 *             renders on top.
 *   2200–    higher-priority modals (one-off; claim a band + document
 *             it next to its consumer)
 *   9999     emergency-priority overlays (debug, profiler) — avoid;
 *            nav's outlier already squats above this anyway
 * ============================================================================ */


/* ────────────────────────────────────────────────────────────────────────────
 * FILTER PILLS — bottom-bar segmented filter
 * ────────────────────────────────────────────────────────────────────────────
 * Consumers (as of 2026-06-09): reports, devices, assets, users, entities,
 * sites, tag-library, automations-library, alert
 *
 * Canonical decision: extracted from theme.css verbatim. entities.html and
 * sites.html previously carried a richer `:hover` override (cyan-tinted
 * background, translateY lift, accent glow shadow) with a comment claiming
 * to "mirror devices.html" — but devices.html has NO such inline override.
 * The comment was inaccurate; the override was drift. Both overrides killed
 * in the same extraction commit. Canonical hover is the theme version below.
 * If the richer hover is later judged correct, it lands HERE as the canonical
 * — not back in two page-inline copies.
 * ──────────────────────────────────────────────────────────────────────────── */

.filter-pills {
  display: flex;
  gap: 8px;
  padding: 12px 16px;
  flex-wrap: wrap;
}

.filter-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 14px;
  border-radius: 20px;
  border: 1px solid var(--border-default);
  background: var(--bg-surface);
  color: var(--text-secondary);
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  transition: all 0.15s ease;
  user-select: none;
  font-family: 'Inter', sans-serif;
}

.filter-pill:hover {
  background: var(--bg-hover);
  color: var(--text-primary);
  border-color: var(--border-strong);
}

.filter-pill.active {
  background: var(--accent-muted);
  color: var(--accent-primary);
  border-color: var(--accent-primary);
  font-weight: 600;
}

/* Accessible focus — per FRONTEND_CONSTITUTION rule 6 (a11y baseline).
   Keyboard users get a visible ring matched to the accent color. The
   :focus-visible pseudo means mouse-click focus stays clean. */
.filter-pill:focus-visible {
  outline: 2px solid var(--accent-primary);
  outline-offset: 2px;
}

.filter-pill .pill-count {
  font-weight: 700;
  font-family: 'JetBrains Mono', monospace;
  font-size: 13px;
}

.filter-pill .pill-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  display: inline-block;
}

/* Filter pill bar — sticky bottom position used on list pages. */
.filter-bar {
  position: sticky;
  bottom: 70px;
  z-index: 50;
  background: transparent;
  padding: 10px 16px;
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  align-items: center;
}

/* `.tag-filter-input` was extracted here from theme.css on 2026-06-09 and
 * then DELETED the same day per discipline #2 (no speculative hoists):
 * `grep -rl 'tag-filter-input' *.html components/ assets/js/` matches zero
 * consumers — the claimed 8-page consumer list was a substring-grep artifact
 * (pages use id="tag-filter" and `.active-tag-filter`, which match the
 * pattern `tag-filter` but not this class). If a list page later needs a
 * tag-filter input, recover the rules from git history (a8142be) or restyle
 * fresh against current tokens. */

/* Accessible focus parity with the rest of the form vocabulary. */
.tag-filter-input:focus-visible {
  border-color: var(--accent-primary);
  box-shadow: var(--input-focus-ring);
}


/* ────────────────────────────────────────────────────────────────────────────
 * DRAWER (asset / admin) — centered modal-drawer pattern
 * ────────────────────────────────────────────────────────────────────────────
 * Consumers (as of 2026-06-09): devices, assets, sites, entities, users
 *
 * Canonical decision (Group A wins):
 *   Pre-extraction the family split into two real groupings, NOT byte-
 *   identical as the original brief estimated:
 *     Group A — list-page pattern: devices, assets, sites
 *     Group B — admin-page pattern: entities, users (richer footer,
 *               explicit .drawer-close X button, slightly different
 *               base modal/header/title rules)
 *
 *   ~⅓ of the family was identical across all 5; ~⅔ split A vs B.
 *
 *   Picked Group A (devices-led per DESIGN_SYSTEM.md). Reasons:
 *     1. Majority — 3 pages no-op, 2 pages converge (entities + users).
 *     2. devices is the canonical reference page; absent any commit
 *        evidence of a deliberate "admin variant" decision, treat the
 *        divergence on entities/users as drift, not intent.
 *
 * Convergence effect:
 *   - devices / assets / sites: no-op (CSS deleted, identical rules
 *     served from this file instead).
 *   - entities / users: drawer modal/header/title shift to the devices
 *     pattern. Spot-checked in both themes at extraction time (entities
 *     drawer + users drawer); rendering shifts are intended convergence.
 *     Any FUTURE breakage (text overlap, clipped buttons) is fixed by a
 *     deliberate edit to the canonical rule below or a documented page-
 *     specific tweak — never by resurrecting the Group B variant.
 *
 * Adopted from Group B (these classes had no Group A definition):
 *   .drawer-close + .drawer-close:hover   (entities, users)
 *   .drawer-btn.danger + :hover           (entities, sites, users)
 *
 * Adopted from devices only:
 *   .drawer-btn.warning + :hover          (devices)
 *
 * OUT OF SCOPE — bespoke drawer patterns kept page-inline:
 *   alert.html:   .drawer-backdrop, .drawer-actions, .drawer-field*,
 *                 .drawer-section* — compact-card pattern, different use case.
 *   service.html: .drawer-empty*, .drawer-loading, .drawer-pane,
 *                 .drawer-asset-*, .drawer-error — asset-detail pane pattern.
 *
 * KNOWN DEFECT (not this commit's regression — surfaced by it).
 *   `--glass-surface` has no light-mode value, so .drawer-modal renders
 *   dark-on-light when the page flips to light theme. Pre-extraction this
 *   was also true on devices.html; this commit just made it visible on 4
 *   more pages. Fix belongs in theme.css (give --glass-surface a light-
 *   mode override), not here. Filed for the v1 token pass.
 * ──────────────────────────────────────────────────────────────────────────── */

.drawer-overlay {
    position: fixed;
    inset: 0;
    background: var(--bg-overlay-light);
    backdrop-filter: blur(3px);
    z-index: 2000;
    display: none;
    align-items: center;
    justify-content: center;
    opacity: 0;
    transition: opacity 0.3s ease;
}

.drawer-overlay.open {
    display: flex;
    opacity: 1;
}

.drawer-modal {
    width: 90%;
    max-width: 600px;
    max-height: 85vh;
    background: var(--glass-surface);
    backdrop-filter: blur(var(--glass-blur));
    border: 1px solid var(--glass-border);
    border-radius: 24px;
    display: flex;
    flex-direction: column;
    box-shadow: var(--shadow-lg);
    transform: scale(0.9);
    opacity: 0;
    transition: all 0.3s ease;
}

.drawer-overlay.open .drawer-modal {
    transform: scale(1);
    opacity: 1;
}

.drawer-header {
    padding: 12px 16px 8px 16px;
    border-bottom: none;
    display: flex;
    justify-content: space-between;
    align-items: center;
    flex-shrink: 0;
}

.drawer-title-area { flex: 1; }

.drawer-title {
    font-size: 18px;
    font-weight: 600;
    color: var(--text-primary);
    margin-bottom: 2px;
}

.drawer-subtitle {
    font-size: 11px;
    color: var(--text-muted);
    font-family: var(--font-mono);
}

.drawer-tags {
    padding: 8px 16px;
    border-bottom: 1px solid var(--border-subtle);
    flex-shrink: 0;
}

.drawer-tags:empty {
    display: none;
}

.drawer-body {
    flex: 1;
    overflow-y: auto;
    padding: 24px;
}

.drawer-footer {
    padding: 16px 24px;
    border-top: 1px solid var(--glass-border);
    display: flex;
    gap: 12px;
    flex-shrink: 0;
}

/* Close (X) button — present in admin-page consumers; harmless on
   list-page consumers that don't render one. */
.drawer-close {
    background: transparent;
    border: none;
    color: var(--text-muted);
    font-size: 24px;
    cursor: pointer;
    padding: 8px;
    border-radius: 8px;
    transition: all 0.15s;
    display: flex;
    align-items: center;
    justify-content: center;
}

.drawer-close:hover {
    color: var(--text-primary);
    background: var(--border-default);
}

/* Footer buttons */
.drawer-btn {
    flex: 1;
    background: var(--bg-surface);
    border: 1px solid var(--glass-border);
    color: var(--text-primary);
    padding: 12px 16px;
    border-radius: 8px;
    font-size: 12px;
    font-weight: 600;
    cursor: pointer;
    transition: all 0.15s;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    font-family: var(--font-sans);
}

.drawer-btn:hover { background: var(--border-default); }
.drawer-btn:disabled { opacity: 0.5; cursor: not-allowed; }

/* Accessible focus — FRONTEND_CONSTITUTION rule 6. */
.drawer-btn:focus-visible {
    outline: 2px solid var(--accent-primary);
    outline-offset: 2px;
}

.drawer-btn.primary {
    background: var(--accent-primary);
    border-color: var(--accent-primary);
    color: var(--text-inverse);
}
.drawer-btn.primary:hover { background: var(--accent-hover); }

.drawer-btn.warning {
    border-color: rgba(245, 158, 11, 0.5);
    color: rgba(245, 158, 11, 0.9);
}
.drawer-btn.warning:hover { background: rgba(245, 158, 11, 0.1); }

.drawer-btn.danger {
    background: transparent;
    border-color: var(--accent-red);
    color: var(--accent-red);
}
.drawer-btn.danger:hover { background: var(--status-red-bg); }


/* ────────────────────────────────────────────────────────────────────────────
 * MODAL — centered overlay + card pattern
 * ────────────────────────────────────────────────────────────────────────────
 * Consumers (as of 2026-06-10): admin, alert, automations-canvas,
 *   automations-library, card-factory, channel-factory, entities, notes,
 *   reports, reports-builder, service, tag-library, users
 *
 * Canonical decision summary (full body-level matrix in chat history):
 *   .modal-overlay   — z-index 2100, var(--overlay-bg), backdrop-filter
 *                       blur(var(--overlay-blur)). Token-driven so light
 *                       mode tunes once in theme.css.
 *   .modal-card      — entrance via scale(0.92) → scale(1), opacity 0 → 1,
 *                       mirroring the drawer card pattern at the modal
 *                       z-band. Glass-surface + blur, 18px radius.
 *   .modal-card-wide — modifier; max-width 640px / width 90%.
 *   .modal-header    — entities/users body (padding 20px 24px,
 *                       border-bottom var(--glass-border)).
 *   .modal-title     — 16px / 600 / text-primary.
 *   .modal-body      — STRUCTURAL scroll area only (padding 24px,
 *                       overflow-y auto, flex 1). The typography variant
 *                       (small descriptive paragraph in confirm modals)
 *                       was a SQUATTER — renamed page-locally to
 *                       .<al|rep|rb|tl>-modal-text-body in commit 1583d9d.
 *   .modal-footer    — STRUCTURAL bar with border-top. Distinct from
 *                       .modal-actions (which has NO border, sits inside
 *                       a body). Earlier sweeps mistook these for
 *                       synonyms; they are not, both are canonical.
 *   .modal-actions   — INLINE button group (flex flex-end, gap 10px,
 *                       no border, no padding bar). Used either as a
 *                       footer-stand-in on simple modals or at the end
 *                       of body content.
 *   .modal-close     — 28x28 button (service body — most-invested variant
 *                       at that size; 32x32 alert variant lost in
 *                       convergence). text-muted, transparent, hover bg.
 *   .modal-field     — form-field block (automations-canvas/channel-
 *                       factory body): flex-column, uppercase label,
 *                       mono input, focus-ring on input.
 *
 * --overlay-bg / --overlay-blur convergence:
 *   reports/automations-library/reports-builder/tag-library previously
 *   used var(--bg-overlay-light) + hardcoded blur(8px). They converge to
 *   the entities/users tokens here, which means a visibly darker dim and
 *   softer blur in dark mode on those 4 pages. Intended — light-mode
 *   tuning now happens via theme.css's --overlay-bg redefinition.
 *
 * Aliases — synonym for .modal-overlay:
 *   .modal-backdrop  — used on alert.html, notes.html only.
 *                       Pages keep their existing class= attrs; the
 *                       multi-selector rule below serves both.
 *                       (billing.html .modal-backdrop was a SQUATTER —
 *                       slide-in panel, not centered overlay — renamed
 *                       to .billing-modal-backdrop in commit 1583d9d.)
 *
 * z-index 2100 sits on the ladder above .drawer-overlay (2000) so a
 * delete-confirm modal opened from inside an open drawer renders on top.
 * Toasts (1000) render UNDER open modals by design — see ladder above.
 * ──────────────────────────────────────────────────────────────────────── */

.modal-overlay,
.modal-backdrop {
    position: fixed;
    inset: 0;
    background: var(--overlay-bg);
    backdrop-filter: blur(var(--overlay-blur));
    z-index: 2100;
    display: none;
    align-items: center;
    justify-content: center;
    opacity: 0;
    transition: opacity 0.25s ease;
}

/* OPEN STATE — canonical `.open`.
   State-class convergence landed 2026-06-10: the earlier
   .visible/.active/.show alternative selectors were retired after a
   settled-predicate sweep showed .active and .show had ZERO
   modal-context consumers (they were used for non-modal UI — toasts,
   ctx-menus, dirty-dots — which keep their .visible/.active/.show
   classes untouched), and .visible was renamed to .open across the
   5 pages that used it for modal opens (automations-canvas, card-
   factory, channel-factory, entities, users) via precision JS
   classList rewrites scoped to canonical modal IDs only. */
.modal-overlay.open,
.modal-backdrop.open {
    display: flex;
    opacity: 1;
}

.modal-card {
    width: 92vw;
    max-width: 440px;
    max-height: 85vh;
    background: var(--glass-surface);
    backdrop-filter: blur(var(--glass-blur));
    border: 1px solid var(--glass-border);
    border-radius: 18px;
    display: flex;
    flex-direction: column;
    box-shadow: var(--shadow-lg);
    transform: scale(0.92);
    opacity: 0;
    transition: all 0.25s ease;
}

.modal-overlay.open .modal-card,
.modal-backdrop.open .modal-card {
    transform: scale(1);
    opacity: 1;
}

.modal-card-wide {
    max-width: 640px;
    width: 90%;
}

.modal-header {
    padding: 20px 24px;
    border-bottom: 1px solid var(--glass-border);
    display: flex;
    justify-content: space-between;
    align-items: center;
    flex-shrink: 0;
}

.modal-title {
    font-size: 16px;
    font-weight: 600;
    color: var(--text-primary);
}

.modal-body {
    padding: 24px;
    overflow-y: auto;
    flex: 1;
}

.modal-footer {
    padding: 16px 24px;
    border-top: 1px solid var(--glass-border);
    display: flex;
    gap: 10px;
    justify-content: flex-end;
    flex-shrink: 0;
}

/* Inline button group — NOT a structural footer, no border-top.
   Used either as the entire footer on simple modals or at the end of
   body content. */
.modal-actions {
    display: flex;
    gap: 10px;
    justify-content: flex-end;
}

.modal-close {
    background: none;
    border: none;
    color: var(--text-muted);
    cursor: pointer;
    padding: 4px;
    border-radius: 6px;
    font-size: 16px;
    width: 28px;
    height: 28px;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: all 0.15s;
}

.modal-close:hover {
    color: var(--text-primary);
    background: var(--bg-inset);
}

.modal-close:focus-visible {
    outline: 2px solid var(--accent-primary);
    outline-offset: 2px;
}

/* Form-field block — flex column wrapping a label + input/select/textarea.
   Pages that need a different field shape rename to page-local. */
.modal-field {
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.modal-field label {
    font-size: 11px;
    font-weight: 500;
    color: var(--text-secondary);
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

.modal-field input,
.modal-field select,
.modal-field textarea {
    background: var(--bg-elevated);
    border: 1px solid var(--glass-border);
    color: var(--text-primary);
    font-family: var(--font-mono);
    font-size: 12px;
    padding: 8px 10px;
    border-radius: 6px;
    outline: none;
    transition: border-color 0.15s;
}

.modal-field input:focus,
.modal-field select:focus,
.modal-field textarea:focus {
    border-color: var(--accent-cyan);
}

.modal-field small {
    font-size: 10px;
    color: var(--text-muted);
}


/* ────────────────────────────────────────────────────────────────────────────
 * ENTITY CARD — actionable card pattern (the platform's "selectable thing")
 * ────────────────────────────────────────────────────────────────────────────
 * Consumers (as of 2026-06-10): admin, alert, assets, devices, entities,
 *   index (fleet panel + sites panel), notes, service, sites, users
 *
 * The name change is deliberate: pre-extraction, the same actionable card
 * lived under .device-card (twice — index fleet card AND devices list row,
 * different components), .gf-card / .geofence-card, .company-card,
 * .user-card, .event-card, .note-card, .history-event-card, .session-row.
 * All renamed onto .entity-card so "selectable entity" is one component
 * across the platform.
 *
 * Two ratified design decisions:
 *   1. Background = raised surface (--surface-2), NOT --bg-inset-deep.
 *      The fleet panel's prior inset usage was a semantic error
 *      (actionable cards sit ON the surface, not RECESSED into it).
 *      Surface scale lives in theme.css (--surface-1/2/3 + --surface-border
 *      + --surface-shadow). Theme-split: pronounced border in dark, glass +
 *      shadow in light.
 *   2. Hover = lift + shadow + top swoosh (translateY(-2px) + var(--shadow-md)
 *      + ::before gradient fade-in). One hover vocabulary platform-wide.
 *
 * Selected = accent-cyan border + outer glow ring. Donor: index.html's
 * fleet card. DISCIPLINE #4 NOTE: glow uses donor's `rgba(…, 0.45)` opacity
 * verbatim (not an earlier-draft `0.18`). Single value across themes; if
 * light-mode requires softening later, split via a token (`--selected-glow`)
 * rather than re-divergence here.
 *
 * Status modifiers — platform vocabulary (current consumers: index, assets):
 *   .status-{running,stopped,offline,last-known}  — left-border color
 *   .flag-{info,warning,urgent,critical}          — severity left-border +
 *     pulse animations on urgent (slow) / critical (fast). Notes' severity
 *     was previously a separate vocabulary (.critical/.urgent/etc.); it's
 *     now converged onto .flag-* (severity-color palette is identical, the
 *     vocabulary unification removes a needless dialect).
 *   Availability ≠ consumption: rules are present on every page loading
 *   components.css, but no visible effect unless JS adds the modifier.
 *
 * Page-local modifiers retained inline (NOT in this canonical block):
 *   .entity-card.hover-linked          (index — Gantt-row linkage)
 *   .entity-card.expanded + children   (index — fleet-panel card expansion)
 *   .entity-card.dim                   (devices, assets — multi-select)
 *   .entity-card.suspended             (entities, users — domain state)
 *   .entity-card.acknowledged/.dismissed (alert — alert lifecycle)
 *
 * Sub-vocabulary canonicals (this block):
 *   .entity-card-row   — grid-layout shape modifier (consumer: assets)
 *   .entity-card-tag / -tag-remove / -tag.overflow — interactive removable
 *     tag chip. β body (entities/users variant) chosen as canonical;
 *     α body (assets/devices, display:none/inline + padding-shift on hover)
 *     converged to β (opacity 0→0.7 fade, no layout shift, max-width
 *     truncation built-in). The α padding-shift idiom was charming but
 *     reflowed on every hover; β stays put. A11y note (carry to v1
 *     a11y pass): opacity-0 remove buttons remain technically clickable
 *     by keyboard/screen reader users — pre-existing behavior across all
 *     β consumers, not a new defect.
 *   .entity-card-label / -label.overflow — display-only tag (γ body from
 *     sites; padding 2px 8px chosen over δ's 1px 6px for legibility).
 *     Consumers: sites + index (sites panel). δ converges up one pixel;
 *     if the denser index sites panel suffers, that's a deliberate page
 *     decision later.
 *
 * Page-local children kept page-inline (single consumers, discipline #2):
 *   .entity-card-name/meta/labels/actions/btn (index sites panel —
 *     other pages have no equivalent children yet)
 *   .entity-card-expanded* / -condensed / -field-label / -link-btn / -sep
 *     (index fleet card)
 *   .entity-card-top (notes)
 *   If another page consumes any of these children, hoist them.
 * ──────────────────────────────────────────────────────────────────────── */

.entity-card {
    position: relative;
    padding: 12px 20px;
    background: var(--glass-surface);
    border: 1px solid var(--surface-border);
    border-radius: 16px;
    overflow: hidden;
    box-shadow: var(--surface-shadow);
    cursor: pointer;
    transition: all 0.25s ease;
}

.entity-card::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 3px;
    background: linear-gradient(
        90deg,
        var(--card-status-color, var(--accent-primary)),
        transparent
    );
    opacity: 0;
    transition: opacity 0.2s ease;
    pointer-events: none;
}

.entity-card:hover {
    transform: translateY(-2px);
    box-shadow: var(--shadow-md);
    border-color: rgba(var(--accent-primary-rgb), 0.3);
}

.entity-card:hover::before {
    opacity: 0.6;
}

.entity-card:focus-visible {
    outline: 2px solid var(--accent-primary);
    outline-offset: 2px;
}

.entity-card.selected {
    transform: translateY(-2px);
    border-color: var(--accent-cyan, #22d3ee);
    box-shadow:
        0 0 0 1px var(--accent-cyan, #22d3ee),
        0 0 24px 2px rgba(var(--accent-primary-rgb), 0.45);
}

.entity-card.selected::before {
    opacity: 1;
}

/* ── Status modifiers — left-border color, platform vocabulary ───────── */
.entity-card.status-running    { border-left-color: var(--status-running); }
.entity-card.status-stopped    { border-left-color: var(--status-stopped); }
.entity-card.status-offline    { border-left-color: var(--status-offline); }
.entity-card.status-last-known { border-left-color: var(--status-last-known); }

/* ── Flag (severity) modifiers — !important wins over status-* ──────── */
.entity-card.has-flag { /* base set position:relative; this rule kept for grep visibility */ }

.entity-card.flag-info {
    border-left-color: var(--severity-info, #22d3ee) !important;
}
.entity-card.flag-warning {
    border-left-color: var(--severity-warning, #f59e0b) !important;
}
.entity-card.flag-urgent {
    border-left-color: var(--severity-urgent, #f97316) !important;
    animation: entity-card-flag-pulse-slow 2s ease-in-out infinite;
}
.entity-card.flag-critical {
    border-left-color: var(--severity-critical, #ef4444) !important;
    animation: entity-card-flag-pulse-fast 1s ease-in-out infinite;
}

@keyframes entity-card-flag-pulse-slow {
    0%, 100% { box-shadow: inset 4px 0 0 var(--severity-urgent, #f97316); }
    50%      { box-shadow: inset 4px 0 0 transparent; }
}
@keyframes entity-card-flag-pulse-fast {
    0%, 100% { box-shadow: inset 4px 0 0 var(--severity-critical, #ef4444); }
    50%      { box-shadow: inset 4px 0 0 transparent; }
}

/* ── Grid-layout variant — the second archetype ──────────────────────
 * THE TWO ENTITY-CARD ARCHETYPES
 *   1. Stacked (default .entity-card alone): content flows top-to-bottom,
 *      no columns. Consumers: index (fleet panel), sites, notes, alert.
 *   2. Grid row (.entity-card.entity-card-row): content distributes
 *      across columns aligned to a header row above the list. Consumers:
 *      assets, devices, entities, users (list pages with .column-headers).
 *
 * They share all the chrome the family unifies (surface, border, hover,
 * ::before swoosh, .selected glow, .status-* + .flag-* modifiers) but
 * are genuinely different in LAYOUT. The 76c6a48 extraction wrongly
 * treated them as one — list-page cards lost their grid and cells
 * collapsed left. This modifier restores grid-row behavior.
 *
 * Column count and ratios are page-specific by definition (each list
 * page's column-headers row has its own column ratio set). Canonical
 * provides the grid BEHAVIOR ONLY; page-local rules declare their own
 * `grid-template-columns` (and any per-page gap/padding overrides).
 *
 * Pattern (page-local declaration alongside .column-headers):
 *   .column-headers,
 *   .entity-card-row {
 *       grid-template-columns: <page-specific>;
 *       gap: <page-specific>;
 *   }
 * ──────────────────────────────────────────────────────────────────── */
.entity-card-row {
    display: grid;
    align-items: center;
}

/* ── Tag chip (interactive, removable) — β body from entities/users ─── */
.entity-card-tag {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 2px 8px;
    border-radius: 10px;
    font-size: 11px;
    font-weight: 500;
    color: var(--text-inverse);
    white-space: nowrap;
    max-width: 120px;
    overflow: hidden;
    text-overflow: ellipsis;
}

.entity-card-tag.overflow {
    background: var(--badge-bg);
    color: var(--text-muted);
    font-size: 10px;
}

.entity-card-tag:hover .entity-card-tag-remove {
    opacity: 0.7;
}

.entity-card-tag-remove {
    cursor: pointer;
    opacity: 0;
    font-size: 13px;
    line-height: 1;
    transition: opacity 0.15s ease;
}

.entity-card-tag-remove:hover {
    opacity: 1;
}

/* (.entity-card-label retired 2026-06-10 — the Option-1 split into
   interactive .entity-card-tag vs display-only .entity-card-label
   was reversed after operator polish-loop surfaced that sites'
   labels weren't display-only by design, they were display-only
   because nobody ever built the handlers. The matrix read drift as
   intent; the rule "two names for two roles is taxonomy" was right
   but I applied it backwards — "display-only" is an absence of
   handlers, not a distinct role. One canonical .entity-card-tag now
   serves every tag surface; pages that can't currently edit just
   render via the same class until handlers ship.

   Viewer-context decision (recorded 2026-06-10, NOT drift):
   The index.html sites panel (the narrow geofence list next to
   the map) renders .entity-card-tag chips but deliberately has NO
   per-card + button or X-remove handlers. That panel is a viewer
   context — a glance-surface that lives next to the map, NOT a
   management surface. Tag editing belongs on /sites, where
   geofence management happens; symmetrising add/remove into the
   280px sidebar would be symmetry-for-symmetry's-sake and add
   noise. If a future seat thinks this asymmetry is drift, read
   this paragraph first.) */

/* ============================================================
   BUTTON FAMILY
   .btn (base) · .btn-primary · .btn-ghost · .btn-danger
   .btn-sm · .btn-icon
   Canonical source: reports-builder modal + service.html hover
   Consumers: alert, automations-library, backlog, card-factory,
   integrations, notes, reports, reports-builder, service,
   tag-library + any future page loading components.css
   ============================================================ */

.btn {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 8px 16px;
    border-radius: 8px;
    font-size: 12px;
    font-weight: 500;
    cursor: pointer;
    font-family: var(--font-sans);
    transition: all 0.15s ease;
    border: 1px solid var(--border-default);
    background: var(--glass-surface);
    color: var(--text-secondary);
}
.btn:hover {
    background: var(--bg-surface-hover);
    border-color: var(--border-strong);
    color: var(--text-primary);
}
.btn[disabled], .btn:disabled {
    opacity: 0.5;
    cursor: not-allowed;
    transform: none;
    box-shadow: none;
}
.btn i { font-size: 14px; }

.btn-primary {
    background: var(--accent-primary);
    color: var(--text-inverse);
    border-color: var(--accent-primary);
    font-weight: 600;
}
.btn-primary:hover {
    background: var(--accent-hover);
    border-color: var(--accent-hover);
    transform: translateY(-1px);
    box-shadow: 0 4px 12px rgba(var(--accent-primary-rgb), 0.3);
}
.btn-primary[disabled], .btn-primary:disabled {
    transform: none;
    box-shadow: none;
}

.btn-ghost {
    background: transparent;
    border-color: var(--glass-border);
    color: var(--text-secondary);
}
.btn-ghost:hover {
    border-color: var(--border-strong);
    color: var(--text-primary);
}

.btn-danger {
    color: var(--accent-red);
    border-color: rgba(239, 68, 68, 0.3);
    background: transparent;
}
.btn-danger:hover { background: var(--status-red-bg); }

/* Solid-red primary (destructive confirm, e.g. reports Archive).
   Compound wins over .btn-danger's transparent fill via specificity. */
.btn-primary.btn-danger {
    background: var(--accent-red);
    border-color: var(--accent-red);
    color: var(--text-inverse);
}
.btn-primary.btn-danger:hover {
    background: var(--accent-red);
    border-color: var(--accent-red);
}

.btn-sm { padding: 5px 10px; font-size: 0.75rem; }
.btn-icon { padding: 8px 10px; }

/* ============================================================
   EMPTY / LOADING STATE FAMILY
   .loading-state · .empty-state
   .empty-state-title · .empty-state-subtitle · .empty-state-cta
   .spinner (animation-only modifier — caller brings the icon)
   @keyframes spin

   Canonical donor: devices.html (flex-column shape)
   Sub-element vocabulary: reports.html (-title/-subtitle/-cta)
   Consumers: alert, assets, automations-library, devices,
   entities, notes, reports, service, sites, tag-library, users
   + login (spin keyframe only)
   NOT canonical: index.html fleet panel (.fp-empty-state),
   billing (.billing-empty-row), webstore (.ws-empty-row)
   ============================================================ */

@keyframes spin { to { transform: rotate(360deg); } }

.loading-state,
.empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 60px 20px;
    text-align: center;
    color: var(--text-muted);
}

.loading-state i,
.empty-state i {
    font-size: 48px;
    margin-bottom: 16px;
    opacity: 0.5;
    display: block;
}

.loading-state i { animation: spin 1s linear infinite; }

.empty-state p { font-size: 13px; }

.empty-state-title {
    font-size: 14px;
    font-weight: 600;
    color: var(--text-primary);
    margin-bottom: 6px;
}

.empty-state-subtitle {
    font-size: 12px;
    color: var(--text-secondary);
    max-width: 460px;
    margin: 0 auto 16px;
    line-height: 1.6;
}

.empty-state-cta {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    background: var(--accent-cyan-a12);
    border: 1px solid var(--accent-cyan-a30);
    color: var(--accent-primary);
    padding: 8px 16px;
    border-radius: 8px;
    font-size: 12px;
    font-weight: 600;
    cursor: pointer;
    font-family: var(--font-sans);
    transition: all 0.15s ease;
}

.empty-state-cta:hover {
    background: var(--accent-cyan-a20);
    border-color: var(--accent-primary);
}

.spinner { animation: spin 1s linear infinite; }

/* ============================================================
   TOAST FAMILY
   .toast · .toast.visible · .toast.success · .toast.error · .toast.info
   .toast .toast-action (+ :hover)
   Canonical: Arch 1 opacity/transform pattern (reports donor)
   Consumers: assets, automations-library, devices, entities,
   notes, reports, reports-builder, service, sites, tag-library,
   users
   NOT canonical: backlog (.bk-toast), card-factory (.cfa-toast),
   channel-factory (.cf-toast), automations-canvas (.ac-toast),
   index (.gf-toast — already namespaced),
   integrations (.toast-host/.toast-msg — already namespaced),
   toast-poller.js (.fc-toast — already namespaced)
   z-index: 1000 per z-index ladder (toast notifications)
   ============================================================ */

.toast {
    position: fixed;
    bottom: 100px;
    left: 50%;
    transform: translateX(-50%) translateY(20px);
    background: var(--glass-surface);
    backdrop-filter: blur(var(--glass-blur));
    border: 1px solid var(--border-subtle);
    border-radius: 12px;
    padding: 12px 20px;
    font-size: 12px;
    color: var(--text-primary);
    z-index: 1000;
    opacity: 0;
    pointer-events: none;
    transition: all 0.3s ease;
    display: flex;
    align-items: center;
    gap: 12px;
    max-width: 540px;
}

.toast.visible {
    opacity: 1;
    pointer-events: auto;
    transform: translateX(-50%) translateY(0);
}

.toast.success { border-color: var(--accent-emerald); color: var(--accent-emerald); }
.toast.error   { border-color: var(--accent-red);     color: var(--accent-red); }
.toast.info    { border-color: var(--accent-primary);  color: var(--accent-primary); }

.toast .toast-action {
    margin-left: 4px;
    background: none;
    border: 1px solid currentColor;
    border-radius: 6px;
    padding: 4px 10px;
    font-size: 11px;
    font-weight: 600;
    color: inherit;
    cursor: pointer;
    font-family: var(--font-sans);
}
.toast .toast-action:hover { opacity: 0.85; }
