/* site.css — Pseudo Studio web admin panel design system.
 *
 * Single dark theme (no light mode, no toggle — spec §9). Every component class listed
 * in the plan's Contract D lives here and is meant to be used verbatim, unmodified, by
 * the page-owning agents: they cannot add their own stylesheet, so this file is the
 * entire visual vocabulary the rest of the site is built from. When in doubt, add a
 * generic class here rather than letting a page reach for an inline style.
 *
 * No CDN fonts — system font stack only, so the panel renders instantly and identically
 * offline (this box has no outbound internet dependency in its critical path).
 */

/* ---------------------------------------------------------------------------
 * Design tokens
 * ------------------------------------------------------------------------- */

:root {
    /* Surfaces — deep slate, not pure black: pure #000 makes the amber accent glow too
       hot and crushes shadow separation between stacked cards. */
    --bg: #0d1117;
    --surface: #161b22;
    --surface-2: #1c2128;
    --surface-3: #232a34;
    --border: #30363d;
    --border-strong: #3d444d;

    /* Text. --text-dim passes AA (4.5:1) on both --bg and --surface; verified, don't
       lighten --bg without re-checking these pairs. */
    --text: #e6edf3;
    --text-dim: #8b949e;
    --text-faint: #6e7681;

    /* Fireworks brand accent — amber into orange. Buttons use a dark foreground on the
       accent fill (see --on-accent): white-on-amber fails AA (~2.3:1), black-ish text
       clears 8:1+. */
    --accent: #f0a020;
    --accent-2: #fb7a1f;
    --accent-strong: #ffb339;
    --accent-bg: rgba(240, 160, 32, 0.14);
    --on-accent: #1a1207;

    --ok: #3fb950;
    --ok-bg: rgba(63, 185, 80, 0.14);
    --warn: #d29922;
    --warn-bg: rgba(210, 153, 34, 0.16);
    --err: #f85149;
    --err-bg: rgba(248, 81, 73, 0.14);
    --info: #58a6ff;
    --info-bg: rgba(88, 166, 255, 0.14);

    /* Spacing scale — every gap/padding in this file traces back to one of these six
       values so the whole site reads as one grid, not a pile of one-off pixel choices. */
    --s1: 0.25rem;
    --s2: 0.5rem;
    --s3: 0.75rem;
    --s4: 1rem;
    --s5: 1.5rem;
    --s6: 2.5rem;

    --radius: 0.5rem;
    --radius-lg: 1rem;

    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.45);
    --shadow: 0 6px 20px rgba(0, 0, 0, 0.35), 0 1px 3px rgba(0, 0, 0, 0.3);
    --shadow-lg: 0 20px 56px rgba(0, 0, 0, 0.5), 0 6px 16px rgba(0, 0, 0, 0.35);

    --font-sans: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
    --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;

    --transition: 150ms ease;
    --z-navbar: 100;
    --z-toast: 500;
}

/* ---------------------------------------------------------------------------
 * Cross-document view transitions — progressive enhancement only (Chrome today).
 * Gated behind no-preference so a reduced-motion user never gets the root fade even
 * as support spreads; unsupported browsers simply ignore the unknown at-rule.
 * ------------------------------------------------------------------------- */

@media (prefers-reduced-motion: no-preference) {
    @view-transition {
        navigation: auto;
    }
}

::view-transition-old(root),
::view-transition-new(root) {
    animation-duration: 140ms;
    animation-timing-function: ease-out;
}

/* ---------------------------------------------------------------------------
 * Reset & base
 * ------------------------------------------------------------------------- */

*,
*::before,
*::after {
    box-sizing: border-box;
}

html {
    color-scheme: dark;
}

body {
    margin: 0;
    min-height: 100vh;
    background: var(--bg);
    color: var(--text);
    font-family: var(--font-sans);
    font-size: 15px;
    line-height: 1.5;
    -webkit-font-smoothing: antialiased;
}

img,
svg {
    max-width: 100%;
    display: block;
}

h1,
h2,
h3,
h4,
p {
    margin: 0 0 var(--s3);
}

