/* ════════════════════════════════════════════════════════════════
   site/components.css — shared, page-invariant idioms (defined once)
   ----------------------------------------------------------------
   Only idioms that are byte-identical everywhere they appear and whose
   class names are specific enough not to collide live here. Where two
   pages used the SAME name for DIFFERENT rules (.modal, .stat-card,
   .stat-value, .range-*, .donut, .loading), those stay page-scoped in
   site/pages/*.css behind a .page-* wrapper — merging them here would
   change rendering. Per-page numeric outliers on an otherwise-shared
   rule (e.g. .pop z-index, .tb-search min-width) are handled with a
   small override in the page file, noted at the rule below.

   Deferred to Task 4: stat tiles, data tables/lists and the two modal
   systems are still duplicated per page. Those are better unified as
   parameterised shared *components* (StatTile, DataTable, Modal) than
   as CSS, so they are intentionally left for the component-extraction
   task rather than force-merged here.

   SCSS-READY: becomes _components.scss.
   ════════════════════════════════════════════════════════════════ */

/* ════════════════════════════════════════════════════════════════
   Keyframes — de-duplicated from the page sheets (were repeated up to
   6×). Page CSS references these by name.
   ════════════════════════════════════════════════════════════════ */

@keyframes skel-shimmer {
    0%   { background-position: 200% 0; }
    100% { background-position: -200% 0; }
}

@keyframes wipe-right {
    from { clip-path: inset(0 100% 0 0); }
    to   { clip-path: inset(0 0 0 0); }
}

@keyframes tb-spin { to { transform: rotate(360deg); } }

