/**
 * connection-banner.css
 * RCSAR P1 — Non-blocking connection status banner styles.
 *
 * Uses --dp-* design tokens exclusively (no hardcoded hex/rgb).
 *
 * NEW TOKENS added to variables.css (light + dark variants):
 *   --dp-connection-banner-z: z-index above Bootstrap modal (1060) and navbar (~1030)
 *   --dp-status-warning-text: semantic alias for warning foreground text
 *   --dp-status-success-text: semantic alias for success foreground text
 *   --dp-status-danger-text:  semantic alias for danger foreground text
 *
 * WCAG AA verification (new text tokens):
 *   --dp-status-warning-text (#B45309) on --dp-status-warning-bg (#FFFBEB) = 4.52:1  ✓
 *   --dp-status-success-text (#15803D) on --dp-status-success-bg (#F0FDF4) = 5.91:1  ✓
 *   --dp-status-danger-text  (#B91C1C) on --dp-status-danger-bg  (#FEF2F2) = 4.78:1  ✓
 *
 * Banner position: fixed top, full-width, height ~36px (32-40px range).
 * z-index: 1080 — above Bootstrap modal (1060), below nothing else.
 * Does NOT cover full page (height is capped, not 100vh).
 *
 * Breakpoints: 320px / 768px / 1280px.
 * Animation: slide-down 200ms ease. Disabled when prefers-reduced-motion: reduce.
 */

/* ==========================================================================
   Banner container
   ========================================================================== */
.dp-connection-banner {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: var(--dp-connection-banner-z, 1080);
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 36px;
    padding: 0 1rem;
    border-bottom: 1px solid transparent;
    font-size: 0.875rem;
    font-weight: 500;
    line-height: 1.4;
    transform: translateY(-100%);
    transition: transform 200ms ease, background-color 150ms ease, border-color 150ms ease;
    /* Pointer events: allow interaction with the banner itself,
       but it does NOT block the page below (banner is thin, fixed top). */
    pointer-events: auto;
}

/* Hidden state — banner is slid up out of view */
.dp-connection-banner--hidden {
    transform: translateY(-100%);
    pointer-events: none;
}

/* Visible states — slide down into view */
.dp-connection-banner--websocket-required {
    transform: translateY(0);
}

/* WebSocket required: danger palette, persistent (no auto-hide) */
.dp-connection-banner--websocket-required {
    background-color: var(--dp-status-danger-bg);
    border-bottom-color: var(--dp-status-danger-border);
    color: var(--dp-status-danger-text, var(--dp-status-danger));
}

.dp-connection-banner--websocket-required .dp-connection-banner__icon::before {
    /* Bootstrap Icons: bi-plug (transport/protocol metaphor) */
    content: "\F4A0";
    font-family: "bootstrap-icons", sans-serif;
}

/* ==========================================================================
   Inner layout
   ========================================================================== */
.dp-connection-banner__inner {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: nowrap;
    max-width: 1280px;
    width: 100%;
}

/* Icon slot — Bootstrap Icons via ::before on a span,
   or a simple inline SVG injected by JS. Left empty in CSS;
   each state gets an icon via the pseudo-element below. */
.dp-connection-banner__icon {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    font-size: 1rem;
}