a {
    color: var(--accent);
    text-decoration: none;
}

a:hover {
    text-decoration: underline;
}

button,
input,
select,
textarea {
    font: inherit;
    color: inherit;
}

code,
pre,
.mono {
    font-family: var(--font-mono);
}

pre {
    margin: 0;
    white-space: pre-wrap;
    word-break: break-word;
}

::selection {
    background: var(--accent);
    color: var(--on-accent);
}

* {
    scrollbar-color: var(--border-strong) var(--surface);
    scrollbar-width: thin;
}

::-webkit-scrollbar {
    width: 10px;
    height: 10px;
}

::-webkit-scrollbar-track {
    background: var(--surface);
}

::-webkit-scrollbar-thumb {
    background: var(--border-strong);
    border-radius: 999px;
}

/* Crisp, visible focus rings everywhere an interactive element can take focus — this is
   an admin panel for production game operations, keyboard users must always be able to
   see where they are. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
    outline: 2px solid var(--accent-strong);
    outline-offset: 2px;
    border-radius: var(--radius);
}

@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }

    ::view-transition-group(*),
    ::view-transition-old(*),
    ::view-transition-new(*) {
        animation: none !important;
    }
}

/* ---------------------------------------------------------------------------
 * Layout primitives
 * ------------------------------------------------------------------------- */

.container {
    width: 100%;
    max-width: 1200px;
    margin-inline: auto;
    padding-inline: var(--s4);
}

main.page {
    padding-block: var(--s5) var(--s6);
}

.grid {
    display: grid;
    gap: var(--s4);
}

.grid.cols-2 {
    grid-template-columns: repeat(2, 1fr);
}

.grid.cols-3 {
    grid-template-columns: repeat(3, 1fr);
}

@media (max-width: 760px) {
    .grid.cols-2,
    .grid.cols-3 {
        grid-template-columns: 1fr;
    }
}

/* Small utility layer — kept intentionally minimal. Page views own no stylesheet of
   their own, so these exist to cover the common layout needs (flex rows, spacing,
   dimmed text) without pushing every page toward inline style="". */
.stack {
    display: flex;
    flex-direction: column;
    gap: var(--s3);
}

.row {
    display: flex;
    align-items: center;
    gap: var(--s3);
}

.row.wrap {
    flex-wrap: wrap;
}

.row.between {
    justify-content: space-between;
}

.text-dim {
    color: var(--text-dim);
}

.text-sm {
    font-size: 0.85em;
}

.w-full {
    width: 100%;
}

.scroll-x {
    overflow-x: auto;
    max-width: 100%;
}

.hidden {
    display: none !important;
}

.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* ---------------------------------------------------------------------------
 * Hero (home page welcome banner — not a strict Contract D class, but every page
 * agent needs it and the design system is where shared visual language lives)
 * ------------------------------------------------------------------------- */

.hero {
    text-align: center;
    padding: var(--s6) var(--s4);
    margin-bottom: var(--s5);
}

.hero h1 {
    font-size: clamp(1.75rem, 1.3rem + 2vw, 2.75rem);
    background: linear-gradient(135deg, var(--accent-strong), var(--accent-2));
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
}

.hero p {
    color: var(--text-dim);
    font-size: 1.05rem;
    max-width: 640px;
    margin-inline: auto;
}

/* ---------------------------------------------------------------------------
 * Navbar
 * ------------------------------------------------------------------------- */

.navbar {
    position: sticky;
    top: 0;
    z-index: var(--z-navbar);
    background: rgba(13, 17, 23, 0.78);
    backdrop-filter: blur(12px) saturate(140%);
    -webkit-backdrop-filter: blur(12px) saturate(140%);
    border-bottom: 1px solid var(--border);
}

.navbar-inner {
    display: flex;
    align-items: center;
    gap: var(--s4);
    min-height: 3.5rem;
    padding-block: var(--s2);
}

.navbar-brand {
    display: inline-flex;
    align-items: center;
    gap: var(--s2);
    font-weight: 700;
    font-size: 1.05rem;
    color: var(--text);
    white-space: nowrap;
}