@keyframes modal-in {
    from { opacity: 0; transform: translate(-50%, -48%) scale(.98); }
    to   { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}

@keyframes toast-in {
    from { opacity: 0; transform: translate(-50%, 10px); }
    to   { opacity: 1; transform: translate(-50%, 0); }
}

@keyframes fly-in {
    from { transform: translateX(60px); opacity: 0; }
    to   { transform: translateX(0); opacity: 1; }
}

@keyframes modal-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes modal-rise { from { opacity: 0; transform: translateY(12px) scale(.99); } to { opacity: 1; transform: none; } }

/* There is deliberately no .menu-backdrop click-catch layer for the .pop menus: the section hosting an
   open menu is lifted above any such layer (.section.menus-open) so the panel below can't clip the menu,
   which means a backdrop can never see a press made inside that section — the search box, the other
   filter buttons, the list. Dismissal is a document-level delegate instead (MenuDismissGate). */

/* Vertical page-content stack used by every dashboard list page. */
.dash { display: flex; flex-direction: column; gap: 34px; }

/* ════════════════════════════════════════════════════════════════
   Toolbar (actions · count · search · range) — used by the list pages
   (Incidents, Requests, Assets, Vulnerabilities) and Security Bulletins.
   ════════════════════════════════════════════════════════════════ */

.tb-actions { display: flex; align-items: center; gap: 2px; flex-wrap: wrap; }
.tb-right { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }

.tb-btn {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: 7px 10px;
    border: none;
    background: transparent;
    color: var(--muted);
    font: 500 13px var(--font-body);
    cursor: pointer;
    border-radius: var(--radius-sm);
    transition: color .15s ease, background .15s ease;
}
.tb-btn:hover { color: var(--text); background: rgba(255, 255, 255, 0.06); }
.tb-btn svg { width: 15px; height: 15px; color: var(--accent); flex: none; }
.tb-btn svg.spin {
    transform-box: fill-box;
    transform-origin: center;
    animation: tb-spin .7s linear infinite;
}

.tb-count { font-size: 13px; color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; }

/* The box is sized by min-width alone — base 210px; page overrides: Vulnerabilities 264,
   Bulletins 220. The input has to be told to fill it (flex: 1 1 0): left to grow 0 it sits at
   the browser's default 20-character intrinsic width and clips the tail off any placeholder
   longer than that, however wide the box around it is. */
.tb-search {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 7px 12px;
    border: 1px solid var(--line);
    background: var(--surface);
    min-width: 210px;
}
.tb-search svg { width: 14px; height: 14px; color: var(--faint); flex: none; }
.tb-search input {
    border: none;
    background: transparent;
    outline: none;
    color: var(--text);
    font: 400 13px var(--font-body);
    flex: 1 1 0;
    min-width: 0;
}
.tb-search input::placeholder { color: var(--faint); }

/* "last update time" range dropdown trigger (Incidents, Requests) */
.tb-rangebtn {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: 7px 10px;
    border: 1px solid var(--line);
    background: var(--surface);
    color: var(--muted);
    font: 500 13px var(--font-body);
    cursor: pointer;
    white-space: nowrap;
    transition: color .15s ease, border-color .15s ease;
}
.tb-rangebtn:hover { color: var(--text); border-color: rgba(255, 255, 255, 0.2); }
.tb-rangebtn > svg { width: 15px; height: 15px; flex: none; }
.tb-rangebtn .tb-chev { width: 13px; height: 13px; opacity: 0.7; margin-left: 2px; }

/* The wait a toolbar control costs when its change is a refetch rather than a re-filter (the range
   selector above). Indeterminate on purpose: the read reports no progress, and a bar that fills at a
   made-up rate claims one. It is absolutely positioned, so the page it appears on has to be its
   containing block (.page-incidents .panel) and the rows under it never move when it comes and goes. */
.tb-loadbar {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 3px;
    overflow: hidden;
    background: color-mix(in srgb, var(--accent) 14%, transparent);
    z-index: 5;
}

/* 32% wide, so translateX(312.5%) lands its left edge exactly on the track's right edge. */
.tb-loadbar::after {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    width: 32%;
    background: linear-gradient(90deg, transparent, var(--accent), transparent);
    animation: tb-loadbar 1.1s ease-in-out infinite;
}

@keyframes tb-loadbar {
    from { transform: translateX(-100%); }
    to   { transform: translateX(312.5%); }
}

/* Still a bar, just not a moving one — animation:none alone would leave a third of a bar parked at the
   left edge, which reads as a stalled 32%. */
@media (prefers-reduced-motion: reduce) {
    .tb-loadbar::after {
        animation: none;
        width: 100%;
        background: var(--accent);
        opacity: 0.55;
    }
}

/* ─── Filter row bits (save · add filter · chips) ─────────────── */

.tb-flabel { font-size: 13px; color: var(--muted); }

/* FilterSetBar's two halves: the saved sets, then the page's Add filter and chips. Each is one flex item
   of the page's .inc-filterbar, sized to its content, so when the row runs out of width the filters drop
   to the next line together and Add filter starts it, rather than landing wherever the wrap fell. Gap is
   inherited so each page's own spacing still applies inside the groups. */
.fs-group {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: inherit;
    min-width: 0;
}

.tb-link {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    border: none;
    background: transparent;
    color: var(--accent);
    font: 500 13px var(--font-body);
    cursor: pointer;
}
.tb-link svg { width: 14px; height: 14px; }
.tb-link:disabled { color: var(--faint); opacity: 0.6; cursor: default; }

/* Saved filter sets (FilterSetBar) — the name applies the set, or clears it again if it's the one
   already applied; the × deletes it. The chip is a span holding two buttons, so the delete control can
   live inside it without nesting a button in a button.
   A resting chip is drawn like the removable .filter-chip beside it — a thing you can pick. The ACCENT is
   reserved for the set currently applied, so which one is on can be read off the row; when every chip was
   accent-styled it couldn't be, which is what UAT reported as "no option to deselect". */
.saved-chip {
    display: inline-flex;
    align-items: stretch;
    border: 1px solid var(--line);
    border-radius: 999px;
    background: var(--surface);
    overflow: hidden;
}
.saved-chip button {
    border: none;
    background: transparent;
    color: var(--muted);
    font: 500 12.5px var(--font-body);
    cursor: pointer;
    transition: color .15s ease, background-color .15s ease;
}
.saved-chip-apply {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 5px 7px 5px 12px;
    max-width: 190px;
}
/* Clips inside the chip rather than stretching the filter row (a set can be renamed to anything). */
.saved-chip-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.saved-chip-tick { flex: none; font-size: 11px; line-height: 1; color: var(--accent); }
.saved-chip-x {
    padding: 4px 10px 4px 4px;
    font-size: 15px;
    line-height: 1;
    color: var(--faint);
}
/* Rename. Narrower than the × beside it and drawn fainter: it is the least-used of the three controls,
   but it is always visible, because a set called "Filter set 3" gives the reader nothing to go on and a
   hidden gesture would leave it that way. */
.saved-chip-edit {
    display: inline-flex;
    align-items: center;
    padding: 4px 3px;
    color: var(--faint);
}
.saved-chip-edit svg { width: 12px; height: 12px; }
.saved-chip-apply:hover { color: var(--text); }
.saved-chip-edit:hover { color: var(--accent); }
.saved-chip-x:hover { color: var(--bad); }

/* The chip while it is being renamed: the same pill with the label replaced by the box, in place. Sized
   in ch so the field is about as wide as the name it will accept and no wider, because the filter row must not
   reflow around an edit. */
.saved-chip.is-editing {
    border-color: var(--accent-line);
    background: var(--surface);
    padding: 0 6px;
}
.saved-chip-input {
    width: 15ch;
    max-width: 190px;
    border: none;
    background: transparent;
    color: var(--text);
    font: 500 12.5px var(--font-body);
    padding: 5px 2px;
}
.saved-chip-input:focus { outline: none; }

.saved-chip.is-active {
    border-color: var(--accent-line);
    background: var(--accent-soft);
}
.saved-chip.is-active button { color: var(--accent-2); }
.saved-chip.is-active .saved-chip-x { color: var(--accent); }
.saved-chip.is-active .saved-chip-x:hover { color: var(--bad); }

/* active filter chips (removable) */
.filter-chip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 4px 4px 4px 12px;
    border: 1px solid var(--line);
    border-radius: 999px;
    background: var(--surface);
    font-size: 12.5px;
    color: var(--muted);
}
.filter-chip button {
    border: none;
    background: transparent;
    color: var(--faint);
    font-size: 16px;
    line-height: 1;
    cursor: pointer;
    padding: 0 4px;
}
.filter-chip button:hover { color: var(--text); }