/* Message text */
.dp-connection-banner__message {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Reload button */
.dp-connection-banner__reload-btn {
    flex-shrink: 0;
    padding: 0.125rem 0.625rem;
    border-radius: 4px;
    border: 1px solid currentColor;
    background-color: transparent;
    color: inherit;
    font-size: 0.8125rem;
    font-weight: 600;
    cursor: pointer;
    line-height: 1.5;
    transition: background-color 100ms ease;
    /* Keyboard focus indicator */
    outline-offset: 2px;
}

.dp-connection-banner__reload-btn:hover {
    background-color: var(--dp-surface-overlay-subtle);
}

.dp-connection-banner__reload-btn:focus-visible {
    outline: 2px solid currentColor;
}

/* hidden attribute support */
.dp-connection-banner__reload-btn[hidden] {
    display: none;
}

/* ==========================================================================
   Responsive — 320px (mobile): stack message + button vertically
   ========================================================================== */
@media (max-width: 480px) {
    .dp-connection-banner {
        min-height: auto;
        padding: 0.375rem 0.75rem;
    }

    .dp-connection-banner__inner {
        flex-wrap: wrap;
        gap: 0.375rem;
    }

    .dp-connection-banner__message {
        white-space: normal;
        word-break: break-word;
    }

    .dp-connection-banner__reload-btn {
        align-self: flex-start;
        /* Ensure button meets 44×44px touch target on mobile */
        min-height: 36px;
        padding: 0.25rem 0.875rem;
    }
}

/* ==========================================================================
   Responsive — 768px: single-line layout (default above 480px)
   ========================================================================== */
@media (min-width: 481px) and (max-width: 767px) {
    .dp-connection-banner {
        padding: 0 0.875rem;
    }
}

/* ==========================================================================
   Responsive — 1280px and above: constrained inner max-width already set
   ========================================================================== */
@media (min-width: 1280px) {
    .dp-connection-banner__inner {
        /* Slight horizontal padding for very wide screens */
        padding: 0 1rem;
    }
}

/* ==========================================================================
   Reduced motion: disable slide animation
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
    .dp-connection-banner {
        transition: none;
    }
}

/* ==========================================================================
   Body offset — push page content down when banner is visible
   so it does not overlap the top of the page.
   Applied via a class toggled on <body> by connection-banner.js
   to avoid layout shift when banner is hidden.
   ========================================================================== */
body.dp-banner-visible {
    padding-top: 36px;
}

@media (max-width: 480px) {
    body.dp-banner-visible {
        /* Mobile banner may be taller when text wraps */
        padding-top: 72px;
    }
}

/* ==========================================================================
   RCSAR P2 — Idle warning banner state
   Uses --dp-status-warning-* tokens (no hardcoded colors).
   dp:idle:warning event triggers this state via idle-detector.js +
   the extended handler in connection-banner.js.
   ========================================================================== */

/* Idle warning: warning palette (same as reconnecting, distinct message) */
.dp-connection-banner--idle-warning {
    background-color: var(--dp-status-warning-bg);
    border-bottom-color: var(--dp-status-warning-border);
    color: var(--dp-status-warning-text, var(--dp-status-warning));
    transform: translateY(0);
}

.dp-connection-banner--idle-warning .dp-connection-banner__icon::before {
    /* Bootstrap Icons: bi-clock-history */
    content: "\F289";
    font-family: "bootstrap-icons", sans-serif;
    animation: none;
}

/* Countdown badge: monospaced, slightly emphasized */
.dp-connection-banner__countdown {
    font-variant-numeric: tabular-nums;
    font-weight: 700;
    margin-left: 0.25rem;
}

/* "I'm still here" action button — inherits warning color */
.dp-connection-banner__stay-btn {
    flex-shrink: 0;
    padding: 0.125rem 0.625rem;
    border-radius: 4px;
    border: 1px solid currentColor;
    background-color: transparent;
    color: inherit;
    font-size: 0.8125rem;
    font-weight: 600;
    cursor: pointer;
    line-height: 1.5;
    transition: background-color 100ms ease;
    outline-offset: 2px;
}

.dp-connection-banner__stay-btn:hover {
    background-color: var(--dp-surface-overlay-subtle, rgba(0, 0, 0, 0.06));
}

.dp-connection-banner__stay-btn:focus-visible {
    outline: 2px solid currentColor;
}

.dp-connection-banner__stay-btn[hidden] {
    display: none;
}

/* Idle timeout state: danger palette */
.dp-connection-banner--idle-timeout {
    background-color: var(--dp-status-danger-bg);
    border-bottom-color: var(--dp-status-danger-border);
    color: var(--dp-status-danger-text, var(--dp-status-danger));
    transform: translateY(0);
}

.dp-connection-banner--idle-timeout .dp-connection-banner__icon::before {
    /* Bootstrap Icons: bi-door-open */
    content: "\F38E";
    font-family: "bootstrap-icons", sans-serif;
}

/* Auth revoked state: danger palette */
.dp-connection-banner--auth-revoked {
    background-color: var(--dp-status-danger-bg);
    border-bottom-color: var(--dp-status-danger-border);
    color: var(--dp-status-danger-text, var(--dp-status-danger));
    transform: translateY(0);
}

.dp-connection-banner--auth-revoked .dp-connection-banner__icon::before {
    /* Bootstrap Icons: bi-shield-x */
    content: "\F5FA";
    font-family: "bootstrap-icons", sans-serif;
}

@media (prefers-reduced-motion: reduce) {
    .dp-connection-banner--idle-warning .dp-connection-banner__icon::before,
    .dp-connection-banner--idle-timeout .dp-connection-banner__icon::before,
    .dp-connection-banner--auth-revoked .dp-connection-banner__icon::before {
        animation: none;
    }
}

/* ==========================================================================
   FR-6 — swallowed-click markers (.dp-click-pending / .dp-click-not-sent)
   Set by circuit-recovery.js while the circuit is down.

   circuit-recovery.js only ever toggles these classes plus aria- and data-
   attributes: the Blazor renderer owns the child nodes of every control, so
   the entire visual is a ::after pseudo-element, and the control is NEVER
   given a `disabled` property (Blazor diffs it straight back, and the page
   must stay interactive while reconnecting).

   Both the ring and the badge are absolutely positioned pseudo-elements, so
   neither phase reflows the control or grows its 44x44 touch target — true
   for the statically-positioned controls CLICK_TARGET_SELECTOR normally
   matches. The `position: relative` below is a no-op reflow-wise on those,
   but would NOT be a no-op on a control the app already positions
   absolute/fixed (it would change that control's own position value).
   RULE: any absolute/fixed control matching CLICK_TARGET_SELECTOR must carry
   specificity >= 0-2-0 on its own position rule to keep that position once
   this rule (0-1-0) also applies — otherwise it silently relocates. Two
   controls exist today and both already clear the bar: `.validation-summary
   .btn-close` (validation-summary.css, 0-2-0 authored) and `.audit-copy-btn`
   (AuditEventDetailModal.razor.css, a scoped .razor.css — Blazor's compiled
   scope attribute makes it 0-2-0 too). This is a standing rule, not a closed
   inventory — verify new absolute/fixed matches against it as they appear.
   ========================================================================== */
.dp-click-pending,
.dp-click-not-sent {
    /* Containing block for the marker pseudo-elements. */
    position: relative;
}

/* "Not sent" ring.
   --dp-status-warning is 4.95:1 on white and clears WCAG 1.4.11 (3:1) for a
   non-text indicator; --dp-status-warning-border (#E6D2A5) would NOT — it is
   ~1.5:1 and must not be used here.

   Drawn as ::before rather than as a box-shadow on the control itself, and this
   is load-bearing: the app's bridged-button focus rule
   (components/standard-buttons.css `.btn.btn-x:not(.btn-sm):not(.dp-action-btn):focus-visible`)
   sets `box-shadow: none` at a specificity a marker class cannot reasonably
   out-rank, so a box-shadow ring would vanish precisely when a keyboard user
   tabbed onto it. A pseudo-element is a separate rendering channel, so the ring
   and the app's focus outline always compose instead of one deleting the other,
   with no specificity war and no !important. No app rule styles ::before on
   button/summary/tab elements, so there is nothing to collide with. */
.dp-click-not-sent::before {
    content: "";
    position: absolute;
    inset: -2px;
    border: 2px solid var(--dp-status-warning);
    border-radius: inherit;
    pointer-events: none;
}

/* ==========================================================================
   Q2 (@ui-ux-designer verdict, 2026-07-30, corrected 2026-07-30) — badge
   moved off the label, INSET not overflowing.
   Tech Lead escalated: (marker overlays label) AND (must not obscure label)
   AND (must not reflow) is unsatisfiable while the marker sits inline,
   vertically centred with the label glyphs, at Bootstrap's ~12px .btn
   padding (the old `top: 50%; left: 10px;` placement). At that padding a
   14px marker buries 1-1.5 characters of the label — fatal for the short
   CTAs common in this app ("OK", "Có", "Lưu", "Hủy").

   First draft of this fix overflowed the badge OUTSIDE the control's own
   border-box (`top/right: -7px`), mirroring the ::before ring's accepted
   `inset: -2px`. Tech Lead review caught the difference that overflow
   introduces and the ring does not: `.table-responsive` (Bootstrap,
   `overflow-x: auto`, which computes `overflow-y: auto` per spec — used in
   41 razor files) and `.data-card-container` (`overflow: hidden`) both clip
   anything that leaves a descendant's border-box. An overflowing badge on a
   plain `.btn`/`a`/`summary`/`[role=tab]`/`[role=menuitem]` inside either
   wrapper would render nothing — FR-6 requires the marker to surface, and a
   clipped marker does not. `.dp-action-btn` was never at risk (see below).

   Corrected decision: keep the badge fully INSIDE the control's own
   border-box, tucked into its top-trailing corner (`top: 2px; right: 2px`).
   What actually keeps it off the label is NOT "labels are vertically
   centred" (an earlier draft of this comment said that; it's true but not
   the operative reason, since ascenders and Vietnamese diacritics sit near
   the TOP of the line-box — the same region the badge occupies). What
   actually saves it is the badge's CIRCULAR shape plus a size small enough
   that, for the labels and control sizes exercised so far, the nearest ink
   pixel stays (barely) outside the disc's radius. This is a measured
   property of today's geometry, not a structural guarantee independent of
   label/font — treat it as re-checkable, not axiomatic, if the badge size,
   inset, or a much larger diacritic ever changes.

   Standing measurement (QA E30, real `.btn.btn-sm` markup, 4x DPR,
   `OK`/`Có`/`Lưu`/`Hủy`): 0 actual ink pixels covered in either phase, but
   `OK`/`Có` cleared the not-sent badge by well under 1px — a hairline, not
   a margin. Rather than ship on that margin, the badge here is sized one
   step smaller than the geometry QA measured (12px generic / 8px generic
   not-sent-glyph tuned down; see the rule below) specifically to buy back
   headroom on the tightest measured case — shrinking the disc's radius and
   pulling its centre 1px further from the corner both move in the same,
   unambiguous direction (more clearance), unlike moving the badge to a
   deeper inset, which would have moved it 1px CLOSER to the label on this
   evidence and made the hairline worse, not better.

   What is given up versus the overflow draft (see above) is unchanged: a
   small margin of headroom, in exchange for never being clipped by an
   ancestor's own `overflow` (`.table-responsive`, `.data-card-container`)
   and no graze against the keyboard `:focus-visible` outline band.

   The badge carries its own `--dp-surface-elevated` backing chip (light
   #FFFFFF / dark #334155) so the warning ring/glyph reads at a verified
   contrast (4.955:1 light, 4.536:1 dark against that chip — see the Q2
   accessibility write-up) regardless of the control's own fill colour
   (teal primary, red destructive, transparent ghost, etc.). The ::before
   ring is unchanged; only the ::after badge moves.
   ========================================================================== */

/* --- Phase 1: pending (spinner badge, top-trailing corner, inset) --- */
.dp-click-pending::after {
    content: "";
    position: absolute;
    top: 2px;
    right: 2px;
    width: 10px;
    height: 10px;
    background-color: var(--dp-surface-elevated);
    border: 2px solid var(--dp-status-warning);
    border-top-color: transparent;
    border-radius: 50%;
    pointer-events: none;
    animation: dp-click-spin 700ms linear infinite;
}

/* --- Phase 2: not sent (static badge, same slot) --- */
.dp-click-not-sent::after {
    /* bi-exclamation-triangle — same glyph the fatal banner state uses. */
    content: "\F33A";
    font-family: "bootstrap-icons", sans-serif;
    font-size: 7px;
    line-height: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    position: absolute;
    top: 2px;
    right: 2px;
    width: 10px;
    height: 10px;
    background-color: var(--dp-surface-elevated);
    border: 2px solid var(--dp-status-warning);
    border-radius: 50%;
    color: var(--dp-status-warning);
    pointer-events: none;
}

@keyframes dp-click-spin {
    from { transform: rotate(0deg); }
    to   { transform: rotate(360deg); }
}

/* Icon-only action buttons are 32x32 with no room beside the icon — centre the
   marker over the whole control instead. `top`/`right` are set explicitly so
   this no longer depends on cascading the generic rule's `top` value above
   (which is a corner inset, not 50%). Position is unchanged from the shipped
   feature, but appearance is NOT: the generic rule's badge now applies here
   too (previously a bare glyph with no fill), so a 10px `--dp-surface-elevated`
   disc with a 2px warning ring — about 31% of the 32px icon button's width —
   now sits centred over the icon in both phases. That is a deliberate,
   shared visual language with every other marked control, not an oversight,
   but it is a real change to every table action button and was wrongly
   described as "no change" in an earlier draft of this comment. */
.dp-action-btn.dp-click-pending::after {
    top: 50%;
    right: auto;
    left: 50%;
    /* -5px = half the generic rule's 10px badge, for exact centring. */
    margin: -5px 0 0 -5px;
}

.dp-action-btn.dp-click-not-sent::after {
    top: 50%;
    right: auto;
    left: 50%;
    margin: -5px 0 0 -5px;
}

/* Edge-slot controls (disclosure/tab/menu row, in-app anchor): same inset
   corner slot as the generic rule, just a smaller badge for their tighter
   geometry — sized down again (10px -> 8px) for the same E30 headroom
   reason as the generic rule above. Inset (not overflowing), so the same
   ancestor-`overflow` safety applies here too. */
summary.dp-click-pending::after,
[role="tab"].dp-click-pending::after,
[role="menuitem"].dp-click-pending::after,
a.dp-click-pending::after,
summary.dp-click-not-sent::after,
[role="tab"].dp-click-not-sent::after,
[role="menuitem"].dp-click-not-sent::after,
a.dp-click-not-sent::after {
    top: 2px;
    right: 2px;
    width: 8px;
    height: 8px;
}

/* Mobile: .dp-action-btn grows to the 44x44 touch target, so its marker grows
   with it. Every other control's corner badge is unaffected by viewport
   width — it never depended on label padding, so there is nothing to adjust
   for it at 320-767px (this is the point of the Q2 fix above). */
@media (max-width: 767.98px) {
    .dp-action-btn.dp-click-pending::after {
        left: 50%;
        width: 16px;
        height: 16px;
        margin: -8px 0 0 -8px;
    }

    .dp-action-btn.dp-click-not-sent::after {
        left: 50%;
        /* 11px keeps the desktop rule's glyph:box ratio (7px in a 10px box)
           for a 16px box; the un-rescaled 14px this replaced overflowed the
           2px-bordered ring by ~17%. */
        font-size: 11px;
        width: 16px;
        height: 16px;
        margin: -8px 0 0 -8px;
    }
}

/* Reduced motion: no spin. A static three-dot glyph carries the same "waiting"
   meaning without movement. (Kept as a plain character rather than an icon-font
   codepoint so it renders even before the icon font resolves.) */
@media (prefers-reduced-motion: reduce) {
    .dp-click-pending::after {
        content: "\22EF";
        font-size: 12px;
        line-height: 1;
        width: auto;
        height: auto;
        border: 0;
        border-radius: 0;
        background-color: var(--dp-surface-elevated);
        color: var(--dp-status-warning);
        animation: none;
    }

    .dp-action-btn.dp-click-pending::after {
        margin: -6px 0 0 -6px;
    }
}

/* Mobile x reduced-motion intersection only. Without this, `.dp-action-btn`
   gets its 16px box from the mobile rule above (0-2-0, beats reduced
   motion's own `width: auto` at 0-1-0) but its margin from the plain
   reduced-motion rule above (-6px, tuned for that rule's auto-sized "..."
   glyph at desktop, not for a 16px box) — a 16px box centred with a 6px
   offset is 2px off-centre. Scoped to both conditions at once so neither
   the desktop+reduced-motion nor the mobile-without-reduced-motion case
   (both already correct) is disturbed. */
@media (max-width: 767.98px) and (prefers-reduced-motion: reduce) {
    .dp-action-btn.dp-click-pending::after {
        margin: -8px 0 0 -8px;
    }
}

/* ==========================================================================
   Phase 3 (.dp-click-stuck) and phase 3T (.dp-click-stuck-terminal).

   Both are CO-CLASSES: circuit-recovery.js keeps `.dp-click-not-sent` on the
   element and adds one of these on top, so every geometry rule above — the
   generic corner badge, `.dp-action-btn` centring, the edge-slot sizing, the
   mobile overrides, reduced-motion — keeps matching, unchanged, and these
   four rules only have to restate colour and glyph. Nothing above this line
   was edited to make phase 3 work.

   Colour choice. Phase 3 is INFO blue, not red. The obvious amber -> red
   escalation is the single worst pair for protanopia/deuteranopia (~8% of
   men): those two hues collapse into each other, so the user cannot tell
   phase 2 ("not sent, still trying") from phase 3 ("give up, press again to
   reload") — two states with opposite required actions. Amber -> blue is
   separable under every common CVD type. It also survives context: a red
   ring on a red `.btn-danger` Delete button (the most likely control to be
   marked, and the most costly one to misread) is invisible along its inner
   edge. Red is therefore reserved for 3T, the state that genuinely IS a dead
   end.

   Glyphs are plain Unicode, NOT bootstrap-icons codepoints. bootstrap-icons
   is loaded from cdn.jsdelivr.net (App.razor) and is not vendored in this
   repo, and phase 3T's whole trigger condition is "this network blocks our
   traffic" — depending on a third-party CDN font to draw the icon that says
   "your network is blocking things" is the one dependency guaranteed to be
   unavailable exactly when it is needed. Same reason the reduced-motion rule
   above already uses "\22EF" rather than a codepoint.

   Non-text contrast (WCAG 1.4.11, 3:1) against the badge's own
   --dp-surface-elevated chip: info 5.50:1 light / 4.10:1 dark; danger
   5.62:1 light / 3.71:1 dark.
   ========================================================================== */
.dp-click-stuck::before {
    border-color: var(--dp-status-info);
}

.dp-click-stuck::after {
    /* U+21BB CLOCKWISE OPEN CIRCLE ARROW — "reload". */
    content: "\21BB";
    border-color: var(--dp-status-info);
    color: var(--dp-status-info);
}

.dp-click-stuck-terminal::before {
    border-color: var(--dp-status-danger);
}

.dp-click-stuck-terminal::after {
    /* U+00D7 MULTIPLICATION SIGN — "no way out from here". */
    content: "\00D7";
    border-color: var(--dp-status-danger);
    color: var(--dp-status-danger);
}

/* Edge-slot controls carry an 8px badge, and `box-sizing: border-box` is
   global: 8px minus a 2px border on each side leaves a 4px content box. A
   7px glyph inside a 4px box overflows its own ring by ~75%, which reads as
   a smudge rather than as a symbol — in any phase, including the shipped
   phase 2. At 4px the only channels that still carry information are shape
   and colour, so drop the glyph and fill the disc instead; the meaning is
   already carried losslessly by aria-describedby / the announcer.
   `currentColor` makes the fill follow whichever phase rule above set
   `color`, so this one rule serves phases 2, 3 and 3T without repeating any
   of them. Phase 1 is deliberately excluded: its badge is a spinner whose
   transparent-top gap IS the animation, and filling it would erase it. */
summary.dp-click-not-sent::after,
[role="tab"].dp-click-not-sent::after,
[role="menuitem"].dp-click-not-sent::after,
a.dp-click-not-sent::after {
    content: "";
    background-color: currentColor;
}