.navbar-brand:hover {
    text-decoration: none;
    color: var(--accent-strong);
}

.navbar-brand-mark {
    color: var(--accent);
    filter: drop-shadow(0 0 6px var(--accent-bg));
}

.navbar-links {
    display: flex;
    align-items: center;
    gap: var(--s1);
    flex: 1;
}

.navbar-link {
    color: var(--text-dim);
    padding: var(--s2) var(--s3);
    border-radius: var(--radius);
    font-size: 0.92rem;
    font-weight: 500;
    transition: color var(--transition), background-color var(--transition);
    white-space: nowrap;
}

.navbar-link:hover {
    color: var(--text);
    background: var(--surface-2);
    text-decoration: none;
}

.navbar-link[aria-current='page'] {
    color: var(--accent-strong);
    background: var(--accent-bg);
}

/* min-width:0 overrides the flex default (min-width:auto, which floors an item at its
   content's intrinsic width) so this cluster - and the dropdown button inside it - can
   actually shrink instead of forcing the whole .navbar-inner row wider than the
   viewport. That forced overflow was the root cause of the mobile horizontal-scroll bug
   (plan §6): a long username/role name in a nowrap row had nowhere to go. */
.navbar-user {
    display: flex;
    align-items: center;
    gap: var(--s3);
    min-width: 0;
}

.navbar-toggle {
    display: none;
    flex-direction: column;
    justify-content: center;
    gap: 4px;
    width: 2.25rem;
    height: 2.25rem;
    /* Root cause of the "cropped hamburger" bug (plan §6): the old inline
       navbar-user buttons row was nowrap text with nowhere to shrink, so the flex
       algorithm shrank this fixed-width box instead, squashing its bars below their
       intended size. The dropdown button is now the only thing built to shrink (it
       truncates its own contents via CSS below), so the toggle never has to give up
       width again - pin it here rather than relying on that indirectly. */
    flex-shrink: 0;
    background: transparent;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    cursor: pointer;
    padding: 0;
}

.navbar-toggle-bar {
    display: block;
    width: 1.1rem;
    height: 2px;
    margin-inline: auto;
    background: var(--text);
    border-radius: 2px;
    transition: transform var(--transition), opacity var(--transition);
}

.navbar-toggle[aria-expanded='true'] .navbar-toggle-bar:nth-child(1) {
    transform: translateY(6px) rotate(45deg);
}

.navbar-toggle[aria-expanded='true'] .navbar-toggle-bar:nth-child(2) {
    opacity: 0;
}

.navbar-toggle[aria-expanded='true'] .navbar-toggle-bar:nth-child(3) {
    transform: translateY(-6px) rotate(-45deg);
}

@media (max-width: 860px) {
    .navbar-toggle {
        display: flex;
        /* .navbar-links is display:none while collapsed, so it no longer contributes to
           the flex row and can't push anything right on its own (that was also part of
           the old bug: the toggle sat flush against the brand with a visible gap before
           navbar-user, instead of the two forming one group at the right edge). Putting
           the auto margin on the first right-side item groups toggle + user menu
           together, same as a typical mobile navbar. */
        margin-left: auto;
    }

    .navbar-links {
        position: absolute;
        top: 100%;
        left: 0;
        right: 0;
        flex-direction: column;
        align-items: stretch;
        background: var(--surface);
        border-bottom: 1px solid var(--border);
        padding: var(--s3);
        gap: var(--s1);
        display: none;
        box-shadow: var(--shadow);
    }

    .navbar-links.open {
        display: flex;
    }
}

/* ---------------------------------------------------------------------------
 * User menu (navbar dropdown) — Contract F1a. #userMenuBtn / #userMenu, wired by
 * app.js's initUserMenu (click toggles; click-outside + Escape close).
 *
 * Sizing: .usermenu-btn caps its own width with min(), and the role badge + username
 * inside it get overflow:hidden + text-overflow:ellipsis + min-width:0. That combination
 * is what actually lets flexbox shrink them below their text's full width (overflow
 * other than visible is what changes an element's automatic minimum size from
 * "content's full width" to effectively 0) - a long custom role name or a long username
 * degrades to an ellipsis instead of forcing the whole navbar row wider than the
 * viewport, which is the actual mechanism behind the plan §6 mobile-overflow fix, not
 * just a fixed-pixel guess at a breakpoint.
 * ------------------------------------------------------------------------- */