.tb-addfilter {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 5px 13px;
    border: 1px dashed var(--line);
    border-radius: 999px;
    background: transparent;
    color: var(--muted);
    font: 500 12.5px var(--font-body);
    cursor: pointer;
    transition: color .15s ease, border-color .15s ease;
}
.tb-addfilter:hover { color: var(--text); border-color: rgba(255, 255, 255, 0.28); }
.tb-addfilter svg { width: 13px; height: 13px; }

/* ════════════════════════════════════════════════════════════════
   Popover menus (Add filter · Customize columns · Range)
   base .pop: z-index 5, min-width 214px.
   page overrides — Requests: z-index 50, min-width 210px;
                    Vulnerabilities: min-width 200px.
   ════════════════════════════════════════════════════════════════ */

.tb-pop { position: relative; }

.pop {
    position: absolute;
    top: calc(100% + 6px);
    left: 0;
    z-index: 5;
    min-width: 214px;
    padding: 8px;
    background: var(--surface-2);
    border: 1px solid var(--line);
    box-shadow: 0 16px 40px -18px rgba(0, 0, 0, 0.7);
    display: flex;
    flex-direction: column;
    gap: 1px;
    /* A long menu (the device inventory's Add filter runs to six groups) scrolls inside itself. Left
       to its content height it ran past the foot of the page, stretched the document beyond .shell and
       showed the unthemed white body underneath. */
    max-height: 60vh;
    overflow-y: auto;
    overscroll-behavior: contain;
}
.pop-right { left: auto; right: 0; }

.pop-title {
    margin: 9px 6px 3px;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--faint);
}
.pop-title:first-child { margin-top: 2px; }

.pop-item {
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 6px 8px;
    font-size: 13px;
    color: var(--muted);
    cursor: pointer;
    border-radius: var(--radius-sm);
}
.pop-item:hover { background: rgba(255, 255, 255, 0.06); color: var(--text); }
.pop-item input { accent-color: var(--accent); width: 15px; height: 15px; cursor: pointer; }

.pop-opt {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    padding: 7px 10px 7px 8px;
    border: none;
    background: transparent;
    color: var(--muted);
    font: 500 13px var(--font-body);
    text-align: left;
    cursor: pointer;
    border-radius: var(--radius-sm);
}
.pop-opt:hover { background: rgba(255, 255, 255, 0.06); color: var(--text); }
.pop-opt.is-sel { color: var(--text); }
.pop-check { width: 13px; flex: none; color: var(--accent); font-size: 12px; }

.pop-custom {
    display: grid;
    gap: 8px;
    margin-top: 5px;
    padding: 9px 8px 4px;
    border-top: 1px solid var(--line-soft);
}
.pop-custom label {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    font-size: 12.5px;
    color: var(--muted);
}
.pop-custom input {
    padding: 5px 8px;
    border: 1px solid var(--line);
    background: var(--surface);
    color: var(--text);
    font: 400 12.5px var(--font-body);
    color-scheme: dark;
}

/* ════════════════════════════════════════════════════════════════
   Modal form fields + toast — shared by the Incidents & Requests
   raise/request modals. (The .modal shell itself stays page-scoped:
   Incidents/Requests use a fixed translate-center shell while
   Assets/Documents use a centred .modal-overlay shell — same class
   name, different rules.)
   ════════════════════════════════════════════════════════════════ */

.fld { display: block; margin-bottom: 16px; }
.fld-label { display: block; margin-bottom: 6px; font-size: 12.5px; font-weight: 600; color: var(--muted); }
.req-star { color: var(--bad); }

/* A label with its character counter on the right. Its own class rather than a change to .fld-label,
   which every other field in the portal shares and reads correctly as a plain block. */
.fld-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.fld-count { flex: none; font-weight: 500; color: var(--faint); font-variant-numeric: tabular-nums; }
/* At the cap the field stops accepting keystrokes, so the counter has to say so — silently swallowing
   what the customer types is the failure this replaced, one step earlier. */
.fld-count.is-full { color: var(--warn); }

.fld-input {
    width: 100%;
    padding: 9px 11px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--bg);
    color: var(--text);
    font: 400 13.5px var(--font-body);
    outline: none;
    resize: vertical;
    color-scheme: dark;
    transition: border-color .15s ease;
}
.fld-input:focus { border-color: var(--accent-line); }
.fld-input::placeholder { color: var(--faint); }
select.fld-input { cursor: pointer; }

/* A required field the submit refused, and what it says. The star beside the label only marks the field
   as required; it can't say "this one, now" — so the border marks which field held the press up and the
   message names it. Both go the moment the field is typed into. Kept in --bad, the star's own colour. */
.fld-input.is-missing { border-color: var(--bad); }
.fld-input.is-missing:focus { border-color: var(--bad); }
.fld-msg { display: block; margin-top: 6px; font-size: 12px; color: var(--bad); }

/* ── Attachment dropzone (Incidents + Requests raise modals) ──
   A <label> wrapping a file input stretched invisibly across it, so both clicking and dropping files
   onto the zone work with the browser's native behaviour — no JS interop, and it still works if the
   circuit is mid-reconnect. */