.usermenu {
    position: relative;
    min-width: 0;
}

.usermenu-btn {
    display: flex;
    align-items: center;
    gap: var(--s2);
    min-width: 0;
    max-width: min(14rem, 42vw);
    padding: 0.3rem 0.6rem;
    background: transparent;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text);
    cursor: pointer;
    transition: background-color var(--transition), border-color var(--transition);
}

.usermenu-btn:hover {
    background: var(--surface-2);
    border-color: var(--border-strong);
}

.usermenu-btn[aria-expanded='true'] {
    background: var(--surface-2);
    border-color: var(--accent);
}

.usermenu-btn .badge-role {
    min-width: 0;
    max-width: min(6rem, 16vw);
    overflow: hidden;
    text-overflow: ellipsis;
}

.usermenu-name {
    font-weight: 600;
    font-size: 0.9rem;
    min-width: 0;
    max-width: min(8rem, 22vw);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.usermenu-chevron {
    flex-shrink: 0;
    width: 0;
    height: 0;
    border-style: solid;
    border-width: 5px 4px 0 4px;
    border-color: var(--text-dim) transparent transparent transparent;
    transition: transform var(--transition);
}

.usermenu-btn[aria-expanded='true'] .usermenu-chevron {
    transform: rotate(180deg);
}

/* position:absolute inside .usermenu (position:relative) — its containing block never
   crosses an overflow:hidden ancestor (verified: .navbar/.navbar-inner/.navbar-user set
   none), so the panel is never clipped, only ever covered by z-index, which --z-navbar
   already puts it above everything but nothing above it. */
/* Addendum A5: the button is already flush against .navbar-inner's own right padding
   (container's padding-inline never gets stripped at any breakpoint - see the media
   queries above, none touch .container/.navbar-inner), so right:0/left:auto here just
   keeps the OPEN panel pinned to that same edge instead of drifting. The clipping risk
   was the width, not the alignment: min(280px, calc(100vw - 2*var(--s3))) caps the panel
   at 280px on anything roomy, and on a narrow phone falls back to 100vw minus the SAME
   --s3 gutter kept on both sides - at 360px that's min(280px, 360 - 24) = 280px, well
   inside the 344px available after the navbar's own 16px right padding, so the panel's
   left edge lands around x=64px, nowhere near either viewport edge. */
.usermenu-panel {
    position: absolute;
    top: calc(100% + var(--s2));
    right: 0;
    left: auto;
    min-width: 12rem;
    max-width: min(280px, calc(100vw - 2 * var(--s3)));
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);
    padding: var(--s2);
    display: flex;
    flex-direction: column;
    gap: 2px;
    z-index: var(--z-navbar);
}

.usermenu-panel[hidden] {
    display: none;
}

.usermenu-item {
    display: block;
    width: 100%;
    text-align: left;
    padding: 0.5rem var(--s3);
    border: none;
    background: transparent;
    color: var(--text);
    font: inherit;
    font-size: 0.9rem;
    font-weight: 500;
    border-radius: var(--radius);
    cursor: pointer;
}

.usermenu-item:hover {
    background: var(--surface-2);
    text-decoration: none;
}

.usermenu-item-danger:hover {
    background: var(--err-bg);
    color: var(--err);
}

/* ---------------------------------------------------------------------------
 * Card
 * ------------------------------------------------------------------------- */

.card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: var(--s5);
    box-shadow: var(--shadow-sm);
}

.card-title {
    font-size: 1.05rem;
    font-weight: 700;
    margin-bottom: var(--s4);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
}

/* ---------------------------------------------------------------------------
 * Buttons
 * ------------------------------------------------------------------------- */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--s2);
    padding: 0.55rem 1rem;
    border-radius: var(--radius);
    border: 1px solid var(--border);
    background: var(--surface-2);
    color: var(--text);
    font-weight: 600;
    font-size: 0.9rem;
    cursor: pointer;
    line-height: 1.2;
    transition: background-color var(--transition), border-color var(--transition),
        transform var(--transition), box-shadow var(--transition);
}