.dropzone {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 26px;
    border: 1.5px dashed var(--line);
    border-radius: var(--radius);
    color: var(--faint);
    font-size: 13px;
    text-align: center;
    cursor: pointer;
    transition: border-color .15s ease, color .15s ease;
}
.dropzone:hover,
.dropzone:focus-within { border-color: var(--accent-line); color: var(--muted); }
.dropzone svg { width: 26px; height: 26px; }
.dropzone-hint { font-size: 11.5px; color: var(--faint); }
.dropzone input[type="file"] {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    opacity: 0;
    cursor: pointer;
}
.dropzone.is-disabled { opacity: .55; cursor: default; }
.dropzone.is-disabled input[type="file"] { cursor: default; }

/* Files picked but not yet sent — listed so the customer can see (and drop) what they chose. */
.attach-list { display: flex; flex-direction: column; gap: 6px; margin-top: 10px; }
.attach-item {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 7px 10px;
    border: 1px solid var(--line-soft);
    border-radius: var(--radius-sm);
    background: var(--bg);
    font-size: 12.5px;
}
/* Every row's leading icon, staged rows included: an inline SVG with a viewBox and no intrinsic size
   fills its flex line, which drew the staged file's paperclip as a full-width graphic (UAT 2026-08-21). */
.attach-item svg { flex: none; width: 15px; height: 15px; color: var(--faint); }
.attach-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); }
.attach-size { color: var(--faint); font-size: 11.5px; white-space: nowrap; }
.attach-x {
    flex: none;
    width: 22px;
    height: 22px;
    border: none;
    background: transparent;
    color: var(--faint);
    font-size: 15px;
    line-height: 1;
    cursor: pointer;
    border-radius: var(--radius-sm);
}
/* Token-based hover so it reads in the light theme too (see app.css [data-theme=light]). */
.attach-x:hover { color: var(--text); background: var(--surface-2); }
.attach-error { margin: 8px 0 0; font-size: 12.5px; color: var(--bad); }

/* Already-attached files on an incident / request detail page — the same row, as a download link. */
a.attach-item { text-decoration: none; transition: border-color .15s ease, background .15s ease; }
a.attach-item:hover { border-color: var(--accent-line); background: var(--surface-2); }
a.attach-item:hover .attach-name { color: var(--accent-2); }
.attach-by { color: var(--faint); font-size: 11.5px; white-space: nowrap; }

.req-toast {
    position: fixed;
    z-index: 220;
    bottom: 26px;
    left: 50%;
    transform: translateX(-50%);
    padding: 11px 18px;
    border: 1px solid var(--accent-line);
    border-radius: var(--radius);
    background: var(--surface-2);
    color: var(--accent-2);
    font-size: 13px;
    font-weight: 500;
    box-shadow: 0 16px 40px -18px rgba(0, 0, 0, 0.7);
    animation: toast-in .25s ease both;
    /* A failure now carries its reason (a field over its limit, Jira's own words), which is a sentence
       rather than a phrase — bounded and wrapped so it can't run off both edges of the viewport. */
    max-width: min(560px, calc(100vw - 32px));
}
@media (prefers-reduced-motion: reduce) { .req-toast { animation: none; } }

/* ════════════════════════════════════════════════════════════════
   Closure-status pill — the Incidents list's verdict cell (colour set
   inline via currentColor).
   ════════════════════════════════════════════════════════════════ */

.cls-pill {
    display: inline-flex;
    align-items: center;
    padding: 3px 10px;
    border: 1px solid currentColor;
    background: transparent;
    font-size: 11.5px;
    font-weight: 600;
    white-space: nowrap;
}

/* ════════════════════════════════════════════════════════════════
   Detail-page kit (.id-*) — shared verbatim by the Incident detail and
   Service-request detail pages. These names appear nowhere else, so
   they are safe to define globally without a page wrapper. Page-unique
   extras (report blocks, type tag, mini avatar, selects) live in the
   respective site/pages/*-detail.css.
   ════════════════════════════════════════════════════════════════ */

.id-back {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    color: var(--muted);
    text-decoration: none;
    font-size: 13px;
    font-weight: 500;
    width: fit-content;
    transition: color .15s ease;
}
.id-back:hover { color: var(--text); }
.id-back svg { width: 15px; height: 15px; }

.id-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 18px;
    flex-wrap: wrap;
}
.id-head h1 {
    margin: 2px 0 0;
    font-family: var(--font-display);
    font-size: 26px;
    font-weight: 700;
    letter-spacing: -0.01em;
    /* Same ink as .content .page-head h1 (site/layout.css) — a detail page's title is a page title.
       The token, not a #fff literal, so it darkens under [data-theme=light]. */
    color: var(--text);
}

.id-meta { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 12px; }

/* The header's controls, on the right of the title: the notification bell, and whatever else the queue
   puts there (the service request's Close button). Wraps rather than squeezing the title. */
.id-head-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }

/* The bell (§52): one reader pausing one ticket's emails. A button rather than a switch, and it names the
   action it performs, while the icon and the tint carry the state it is in. */
.id-bell {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: 8px 14px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--muted);
    font: 600 13px var(--font-body);
    cursor: pointer;
    transition: border-color .15s ease, color .15s ease;
}
.id-bell:hover { border-color: var(--accent-line); color: var(--text); }
/* Paused reads as pressed, so the next press plainly turns it back on, the same treatment a reaction
   chip of the reader's own gets. */
.id-bell.is-muted { border-color: var(--accent-line); color: var(--accent); background: var(--surface-2); }
.id-bell:disabled { opacity: .55; cursor: default; }
.id-bell svg { width: 15px; height: 15px; flex: none; }