.btn:hover {
    background: var(--surface-3);
    border-color: var(--border-strong);
}

.btn:active {
    transform: translateY(1px);
}

.btn:disabled {
    opacity: 0.5;
    cursor: not-allowed;
    transform: none;
}

.btn-primary {
    background: linear-gradient(135deg, var(--accent-strong), var(--accent-2));
    border-color: transparent;
    color: var(--on-accent);
    box-shadow: 0 2px 10px rgba(240, 160, 32, 0.25);
}

.btn-primary:hover {
    filter: brightness(1.06);
    background: linear-gradient(135deg, var(--accent-strong), var(--accent-2));
}

.btn-danger {
    background: var(--err);
    border-color: transparent;
    color: #2a0d0b;
}

.btn-danger:hover {
    background: #ff6a62;
}

.btn-ghost {
    background: transparent;
    border-color: transparent;
}

.btn-ghost:hover {
    background: var(--surface-2);
    border-color: var(--border);
}

.btn-sm {
    padding: 0.35rem 0.7rem;
    font-size: 0.8rem;
}

/* ---------------------------------------------------------------------------
 * Forms
 * ------------------------------------------------------------------------- */

.field {
    display: flex;
    flex-direction: column;
    gap: var(--s2);
    margin-bottom: var(--s4);
}

.field label {
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--text-dim);
}

.field-hint {
    font-size: 0.78rem;
    color: var(--text-faint);
}

input,
select,
textarea {
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 0.55rem 0.75rem;
    color: var(--text);
    transition: border-color var(--transition), box-shadow var(--transition);
    width: 100%;
}

input::placeholder,
textarea::placeholder {
    color: var(--text-faint);
}

input:hover,
select:hover,
textarea:hover {
    border-color: var(--border-strong);
}

input:focus,
select:focus,
textarea:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-bg);
}

textarea {
    resize: vertical;
    min-height: 5rem;
}

select {
    cursor: pointer;
}

/* ---------------------------------------------------------------------------
 * Segmented control (env toggle) — production option carries a persistent warn tint
 * even when unselected, so an operator sees the danger zone before they click into it.
 * Convention for page authors: <button data-env="production"> gets the tint automatically.
 * ------------------------------------------------------------------------- */

.seg {
    display: inline-flex;
    padding: 3px;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    gap: 2px;
}

.seg button {
    border: none;
    background: transparent;
    color: var(--text-dim);
    padding: 0.4rem 0.9rem;
    border-radius: calc(var(--radius) - 3px);
    font-weight: 600;
    font-size: 0.85rem;
    cursor: pointer;
    transition: background-color var(--transition), color var(--transition);
}

.seg button:hover {
    color: var(--text);
}

.seg button[aria-pressed='true'] {
    background: var(--accent);
    color: var(--on-accent);
}

.seg button[data-env='production'] {
    color: var(--warn);
}

.seg button[data-env='production'][aria-pressed='true'] {
    background: var(--warn);
    color: #241a02;
}

/* ---------------------------------------------------------------------------
 * Tabs (page-level view switcher, e.g. Log History's Console actions / Web server
 * tabs) — distinct from .seg: .seg is a compact filter/target toggle, .tabs is a
 * full-width underline switcher for swapping an entire panel below it.
 * ------------------------------------------------------------------------- */

.tabs {
    display: flex;
    gap: var(--s1);
    border-bottom: 1px solid var(--border);
}

.tab {
    appearance: none;
    background: transparent;
    border: none;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
    color: var(--text-dim);
    padding: var(--s3) var(--s4);
    font-weight: 600;
    font-size: 0.9rem;
    cursor: pointer;
    transition: color var(--transition), border-color var(--transition);
}

.tab:hover {
    color: var(--text);
}

.tab[aria-selected='true'] {
    color: var(--accent-strong);
    border-bottom-color: var(--accent-strong);
}

/* ---------------------------------------------------------------------------
 * Table
 * ------------------------------------------------------------------------- */

.table-wrap {
    overflow-x: auto;
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background: var(--surface);
}

.table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.88rem;
}

.table th,
.table td {
    padding: var(--s3) var(--s4);
    text-align: left;
    border-bottom: 1px solid var(--border);
    white-space: nowrap;
}

.table td {
    white-space: normal;
}

.table thead th {
    position: sticky;
    top: 0;
    background: var(--surface-2);
    color: var(--text-dim);
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    z-index: 1;
}

.table tbody tr:hover {
    background: var(--surface-2);
}

.table tbody tr:nth-child(even) {
    background: rgba(255, 255, 255, 0.015);
}

.table tbody tr:last-child td {
    border-bottom: none;
}

/* Opt-in responsive card-stack for narrow screens. Requires each <td> to carry a
   data-label attribute matching its column header — pages that don't set data-label
   just get a blank label prefix, so this degrades safely rather than breaking. */
@media (max-width: 700px) {
    .table.stacked thead {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip: rect(0, 0, 0, 0);
    }

    .table.stacked,
    .table.stacked tbody,
    .table.stacked tr,
    .table.stacked td {
        display: block;
        width: 100%;
    }

    .table.stacked tr {
        border-bottom: 1px solid var(--border);
        padding: var(--s3) var(--s4);
    }

    .table.stacked td {
        border-bottom: none;
        padding: var(--s1) 0;
        display: flex;
        justify-content: space-between;
        gap: var(--s3);
        white-space: normal;
        text-align: right;
    }

    .table.stacked td::before {
        content: attr(data-label);
        color: var(--text-dim);
        font-weight: 600;
        font-size: 0.78rem;
        text-align: left;
    }
}

/* ---------------------------------------------------------------------------
 * Badges & chips
 * ------------------------------------------------------------------------- */

.badge {
    display: inline-flex;
    align-items: center;
    gap: 0.3em;
    padding: 0.2rem 0.55rem;
    border-radius: 999px;
    font-size: 0.72rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    background: var(--surface-2);
    color: var(--text-dim);
    white-space: nowrap;
}

.badge-ok {
    background: var(--ok-bg);
    color: var(--ok);
}

.badge-warn {
    background: var(--warn-bg);
    color: var(--warn);
}

.badge-err {
    background: var(--err-bg);
    color: var(--err);
}

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

.badge-neutral {
    background: var(--surface-2);
    color: var(--text-dim);
}

/* Navbar role badge — mapped by data-role so views.js only has to emit the plain role
   string, not choose a color class server-side. Neutral background/color is an explicit
   default (not just inherited from .badge) so a custom/unknown role name (roles.json,
   spec plans/web-logs-and-roles-rework.md §5.2) always renders legibly on its own; the
   three specific overrides below win on specificity for the built-in names. */
.badge-role {
    background: var(--surface-2);
    color: var(--text-dim);
    text-transform: capitalize;
}

.badge-role[data-role='admin'] {
    background: var(--accent-bg);
    color: var(--accent-strong);
}

.badge-role[data-role='operator'] {
    background: var(--info-bg);
    color: var(--info);
}

.badge-role[data-role='viewer'] {
    background: var(--surface-2);
    color: var(--text-dim);
}

.chip {
    display: inline-flex;
    align-items: center;
    gap: 0.35em;
    padding: 0.3rem 0.75rem;
    border-radius: 999px;
    border: 1px solid var(--border);
    background: var(--surface-2);
    color: var(--text-dim);
    font-size: 0.8rem;
    font-weight: 600;
    cursor: pointer;
    transition: background-color var(--transition), border-color var(--transition), color var(--transition);
}

.chip:hover {
    border-color: var(--border-strong);
    color: var(--text);
}

.chip.on {
    background: var(--accent-bg);
    border-color: var(--accent);
    color: var(--accent-strong);
}

/* ---------------------------------------------------------------------------
 * Toasts
 * ------------------------------------------------------------------------- */