.id-chip {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    font-size: 13px;
    color: var(--muted);
}
.id-dot { width: 9px; height: 9px; flex: none; border-radius: 50%; }

.id-grid {
    display: grid;
    grid-template-columns: 1.7fr 1fr;
    gap: 18px;
    align-items: start;
}
.id-main, .id-side { display: flex; flex-direction: column; gap: 18px; min-width: 0; }

.id-h2 {
    margin: 0 0 14px;
    font-family: var(--font-display);
    font-size: 15px;
    font-weight: 600;
    padding-bottom: 10px;
    border-bottom: 1px solid var(--line-soft);
}

.id-comments { display: flex; flex-direction: column; gap: 16px; }
.id-comment { display: flex; gap: 12px; }

.id-avatar {
    width: 32px;
    height: 32px;
    flex: none;
    display: grid;
    place-items: center;
    border-radius: 50%;
    /* The puck holds either initials or the author's photo, so it clips the image to the circle. */
    overflow: hidden;
    font-size: 11px;
    font-weight: 700;
    color: #04201D;
    background: linear-gradient(140deg, #5EEAD9, #14B8A6);
}
.id-comment:not(.is-customer) .id-avatar { background: linear-gradient(140deg, #8AB4FF, #4D85F5); }
/* Fills the puck and keeps the aspect: a portrait crops rather than squashing (§23's both-axes rule). */
.id-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* ── Reusable user avatar (photo or initials puck) — see Components/UserAvatar.razor ── */
.user-avatar {
    flex: none;
    display: grid;
    place-items: center;
    border-radius: 50%;
    overflow: hidden;
    font-weight: 700;
    line-height: 1;
    text-transform: uppercase;
    color: #04201D;
    background: linear-gradient(140deg, #8AB4FF, #4D85F5);
}
.user-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
.user-avatar.ua-sm { width: 24px; height: 24px; font-size: 9px; }
.user-avatar.ua-md { width: 32px; height: 32px; font-size: 11px; }
.user-avatar.ua-lg { width: 40px; height: 40px; font-size: 13px; }

/* Name + avatar pairing (roster rows, assignee display) */
.user-cell { display: inline-flex; align-items: center; gap: 10px; }
.user-photo-row { display: flex; gap: 20px; align-items: center; flex-wrap: wrap; }
.user-photo-controls { display: flex; flex-direction: column; gap: 8px; }

.id-comment-body {
    flex: 1;
    min-width: 0;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--surface);
    padding: 10px 14px 12px;
}
.id-comment.is-customer .id-comment-body { border-color: var(--accent-line); }

.id-comment-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; margin-bottom: 5px; }
/* Same reason as the composer footer: an unbroken display name would otherwise run past
   the panel edge instead of wrapping. */
.id-comment-head strong { font-size: 13.5px; overflow-wrap: anywhere; }
/* Which side of the conversation a comment came from — "SecOps" or the customer's own org name.
   The avatar/border tint alone didn't read as a side in UAT, so the pill spells it out (§8). Colours
   match the avatars: blue for SecOps, the accent for the customer. An org name can be long, so the
   pill clamps to one ellipsised line with the full name on its title. */
.id-org {
    flex: none;
    max-width: 45%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 10.5px;
    font-weight: 600;
    letter-spacing: .02em;
    padding: 1px 8px;
    border-radius: 999px;
    border: 1px solid rgba(77, 133, 245, .45);
    color: #6C9BF7;
}
.id-comment.is-customer .id-org { border-color: var(--accent-line); color: var(--accent); }
/* An internal analyst note, which only a staff reader is ever served (§40). Deliberately neither side's
   colour: it is not a side of the conversation, it is the one comment in the thread the customer will
   never see, so it is drawn in the warning ink beside whichever side pill it follows. */
.id-internal { border-color: rgba(255, 180, 84, .45); color: var(--warn); }
.id-comment.is-customer .id-internal { border-color: rgba(255, 180, 84, .45); color: var(--warn); }
.id-when { font-size: 12px; color: var(--faint); margin-left: auto; }
/* Comment text is plain text with real line breaks in it — Jira's ADF blocks and hard breaks come
   through as "\n" (JiraService.ExtractAdfText), and a portal textarea keeps whatever the writer typed.
   pre-wrap belongs on the shared rule, not a page override: every thread that uses this kit (incidents,
   service requests, remediation tickets) is fed the same way, and only Incident detail had the rule, so
   the other two ran a signature and its sign-off together on one line. */
.id-comment-body p {
    margin: 0;
    font-size: 13.5px;
    line-height: 1.55;
    color: var(--muted);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

/* A formatted comment: paragraphs, bullets and marks the writer applied (§41). pre-wrap above already
   draws the breaks inside a block, so these rules only have to space the blocks apart and draw the marks.
   A single-paragraph comment is unaffected, which is what the remediation thread and every comment written
   before formatting existed still are. */
.id-comment-body p + p,
.id-comment-body p + ul,
.id-comment-body p + ol,
.id-comment-body ul + p,
.id-comment-body ol + p { margin-top: 8px; }
.id-comment-body ul,
.id-comment-body ol { margin: 0; padding-left: 20px; }
.id-comment-body li {
    font-size: 13.5px;
    line-height: 1.55;
    color: var(--muted);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}
.id-comment-body li + li { margin-top: 2px; }
.id-comment-body strong { color: var(--text); font-weight: 700; }

.id-empty { margin: 0 0 18px; font-size: 13.5px; color: var(--faint); }

/* The composer sits ABOVE the thread — a new comment lands directly under it (§8). */
.id-addcomment {
    border-bottom: 1px solid var(--line-soft);
    padding-bottom: 16px;
    margin-bottom: 18px;
}
/* The writing surface. A contenteditable div rather than a textarea since a comment gained formatting
   (§41), so it wears the textarea's own look: same border, background, radius, type and focus ring, and it
   grows with the writing up to a cap instead of being dragged. */
.id-editor {
    width: 100%;
    min-height: 66px;              /* the three rows the textarea showed */
    max-height: 40vh;
    overflow-y: auto;
    padding: 10px 12px;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--bg);
    color: var(--text);
    font: 400 13.5px var(--font-body);
    line-height: 1.55;
    outline: none;
    transition: border-color .15s ease;
    position: relative;            /* anchors the placeholder below */
}
.id-editor:focus { border-color: var(--accent-line); }
/* The placeholder. Driven by a class rather than :empty, because a contenteditable surface the writer has
   emptied again still holds a <br>, so :empty would leave the hint hidden on an empty box. comments.js
   owns the class (never a Blazor render), or the hint sits under the first character typed. It is
   positioned out of the flow: an inline ::before is a box the caret sits after, so focusing the empty
   surface put the caret at the end of the hint rather than the start of the line. */
.id-editor.is-empty::before {
    content: attr(data-placeholder);
    position: absolute;
    top: 10px;                     /* the surface's own padding */
    left: 12px;
    right: 12px;
    color: var(--faint);
    pointer-events: none;
    user-select: none;
}
/* Posting: the surface is contenteditable="false" for as long as the write is in flight, and it says so
   the way every other control in this kit does. */
.id-editor[contenteditable="false"] { opacity: .55; }
.id-editor p { margin: 0; }
.id-editor ul, .id-editor ol { margin: 4px 0; padding-left: 20px; }
.id-editor strong, .id-editor b { font-weight: 700; }

/* The formatting buttons, drawn as what they do. They sit in the same strip as the emoji and attach
   controls: one row of tools under the surface, rather than a second toolbar above it. */
.id-mark-b { font-weight: 800; }
.id-mark-i { font-style: italic; font-family: var(--font-body); }
.id-mark-u { text-decoration: underline; }
.id-mark-list { font-size: 17px; }
.id-tool-sep {
    flex: none;
    width: 1px;
    align-self: stretch;
    margin: 2px 3px;
    background: var(--line);
}

.id-addcomment-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    /* Wraps rather than overflows: the tool strip grew by four formatting buttons (§41), and a panel too
       narrow to hold the row must drop the Comment button to a second line, not push it past the panel's
       overflow:hidden edge where it reads as missing (§26). */
    flex-wrap: wrap;
    gap: 12px;
    margin-top: 10px;
}
.id-hint { font-size: 12.5px; color: var(--faint); }
.id-hint strong { color: var(--muted); font-weight: 600; }

/* The "Posting as {name}" hint is the only elastic item in the footer, and a display name
   is one unbroken word — without min-width:0 that word is the flex floor, so it pushes the
   Comment button past the panel's overflow:hidden edge and the button reads as missing
   (see panopticon-conventions §26). Clamp the hint to one ellipsised line, hold the button
   at its natural width. The full name is on the span's title. */
.id-addcomment-foot .id-hint {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.id-addcomment-foot .idt-btn { flex: none; }

/* ── Comment thread: composer tools, emoji, reactions (§38) ──
   The emoji and attach controls sit at the left of the composer footer, before the elastic hint, so the
   Comment button keeps its place on the right. Both are held at their natural size: the hint above is
   the only thing in that row allowed to shrink. */
.id-compose-tools { display: flex; align-items: center; gap: 6px; flex: none; }
.id-tool {
    position: relative;
    flex: none;
    width: 30px;
    height: 30px;
    display: grid;
    place-items: center;
    padding: 0;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--muted);
    font-size: 14px;
    line-height: 1;
    cursor: pointer;
    transition: border-color .15s ease, color .15s ease;
}
.id-tool:hover { border-color: var(--accent-line); color: var(--text); }
.id-tool.is-on { border-color: var(--accent-line); color: var(--accent); }
.id-tool svg { width: 15px; height: 15px; }
/* Same invisible-input trick as .dropzone: the label IS the button, so the native picker opens with no
   JS interop and keeps working while the circuit reconnects. */
.id-tool input[type="file"] {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    opacity: 0;
    cursor: pointer;
}
.id-tool.is-disabled { opacity: .55; cursor: default; }
.id-tool.is-disabled input[type="file"] { cursor: default; }

/* Emoji palettes: the composer's insert grid and a comment's reaction strip. Both expand INLINE rather
   than floating: .content .panel is overflow:hidden (§26), so an absolutely positioned panel in here
   would be clipped at the panel edge. */
.id-emoji-grid,
.id-react-picker {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    margin-top: 10px;
    padding: 8px;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--surface);
}
.id-react-picker { margin-top: 8px; }
.id-emoji {
    width: 30px;
    height: 30px;
    display: grid;
    place-items: center;
    padding: 0;
    border: 1px solid transparent;
    border-radius: var(--radius-sm);
    background: transparent;
    font-size: 16px;
    line-height: 1;
    cursor: pointer;
}
.id-emoji:hover { border-color: var(--accent-line); background: var(--surface-2); }

/* Files staged with a comment but not yet sent: the Comment button uploads them (§23). */
.id-staged { margin-top: 10px; }
.id-staged-hint { display: block; margin-top: 6px; }
/* Files that were sent with a comment, inside its bubble. */
.id-comment-files { margin-top: 10px; }

.id-error { margin: 10px 0 0; font-size: 12.5px; color: var(--bad); }

/* One chip per emoji: the glyph, the count, and every reactor's name on the hover, which is the whole
   point of recording who ("hover over it and it will show who added it", UAT 2026-08-20). Rendered as a
   <span> instead of a <button> for a read-only role: they still see the reactions, they just can't add
   one (§32). */
.id-reactions { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 10px; }
.id-react {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 2px 8px;
    border: 1px solid var(--line);
    border-radius: 999px;
    background: var(--bg);
    color: var(--muted);
    font: 600 11.5px var(--font-body);
    line-height: 1.7;
    cursor: pointer;
    transition: border-color .15s ease, background .15s ease;
}
button.id-react:hover { border-color: var(--accent-line); background: var(--surface-2); }
span.id-react { cursor: default; }
/* The caller's own reaction reads as pressed, so the next click plainly takes it off. */
.id-react.is-mine { border-color: var(--accent-line); color: var(--accent); background: var(--surface-2); }
.id-react-glyph { font-size: 13px; line-height: 1; }
.id-react-n { font-variant-numeric: tabular-nums; }
.id-react-add { color: var(--faint); font-size: 13px; padding: 2px 9px; }