.toast-stack {
    position: fixed;
    bottom: var(--s5);
    right: var(--s5);
    z-index: var(--z-toast);
    display: flex;
    flex-direction: column;
    gap: var(--s2);
    max-width: min(360px, calc(100vw - 2 * var(--s4)));
}

.toast {
    background: var(--surface-3);
    border: 1px solid var(--border-strong);
    border-left: 3px solid var(--info);
    border-radius: var(--radius);
    padding: var(--s3) var(--s4);
    box-shadow: var(--shadow-lg);
    font-size: 0.88rem;
    animation: toast-in 180ms ease-out;
}

@keyframes toast-in {
    from {
        opacity: 0;
        transform: translateY(8px);
    }

    to {
        opacity: 1;
        transform: translateY(0);
    }
}

.toast-ok {
    border-left-color: var(--ok);
}

.toast-err {
    border-left-color: var(--err);
}

.toast-warn {
    border-left-color: var(--warn);
}

/* ---------------------------------------------------------------------------
 * Modal (native <dialog>)
 * ------------------------------------------------------------------------- */

dialog.modal {
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: 0;
    background: var(--surface);
    color: var(--text);
    box-shadow: var(--shadow-lg);
    width: min(480px, calc(100vw - 2 * var(--s4)));
}

dialog.modal::backdrop {
    background: rgba(3, 5, 8, 0.65);
    backdrop-filter: blur(2px);
}

@media (prefers-reduced-motion: no-preference) {
    dialog.modal {
        animation: modal-in 160ms ease-out;
    }

    @starting-style {
        dialog.modal[open] {
            opacity: 0;
            transform: translateY(8px) scale(0.98);
        }
    }
}

@keyframes modal-in {
    from {
        opacity: 0;
        transform: translateY(8px) scale(0.98);
    }

    to {
        opacity: 1;
        transform: translateY(0) scale(1);
    }
}

.modal-body {
    padding: var(--s5);
    display: flex;
    flex-direction: column;
    gap: var(--s3);
}

.modal-title {
    font-size: 1.1rem;
    margin: 0;
}

.modal-message {
    color: var(--text-dim);
    margin: 0;
}

.modal-actions {
    display: flex;
    justify-content: flex-end;
    gap: var(--s2);
    margin-top: var(--s2);
}

/* ---------------------------------------------------------------------------
 * Empty state
 * ------------------------------------------------------------------------- */

.empty {
    text-align: center;
    padding: var(--s6) var(--s4);
    color: var(--text-dim);
    border: 1px dashed var(--border);
    border-radius: var(--radius-lg);
}

.empty strong {
    display: block;
    color: var(--text);
    margin-bottom: var(--s1);
}

/* ---------------------------------------------------------------------------
 * kbd
 * ------------------------------------------------------------------------- */

.kbd {
    display: inline-block;
    padding: 0.1rem 0.4rem;
    font-family: var(--font-mono);
    font-size: 0.75rem;
    background: var(--surface-2);
    border: 1px solid var(--border-strong);
    border-bottom-width: 2px;
    border-radius: 0.3rem;
    color: var(--text-dim);
}

/* ---------------------------------------------------------------------------
 * Spinner
 * ------------------------------------------------------------------------- */

.spin {
    display: inline-block;
    width: 1em;
    height: 1em;
    border: 2px solid var(--border);
    border-top-color: var(--accent);
    border-radius: 50%;
    animation: spin 0.6s linear infinite;
    vertical-align: -0.15em;
}

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

/* ---------------------------------------------------------------------------
 * Skeleton loaders — shown until first real data lands in any async panel.
 * ------------------------------------------------------------------------- */

.skeleton {
    position: relative;
    overflow: hidden;
    background: var(--surface-2);
    border-radius: var(--radius);
}

.skeleton::after {
    content: '';
    position: absolute;
    inset: 0;
    transform: translateX(-100%);
    background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.06), transparent);
    animation: skeleton-shimmer 1.4s infinite;
}

@media (prefers-reduced-motion: reduce) {
    .skeleton::after {
        animation: none;
    }
}

@keyframes skeleton-shimmer {
    100% {
        transform: translateX(100%);
    }
}