.idt-btn {
    padding: 8px 15px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--text);
    font: 600 13px var(--font-body);
    cursor: pointer;
    text-decoration: none;
    transition: border-color .15s ease, filter .15s ease;
}
.idt-btn.primary { background: var(--accent); border-color: var(--accent); color: #04201D; }
.idt-btn.primary:hover { filter: brightness(1.08); }
.idt-btn.primary:disabled { opacity: 0.5; cursor: default; filter: none; }

.id-props { margin: 0; display: grid; gap: 14px; }
.id-props > div { display: grid; gap: 5px; }
.id-props dt {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--faint);
}
.id-props dd {
    margin: 0;
    font-size: 13.5px;
    color: var(--text);
    display: flex;
    align-items: center;
    gap: 8px;
}

.id-activity { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 14px; }
.id-activity li { display: flex; gap: 11px; }
.id-adot {
    width: 8px; height: 8px;
    flex: none;
    margin-top: 5px;
    border-radius: 50%;
    background: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-soft);
}
.id-activity li > div { display: grid; gap: 2px; }
.id-atext { font-size: 13px; color: var(--muted); }
.id-activity time { font-size: 11.5px; color: var(--faint); font-variant-numeric: tabular-nums; }

.id-missing { display: flex; flex-direction: column; align-items: flex-start; gap: 12px; }
.id-missing h1 { margin: 0; font-family: var(--font-display); color: var(--text); }
.id-missing p { margin: 0; color: var(--muted); }

/* ─── Loading state (DetailSkeleton.razor) ─────────────────────────
   The page's own shape, shimmering, while the detail read is in flight. Prerendering is off
   (RenderModes), so the first paint of a detail page happens before its data exists: rendering
   nothing until the read landed left the reader on a blank page, and a click on a queue row read
   as a click that did nothing. Same gradient and rhythm as the queue's own panel shimmer
   (site/pages/incidents.css), so both halves of the journey shimmer alike. Defined here, beside
   the kit it stands in for, because all three .id-* detail pages share it. */
.id-skel { display: flex; flex-direction: column; gap: 34px; }

.id-sk {
    display: block;
    border-radius: var(--radius-sm);
    background-color: var(--surface-2);
    background-image: linear-gradient(100deg, transparent 30%, rgba(255, 255, 255, 0.06) 50%, transparent 70%);
    background-size: 200% 100%;
    background-repeat: no-repeat;
    animation: skel-shimmer 1.4s linear infinite;
}

/* A white sweep is invisible on a light surface, so the light theme sweeps in ink instead. */
html[data-theme="light"] .id-sk {
    background-image: linear-gradient(100deg, transparent 30%, rgba(16, 24, 32, 0.055) 50%, transparent 70%);
}

@media (prefers-reduced-motion: reduce) {
    .id-sk { animation: none; }
}

.id-sk-back { width: 88px; height: 13px; }

/* Mirrors .id-head's stack: eyebrow, title, then the chip row. */
.id-sk-head { display: flex; flex-direction: column; align-items: flex-start; }
.id-sk-eyebrow { width: 190px; height: 12px; margin-bottom: 10px; }
.id-sk-title { width: min(560px, 72%); height: 26px; }
.id-sk-meta { display: flex; gap: 12px; margin-top: 18px; }
.id-sk-chip { width: 104px; height: 13px; }

/* The bar plus the rule .id-h2 draws under it, at the same spacing. */
.id-sk-h2 { padding-bottom: 10px; margin-bottom: 14px; border-bottom: 1px solid var(--line-soft); }
.id-sk-h2 .id-sk { width: 132px; height: 14px; }

.id-sk-lines { display: flex; flex-direction: column; gap: 13px; }
.id-sk-line { height: 12px; }

/* Mirrors .id-props: a label over a value, per row. */
.id-sk-props { display: grid; gap: 16px; }
.id-sk-props > div { display: grid; gap: 7px; }
.id-sk-label { width: 76px; height: 10px; }
.id-sk-value { height: 13px; }

@media (max-width: 1080px) {
    .id-grid { grid-template-columns: 1fr; }
}

/* ─── Rich-text editor (RichTextEditor.razor + js/richtext.js) ────────────────────
   Self-contained WYSIWYG for authoring free-form security bulletins. Class names are
   rte-* prefixed to stay clear of the Bootstrap / kit .btn·.modal collisions. */

.rte { display: flex; flex-direction: column; border: 1px solid var(--line); border-radius: var(--radius, 8px); background: var(--surface-2); overflow: hidden; }
.rte-disabled { opacity: 0.6; pointer-events: none; }

.rte-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; padding: 6px 8px; border-bottom: 1px solid var(--line); background: var(--surface); }

.rte-btn {
    min-width: 30px;
    height: 28px;
    padding: 0 8px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 1px solid transparent;
    border-radius: 6px;
    background: transparent;
    color: var(--muted);
    font: 600 12.5px var(--font-body);
    cursor: pointer;
    transition: color .12s ease, background .12s ease, border-color .12s ease;
}
.rte-btn:hover { color: var(--text); background: rgba(255, 255, 255, 0.06); border-color: var(--line); }
.rte-sep { width: 1px; align-self: stretch; margin: 2px 4px; background: var(--line); }

.rte-surface {
    min-height: 220px;
    max-height: 60vh;
    overflow-y: auto;
    padding: 14px 16px;
    font-size: 13.5px;
    line-height: 1.65;
    color: var(--text);
    outline: none;
}
.rte-surface:empty::before { content: attr(data-placeholder); color: var(--faint); }
.rte-surface h1, .rte-surface h2 { font-family: var(--font-display); font-size: 17px; font-weight: 700; margin: 16px 0 8px; color: var(--text); }
.rte-surface h3, .rte-surface h4 { font-size: 14px; font-weight: 700; margin: 14px 0 6px; }
.rte-surface p { margin: 0 0 10px; }
.rte-surface ul, .rte-surface ol { margin: 0 0 10px; padding-left: 22px; }
.rte-surface blockquote { margin: 0 0 10px; padding: 6px 12px; border-left: 3px solid var(--accent); background: var(--surface); }
.rte-surface a { color: var(--accent-2); }
.rte-surface img { max-width: 100%; height: auto; display: block; margin: 10px 0; border: 1px solid var(--line); border-radius: 6px; }

.rte-file { display: none; }

.rte-status { padding: 6px 12px; font-size: 12px; color: var(--faint); border-top: 1px solid var(--line); }
.rte-status-error { color: var(--danger, #ef4444); }

/* ════════════════════════════════════════════════════════════════
   List footer + pagination — shared by every paged list
   The <Pager> control (Components/Pager.razor) and the footer it
   sits in. Unscoped on purpose: the same control pages the customer
   pages and the admin kit's tables, so the rules can't hang off one
   page's wrapper the way the Incidents / Vulnerabilities / Assets
   copies do (those keep their page-scoped rules, which win on
   specificity and so still own the look of their own footers).
   ════════════════════════════════════════════════════════════════ */

.list-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 18px;
    flex-wrap: wrap;
    padding: 12px 16px;
    border-top: 1px solid var(--line-soft);
    color: var(--faint);
    font-size: 12.5px;
}
.list-foot-info { display: inline-flex; align-items: center; gap: 18px; flex-wrap: wrap; }
.list-foot strong { color: var(--muted); font-variant-numeric: tabular-nums; }

.pager { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; }

.pager-size { display: inline-flex; align-items: center; gap: 8px; font-size: 12.5px; color: var(--faint); }
.pager-size select {
    padding: 5px 8px;
    border: 1px solid var(--line);
    background: var(--surface);
    color: var(--text);
    font: 500 12.5px var(--font-body);
    cursor: pointer;
    color-scheme: dark;
}
/* The tokens flip with the theme but color-scheme doesn't, and a dark native dropdown over a light
   page is the one part of the control the tokens can't reach. */
html[data-theme="light"] .pager-size select { color-scheme: light; }

.pager-nav { display: inline-flex; align-items: center; gap: 10px; }
.pager-page { font-size: 12.5px; color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; }

.pager-btn {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 6px 11px;
    border: 1px solid var(--line);
    background: var(--surface);
    color: var(--muted);
    font: 500 12.5px var(--font-body);
    cursor: pointer;
    transition: color .15s ease, border-color .15s ease;
}
.pager-btn:hover:not(:disabled) { color: var(--text); border-color: var(--hair); }
.pager-btn:disabled { opacity: 0.4; cursor: default; }
.pager-btn svg { width: 13px; height: 13px; flex: none; }