.skeleton-bar {
    height: 0.9em;
    width: 100%;
    margin-bottom: var(--s2);
}

.skeleton-bar:last-child {
    margin-bottom: 0;
    width: 60%;
}

.skeleton-card {
    height: 6rem;
    border-radius: var(--radius-lg);
}

.skeleton-row {
    height: 2.4rem;
    margin-bottom: var(--s1);
    border-radius: var(--radius);
}

/* ---------------------------------------------------------------------------
 * Log row (live + historical viewers)
 * ------------------------------------------------------------------------- */

.log-row {
    display: flex;
    gap: var(--s3);
    padding: var(--s2) var(--s3);
    border-radius: var(--radius);
    font-family: var(--font-mono);
    font-size: 0.82rem;
    line-height: 1.45;
    align-items: flex-start;
}

.log-row:hover {
    background: var(--surface-2);
}

.log-row .log-ts {
    flex-shrink: 0;
    color: var(--text-faint);
    width: 9rem;
}

.log-row .log-tag {
    flex-shrink: 0;
}

.log-row .log-text {
    flex: 1;
    min-width: 0;
    word-break: break-word;
    white-space: pre-wrap;
}

.log-row.lvl-err {
    background: var(--err-bg);
}

.log-row.lvl-warn {
    background: var(--warn-bg);
}

.log-row.lvl-info {
    background: var(--info-bg);
}

/* Action row (live-logs entries carrying `action`, i.e. tag "web-actions") — a left
   accent so a structured console/publish/users/roles/auth action is scannable at a
   glance against plain pm2 text lines in the same merged timeline. The lvl-err/warn/info
   background above still applies (keyed off outcome, not text heuristics, for these
   rows); this just adds the extra edge. */
.log-row.action-row {
    border-left: 2px solid var(--border-strong);
    padding-left: calc(var(--s3) - 2px);
}

/* ---------------------------------------------------------------------------
 * Disclosure — the double-arrow fix.
 * The old panel hid the marker in one place but not consistently across engines and
 * then drew its own ::before chevron on top, so Firefox (which already respects
 * list-style:none on <summary>) showed a native triangle AND the custom one. Fix: kill
 * the native marker on every engine explicitly, THEN draw exactly one chevron via a
 * border-triangle (no glyph/font dependency, so it can't visually double with anything).
 * ------------------------------------------------------------------------- */

details.disclosure > summary {
    list-style: none;
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: var(--s2);
    padding: var(--s2) 0;
    font-weight: 600;
    color: var(--text);
    outline-offset: 2px;
}

details.disclosure > summary::-webkit-details-marker {
    display: none;
}

details.disclosure > summary::before {
    content: '';
    flex-shrink: 0;
    width: 0;
    height: 0;
    border-style: solid;
    border-width: 5px 0 5px 7px;
    border-color: transparent transparent transparent var(--text-dim);
    transition: transform var(--transition);
}

details.disclosure[open] > summary::before {
    transform: rotate(90deg);
}

details.disclosure .disclosure-body {
    padding: var(--s2) 0 var(--s3) var(--s5);
}

/* ---------------------------------------------------------------------------
 * Status pill (bot/stream connection state)
 * ------------------------------------------------------------------------- */

.status-pill {
    display: inline-flex;
    align-items: center;
    gap: var(--s2);
    font-size: 0.82rem;
    font-weight: 600;
    color: var(--text-dim);
}

.status-pill::before {
    content: '';
    width: 0.55rem;
    height: 0.55rem;
    border-radius: 50%;
    background: currentColor;
    flex-shrink: 0;
}

.status-pill.live {
    color: var(--ok);
}

.status-pill.live::before {
    animation: status-pulse 1.8s ease-in-out infinite;
}

.status-pill.reconnecting {
    color: var(--warn);
}

.status-pill.reconnecting::before {
    animation: status-pulse 0.9s ease-in-out infinite;
}

.status-pill.dead {
    color: var(--err);
}

@keyframes status-pulse {
    0%, 100% {
        opacity: 1;
    }

    50% {
        opacity: 0.35;
    }
}
