/* File: shared/css/portal-chrome.css
 *
 * The application shell every signed-in portal screen sits in: a navigation rail down the
 * leading edge, a bar across the top of the content column, and the content itself.
 *
 * WHY THIS FILE EXISTS
 *
 * The three portals previously each carried their own chrome geometry, and a later change
 * layered a sidebar on top of one of them as a stack of `:not(.is-dark)` overrides appended
 * "last in the cascade". The result was one element styled as a top bar by one block and as a
 * sidebar by another, with three media queries disagreeing about its width. That is the
 * conflict this file removes: the SHAPE of the shell is defined once, here, and a portal
 * contributes only its palette and its wording.
 *
 * WHAT BELONGS WHERE
 *
 *   here                 geometry, states, motion, focus, the premium rail treatment
 *   portal-*-identity    the palette tokens below, and the portal's own public pages
 *
 * A portal skins the rail by setting the --rail-* tokens on its body class. Nothing here
 * hard-codes a colour that a portal might want to differ on.
 *
 * THE ACCOUNT CARD IS IN THE TOP BAR, NOT THE RAIL. It used to sit at the top of the rail,
 * above the navigation, where it competed with the brand for the most valuable position in
 * the layout and pushed the first destination below the fold on short screens. The rail is
 * for going places; the bar is for who you are and what you can do about it.
 */

/* ------------------------------------------------------------------ *
 * Tokens
 *
 * Defaults are the ink rail used by the department and administration portals. The Support
 * portal overrides them for its lighter treatment.
 * ------------------------------------------------------------------ */

:root {
    /* ONE rail width for the whole system, expanded and collapsed. Declared here and nowhere
     * else: the three portal identity stylesheets had each accumulated their own 248px, 220px
     * and 228px against this, which is why the rail's painted edge and the content's starting
     * edge landed in different places and read as two different sidebar widths.
     *
     * 264px is the conventional corporate figure: wide enough for a two-line row (name plus its
     * subtitle) at 13px without the name ever truncating, narrow enough to leave a comfortable
     * measure for content on a 1280px screen. 72px collapsed is a 32px icon tile centred in a
     * 20px gutter either side — the standard icon-rail width. */
    --rail-w: 264px;
    --rail-w-collapsed: 72px;
    --topbar-h: 64px;

    --rail-bg: #16181a;
    --rail-bg-top: #1b1e20;
    --rail-edge: rgba(255, 255, 255, .09);
    --rail-text: rgba(255, 255, 255, .74);
    --rail-text-strong: #ffffff;
    --rail-text-faint: rgba(255, 255, 255, .34);
    --rail-tile: rgba(255, 255, 255, .06);
    --rail-tile-hover: rgba(255, 255, 255, .11);
    --rail-active-bg: rgba(255, 255, 255, .085);
    --rail-accent: #ffffff;

    /* Counter badges are plates, not red body text. --ecv-red is deliberately lifted in
     * dark mode for use as text on ink; using it as a badge background produced the pale
     * salmon/white combination seen in the rail. This dedicated pair stays above WCAG AA
     * in every theme while the rail itself remains consistent across all portals. */
    --rail-badge-bg: #a62118;
    --rail-badge-bg-emphasis: #8f1c14;
    --rail-badge-text: #ffffff;
    --rail-badge-border: rgba(255, 255, 255, .42);

    --topbar-bg: rgba(255, 255, 255, .86);
    --topbar-edge: var(--ecv-line, #e5e6e4);
    --topbar-text: var(--ecv-ink, #17191b);
    /* Explicit rather than aliased to --ecv-muted: the bar subtitle is 11.5px running text
     * and #73787b on the warm canvas is 4.09:1, just under the 4.5:1 AA floor. */
    --topbar-muted: #5f6467;
}

/* ------------------------------------------------------------------ *
 * Shell geometry
 *
 * The rail is fixed so the content column scrolls under the top bar without the rail
 * moving; the content column is inset by the rail's width with a logical property, so RTL
 * mirrors the whole shell without a single extra rule.
 * ------------------------------------------------------------------ */

body.has-portal-chrome {
    margin: 0;
    padding-inline-start: var(--rail-w);
    min-height: 100svh;
}

.prail {
    position: fixed;
    inset-block: 0;
    inset-inline-start: 0;
    z-index: 60;
    width: var(--rail-w);
    display: flex;
    flex-direction: column;
    background: linear-gradient(180deg, var(--rail-bg-top) 0%, var(--rail-bg) 34%);
    border-inline-end: 1px solid var(--rail-edge);
    /* A hairline of light down the inner edge. This is the whole trick behind a rail reading
     * as a machined panel rather than a flat block: one pixel, barely visible, but the eye
     * reads it as a bevel. */
    box-shadow: inset -1px 0 0 rgba(255, 255, 255, .04);
    overflow: hidden;
}

/* ------------------------------------------------------------------ *
 * Rail — brand
 * ------------------------------------------------------------------ */

.prail-brand {
    display: flex;
    flex-direction: column;
    /* Centered as a composition, not merely aligned: the wordmark and its label share one
     * axis, with more air above than below so the block reads as set, like a title page. */
    align-items: center;
    text-align: center;
    gap: 12px;
    padding: 30px 18px 22px;
    color: inherit;
    text-decoration: none;
    position: relative;
}

/* A rule that fades out at both ends instead of hitting the edges. A full-width border is
 * the single most common tell of an unconsidered sidebar. */
.prail-brand::after {
    content: "";
    position: absolute;
    inset-inline: 20px;
    bottom: 0;
    height: 1px;
    background: linear-gradient(90deg, transparent, var(--rail-edge) 22%, var(--rail-edge) 78%, transparent);
}

.prail-brand img {
    /* The mark carries the rail's identity; at 118px it read as an afterthought. 168px is
     * the widest the wordmark can sit while keeping clear margins inside the 264px rail. */
    width: 168px;
    max-width: 100%;
    height: auto;
    filter: invert(1);
    mix-blend-mode: screen;
    opacity: .82;
}

.prail-brand-label {
    /* Its box exists before its words do. The pre-paint skeleton ships this label empty —
     * what it says depends on a language nothing has read yet — and an empty inline span is
     * zero-high, so the brand block used to grow by a line the moment the text landed. A
     * min height in em holds the space in both languages, whatever size each one sets. */
    display: block;
    min-block-size: 1.3em;
    font-size: 10px;
    font-weight: 700;
    letter-spacing: .3em;
    /* Centered tracking trick: trailing letter-space on the last glyph would push the text
     * optically left, so the same amount pads the start. */
    padding-inline-start: .3em;
    text-transform: uppercase;
    color: var(--rail-text-faint);
    white-space: nowrap;
}

html[dir="rtl"] .prail-brand-label {
    letter-spacing: normal;
    text-transform: none;
    font-size: 11.5px;
}

.prail-brand:focus-visible {
    outline: 2px solid var(--rail-accent);
    outline-offset: -3px;
    border-radius: 6px;
}

/* ------------------------------------------------------------------ *
 * Rail — navigation
 * ------------------------------------------------------------------ */

/* ------------------------------------------------------------------ *
 * The rail's one action
 *
 * A portal that exists so people can DO one thing says so at the top of its rail. The rail is
 * ink, so the strongest statement available is its inverse: a filled paper plate with ink
 * text — the highest contrast the palette holds, and impossible to confuse with the grey
 * destinations under it.
 *
 * Both colours come from the rail's own tokens rather than from literals, so a rail that is
 * ever re-skinned takes the plate with it instead of leaving white on white.
 *
 * Opt-in: only a portal that declares `railAction` in its descriptor gets one. A department
 * works a queue and an administrator configures — neither has a single dominant action, and
 * inventing one for them would be a button nobody presses at the top of every screen.
 * ------------------------------------------------------------------ */

.prail-action {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    margin: 0 18px 20px;
    min-height: 46px;
    padding: 0 16px;
    border: 0;
    border-radius: 11px;
    background: var(--rail-text-strong, #ffffff);
    color: var(--rail-bg, #16181a);
    font-size: 13.5px;
    font-weight: 800;
    letter-spacing: .01em;
    text-align: center;
    text-decoration: none;
    box-shadow: 0 6px 18px rgba(0, 0, 0, .34);
    transition: transform .18s cubic-bezier(.16, 1, .3, 1), box-shadow .18s ease,
        background-color .18s ease, color .18s ease;
}

.prail-action:hover {
    /* The hover INVERTS the plate rather than tinting it: paper-on-ink becomes ink-on-paper,
       both drawn from the rail's own two tokens. Mixing the two produced a grey a step away
       from the resting state — a change you had to look for, on the one control in the chrome
       whose whole point is that you cannot miss it.

       The paper ring is what keeps the inverted plate legible: an ink plate on an ink rail
       would otherwise read as switched off. Ring plus label are the same colour the plate had
       at rest, so nothing about the pair is new — the two states simply swap. */
    background: var(--rail-bg, #16181a);
    color: var(--rail-text-strong, #ffffff);
    transform: translateY(-1px);
    box-shadow:
        0 0 0 2px var(--rail-text-strong, #ffffff),
        0 10px 26px rgba(0, 0, 0, .5);
}

.prail-action:active {
    transform: translateY(0);
}

.prail-action i {
    font-size: 12px;
}

.prail-action:focus-visible {
    outline: 2px solid var(--rail-text-strong, #ffffff);
    outline-offset: 3px;
}

.prail-nav {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 14px 10px 18px;
    display: flex;
    flex-direction: column;
    gap: 2px;
    /* A thin, unobtrusive scrollbar; the rail must not gain a chunky grey gutter the moment
     * a portal adds a tenth destination. */
    scrollbar-width: thin;
    scrollbar-color: rgba(255, 255, 255, .16) transparent;
}

.prail-nav::-webkit-scrollbar { width: 6px; }
.prail-nav::-webkit-scrollbar-thumb {
    background: rgba(255, 255, 255, .16);
    border-radius: 999px;
}
.prail-nav::-webkit-scrollbar-track { background: transparent; }

.prail-group {
    margin: 14px 12px 6px;
    font-size: 8.5px;
    font-weight: 700;
    letter-spacing: .2em;
    text-transform: uppercase;
    color: var(--rail-text-faint);
}

.prail-group:first-child { margin-top: 2px; }

html[dir="rtl"] .prail-group {
    letter-spacing: normal;
    text-transform: none;
    font-size: 11px;
}

.prail-item {
    position: relative;
    display: flex;
    align-items: center;
    gap: 12px;
    min-height: 46px;
    padding: 7px 12px;
    border-radius: 10px;
    color: var(--rail-text);
    text-decoration: none;
    transition: background-color .16s ease, color .16s ease;
}

/* The icon sits in its own tile. It gives the row a fixed optical left edge whatever the
 * glyph's width, and it is the element that carries the hover and active lift — moving the
 * whole row would feel cheap. */
.prail-item-icon {
    flex: 0 0 auto;
    display: grid;
    place-items: center;
    width: 32px;
    height: 32px;
    border-radius: 9px;
    background: var(--rail-tile);
    color: var(--rail-text);
    font-size: 12.5px;
    transition: background-color .16s ease, color .16s ease, transform .16s ease;
}

.prail-item-copy {
    display: flex;
    flex-direction: column;
    min-width: 0;
    line-height: 1.25;
}

.prail-item-name {
    font-size: 13px;
    font-weight: 700;
    letter-spacing: -.005em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* The subtitle is what makes the rail read as considered rather than as a list of nouns. It
 * is one short phrase saying what the destination is for. */
.prail-item-sub {
    font-size: 10.5px;
    font-weight: 400;
    color: var(--rail-text-faint);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.prail-item:hover {
    background: var(--rail-tile);
    color: var(--rail-text-strong);
}

.prail-item:hover .prail-item-icon {
    background: var(--rail-tile-hover);
    color: var(--rail-text-strong);
    transform: translateY(-1px);
}

.prail-item.is-active {
    background: var(--rail-active-bg);
    color: var(--rail-text-strong);
}

.prail-item.is-active .prail-item-icon {
    background: var(--rail-accent);
    color: var(--rail-bg);
}

.prail-item.is-active .prail-item-sub {
    color: rgba(255, 255, 255, .5);
}

/* The active marker: a short capsule on the leading edge, inset from the row's corners so it
 * reads as a machined indicator rather than a border. */
.prail-item.is-active::before {
    content: "";
    position: absolute;
    inset-inline-start: -10px;
    top: 50%;
    transform: translateY(-50%);
    width: 3px;
    height: 22px;
    border-radius: 0 3px 3px 0;
    background: var(--rail-accent);
}

html[dir="rtl"] .prail-item.is-active::before {
    border-radius: 3px 0 0 3px;
}

.prail-item:focus-visible {
    outline: 2px solid var(--rail-accent);
    outline-offset: -2px;
}

.prail-badge {
    flex: 0 0 auto;
    margin-inline-start: auto;
    display: inline-grid;
    place-items: center;
    min-width: 22px;
    height: 22px;
    padding: 0 7px;
    box-sizing: border-box;
    border: 1px solid var(--rail-badge-border, rgba(255, 255, 255, .42));
    border-radius: 999px;
    background-color: var(--rail-badge-bg, #a62118);
    color: var(--rail-badge-text, #ffffff) !important;
    box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, .16),
        0 2px 7px rgba(0, 0, 0, .32);
    font-size: 10.5px;
    font-weight: 800;
    line-height: 1;
    letter-spacing: 0;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    text-shadow: 0 1px 1px rgba(0, 0, 0, .24);
    transition: background-color .16s ease, border-color .16s ease, transform .16s ease;
}

.prail-badge[hidden] { display: none; }

.prail-item:hover .prail-badge,
.prail-item.is-active .prail-badge {
    background-color: var(--rail-badge-bg-emphasis, #8f1c14);
    border-color: rgba(255, 255, 255, .62);
}

.prail-item:hover .prail-badge {
    transform: translateY(-1px);
}

@media (forced-colors: active) {
    .prail-badge {
        border: 1px solid HighlightText;
        background: Highlight;
        color: HighlightText !important;
        box-shadow: none;
        text-shadow: none;
    }
}

/* ------------------------------------------------------------------ *
 * Rail — foot
 *
 * Deliberately not the account. A quiet line naming the portal, so the rail has a bottom
 * edge that is composed rather than simply where the list stopped.
 * ------------------------------------------------------------------ */

.prail-foot {
    padding: 14px 20px 18px;
    position: relative;
    font-size: 10px;
    line-height: 1.5;
    color: var(--rail-text-faint);
}

.prail-foot::before {
    content: "";
    position: absolute;
    inset-inline: 20px;
    top: 0;
    height: 1px;
    background: linear-gradient(90deg, transparent, var(--rail-edge) 22%, var(--rail-edge) 78%, transparent);
}

.prail-foot b {
    display: block;
    color: var(--rail-text);
    font-size: 10.5px;
    letter-spacing: .1em;
    text-transform: uppercase;
}

html[dir="rtl"] .prail-foot b {
    letter-spacing: normal;
    text-transform: none;
    font-size: 12px;
}

/* ------------------------------------------------------------------ *
 * Collapsed rail
 *
 * Below 1180px the rail keeps only its icons. Nothing is hidden behind a hamburger: every
 * destination stays one click away, which a drawer cannot promise.
 * ------------------------------------------------------------------ */

@media (max-width: 1180px) {
    body.has-portal-chrome { padding-inline-start: var(--rail-w-collapsed); }

    .prail { width: var(--rail-w-collapsed); }

    .prail-brand { padding: 18px 10px 14px; align-items: center; }
    .prail-brand::after { inset-inline: 12px; }
    .prail-brand img { width: 42px; }
    .prail-brand-label { display: none; }

    /* Collapsed: the plate keeps its contrast and loses its label, like every row below it. */
    .prail-action {
        margin: 0 12px 14px;
        min-height: 44px;
        padding: 0;
    }

    .prail-action span { display: none; }
    .prail-action i { font-size: 14px; }

    .prail-nav { padding-inline: 12px; align-items: center; }

    .prail-item {
        justify-content: center;
        padding: 7px 0;
        width: 100%;
    }

    .prail-item-copy { display: none; }
    .prail-item.is-active::before { inset-inline-start: -12px; }

    .prail-badge {
        position: absolute;
        top: 2px;
        inset-inline-end: 2px;
        margin: 0;
        min-width: 18px;
        height: 18px;
        padding: 0 5px;
        font-size: 9.5px;
    }

    .prail-group,
    .prail-foot { display: none; }
}

/* ------------------------------------------------------------------ *
 * Top bar
 *
 * Sits above the content column only — the rail is fixed and the bar begins after it — so
 * the two never overlap and neither has to know the other's width beyond the shared token.
 *
 * Translucent with a backdrop blur where supported, so content scrolling underneath reads as
 * passing behind glass rather than being clipped.
 * ------------------------------------------------------------------ */

.ptopbar {
    position: sticky;
    top: 0;
    z-index: 40;
    display: flex;
    align-items: center;
    gap: 14px;
    min-height: var(--topbar-h);
    padding: 0 clamp(16px, 2.4vw, 32px);
    background: var(--topbar-bg);
    border-bottom: 1px solid var(--topbar-edge);
    color: var(--topbar-text);
}

@supports (backdrop-filter: blur(12px)) {
    .ptopbar { backdrop-filter: saturate(1.4) blur(12px); }
}

.ptopbar-spacer {
    flex: 1 1 auto;
}

.ptopbar-context {
    display: flex;
    flex-direction: column;
    min-width: 0;
    margin-inline-end: auto;
    line-height: 1.25;
}

.ptopbar-title {
    font-size: 15px;
    font-weight: 700;
    letter-spacing: -.01em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.ptopbar-sub {
    font-size: 11.5px;
    color: var(--topbar-muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.ptopbar-actions {
    display: flex;
    align-items: center;
    gap: 8px;
    flex: 0 0 auto;
}

.ptopbar-btn {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    min-height: 38px;
    padding: 0 13px;
    border: 1px solid var(--topbar-edge);
    border-radius: 9px;
    background: var(--ecv-paper, #fff);
    color: var(--topbar-text);
    font-family: inherit;
    font-size: 12.5px;
    font-weight: 700;
    text-decoration: none;
    cursor: pointer;
    white-space: nowrap;
}

.ptopbar-btn:hover { background: var(--ecv-soft, #eff0ee); }

.ptopbar-btn:focus-visible {
    outline: 2px solid var(--topbar-text);
    outline-offset: 2px;
}

/* ------------------------------------------------------------------ *
 * The account card — in the top bar
 * ------------------------------------------------------------------ */

.pacct {
    position: relative;
    flex: 0 0 auto;
}

.pacct-card {
    display: flex;
    align-items: center;
    gap: 10px;
    min-height: 42px;
    padding: 4px 10px 4px 4px;
    border: 1px solid var(--topbar-edge);
    border-radius: 999px;
    background: var(--ecv-paper, #fff);
    color: var(--topbar-text);
    font-family: inherit;
    cursor: pointer;
    text-align: start;
    transition: background-color .16s ease, border-color .16s ease;
}

html[dir="rtl"] .pacct-card { padding: 4px 4px 4px 10px; }

.pacct-card:hover {
    background: var(--ecv-soft, #eff0ee);
    border-color: var(--ecv-muted, #73787b);
}

.pacct-card:focus-visible {
    outline: 2px solid var(--topbar-text);
    outline-offset: 2px;
}

.pacct-avatar {
    position: relative;
    flex: 0 0 auto;
    display: grid;
    place-items: center;
    width: 32px;
    height: 32px;
    border-radius: 50%;
    background: var(--ecv-ink, #17191b);
    color: #fff;
    font-size: 11.5px;
    font-weight: 700;
    overflow: hidden;
}

.pacct-avatar img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.pacct-copy {
    display: flex;
    flex-direction: column;
    min-width: 0;
    max-width: 20ch;
    line-height: 1.25;
}

.pacct-copy strong {
    font-size: 12.5px;
    font-weight: 700;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.pacct-copy small {
    font-size: 10.5px;
    color: var(--topbar-muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.pacct-chevron {
    flex: 0 0 auto;
    font-size: 10px;
    color: var(--topbar-muted);
    transition: transform .18s ease;
}

.pacct-card[aria-expanded="true"] .pacct-chevron { transform: rotate(180deg); }

.pacct-panel {
    position: absolute;
    inset-inline-end: 0;
    top: calc(100% + 8px);
    z-index: 70;
    min-width: 262px;
    padding: 8px;
    border: 1px solid var(--topbar-edge);
    border-radius: 14px;
    background: var(--ecv-paper, #fff);
    box-shadow: 0 20px 48px rgba(23, 25, 27, .18);
}

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

.pacct-panel-head {
    padding: 8px 10px 12px;
    border-bottom: 1px solid var(--topbar-edge);
    margin-bottom: 6px;
}

.pacct-panel-head strong {
    display: block;
    font-size: 13px;
    overflow-wrap: anywhere;
}

.pacct-panel-head small {
    display: block;
    margin-top: 2px;
    font-size: 11.5px;
    color: var(--topbar-muted);
    overflow-wrap: anywhere;
}

.pacct-role {
    display: inline-block;
    margin-top: 8px;
    padding: 3px 9px;
    border-radius: 999px;
    background: var(--ecv-soft, #eff0ee);
    font-size: 10px;
    font-weight: 700;
    letter-spacing: .12em;
    text-transform: uppercase;
}

html[dir="rtl"] .pacct-role {
    letter-spacing: normal;
    text-transform: none;
    font-size: 11.5px;
}

.pacct-panel a,
.pacct-panel .pacct-action {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    min-height: 42px;
    padding: 0 10px;
    border: 0;
    border-radius: 9px;
    background: transparent;
    color: var(--topbar-text);
    font-family: inherit;
    font-size: 13px;
    font-weight: 700;
    text-align: start;
    text-decoration: none;
    cursor: pointer;
}

.pacct-panel a:hover,
.pacct-panel .pacct-action:hover { background: var(--ecv-soft, #eff0ee); }

.pacct-panel a:focus-visible,
.pacct-panel .pacct-action:focus-visible {
    outline: 2px solid var(--topbar-text);
    outline-offset: -2px;
}

.pacct-panel .pacct-action i { width: 16px; text-align: center; }

.pacct-divider {
    height: 1px;
    margin: 6px 4px;
    background: var(--topbar-edge);
}

/* On a narrow screen the card keeps its avatar and loses its words; the panel still carries
 * the full name and address, so nothing is lost. */
@media (max-width: 720px) {
    .pacct-copy,
    .pacct-chevron { display: none; }

    .pacct-card {
        padding: 4px;
        border-radius: 50%;
    }

    .ptopbar-sub { display: none; }
}

/* ------------------------------------------------------------------ *
 * Content column
 * ------------------------------------------------------------------ */

.pcontent {
    padding: clamp(18px, 2.6vw, 30px) clamp(16px, 2.4vw, 32px) 90px;
    max-width: 100%;
    min-width: 0;
}

/* Nothing in the shell may make the page scroll sideways. */
body.has-portal-chrome { overflow-x: hidden; }

/* ------------------------------------------------------------------ *
 * The boot skeleton, and continuity across documents
 *
 * tools/shell-boot.js writes a geometry-complete rail and bar into every private screen so
 * the shell is part of the browser's FIRST PAINT — before the module graph has even been
 * fetched. These rules are what make that painted frame look like the finished one rather
 * than like an empty box.
 * ------------------------------------------------------------------ */

/* Placeholder rows: the rail's list occupies its real height immediately, so nothing inside
 * the rail shifts when the destinations arrive. Same 46px min-height as .prail-item.
 *
 * Deliberately still, not shimmering. An animated placeholder that is replaced 80ms later
 * reads as a glitch; a quiet one reads as the panel it is about to become. */
.prail-boot-row {
    display: block;
    min-height: 46px;
    margin: 2px 10px;
    border-radius: 10px;
    background: rgba(255, 255, 255, .04);
}

/* The boot skeleton's brand is a <span>, not the <a> the rendered rail uses, so it must not
 * pick up the link's hover treatment before it becomes one. */
span.prail-brand { cursor: default; }

/* ------------------------------------------------------------------ *
 * Cross-document view transitions
 *
 * These three portals are static multi-page applications: every destination in the rail is a
 * real <a href> and every click loads a new document. That is deliberate and it stays — the
 * page modules are top-level scripts with no unmount step, so re-running them inside a
 * client-side router would stack listeners and Realtime subscriptions with nothing to tear
 * them down. See DEPLOY_RUNBOOK.md for the full reasoning.
 *
 * What a cross-document view transition gives is the part that was actually broken: the
 * browser holds the OUTGOING frame on screen until the incoming document is ready to paint,
 * so there is no white gap between the two. With the rail and the bar named below, it
 * morphs those two boxes rather than cross-fading the whole viewport — so the shell reads as
 * one continuous element across the navigation even though the document underneath it was
 * replaced.
 *
 * This is continuity ON TOP OF a fixed lifecycle, not a cover for one. The blank frame, the
 * double render and the artificial loader were all removed first; nothing here depends on a
 * timer, a delay or an opacity trick. In a browser without view transitions (Firefox today)
 * every one of those fixes still applies — the shell simply paints instantly instead of
 * morphing.
 * ------------------------------------------------------------------ */

@view-transition { navigation: auto; }

.prail { view-transition-name: ecvalla-rail; }
.ptopbar { view-transition-name: ecvalla-topbar; }

/* The shell is the constant, so it must not animate at all: a fade on the rail is precisely
 * the flash this work removed. Naming it without animating it tells the browser to carry the
 * same box across, untouched. Only the content underneath is allowed to move. */
::view-transition-group(ecvalla-rail),
::view-transition-group(ecvalla-topbar) { animation: none; }
::view-transition-old(ecvalla-rail),
::view-transition-new(ecvalla-rail),
::view-transition-old(ecvalla-topbar),
::view-transition-new(ecvalla-topbar) { animation: none; mix-blend-mode: normal; }

::view-transition-old(root) { animation: ecvalla-content-out 90ms ease both; }
::view-transition-new(root) { animation: ecvalla-content-in 140ms ease both; }

@keyframes ecvalla-content-out { to { opacity: 0; } }
@keyframes ecvalla-content-in { from { opacity: 0; } }

@media (prefers-reduced-motion: reduce) {
    ::view-transition-old(root),
    ::view-transition-new(root) { animation: none; }
}

@media (prefers-reduced-motion: reduce) {
    .prail-item,
    .prail-item-icon,
    .pacct-card,
    .pacct-chevron {
        transition: none;
    }

    .prail-item:hover .prail-item-icon { transform: none; }
}

/* ------------------------------------------------------------------ *
 * The public bar
 *
 * For a portal's home, sign-in and no-access pages. No rail: those pages offer one way in,
 * and a list of destinations nobody can open yet is noise.
/* ------------------------------------------------------------------ *
 * Public bar — the minimal chrome for public pages (Theme-Aware)
 * ------------------------------------------------------------------ */

/* The mount, stamped by renderPortalPublicBar. Stickiness lives here rather than on the bar
   itself because a sticky element travels only within its containing block, and the bar's
   own parent is exactly its height. The bar below keeps its sticky declaration so a bar
   rendered into some other container still behaves; this is what makes it float on a page. */
.pbar-mount {
    position: sticky;
    top: 0;
    z-index: 50;
}

.pbar {
    position: sticky;
    top: 0;
    z-index: 50;
    display: flex;
    align-items: center;
    gap: 12px;
    min-height: 66px;
    padding: 10px clamp(16px, 3vw, 40px);
    background: var(--ecv-paper, #ffffff);
    border-bottom: 1px solid var(--ecv-line, #e5e6e4);
    color: var(--text-color, #17191b);
    flex-wrap: wrap;
    row-gap: 8px;
    transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

.pbar-brand {
    display: flex;
    align-items: center;
    gap: 12px;
    flex: 0 0 auto;
    color: inherit;
    text-decoration: none;
}

.pbar-mark {
    height: 26px;
    width: auto;
    display: block;
}

.pbar-brand span {
    font-size: 10px;
    font-weight: 700;
    letter-spacing: .2em;
    text-transform: uppercase;
    padding-inline-start: 12px;
    border-inline-start: 1px solid currentColor;
    opacity: .75;
    white-space: nowrap;
}

html[dir="rtl"] .pbar-brand span {
    letter-spacing: normal;
    text-transform: none;
    font-size: 12.5px;
}

.pbar-brand:focus-visible {
    outline: 2px solid currentColor;
    outline-offset: 4px;
    border-radius: 4px;
}

.pbar-spacer {
    flex: 1 1 auto;
    min-width: 0;
}

.pbar-btn,
.pbar-action {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    min-height: 40px;
    padding: 0 16px;
    border: 1px solid var(--ecv-line, #e5e6e4);
    border-radius: 8px;
    background: transparent;
    color: inherit;
    font-family: inherit;
    font-size: 13px;
    font-weight: 700;
    text-decoration: none;
    cursor: pointer;
    white-space: nowrap;
    flex: 0 0 auto;
    transition: background-color 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
}

.pbar-btn:hover {
    background: var(--ecv-soft, #f0f0ee);
}

.pbar-action {
    background: var(--ecv-ink, #17191b);
    border-color: var(--ecv-ink, #17191b);
    color: #ffffff;
}

.pbar-action:hover {
    background: var(--ecv-ink-soft, #2b2e30);
    transform: translateY(-1px);
}

.pbar-btn:focus-visible,
.pbar-action:focus-visible {
    outline: 2px solid currentColor;
    outline-offset: 2px;
}

/* ================================================================== *
 * Dark Theme Dynamic Styling for .pbar
 * ================================================================== */

html.dark-theme .pbar,
[data-theme="dark"] .pbar {
    background: #14171a;
    border-bottom-color: rgba(255, 255, 255, 0.12);
    color: #ffffff;
}

html.dark-theme .pbar-mark,
[data-theme="dark"] .pbar-mark {
    filter: invert(1);
    mix-blend-mode: screen;
}

html.dark-theme .pbar-btn,
[data-theme="dark"] .pbar-btn {
    border-color: rgba(255, 255, 255, 0.2);
    color: #ffffff;
}

html.dark-theme .pbar-btn:hover,
[data-theme="dark"] .pbar-btn:hover {
    background: rgba(255, 255, 255, 0.1);
    border-color: rgba(255, 255, 255, 0.35);
}

html.dark-theme .pbar-action,
[data-theme="dark"] .pbar-action {
    background: #ffffff;
    border-color: #ffffff;
    color: #17191b;
}

html.dark-theme .pbar-action:hover,
[data-theme="dark"] .pbar-action:hover {
    background: rgba(255, 255, 255, 0.9);
}

@media (max-width: 560px) {
    .pbar-brand span { display: none; }
    .pbar-btn span { display: none; }
}

/* ------------------------------------------------------------------ *
 * The shell owns its own geometry
 *
 * These four declarations are !important, which is deliberate and worth the explanation.
 *
 * The rail's width and the content's inset are a matched pair: if anything changes one without
 * the other, the content either hides under the rail or floats away from it. That has already
 * happened repeatedly in this codebase — a stack of `.ap-portal:not(.is-dark) {
 * padding-inline-start: … }` rules appended to a portal's identity stylesheet, at 220px, then
 * 228px, then 248px, against a 268px rail, each one appearing to be a reasonable local tweak.
 *
 * So the shell asserts the pair and refuses to be outbid. A portal that genuinely wants a
 * different rail width sets --rail-w; it does not set the padding, because the padding is not
 * a design decision, it is an consequence of the width.
 * ------------------------------------------------------------------ */

body.has-portal-chrome {
    padding-inline-start: var(--rail-w) !important;
    padding-inline-end: 0 !important;
}

body.has-portal-chrome .prail {
    width: var(--rail-w) !important;
    min-width: var(--rail-w) !important;
    max-width: var(--rail-w) !important;
    /* The 1px edge border must live INSIDE the declared width, or the rail is genuinely
     * one pixel wider than the content inset that mirrors it. */
    box-sizing: border-box !important;
}

@media (max-width: 1180px) {
    body.has-portal-chrome {
        padding-inline-start: var(--rail-w-collapsed) !important;
    }

    body.has-portal-chrome .prail {
        width: var(--rail-w-collapsed) !important;
        min-width: var(--rail-w-collapsed) !important;
        max-width: var(--rail-w-collapsed) !important;
    }
}

/* ------------------------------------------------------------------ *
 * Theme selector, inside the account menu
 *
 * Three explicit options — Light, Dark, System — rather than a cycling button, because
 * 'system' is a real choice and a cycler never shows which state you are in. The pressed
 * option is marked with aria-pressed as well as colour.
 * ------------------------------------------------------------------ */

.pacct-theme {
    padding: 6px 10px 8px;
}

.pacct-theme-label {
    display: block;
    margin-bottom: 7px;
    font-size: 10px;
    font-weight: 700;
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--topbar-muted, #73787b);
}

html[dir="rtl"] .pacct-theme-label {
    letter-spacing: normal;
    text-transform: none;
    font-size: 11.5px;
}

.pacct-theme-options {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 4px;
    padding: 3px;
    border: 1px solid var(--topbar-edge, #e5e6e4);
    border-radius: 9px;
    background: var(--sys-surface-secondary, #eff0ee);
}

.pacct-theme-options button {
    min-height: 32px;
    padding: 0 6px;
    border: 0;
    border-radius: 7px;
    background: transparent;
    color: var(--sys-text-secondary, #73787b);
    font-family: inherit;
    font-size: 11.5px;
    font-weight: 700;
    cursor: pointer;
    white-space: nowrap;
}

.pacct-theme-options button:hover {
    color: var(--sys-text-primary, #17191b);
}

.pacct-theme-options button[aria-pressed="true"] {
    background: var(--sys-surface-raised, #fff);
    color: var(--sys-text-primary, #17191b);
    box-shadow: 0 1px 2px rgba(23, 25, 27, .12);
}

.pacct-theme-options button:focus-visible {
    outline: 2px solid var(--sys-text-primary, #17191b);
    outline-offset: 1px;
}


/* ------------------------------------------------------------------ *
 * The ticket page (.tfull) — the shared pattern for opening one record as a full page
 * with a back control. Used by the department queue and the Support portal's My
 * Tickets; moved here from the department stylesheet the day the second portal
 * adopted it. Tokens only, so it follows both themes.
 * ------------------------------------------------------------------ */

/* --- Full-page ticket workspace (.tfull) --------------------------------- */
.tfull {
    width: 100% !important;
    max-width: 100% !important;
    display: block;
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

.tfull[hidden] {
    display: none !important;
}

.tfull-body {
    width: 100% !important;
    max-width: 100% !important;
    margin: 0;
    padding: 0 0 30px;
    box-sizing: border-box;
}

.ops-detail-grid {
    display: grid;
    grid-template-columns: minmax(0, 1.7fr) minmax(300px, 380px);
    align-items: start;
    gap: 12px;
    width: 100%;
    box-sizing: border-box;
}

.ops-detail-main, .ops-detail-side {
    display: grid;
    gap: 16px;
    min-width: 0;
}

.ops-detail-card {
    min-width: 0;
    border: 1px solid var(--ops-line);
    border-radius: 12px;
    background: var(--ecv-paper);
    overflow: hidden;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03);
}

.ops-detail-card > header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    min-height: 48px;
    padding: 0 18px;
    border-bottom: 1px solid var(--ops-line);
}

.ops-detail-card h3, .dept-detail .ops-detail-card h3 {
    margin: 0;
    color: var(--text-color);
    font-size: 12.5px;
    font-weight: 700;
}

.ops-detail-card .dept-description {
    padding: 18px;
    margin: 0;
    font-size: 13px;
    line-height: 1.6;
    color: var(--text-color);
}

.ops-detail-card .dept-attachments { padding: 0 18px 16px; }
.ops-detail-card.is-internal { border-inline-start: 3px solid var(--warning-color); background: var(--ecv-soft); }

.ops-thread { padding: 14px 18px; display: grid; gap: 10px; }
.ops-thread .note {
    border: 0;
    border-inline-start: 3px solid var(--ecv-line);
    border-radius: 8px;
    background: var(--ecv-soft);
    padding: 12px 14px;
    margin: 0;
    font-size: 12.5px;
    line-height: 1.5;
}
.ops-thread .note header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    margin-bottom: 6px;
    font-size: 11.5px;
    color: var(--text-light);
}
.ops-thread .note header b { color: var(--text-color); }
.ops-thread .note p { margin: 0; color: var(--text-color); }

.note.is-action-request {
    border-inline-start-color: var(--warning-color) !important;
    background: rgba(245, 158, 11, 0.09) !important;
}
.note-badge-action {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 2px 6px;
    border-radius: 4px;
    background: var(--warning-color);
    color: #000;
    font-size: 9.5px;
    font-weight: 700;
    text-transform: uppercase;
}

.ops-composer { margin: 0; padding: 14px 18px; border-top: 1px solid var(--ops-line); background: var(--ecv-soft); }
.ops-composer textarea {
    width: 100%;
    border: 1px solid var(--ecv-line);
    border-radius: 8px;
    background: var(--ecv-paper);
    color: var(--text-color);
    padding: 12px;
    font: inherit;
    font-size: 12.5px;
    resize: vertical;
    box-sizing: border-box;
    min-height: 85px;
}
.ops-composer textarea:focus { border-color: var(--ecv-ink); outline: none; }
.ops-composer-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    margin-top: 10px;
    flex-wrap: wrap;
}
.ops-composer small { color: var(--ops-muted); font-size: 11px; }

.ops-reply-actions {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-inline-start: auto;
}
.ops-reply-actions .dept-btn {
    min-height: 36px;
    font-size: 12px;
    font-weight: 600;
    border-radius: 8px;
}

.ops-properties .dept-facts { display: block; margin: 0; border: 0; border-radius: 0; background: var(--ecv-paper); }
.ops-properties .dept-facts > div {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 15px;
    padding: 10px 18px;
    border-bottom: 1px solid var(--ecv-line);
    font-size: 11.5px;
}
.ops-properties .dept-facts dt {
    font-size: 10.5px;
    font-weight: 600;
    color: var(--text-light);
    display: flex;
    align-items: center;
    gap: 6px;
}
.ops-properties .dept-facts dt i { color: var(--ops-muted); font-size: 11px; }
.ops-properties .dept-facts dd { margin: 0; max-width: 62%; font-size: 11.5px; text-align: end; font-weight: 600; color: var(--text-color); overflow-wrap: anywhere; }

.ops-detail-side .dept-status-form { margin: 0; padding: 16px 18px; display: grid; gap: 14px; }
.ops-form-group { display: grid; gap: 6px; }
.ops-form-group label {
    font-size: 11px;
    font-weight: 700;
    color: var(--text-color);
    display: flex;
    align-items: center;
    gap: 6px;
}
.ops-form-group label.is-required::after {
    content: ' *';
    color: var(--danger-color, #ef4444);
}
.ops-input, .ops-textarea, .ops-status-select {
    width: 100%;
    border: 1px solid var(--ecv-line);
    border-radius: 8px;
    background: var(--ecv-canvas, #f5f5f3);
    color: var(--text-color);
    padding: 9px 12px;
    font: inherit;
    font-size: 12px;
    box-sizing: border-box;
    transition: border-color 0.15s, box-shadow 0.15s;
}
.ops-status-select {
    cursor: pointer;
}
.ops-status-select option {
    background: var(--ecv-paper, #1b1d20);
    color: var(--text-color, #f0f0f0);
    padding: 6px 10px;
}
.ops-input:focus, .ops-textarea:focus, .ops-status-select:focus {
    border-color: var(--ecv-ink);
    outline: none;
    box-shadow: 0 0 0 3px rgba(23, 25, 27, 0.08);
}
.ops-input-hint {
    font-size: 10.5px;
    color: var(--ops-muted);
    line-height: 1.35;
}
.ops-dynamic-status-fields { display: grid; gap: 12px; }
.ops-field-group {
    display: grid;
    gap: 12px;
    padding: 12px;
    border-radius: 9px;
    background: var(--ecv-soft);
    border: 1px dashed var(--ecv-line);
    animation: fadeInGroup 0.2s ease;
}
@keyframes fadeInGroup {
    from { opacity: 0; transform: translateY(-4px); }
    to { opacity: 1; transform: translateY(0); }
}
.dept-status-actions { display: flex; align-items: center; gap: 12px; margin-top: 4px; }
.dept-status-actions .dept-btn { min-height: 38px; font-size: 12px; font-weight: 700; }
.dept-saved { font-size: 11.5px; color: var(--success-color, #10b981); font-weight: 700; display: inline-flex; align-items: center; gap: 5px; }



html[dir="rtl"] .ops-row-arrow i, html[dir="rtl"] .ops-panel-head>a i, html[dir="rtl"] .ops-pagination i { transform: scaleX(-1); }
html[dir="rtl"] .ops-ticket-table .queue-row.is-selected { box-shadow: inset -3px 0 var(--ecv-ink); }
.pill { letter-spacing: .03em; text-transform: none; font-size: 8.5px; padding: 4px 7px; }

@media (max-width: 1250px) { .ops-kpi-grid { grid-template-columns: repeat(4, 1fr); } }
@media (max-width: 850px) {
    .ops-page-head { align-items: flex-start; }
    .ops-kpi-grid { grid-template-columns: repeat(2, 1fr); }
    .ops-dashboard-grid { grid-template-columns: 1fr; }
    .ops-panel-full { grid-column: auto; }
    .ops-advanced-filters { align-items: stretch; flex-direction: column; }
    .ops-link-btn { margin: 0; align-self: flex-start; }
    .ops-detail-grid { grid-template-columns: 1fr; }
    .ops-detail-side { order: -1; }
    .ops-table-wrap { overflow: visible; }
    .ops-ticket-table, .ops-ticket-table tbody { display: block; }
    .ops-ticket-table thead { display: none; }
    .ops-ticket-table .queue-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 9px; margin: 8px; padding: 13px; border: 1px solid var(--ops-line); border-radius: 9px; }
    .ops-ticket-table td { display: block; height: auto; max-width: none; padding: 0; border: 0; font-size: 10px; white-space: normal; }
    .ops-ticket-table td::before { content: attr(data-label); display: block; margin-bottom: 3px; color: var(--text-light); font-size: 8px; font-weight: 700; text-transform: uppercase; }
    .ops-ticket-table td:nth-child(2) { grid-column: 1/-1; grid-row: 1; }
    .ops-ticket-table td:nth-child(1) { grid-column: 1; }
    .ops-ticket-table td:nth-child(9), .ops-ticket-table td:nth-child(10), .ops-ticket-table td:nth-child(11) { display: none; }
    .ops-ticket-table .queue-subject { max-width: none; font-size: 13px; }
    .ops-table-skeleton { display: block; }
    .ops-table-skeleton td { display: block; }
    .ops-queue-toolbar { flex-wrap: wrap; }
    .ops-search { min-width: 100%; max-width: none; }
}
@media (max-width: 560px) {
    .ops-page-head { display: block; }
    .ops-head-actions { margin-top: 14px; }
    .ops-live { display: none; }
    .ops-kpi-grid { gap: 6px; }
    .ops-kpi { min-height: 95px; padding: 12px; }
    .ops-kpi strong { font-size: 23px; }
    .ops-ticket-row { grid-template-columns: 4px minmax(0, 1fr) auto; }
    .ops-ticket-row>.pill, .ops-ticket-row>.ops-sla { display: none; }
    .ops-detail-head { display: block; }
    .ops-detail-actions { margin-top: 12px; }
    .ops-detail-actions .ops-icon-btn { margin-inline-start: auto; }
    .ops-sort { flex: 1; }
    .ops-sort select { width: 100%; }
}
@media (prefers-reduced-motion: reduce) { * { scroll-behavior: auto !important; } .ops-kpi, .queue-row { transition: none !important; } }
.ops-performance { scroll-margin-top: 20px; }
.ops-performance-grid { display: grid; grid-template-columns: repeat(6, minmax(120px, 1fr)); gap: 1px; background: var(--ops-line); }
.ops-performance-grid article { min-height: 92px; padding: 14px; background: var(--ecv-paper); }
.ops-performance-grid span { display: block; color: var(--ops-muted); font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: .07em; }
.ops-performance-grid strong { display: block; margin: 9px 0 5px; color: var(--ops-navy); font-size: 22px; font-variant-numeric: tabular-nums; }
.ops-performance-grid small { display: block; color: var(--text-light); font-size: 9px; line-height: 1.4; }
@media (max-width: 850px) { .ops-performance-grid { grid-template-columns: repeat(2, 1fr); } }
.dp-content-loading, .up-content-loading {
    position: fixed;
    inset-block: 0;
    /* The loader replaces the page canvas only. It must begin exactly after the
     * live rail; the old 244px literal overlapped the current 264px sidebar and
     * made its trailing content look hidden during navigation. */
    inset-inline-start: var(--rail-w, 264px);
    inset-inline-end: 0;
    z-index: 9999;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 24px;
    background: var(--ecv-canvas, #f5f5f3);
    opacity: 1;
    transition: opacity .3s ease;
    pointer-events: all;
}
.dp-content-loading.is-done, .up-content-loading.is-done {
    opacity: 0;
    pointer-events: none;
}
.dp-content-loading .up-loading-mark, .up-content-loading .up-loading-mark {
    display: block;
    width: 220px;
    max-width: 70vw;
    height: auto;
    animation: dp-loader-breathe 2.2s ease-in-out infinite;
}
.dp-content-loading .up-loading-track, .up-content-loading .up-loading-track {
    position: relative;
    width: 216px;
    height: 2px;
    overflow: hidden;
    border-radius: 2px;
    background: #dfe4e8;
}
.dp-content-loading .up-loading-track span, .up-content-loading .up-loading-track span {
    position: absolute;
    inset-block: 0;
    width: 40%;
    border-radius: 2px;
    background: var(--ecv-ink, #17191b);
    animation: dp-loader-sweep 1.4s cubic-bezier(.45, 0, .2, 1) infinite;
}
@keyframes dp-loader-breathe {
    50% { transform: scale(1.02); opacity: .78; }
}
@keyframes dp-loader-sweep {
    0% { inset-inline-start: -40%; }
    100% { inset-inline-start: 100%; }
}
@media (max-width: 920px) {
    .dp-content-loading, .up-content-loading {
        inset-inline-start: var(--rail-w-collapsed, 72px);
    }
}

html.dark-theme .dp-content-loading .up-loading-mark,
html[data-theme="dark"] .dp-content-loading .up-loading-mark {
    filter: invert(1);
    opacity: .86;
}
@media (prefers-reduced-motion: reduce) {
    .dp-content-loading .up-loading-mark,
    .dp-content-loading .up-loading-track span {
        animation: none;
    }
}


/* ------------------------------------------------------------------ *
 * The ticket page — shared-scope repairs and the premium header
 *
 * The block above arrived from the department stylesheet carrying two problems into shared
 * scope. First, its rules resolve --ops-* tokens that only the department portal defines,
 * so on the Support portal every hairline and muted label silently lost its colour: the
 * aliases below give those names system values wherever a .tfull renders. Second, its
 * header had drifted flat — a static bar and a 30px square-ish back control. The header is
 * the piece that has to feel machined, so it is re-asserted here: glass that the work
 * scrolls under, a round back control that lifts like the rail's icon tiles, the reference
 * set small and tracked above a clamped subject.
 * ------------------------------------------------------------------ */

.tfull {
    --ops-line: var(--sys-border, var(--ecv-line, #e5e6e4));
    --ops-muted: var(--sys-text-secondary, var(--text-light, #73787b));
    --ops-navy: var(--sys-text-primary, var(--text-color, #17191b));
    --ops-soft: var(--ecv-soft, #eff0ee);
    --ops-radius: 12px;
    background: var(--sys-surface-page, var(--ecv-canvas, #f5f5f3));
    min-height: calc(100svh - var(--topbar-h, 64px));
}

/* Sits directly under the shell's top bar and stays there. Flush is the whole point, so it
   states the things that were making it not flush: no corner radius on a bar that spans the
   column, no margin under it, and no padding of its own — .tfull-head-inner owns that, and
   body.has-ticket-page has already taken the list's padding off the content column. */
.tfull-head {
    position: sticky;
    top: var(--topbar-h, 64px);
    z-index: 30;
    width: 100%;
    box-sizing: border-box;
    margin: 0;
    padding: 0;
    border-radius: 0;
    box-shadow: none;
    background: var(--topbar-bg, rgba(255, 255, 255, .86));
    border-bottom: 1px solid var(--sys-border, var(--ecv-line, #e5e6e4));
}

@supports (backdrop-filter: blur(12px)) {
    .tfull-head { backdrop-filter: saturate(1.4) blur(12px); }
}

.tfull-head-inner {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    gap: 14px;
    max-width: 1560px;
    margin: 0 auto;
    padding: 14px clamp(14px, 2.4vw, 32px);
}

.tfull-back {
    flex: 0 0 auto;
    display: grid;
    place-items: center;
    width: 42px;
    height: 42px;
    padding: 0;
    border: 1px solid var(--sys-border, var(--ecv-line, #e5e6e4));
    border-radius: 50%;
    background: var(--sys-surface-raised, var(--ecv-paper, #fff));
    color: var(--sys-text-primary, var(--ecv-ink, #17191b));
    font-size: 14px;
    cursor: pointer;
    transition: background-color .16s ease, border-color .16s ease, transform .16s ease;
}

.tfull-back:hover {
    background: var(--sys-surface-hover, var(--ecv-soft, #eff0ee));
    border-color: var(--sys-border-strong, var(--ecv-muted, #73787b));
    transform: translateY(-1px);
}

.tfull-back:focus-visible {
    outline: 2px solid var(--sys-text-primary, #17191b);
    outline-offset: 2px;
}

html[dir="rtl"] .tfull-back i { transform: scaleX(-1); }

.tfull-title {
    min-width: 0;
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 2px;
}

.tfull-eyebrow {
    margin: 0;
    /* Its own line box. The deleted block set line-height: 1.1 against a font size the later
       block had already grown, which is what pulled the reference down onto the subject. */
    line-height: 1.4;
    font-size: 10px;
    font-weight: 700;
    letter-spacing: .16em;
    text-transform: uppercase;
    color: var(--sys-text-muted, var(--text-muted, #6c7174));
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

html[dir="rtl"] .tfull-eyebrow { letter-spacing: normal; text-transform: none; font-size: 12px; }

.tfull-title h1 {
    margin: 0;
    font-size: clamp(16px, 2vw, 22px);
    line-height: 1.25;
    letter-spacing: -.015em;
    color: var(--sys-text-primary, var(--text-color, #17191b));
    overflow: hidden;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
}

.tfull-head-actions { flex: 0 0 auto; display: flex; align-items: center; gap: 10px; }

.tfull-body {
    max-width: 1560px;
    margin: 0 auto;
    padding: clamp(14px, 2.4vw, 26px) clamp(14px, 2.4vw, 32px) 80px;
}

@media (max-width: 700px) {
    .tfull-head-inner { flex-wrap: wrap; row-gap: 10px; }
    .tfull-title { flex-basis: calc(100% - 60px); }
    .tfull-head-actions { flex-basis: 100%; justify-content: flex-start; }
}

/* ==========================================================================
   UNIFIED ULTRA-ELITE DATA TABLE SYSTEM (Shared across all platforms)
   ========================================================================== */

.up-table-wrap,
.ops-table-wrap {
    width: 100%;
    overflow-y: auto;
    overflow-x: auto;
    background: var(--ecv-paper, var(--card-bg, #1e2124));
    border: 1px solid var(--ecv-line, #e5e6e4);
    border-radius: var(--ecv-radius-card, 8px);
    scrollbar-width: thin;
}

table.up-table,
table.ops-ticket-table,
.up-table {
    width: 100%;
    border-collapse: separate;
    border-spacing: 0;
    white-space: nowrap;
    font-size: 12.5px;
    color: var(--text-color);
}

table.up-table th,
table.ops-ticket-table th,
.up-table th {
    position: sticky;
    top: 0;
    z-index: 10;
    height: 42px;
    padding: 0 12px;
    border-bottom: 1px solid var(--ecv-line, #e5e6e4);
    background: var(--ecv-soft, #25282c);
    color: var(--text-color);
    font-size: 11px;
    text-align: center;
    text-transform: uppercase;
    letter-spacing: .06em;
    font-weight: 700;
    user-select: none;
    cursor: pointer;
    transition: background 0.15s, color 0.15s;
    white-space: nowrap;
}

table.up-table th:hover,
table.ops-ticket-table th:hover,
.up-table th:hover {
    background: var(--ecv-canvas, #2d3136);
    color: var(--text-color);
}

table.up-table th i,
table.ops-ticket-table th i,
.up-table th i {
    margin-inline-start: 4px;
    font-size: 10px;
    opacity: 0.55;
    transition: opacity 0.15s, transform 0.15s;
}

table.up-table th:hover i,
table.up-table th.is-sorted i,
table.ops-ticket-table th:hover i,
table.ops-ticket-table th.is-sorted i,
.up-table th:hover i,
.up-table th.is-sorted i {
    opacity: 1;
    color: var(--ecv-ink);
}

table.up-table td,
table.ops-ticket-table td,
.up-table td {
    height: 48px;
    padding: 0 12px;
    border-bottom: 1px solid var(--ecv-line, #e5e6e4);
    color: var(--text-color);
    font-size: 12.5px;
    text-align: center;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    background: var(--ecv-paper, var(--card-bg, #1e2124));
    vertical-align: middle;
}

table.up-table tbody tr,
table.ops-ticket-table tbody tr,
.up-table tbody tr {
    cursor: pointer;
    transition: background 0.15s ease;
}

table.up-table tbody tr:hover td,
table.ops-ticket-table tbody tr:hover td,
.up-table tbody tr:hover td {
    background: var(--ecv-soft, #25282c) !important;
}

table.up-table tbody tr:last-child td,
table.ops-ticket-table tbody tr:last-child td,
.up-table tbody tr:last-child td {
    border-bottom: none;
}

/* Universal Empty State for Tables, Cards & Panels */
.up-empty,
.ops-empty,
.adm-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 36px 20px;
    text-align: center;
    color: var(--text-light, #73787b);
    width: 100%;
    box-sizing: border-box;
}

/* The medallion — the round plate an empty state's icon sits in.
 *
 * This used to match `> span` unconditionally, because some empty states wrap their icon in a
 * span. But others use the span for the SENTENCE, and those were being forced into a 44px
 * circle: "Select a ticket to see its details." rendered one word per line, overflowing a grey
 * dot, on the admin Tickets screen. Match on what an element CONTAINS rather than on what it
 * is — an icon holder holds an icon — so both markup shapes get the treatment they meant. */
.up-empty > i,
.ops-empty > i,
.adm-empty > i,
.up-empty > span:has(> i),
.ops-empty > span:has(> i),
.adm-empty > span:has(> i) {
    display: grid;
    place-items: center;
    width: 44px;
    height: 44px;
    margin-bottom: 12px;
    border-radius: 50%;
    background: var(--ecv-soft, #25282c);
    color: var(--text-light, #73787b);
    font-size: 16px;
}

/* A span with no icon in it is the label. It reads as a sentence: full measure, normal size,
 * and a cap on the line length so a long one does not run the width of a wide screen. */
.up-empty > span:not(:has(i)),
.ops-empty > span:not(:has(i)),
.adm-empty > span:not(:has(i)) {
    display: block;
    max-width: 46ch;
    margin: 0;
    font-size: 13px;
    line-height: 1.62;
    color: var(--text-light, #73787b);
}

.up-empty b,
.ops-empty b,
.adm-empty b {
    font-size: 13.5px;
    font-weight: 700;
    color: var(--text-color, #17191b);
    margin-bottom: 4px;
}

.up-empty p,
.ops-empty p,
.adm-empty p {
    margin: 0;
    font-size: 12px;
    color: var(--text-light, #73787b);
    max-width: 320px;
}

.up-table tr.up-table-empty td,
.ops-ticket-table tr.ops-table-empty td,
table tr.table-empty td {
    padding: 36px 16px;
    text-align: center;
    background: var(--ecv-paper, var(--card-bg, #1e2124)) !important;
}

/* ==========================================================================
   ULTRA-ELITE PREMIUM UNIVERSAL TOOLTIP SYSTEM (Light & Dark Theme Matched)
   ========================================================================== */

[data-tooltip] {
    position: relative;
}

[data-tooltip]::before,
[data-tooltip]::after {
    position: absolute;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.18s cubic-bezier(0.16, 1, 0.3, 1), transform 0.18s cubic-bezier(0.16, 1, 0.3, 1);
    z-index: 1000;
}

/* Tooltip bubble */
[data-tooltip]::after {
    content: attr(data-tooltip);
    bottom: calc(100% + 7px);
    left: 50%;
    transform: translateX(-50%) translateY(4px) scale(0.96);
    padding: 5px 10px;
    border-radius: 6px;
    background: var(--ecv-canvas, #17191b);
    color: var(--text-color, #ffffff);
    border: 1px solid var(--ecv-line, rgba(255, 255, 255, 0.15));
    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.4);
    font-size: 11px;
    font-weight: 600;
    line-height: 1.2;
    white-space: nowrap;
    text-transform: none;
    letter-spacing: normal;
}

/* Tooltip caret */
[data-tooltip]::before {
    content: "";
    bottom: calc(100% + 3px);
    left: 50%;
    transform: translateX(-50%) translateY(4px);
    border-width: 4px 4px 0 4px;
    border-style: solid;
    border-color: var(--ecv-canvas, #17191b) transparent transparent transparent;
}

[data-tooltip]:hover::after {
    opacity: 1;
    transform: translateX(-50%) translateY(0) scale(1);
}

[data-tooltip]:hover::before {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
}

/* Bottom tooltip variant */
[data-tooltip-pos="bottom"]::after {
    bottom: auto;
    top: calc(100% + 7px);
    transform: translateX(-50%) translateY(-4px) scale(0.96);
}

[data-tooltip-pos="bottom"]::before {
    bottom: auto;
    top: calc(100% + 3px);
    transform: translateX(-50%) translateY(-4px);
    border-width: 0 4px 4px 4px;
    border-color: transparent transparent var(--ecv-canvas, #17191b) transparent;
}

[data-tooltip-pos="bottom"]:hover::after {
    transform: translateX(-50%) translateY(0) scale(1);
}

[data-tooltip-pos="bottom"]:hover::before {
    transform: translateX(-50%) translateY(0);
}

/* ==========================================================================
   ULTRA-ELITE PREMIUM UNIVERSAL MODAL SYSTEM (Light & Dark Theme Unified)
   ========================================================================== */

.up-modal-backdrop,
.adm-modal-backdrop {
    position: fixed;
    inset: 0;
    z-index: 100;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
    background-color: rgba(15, 17, 19, 0.72);
    backdrop-filter: blur(5px);
    -webkit-backdrop-filter: blur(5px);
    animation: upModalFadeIn 0.2s cubic-bezier(0.16, 1, 0.3, 1);
}

.up-modal-backdrop[hidden],
.adm-modal-backdrop[hidden] {
    display: none !important;
}

.up-modal {
    width: min(600px, 100%);
    max-height: min(88svh, 820px);
    display: flex;
    flex-direction: column;
    background-color: var(--ecv-paper, #1e2124);
    border: 1px solid var(--ecv-line, rgba(255, 255, 255, 0.12));
    border-radius: var(--ecv-radius-modal, 12px);
    box-shadow: 0 24px 48px rgba(0, 0, 0, 0.45);
    overflow: hidden;
    animation: upModalScaleUp 0.24s cubic-bezier(0.16, 1, 0.3, 1);
}

.up-modal-sm {
    width: min(460px, 100%);
}

.up-modal-lg {
    width: min(920px, 100%);
}

.up-modal-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 16px 20px;
    border-bottom: 1px solid var(--ecv-line, rgba(255, 255, 255, 0.1));
    background-color: var(--ecv-soft, #25282c);
    flex-shrink: 0;
}

.up-modal-head h2 {
    margin: 0;
    font-size: 16px;
    font-weight: 700;
    color: var(--text-color, #ffffff);
    display: flex;
    align-items: center;
    gap: 8px;
}

.up-modal-body {
    display: flex;
    flex-direction: column;
    gap: 14px;
    padding: 20px;
    overflow-y: auto;
    color: var(--text-color, #ffffff);
}

.up-modal-row {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 12px;
}

.up-modal-foot {
    display: flex;
    justify-content: flex-end;
    align-items: center;
    gap: 10px;
    padding: 14px 20px;
    border-top: 1px solid var(--ecv-line, rgba(255, 255, 255, 0.1));
    background-color: var(--ecv-soft, #25282c);
    flex-shrink: 0;
}

@keyframes upModalFadeIn {
    from { opacity: 0; }
    to { opacity: 1; }
}

@keyframes upModalScaleUp {
    from { opacity: 0; transform: scale(0.95) translateY(8px); }
    to { opacity: 1; transform: scale(1) translateY(0); }
}

@media (max-width: 560px) {
    .up-modal-backdrop {
        align-items: flex-end;
        padding: 0;
    }

    .up-modal {
        width: 100%;
        max-height: 92svh;
        border-radius: var(--ecv-radius-modal, 12px) var(--ecv-radius-modal, 12px) 0 0;
    }

    .up-modal-row {
        grid-template-columns: 1fr;
    }
}


/* ------------------------------------------------------------------ *
 * The ticket page inside a portal shell
 *
 * A shell's content column is padded for a list: comfortable air above the first card. The
 * ticket page brings its own sticky header, which must meet the top bar with nothing
 * between them — that gap above the ticket id and subject was the list's padding, still
 * being applied to a page that does not want it. `has-ticket-page` is set by the screen
 * when it swaps views, so the two cases are told apart rather than compromised between.
 *
 * The horizontal padding stays: it is the header's own inner rhythm, not the shell's.
 * ------------------------------------------------------------------ */

body.has-ticket-page .up-main,
body.has-ticket-page .pcontent {
    padding-top: 0;
    padding-inline: 0;
    padding-bottom: 0;
}

/* Fit the content area exactly. `min-width: 0` is what actually permits it: a flex/grid
 * child's automatic minimum is its content, so a wide conversation row or a long
 * attachment name would otherwise push the page past the column and take the whole shell
 * sideways with it. Wide children scroll inside themselves; the page never does. */
body.has-ticket-page .tfull {
    width: 100%;
    max-width: 100%;
    min-width: 0;
    box-sizing: border-box;
    overflow-x: clip;
}

/* The body fills the column it is given rather than centring inside a narrower measure —
 * the shell's rail already provides the outer margin. */
body.has-ticket-page .tfull-body {
    max-width: 100%;
    min-width: 0;
}

body.has-ticket-page .tfull-head-inner {
    max-width: 100%;
}

/* ================================================================== *
 * Front-door texture
 *
 * One background for all three front doors. The Support portal's home had it first; putting
 * it here rather than copying it into three identity files is the same rule the rail
 * geometry follows — a treatment shared by every portal is declared once, and each portal
 * only chooses where to mount it.
 *
 * What it is: two tonal bands of a single wave shape drifting against each other, low in the
 * frame, under two very faint washes of light. The ink is in low single figures per cent,
 * because the job of a background is to give the type somewhere to sit. Everything moves on
 * transforms alone, so none of it costs a repaint, and all of it stops under
 * prefers-reduced-motion.
 *
 * To mount it: give the host `position: relative; isolation: isolate` and make
 * .phero-canvas its FIRST child. The isolation is what makes z-index: -1 mean "above the
 * host's own background, below its content" instead of "behind the page".
 *
 * The ink channel is a variable, so an ink front door (body.is-dark) draws the same texture
 * in paper without a second copy of the rules.
 * ================================================================== */

/* Fixed to the viewport, not to the section: the content scrolls over a backdrop that stays
   where it is, which is the parallax on its own — no script required, and identical on every
   browser and every device. shared/js/portal-parallax.js then adds depth on top of that by
   writing --pw-y, and the two layers below read it at different rates so the waves drift a
   little against the light. With no script, --pw-y is 0 and the backdrop simply holds still.
   Nothing here is load-bearing: it is a background. */
.phero-canvas {
    --pw-ink: 23 25 27;
    --pw-y: 0px;

    position: fixed;
    /* Overscanned above and below the viewport. The parallax moves this layer and the waves
       inside it in opposite proportions, and a backdrop sized exactly to the screen would
       drag an unpainted edge into view the moment it moved. The overscan is comfortably more
       than the travel that portal-parallax.js is allowed to ask for. */
    inset: -140px 0;
    z-index: -1;
    overflow: hidden;
    pointer-events: none;
    transform: translate3d(0, calc(var(--pw-y) * -0.10), 0);
}

:root.dark-theme .phero-canvas,
:root[data-theme="dark"] .phero-canvas {
    --pw-ink: 233 234 232;
}

/* The ink front doors are ink in both themes, so their texture is paper in both. */
body.is-dark .phero-canvas {
    --pw-ink: 255 255 255;
}

.phero-glow {
    position: absolute;
    display: block;
    border-radius: 50%;
    filter: blur(70px);
    will-change: transform;
}

.phero-glow-a {
    inset-block-start: -18vh;
    inset-inline-start: -12vw;
    width: 46vw;
    height: 46vw;
    min-width: 420px;
    min-height: 420px;
    background: radial-gradient(circle, rgb(var(--pw-ink) / .05), transparent 70%);
    animation: pw-drift-a 44s cubic-bezier(.16, 1, .3, 1) infinite alternate;
}

.phero-glow-b {
    inset-block-end: -22vh;
    inset-inline-end: -8vw;
    width: 40vw;
    height: 40vw;
    min-width: 380px;
    min-height: 380px;
    background: radial-gradient(circle, rgb(var(--pw-ink) / .04), transparent 70%);
    animation: pw-drift-b 56s cubic-bezier(.16, 1, .3, 1) infinite alternate;
}

/* The texture reaches every edge of the screen — no vignette at the sides, and the only fade
   is a short one at the very top, which keeps the first headline on clean ground. One mask
   layer, deliberately: two would need mask-composite, and where that is not understood the
   layers add up instead of intersecting, which is the opposite of the intent. */
.phero-waves {
    position: absolute;
    inset: 0;
    display: block;
    width: 100%;
    height: 100%;
    overflow: visible;
    /* The second depth. The canvas above already moved at a tenth of the scroll; the waves
       take another quarter, so they separate from the light behind them as the page moves. */
    transform: translate3d(0, calc(var(--pw-y) * -0.25), 0);
    -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 26%);
    mask-image: linear-gradient(to bottom, transparent 0%, #000 26%);
}

.pwave path {
    stroke: none;
}

.pwave-1 path { fill: rgb(var(--pw-ink) / .028); }
.pwave-2 path { fill: rgb(var(--pw-ink) / .020); }

/* Each path is six periods wide inside a box two periods across, and each layer travels
   exactly two periods before it repeats — the same picture, so the reset is invisible. Slow
   enough to be felt rather than watched. */
.pwave { will-change: transform; }
.pwave-1 { animation: pw-wave 52s linear infinite; }
.pwave-2 { animation: pw-wave 74s linear infinite reverse; }

@keyframes pw-wave {
    from { transform: translateX(0); }
    to { transform: translateX(-1440px); }
}

@keyframes pw-drift-a {
    from { transform: translate3d(0, 0, 0) scale(1); }
    to { transform: translate3d(6vw, 5vh, 0) scale(1.12); }
}

@keyframes pw-drift-b {
    from { transform: translate3d(0, 0, 0) scale(1.08); }
    to { transform: translate3d(-5vw, -6vh, 0) scale(1); }
}

@media (prefers-reduced-motion: reduce) {
    .phero-glow,
    .pwave {
        animation: none !important;
    }
}

/* ================================================================== *
 * Front-door hero: auth-state slots
 *
 * Every home page ships its guest state AND its signed-in state hidden, then reveals exactly
 * one once the session is known. That is deliberate — neither state may flash before the
 * answer is in. The cost is that the hero first lays out with those bands missing and then
 * they appear, shoving everything below them down. On screen that reads as the page
 * "settling", and it is the shifting these slots exist to remove.
 *
 * A slot holds its band's height whether or not a child is showing, so revealing a state
 * paints in place. The slot also OWNS the rhythm above the band: if the margin sat on the
 * child instead, it would appear along with the child and the shift would come straight back.
 * ================================================================== */

.phero-slot {
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
}

/* Tall enough for a single primary action — the tallest of the three portals' buttons, since
 * the reserve only has to hold while both states are still hidden. */
.phero-slot-actions {
    min-height: clamp(52px, 6vh, 68px);
    margin-block: clamp(22px, 2.9vh, 36px) 0;
}

/* One line of the small print. */
.phero-slot-note {
    min-height: 1.6em;
}

/* The rhythm belongs to the slot, not to the band inside it. */
.phero-slot > .uhero-actions,
.phero-slot > .dhero-actions,
.phero-slot > .ahero-actions,
.phero-slot > .uhero-note,
.phero-slot > .dhero-note,
.phero-slot > .ahero-note {
    margin-block: 0;
}

/* ================================================================== *
 * Confirmation dialog (shared/js/confirm-dialog.js)
 *
 * Used wherever a click is about to do something a second click cannot undo — sign out
 * first. It paints entirely from --sys-* tokens, so it is correct in both themes without a
 * dark-mode block of its own.
 *
 * The spinner is the point of the design: the dialog stays on screen while the work runs, so
 * the state has to be visible rather than implied.
 * ================================================================== */

.cdlg-host {
    position: fixed;
    inset: 0;
    z-index: 9000;
    display: grid;
    place-items: center;
    padding: 24px;
}

.cdlg-veil {
    position: absolute;
    inset: 0;
    background: rgba(10, 12, 13, .62);
    backdrop-filter: blur(3px);
    opacity: 0;
    transition: opacity .2s ease;
}

.cdlg-host[data-open="true"] .cdlg-veil { opacity: 1; }

.cdlg {
    position: relative;
    width: min(440px, 100%);
    padding: 30px 30px 26px;
    border: 1px solid var(--sys-border, #e5e6e4);
    border-radius: 16px;
    background: var(--sys-surface-raised, #ffffff);
    color: var(--sys-text-primary, #17191b);
    box-shadow: 0 28px 70px rgba(10, 12, 13, .34);
    opacity: 0;
    transform: translateY(10px) scale(.985);
    transition: opacity .22s ease, transform .22s cubic-bezier(.16, 1, .3, 1);
}

.cdlg-host[data-open="true"] .cdlg {
    opacity: 1;
    transform: none;
}

/* Title row: the mark beside the heading, not stacked above it. The icon is an adjective on
 * the question — stacking it put a band of empty space between the reader and the sentence. */
.cdlg-head {
    display: flex;
    align-items: center;
    gap: 14px;
    margin-bottom: 14px;
}

.cdlg-head .cdlg-title { margin: 0; }

.cdlg-mark {
    display: grid;
    place-items: center;
    flex: 0 0 auto;
    width: 46px;
    height: 46px;
    border-radius: 13px;
    background: var(--sys-surface-sunken, #f4f4f2);
    color: var(--sys-text-secondary, #73787b);
    font-size: 18px;
}

.cdlg-host[data-tone="danger"] .cdlg-mark {
    background: var(--ecv-red-wash, rgba(173, 41, 31, .1));
    color: var(--ecv-red, #ad291f);
}

.cdlg-title {
    margin: 0 0 10px;
    font-size: 20px;
    font-weight: 800;
    letter-spacing: -.014em;
    line-height: 1.25;
}

.cdlg-body {
    margin: 0;
    color: var(--sys-text-secondary, #73787b);
    font-size: 14px;
    line-height: 1.65;
}

/* The reason a decision asks for before it will run. Sized so a sentence or two is visibly
   the expected answer — a single-line box asks for a word, and a word is not a reason. */
.cdlg-field-label {
    display: block;
    margin: 18px 0 7px;
    font-size: 12.5px;
    font-weight: 700;
    color: var(--sys-text-primary, #17191b);
}

.cdlg-field {
    display: block;
    width: 100%;
    box-sizing: border-box;
    padding: 11px 13px;
    border: 1px solid var(--sys-border, #e5e6e4);
    border-radius: 9px;
    /* Sunken, not the dialog's own surface: an input that shares its container's colour is a
       rectangle, not a place to type — and in dark the two were within a shade of each other. */
    background: var(--sys-surface-sunken, #f7f7f5);
    color: var(--sys-text-primary, #17191b);
    font: inherit;
    font-size: 13.5px;
    line-height: 1.55;
    resize: vertical;
}

.cdlg-field::placeholder {
    color: var(--sys-text-secondary, #73787b);
    opacity: .75;
}

.cdlg-field:focus-visible {
    outline: 2px solid var(--sys-action-primary, #17191b);
    outline-offset: -1px;
    border-color: transparent;
}

.cdlg-field:disabled {
    opacity: .6;
    cursor: not-allowed;
}

.cdlg-error {
    margin: 16px 0 0;
    padding: 10px 13px;
    border: 1px solid var(--ecv-red-line, rgba(173, 41, 31, .3));
    border-radius: 9px;
    background: var(--ecv-red-wash, rgba(173, 41, 31, .08));
    color: var(--ecv-red, #ad291f);
    font-size: 13px;
    line-height: 1.55;
}

.cdlg-actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 10px;
    margin-top: 26px;
}

.cdlg-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 9px;
    min-height: 44px;
    padding: 0 18px;
    border: 1px solid transparent;
    border-radius: 10px;
    font: inherit;
    font-size: 13.5px;
    font-weight: 700;
    cursor: pointer;
    transition: background-color .16s ease, border-color .16s ease, color .16s ease;
}

.cdlg-btn.is-ghost {
    border-color: var(--sys-border, #e5e6e4);
    background: transparent;
    color: var(--sys-text-primary, #17191b);
}

.cdlg-btn.is-ghost:hover:not(:disabled) {
    background: var(--sys-surface-sunken, #f4f4f2);
}

/* The action pair, never a literal colour: a hard-coded #fff here is white-on-white the
 * moment the theme flips. */
.cdlg-btn.is-go {
    background: var(--sys-action-primary, #17191b);
    color: var(--sys-action-primary-text, #ffffff);
}

.cdlg-host[data-tone="danger"] .cdlg-btn.is-go {
    background: var(--sys-action-destructive, #ad291f);
    color: var(--sys-action-destructive-text, #ffffff);
}

.cdlg-btn:disabled {
    cursor: default;
    opacity: .78;
}

.cdlg-btn:focus-visible {
    outline: 2px solid var(--sys-action-primary, #17191b);
    outline-offset: 2px;
}

.cdlg-spinner {
    width: 15px;
    height: 15px;
    border: 2px solid currentColor;
    border-top-color: transparent;
    border-radius: 50%;
    animation: cdlg-spin .7s linear infinite;
}

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

/* Reduced motion keeps the spinner meaningful — a still ring says nothing — but slows it to
 * something that does not pulse, and drops the entrance movement entirely. */
@media (prefers-reduced-motion: reduce) {
    .cdlg { transition: opacity .2s ease; transform: none; }
    .cdlg-spinner { animation-duration: 1.6s; }
}

@media (max-width: 520px) {
    .cdlg { padding: 24px 22px 22px; }
    .cdlg-actions { flex-direction: column-reverse; }
    .cdlg-btn { width: 100%; }
}

/* ================================================================== *
 * Access refused (shared/js/access-denied.js)
 *
 * Shown on a staff portal to somebody who authenticated correctly and still may not come in.
 * It is a full-surface dialog rather than a banner because there is nothing behind it they are
 * allowed to look at, and it has to stand on its own on a front door that is deliberately ink
 * in both themes — so it paints from --sys-* tokens and reads correctly either way.
 *
 * Three states share this frame: not entitled (with the request control), pending (with the
 * date and a way to withdraw), and granted (which appears live when an administrator acts).
 * Blocked uses the same frame with the request control absent — see the module's header for
 * why that is deliberate rather than an oversight.
 * ================================================================== */

.adny-host {
    position: fixed;
    inset: 0;
    z-index: 9200;
    display: grid;
    place-items: center;
    padding: 24px;
    overflow-y: auto;
}

.adny-veil {
    position: absolute;
    inset: 0;
    background: rgba(8, 10, 11, .74);
    backdrop-filter: blur(4px);
    opacity: 0;
    transition: opacity .24s ease;
}

.adny-host[data-open="true"] .adny-veil { opacity: 1; }

.adny {
    position: relative;
    width: min(520px, 100%);
    padding: 34px 34px 30px;
    border: 1px solid var(--sys-border, #e5e6e4);
    border-radius: 18px;
    background: var(--sys-surface-raised, #ffffff);
    color: var(--sys-text-primary, #17191b);
    box-shadow: 0 34px 90px rgba(8, 10, 11, .46);
    opacity: 0;
    transform: translateY(12px) scale(.98);
    transition: opacity .26s ease, transform .26s cubic-bezier(.16, 1, .3, 1);
}

.adny-host[data-open="true"] .adny { opacity: 1; transform: none; }

/* Title row: the mark beside the heading rather than above it. */
.adny-head {
    display: flex;
    align-items: center;
    gap: 15px;
    margin-bottom: 16px;
}

.adny-head-copy { min-width: 0; }

.adny-mark {
    display: grid;
    place-items: center;
    flex: 0 0 auto;
    width: 48px;
    height: 48px;
    border-radius: 14px;
    background: var(--sys-surface-sunken, #f4f4f2);
    color: var(--sys-text-secondary, #73787b);
    font-size: 19px;
}

.adny-host[data-mode="blocked"] .adny-mark {
    background: var(--ecv-red-wash, rgba(173, 41, 31, .12));
    color: var(--ecv-red, #ad291f);
}

.adny-host[data-mode="granted"] .adny-mark {
    background: var(--ecv-green-wash, rgba(19, 129, 59, .12));
    color: var(--ecv-green, #13813b);
}

.adny-eyebrow {
    display: block;
    margin: 0 0 5px;
    color: var(--sys-text-secondary, #73787b);
    font-size: 10.5px;
    font-weight: 700;
    letter-spacing: .16em;
    text-transform: uppercase;
}

.adny-title {
    margin: 0;
    font-size: 23px;
    font-weight: 800;
    letter-spacing: -.018em;
    line-height: 1.24;
}

.adny-body {
    margin: 0;
    color: var(--sys-text-secondary, #73787b);
    font-size: 14px;
    line-height: 1.68;
}

/* Who they are signed in as. Stated plainly, because the commonest cause of this screen is
 * being signed in with the wrong account. */
.adny-who {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-top: 22px;
    padding: 12px 14px;
    border: 1px solid var(--sys-border, #e5e6e4);
    border-radius: 12px;
    background: var(--sys-surface-sunken, #f4f4f2);
}

.adny-who-avatar {
    display: grid;
    place-items: center;
    flex: 0 0 auto;
    width: 36px;
    height: 36px;
    border-radius: 10px;
    background: var(--sys-action-primary, #17191b);
    color: var(--sys-action-primary-text, #ffffff);
    font-size: 12px;
    font-weight: 800;
}

.adny-who-copy { min-width: 0; }

.adny-who-copy strong {
    display: block;
    overflow: hidden;
    font-size: 13.5px;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.adny-who-copy small {
    display: block;
    overflow: hidden;
    margin-top: 2px;
    color: var(--sys-text-secondary, #73787b);
    font-size: 12px;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.adny-state { margin-top: 20px; }

.adny-chip {
    display: inline-flex;
    align-items: center;
    gap: 9px;
    margin: 0;
    padding: 9px 14px;
    border: 1px solid var(--sys-border, #e5e6e4);
    border-radius: 999px;
    font-size: 12.5px;
    font-weight: 700;
}

.adny-chip.is-pending {
    border-color: var(--ecv-amber-line, rgba(176, 122, 12, .32));
    background: var(--ecv-amber-wash, rgba(176, 122, 12, .12));
    color: var(--ecv-amber, #b07a0c);
}

.adny-chip.is-blocked {
    border-color: var(--ecv-red-line, rgba(173, 41, 31, .32));
    background: var(--ecv-red-wash, rgba(173, 41, 31, .1));
    color: var(--ecv-red, #ad291f);
}

.adny-chip.is-granted {
    border-color: var(--ecv-green-line, rgba(19, 129, 59, .32));
    background: var(--ecv-green-wash, rgba(19, 129, 59, .1));
    color: var(--ecv-green, #13813b);
}

.adny-field { display: block; }

.adny-field-label {
    display: block;
    margin-bottom: 7px;
    font-size: 12.5px;
    font-weight: 700;
}

.adny-reason {
    display: block;
    width: 100%;
    padding: 11px 13px;
    border: 1px solid var(--sys-border, #e5e6e4);
    border-radius: 10px;
    background: var(--sys-surface-sunken, #eff0ee);
    color: var(--sys-text-primary, #17191b);
    font: inherit;
    font-size: 13.5px;
    line-height: 1.6;
    resize: vertical;
    box-sizing: border-box;
}

.adny-reason:focus-visible {
    outline: 2px solid var(--sys-action-primary, #17191b);
    outline-offset: 1px;
}

.adny-field-hint {
    display: block;
    margin-top: 7px;
    color: var(--sys-text-secondary, #73787b);
    font-size: 12px;
    line-height: 1.55;
}

.adny-error {
    margin: 16px 0 0;
    padding: 11px 13px;
    border: 1px solid var(--ecv-red-line, rgba(173, 41, 31, .3));
    border-radius: 10px;
    background: var(--ecv-red-wash, rgba(173, 41, 31, .08));
    color: var(--ecv-red, #ad291f);
    font-size: 13px;
    line-height: 1.55;
}

.adny-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    margin-top: 24px;
}

.adny-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 9px;
    min-height: 44px;
    padding: 0 18px;
    border: 1px solid transparent;
    border-radius: 11px;
    font: inherit;
    font-size: 13.5px;
    font-weight: 700;
    cursor: pointer;
    transition: background-color .16s ease, border-color .16s ease;
}

/* Never a literal #fff on a themed plate: that is white-on-white the moment the theme flips. */
.adny-btn.is-go {
    background: var(--sys-action-primary, #17191b);
    color: var(--sys-action-primary-text, #ffffff);
}

.adny-btn.is-ghost {
    border-color: var(--sys-border, #e5e6e4);
    background: transparent;
    color: var(--sys-text-primary, #17191b);
}

.adny-btn.is-ghost:hover:not(:disabled) {
    background: var(--sys-surface-sunken, #f4f4f2);
}

.adny-btn:disabled { cursor: default; opacity: .76; }

.adny-btn:focus-visible {
    outline: 2px solid var(--sys-action-primary, #17191b);
    outline-offset: 2px;
}

.adny-spinner {
    width: 15px;
    height: 15px;
    border: 2px solid currentColor;
    border-top-color: transparent;
    border-radius: 50%;
    animation: cdlg-spin .7s linear infinite;
}

@media (prefers-reduced-motion: reduce) {
    .adny { transition: opacity .2s ease; transform: none; }
    .adny-spinner { animation-duration: 1.6s; }
}

@media (max-width: 560px) {
    .adny { padding: 26px 22px 22px; }
    .adny-actions { flex-direction: column; }
    .adny-btn { width: 100%; }
}

/* The placeholder carries a worked example, so it has to be readable rather than a hint of
 * text. The browser default measured 4.22:1 in both themes — just under the 4.5 floor. Setting
 * it from the same token as the other secondary text, with opacity reset, puts it well clear;
 * UA stylesheets apply their own opacity to placeholders, which is what pulled it under. */
.adny-reason::placeholder {
    color: var(--sys-text-secondary, #73787b);
    opacity: 1;
}

/* The title had been breaking as "You do not have / access to this portal" — a break placed by
 * the column width rather than by the sentence. A wider dialog gives it more room, and
 * `text-wrap: balance` lets the browser even out the lines it does still need, so the break
 * falls at a phrase rather than mid-thought. */
.adny { width: min(580px, 100%); }

.adny-title {
    font-size: clamp(20px, 1.35vw + .5rem, 24px);
    text-wrap: balance;
    hyphens: none;
}

/* A reason that has been started but is still too short to be useful. Only ever shown after
 * they have typed something — an empty field is not yet a mistake. */
.adny-field.is-invalid .adny-reason {
    border-color: var(--ecv-red-line, rgba(173, 41, 31, .4));
}

.adny-field.is-invalid .adny-field-hint {
    color: var(--ecv-red, #ad291f);
}

/* Disabled because the reason is not there yet, not because the action is unavailable — so it
 * reads as waiting rather than as broken. */
.adny-btn.is-go[aria-disabled="true"] {
    opacity: .55;
    cursor: not-allowed;
}

/* The option grid takes its column count from how many options there actually are.
 *
 * `.pacct-theme-options` was written for the three theme choices and hard-coded to three
 * columns. The language group reuses it and has only two, so Arabic and English were laid into
 * a three-column track with an empty third — which is why they sat at odd widths with dead
 * space beside them. Counting the children in CSS keeps the two groups sharing one component
 * without either one carrying the other's column count. */
.pacct-theme-options:has(> :nth-child(2):last-child) {
    grid-template-columns: repeat(2, 1fr);
}

.pacct-theme-options:has(> :nth-child(4)) {
    grid-template-columns: repeat(2, 1fr);
}

/* ================================================================== *
 * Ticket page — density, and message text that reads in both themes
 *
 * PADDING. The page carried 80px of dead space below the last card, 16px grid gaps, 48px card
 * header bands and 18px inside every panel. Individually reasonable; stacked down a page of
 * four cards it pushed a ticket with one short description well past the fold. The values below
 * are the same rhythm, one step tighter, so a small ticket fits the viewport and a long one
 * still breathes.
 *
 * CONTRAST. Message bodies were painting from `--text-color` on a `--ecv-soft` bubble. That is
 * correct in light mode and marginal on ink, because the bubble sits a step lighter than the
 * card while the text stays put. Both are tokens now paired deliberately, and the metadata
 * tier (author, timestamp) is lifted off `--text-light`, which is the weakest thing on the
 * screen and was carrying the "who said this" line.
 * ================================================================== */

.tfull-body {
    padding-bottom: 28px;
}

.ops-detail-main,
.ops-detail-side {
    gap: 10px;
}

.ops-detail-grid {
    gap: 10px;
}

.ops-detail-card > header {
    min-height: 40px;
    padding: 0 14px;
}

.ops-detail-card .dept-description {
    padding: 13px 14px;
}

.ops-detail-card .dept-attachments {
    padding: 0 14px 12px;
}

.ops-thread {
    padding: 11px 14px;
    gap: 8px;
}

.ops-composer {
    padding: 12px 14px 13px;
}

/* --- Message text --- */

/* The bubble and its text as a pair. --sys-text-primary is the lifted value on ink and the
 * near-black one on paper, so the pairing holds in both themes rather than only in the one it
 * was designed against. */
.ops-thread .note {
    background: var(--sys-surface-sunken, var(--ecv-soft, #eff0ee));
    color: var(--sys-text-primary, #17191b);
}

.ops-thread .note p,
.ops-thread .note .note-body {
    color: var(--sys-text-primary, #17191b);
}

/* Who and when. Subordinate to the message, but still reading text — this line was on
 * --text-light, the faintest tier in the system, which is a caption colour and not a
 * name-and-timestamp colour. */
.ops-thread .note header {
    color: var(--sys-text-secondary, #73787b);
}

.ops-thread .note header b {
    color: var(--sys-text-primary, #17191b);
}

/* The requester-facing thread in the Support portal */
.up-msg-bubble {
    color: var(--sys-text-primary, #17191b);
}

.up-msg-bubble p {
    color: inherit;
}

.up-msg > .up-msg-main > header {
    color: var(--sys-text-secondary, #73787b);
}

.up-msg > .up-msg-main > header b {
    color: var(--sys-text-primary, #17191b);
}

.up-thread-empty {
    margin: 0;
    padding: 18px 2px;
    color: var(--sys-text-secondary, #73787b);
    font-size: 13px;
}

.up-msg.is-mine .up-msg-bubble {
    background: #17191b;
    border-color: #17191b;
    color: #ffffff !important;
}

.up-msg.is-mine .up-msg-bubble p {
    background: none;
    color: #ffffff !important;
}

html.dark-theme .up-msg.is-mine .up-msg-bubble,
[data-theme="dark"] .up-msg.is-mine .up-msg-bubble,
body.dark-theme .up-msg.is-mine .up-msg-bubble {
    background: linear-gradient(145deg, #2b3036 0%, #1e2125 100%) !important;
    border-color: rgba(255, 255, 255, 0.15) !important;
    color: #ffffff !important;
}

html.dark-theme .up-msg.is-mine .up-msg-bubble p,
[data-theme="dark"] .up-msg.is-mine .up-msg-bubble p,
body.dark-theme .up-msg.is-mine .up-msg-bubble p {
    color: #ffffff !important;
}

.up-msg:not(.is-mine) .up-msg-bubble {
    background-color: var(--bg-white, #ffffff);
    color: var(--sys-text-primary, #17191b);
}

html.dark-theme .up-msg:not(.is-mine) .up-msg-bubble,
[data-theme="dark"] .up-msg:not(.is-mine) .up-msg-bubble,
body.dark-theme .up-msg:not(.is-mine) .up-msg-bubble {
    background-color: #1e2226 !important;
    border-color: rgba(255, 255, 255, 0.08) !important;
    color: #e9eae8 !important;
}

.up-msg > .up-msg-main > header span,
.up-msg-bubble time,
.up-thread-empty,
.ops-thread .note header,
.ops-thread .note header span {
    color: var(--sys-text-muted, #6c7174);
}

/* The department note's timestamp sits on the sunken bubble, which is a lighter plate than the
 * card behind it — so --sys-text-muted, which clears 4.5:1 on paper and on the canvas, lands at
 * 4.32:1 here. A dedicated pair for this one surface rather than darkening the global token,
 * which everything else already passes against. */
:root {
    --sys-text-muted-sunken: #5f6467;
}

:root.dark-theme,
:root[data-theme="dark"] {
    --sys-text-muted-sunken: #b6bcbf;
}

/* The metadata tier in a note header: the author line and the timestamp.
 *
 * This used to be a blanket , which is a trap — it captures any BADGE inside the
 * header too and paints its text the muted grey, on whatever plate that badge has. It did exactly
 * that twice: to the action-requested chip, and then to the role pill, whose near-white
 * administrator plate ended up with grey text at 1.71:1.
 *
 * A badge brings its own colour pair. The exclusions name them, so a new badge is legible by
 * default rather than after somebody measures it. */
.ops-thread .note header,
.ops-thread .note header span:not(.note-role):not(.note-badge-action):not(.note-who) {
    color: var(--sys-text-muted-sunken, #5f6467);
}

/* ================================================================== *
 * Ticket detail page — layout and a chat-shaped conversation
 *
 * DESCRIPTION BESIDE ATTACHMENTS. Both are short, static facts about the ticket, and stacking
 * them pushed the conversation — the part people came to read — below the fold. They share a
 * row now, and the conversation spans the full width beneath them.
 *
 * The cards are addressed by what they CONTAIN (`:has(#detailDescription)`) rather than by
 * position. An nth-child rule here would silently point at the wrong card the first time
 * somebody adds a section above.
 *
 * A CHAT SHAPE. The thread was a column of identical left-aligned cards, so telling your own
 * team's replies from the requester's meant reading every name. Sides carry that instead: the
 * requester on the left, the team on the right, each bubble only as wide as it needs to be,
 * with the timestamp inside it. Both staff views (department and administration) get the same
 * treatment, so a ticket looks the same wherever it is worked.
 *
 * SCROLLING. The thread scrolls inside its own box, and only once there is more in it than
 * fits — a short conversation does not get a scrollbar it does not need. The page keeps its own
 * vertical scrollbar for everything else.
 * ================================================================== */

.ops-detail-main {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    align-content: start;
}

/* The conversation is the point of the page: full width, under the two fact cards. */
.ops-detail-main > .ops-detail-card:has(#threadForm),
.ops-detail-main > .ops-detail-card:has(#threadList) {
    grid-column: 1 / -1;
}

/* Below the split, everything stacks — two half-width cards on a narrow screen are worse than
 * one full-width one. */
@media (max-width: 900px) {
    .ops-detail-main {
        grid-template-columns: 1fr;
    }
}

/* --- The thread as a conversation ------------------------------------------- */

.ops-thread {
    display: flex;
    flex-direction: column;
    gap: 8px;
    /* Tall enough to hold a real exchange, capped so the composer stays reachable without
     * scrolling the whole page to find it. `auto` means no scrollbar until it is earned. */
    max-height: min(46vh, 460px);
    overflow-y: auto;
    scrollbar-width: thin;
}

/* A bubble, not a card: no full-width plate, no left rule, a tail corner instead. */
.ops-thread .note {
    max-width: min(78%, 620px);
    border: 0;
    border-inline-start: 0;
    border-radius: 12px;
    padding: 9px 12px 7px;
}

/* The requester speaks on the left, on a neutral plate. */
.ops-thread .note.is-requester {
    align-self: flex-start;
    border-end-start-radius: 4px;
    background: var(--sys-surface-sunken, var(--ecv-soft, #eff0ee));
}

/* The team answers on the right. A tinted plate rather than the inverted ink one — a wall of
 * high-contrast blocks is harder to read than a tint, and the ink plate is what made the
 * requester's own bubbles unreadable when the theme flipped. */
.ops-thread .note.is-staff {
    align-self: flex-end;
    border-end-end-radius: 4px;
    background: var(--ecv-green-wash, rgba(19, 129, 59, .1));
    border: 1px solid var(--ecv-green-line, rgba(19, 129, 59, .22));
}

/* An action request is the one message that must not blend in, whichever side sent it. */
.ops-thread .note.is-action-request {
    align-self: stretch;
    max-width: none;
    border-radius: 10px;
}

.ops-thread .note header {
    margin-bottom: 4px;
    gap: 12px;
    font-size: 11px;
}

.ops-thread .note p {
    line-height: 1.55;
    overflow-wrap: anywhere;
}

/* The same shape in the administration portal's thread. */
.adm-msg {
    max-width: min(78%, 620px);
    border-radius: 12px;
}

.adm-msg.is-requester {
    margin-inline-end: auto;
    border-end-start-radius: 4px;
}

.adm-msg.is-staff {
    margin-inline-start: auto;
    border-end-end-radius: 4px;
}

/* --- The page scrolls, and says so ----------------------------------------- */

.tfull-body {
    overflow-y: auto;
    overscroll-behavior: contain;
}

/* ================================================================== *
 * Ticket page — the body owns the vertical scroll
 *
 * `overflow-y: auto` on its own did nothing here: the element had no bounded height, so it grew
 * to fit its content and the page scrolled instead. Giving the page a fixed height and letting
 * the body be the flexible row is what turns that into a real scroll region — and it keeps the
 * ticket's sticky header in place while the content moves under it, rather than the header
 * scrolling away with everything else.
 * ================================================================== */

.tfull {
    display: flex;
    flex-direction: column;
    height: calc(100svh - var(--topbar-h, 64px));
    min-height: 0;
}

.tfull-head {
    flex: 0 0 auto;
}

.tfull-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
}

/* A visible track rather than an overlay scrollbar, so it is obvious there is more below. */
.tfull-body {
    scrollbar-width: thin;
    scrollbar-gutter: stable;
}

.tfull-body::-webkit-scrollbar { width: 10px; }
.tfull-body::-webkit-scrollbar-track { background: transparent; }

.tfull-body::-webkit-scrollbar-thumb {
    min-height: 42px;
    border: 3px solid transparent;
    border-radius: 999px;
    background: var(--ecv-muted, #73787b);
    background-clip: padding-box;
}

.tfull-body::-webkit-scrollbar-thumb:hover {
    background: var(--sys-text-primary, #17191b);
    background-clip: padding-box;
}

/* The inline validation message, under the field it is about. */
.ops-field-error {
    margin: 6px 0 0;
    display: flex;
    align-items: flex-start;
    gap: 6px;
    color: var(--ecv-red, #ad291f);
    font-size: 11.5px;
    line-height: 1.5;
}

.ops-field-error::before {
    content: "\f06a";                    /* fa-circle-exclamation, FA 6.0-beta3 */
    font-family: "Font Awesome 6 Free";
    font-weight: 900;
    flex: 0 0 auto;
    font-size: 11px;
    line-height: 1.55;
}

.ops-field-group [aria-invalid="true"] {
    border-color: var(--ecv-red-line, rgba(173, 41, 31, .45));
}

.ops-field-group [aria-invalid="true"]:focus-visible {
    outline-color: var(--ecv-red, #ad291f);
}

/* Save disabled means "not yet", not "broken", so it rests rather than looking faulty. */
.dept-status-actions .dept-btn:disabled {
    opacity: .5;
    cursor: not-allowed;
}

/* ================================================================== *
 * Collapsible card (internal notes)
 *
 * Folds to its header when there is nothing in it. The header is the control, so opening an
 * empty panel to write the first note is one click — collapsed-when-empty is a default, not a
 * lock.
 *
 * The body is hidden with `display: none` rather than a height animation. A height transition on
 * a panel whose content arrives from a realtime snapshot animates from the wrong number every
 * time a note lands, and lands on a scrollbar flash; a fold that is simply there or not is
 * honest about what it knows.
 * ================================================================== */

.ops-detail-card.is-collapsible > header {
    cursor: default;
}

.ops-collapse-body {
    display: block;
}

.ops-detail-card.is-collapsible[data-open="false"] > .ops-collapse-body {
    display: none;
}

/* Internal notes keeps its warning edge even when closed — the header alone still has to read
 * as the team-only card. */
.ops-detail-card.is-collapsible[data-open="false"] > header {
    border-bottom: 0;
}

.ops-collapse-btn {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    min-height: 28px;
    padding: 0 8px;
    border: 1px solid transparent;
    border-radius: 8px;
    background: transparent;
    color: var(--sys-text-secondary, #73787b);
    font: inherit;
    font-size: 12px;
    cursor: pointer;
    transition: background-color .16s ease, color .16s ease;
}

.ops-collapse-btn:hover {
    background: var(--sys-surface-sunken, var(--ecv-soft, #eff0ee));
    color: var(--sys-text-primary, #17191b);
}

.ops-collapse-btn:focus-visible {
    outline: 2px solid var(--sys-action-primary, #17191b);
    outline-offset: 2px;
}

/* The chevron points the way the panel will go, so the control says what it does before it is
 * pressed rather than reporting what it did. */
.ops-collapse-btn i {
    transition: transform .18s ease;
}

.ops-detail-card.is-collapsible[data-open="true"] .ops-collapse-btn i {
    transform: rotate(180deg);
}

/* How many notes there are, so a closed panel still tells you whether it is worth opening. */
.ops-collapse-count {
    display: inline-grid;
    place-items: center;
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    border-radius: 999px;
    background: var(--ecv-amber-wash, rgba(176, 122, 12, .18));
    color: var(--ecv-amber, #8a5d00);
    font-size: 11px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
}

@media (prefers-reduced-motion: reduce) {
    .ops-collapse-btn i { transition: none; }
}

/* A visually hidden label, so the button's purpose is announced even though it shows only a
 * chevron and a count. */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* ================================================================== *
 * Action required from the requester
 *
 * When the team asks the requester for something, the ticket stops until they answer. That is
 * not a remark in a conversation, so it does not get a chat bubble: it gets a callout above the
 * composer, with the one control that ends the wait.
 *
 * Amber, because this is the third state — green says done, red says wrong, and "waiting for a
 * person" is neither. Every colour is a token so both themes get the lifted variants; the
 * measured floor for this card is stated in the verification, not assumed.
 * ================================================================== */

.up-action-card {
    display: flex;
    align-items: flex-start;
    gap: 14px;
    margin: 4px 0 18px;
    padding: 16px 18px;
    border: 1px solid var(--ecv-amber-line, rgba(176, 122, 12, .32));
    border-inline-start: 3px solid var(--ecv-amber, #8a5d00);
    border-radius: 12px;
    background: var(--ecv-amber-wash, rgba(176, 122, 12, .1));
}

.up-action-mark {
    display: grid;
    place-items: center;
    flex: 0 0 auto;
    width: 38px;
    height: 38px;
    border-radius: 10px;
    background: var(--sys-action-warning, #8a5d00);
    color: var(--sys-action-warning-text, #ffffff);
    font-size: 15px;
}

.up-action-copy {
    flex: 1 1 auto;
    min-width: 0;
}

.up-action-eyebrow {
    margin: 0 0 4px;
    color: var(--ecv-amber, #8a5d00);
    font-size: 10.5px;
    font-weight: 800;
    letter-spacing: .14em;
    text-transform: uppercase;
}

.up-action-title {
    margin: 0 0 8px;
    color: var(--sys-text-primary, #17191b);
    font-size: 15px;
    font-weight: 800;
    letter-spacing: -.01em;
    line-height: 1.3;
}

/* What was actually asked for. The largest thing in the card after the heading, because it is
 * the thing the person has to read and act on. */
.up-action-text {
    margin: 0;
    padding: 10px 12px;
    border-radius: 9px;
    background-color: var(--ecv-paper);
    color: var(--text-color);
    font-size: 13.5px;
    line-height: 1.6;
    overflow-wrap: anywhere;
    white-space: pre-wrap;
}

.up-action-hint {
    margin: 9px 0 0;
    color: var(--sys-text-muted, #6c7174);
    font-size: 12px;
    line-height: 1.55;
}

.up-action-error {
    margin: 10px 0 0;
    padding: 9px 12px;
    border: 1px solid var(--ecv-red-line, rgba(173, 41, 31, .32));
    border-radius: 9px;
    background: var(--ecv-red-wash, rgba(173, 41, 31, .08));
    color: var(--ecv-red, #ad291f);
    font-size: 12.5px;
    line-height: 1.5;
}

.up-action-side {
    flex: 0 0 auto;
}

.up-action-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 9px;
    min-height: 42px;
    padding: 0 18px;
    border: 0;
    border-radius: 10px;
    background: var(--sys-action-warning, #8a5d00);
    color: var(--sys-action-warning-text, #ffffff);
    font: inherit;
    font-size: 13px;
    font-weight: 700;
    white-space: nowrap;
    cursor: pointer;
    transition: filter .16s ease;
}

.up-action-btn:hover:not(:disabled) { filter: brightness(1.08); }
.up-action-btn:disabled { cursor: default; opacity: .8; }

.up-action-btn:focus-visible {
    outline: 2px solid var(--sys-action-primary, #17191b);
    outline-offset: 2px;
}

.up-action-spinner {
    width: 14px;
    height: 14px;
    border: 2px solid currentColor;
    border-top-color: transparent;
    border-radius: 50%;
    animation: cdlg-spin .7s linear infinite;
}

/* Done: the same card, in green, saying what happened rather than vanishing. */
.up-action-card.is-done {
    border-color: var(--ecv-green-line, rgba(19, 129, 59, .32));
    border-inline-start-color: var(--ecv-green, #13813b);
    background: var(--ecv-green-wash, rgba(19, 129, 59, .1));
}

.up-action-card.is-done .up-action-mark {
    /* Same reasoning as amber: --ecv-green is the text tint (#6fcf95 in dark), not a plate. */
    background: var(--sys-state-success-plate, #13813b);
    color: #ffffff;
}

.up-action-card.is-done .up-action-title {
    margin-bottom: 4px;
}

@media (prefers-reduced-motion: reduce) {
    .up-action-spinner { animation-duration: 1.6s; }
}

@media (max-width: 620px) {
    .up-action-card { flex-wrap: wrap; }
    .up-action-side { width: 100%; }
    .up-action-btn { width: 100%; }
}

/* --- The department's action-request bubble --------------------------------- *
 * The badge was amber text on an amber plate — measured unreadable. Its own pairing, and the
 * bubble keeps the amber edge that marks it out from an ordinary reply. */

/* Specific enough to beat `.ops-thread .note header span`, which sets the metadata grey and
 * would otherwise paint this badge's text on its own amber plate at 1.04:1. */
.ops-thread .note header .note-badge-action,
.note-badge-action {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 2px 9px;
    border-radius: 999px;
    background: var(--sys-action-warning, #8a5d00);
    color: var(--sys-action-warning-text, #ffffff);
    font-size: 10.5px;
    font-weight: 800;
    letter-spacing: .06em;
    text-transform: uppercase;
}

.ops-thread .note.is-action-request {
    border: 1px solid var(--ecv-amber-line, rgba(176, 122, 12, .32));
    border-inline-start: 3px solid var(--ecv-amber, #8a5d00);
    background: var(--ecv-amber-wash, rgba(176, 122, 12, .1)) !important;
}

.ops-thread .note.is-action-request p {
    color: var(--sys-text-primary, #17191b);
}

/* The two answers to a request.
 *
 * Done is the filled button because it is the expected outcome; "I need more help" is a ghost
 * beside it — equally available, but not the thing being suggested. Making them identical would
 * ask the person to choose rather than to answer. */

.up-action-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 9px;
    margin-top: 12px;
}

.up-action-btn.is-ghost {
    background: transparent;
    border: 1px solid var(--ecv-amber-line, rgba(176, 122, 12, .45));
    color: var(--ecv-amber, #8a5d00);
}

.up-action-btn.is-ghost:hover:not(:disabled) {
    filter: none;
    background: var(--ecv-amber-wash, rgba(176, 122, 12, .14));
}

/* Asking what they are stuck on. Inside the card, because it is part of answering the request
 * rather than a new conversation. */
.up-action-help {
    margin-top: 12px;
    padding: 12px;
    border: 1px solid var(--ecv-amber-line, rgba(176, 122, 12, .28));
    border-radius: 10px;
    background-color: var(--ecv-paper);
}

.up-action-help label {
    display: block;
    margin-bottom: 6px;
    color: var(--text-color);
    font-size: 12px;
    font-weight: 700;
}

.up-action-help textarea {
    display: block;
    width: 100%;
    padding: 9px 11px;
    border: 1px solid var(--ecv-line);
    border-radius: 9px;
    background-color: var(--ecv-soft);
    color: var(--text-color);
    font: inherit;
    font-size: 13px;
    line-height: 1.55;
    resize: vertical;
    box-sizing: border-box;
}

.up-action-help textarea::placeholder {
    color: var(--text-light);
    opacity: 1;
}

.up-action-help-foot {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-top: 10px;
}

.up-action-link {
    border: 0;
    background: none;
    padding: 0;
    color: var(--text-light);
    font: inherit;
    font-size: 12.5px;
    text-decoration: underline;
    text-underline-offset: 2px;
    cursor: pointer;
}

.up-action-link:hover { color: var(--text-color); }

/* Disabled here means "not yet", so it rests rather than looking broken. */
.up-action-btn[aria-disabled="true"] { opacity: .55; cursor: not-allowed; }

/* Asking for help is not a failure, so its confirmation is neutral rather than green — nothing
 * has been completed, the ticket has simply moved back to the team. */
.up-action-card.is-help {
    border-color: var(--sys-border, #e5e6e4);
    border-inline-start-color: var(--sys-action-primary, #17191b);
    background: var(--sys-surface-sunken, var(--ecv-soft, #eff0ee));
}

.up-action-card.is-help .up-action-mark {
    background: var(--sys-action-primary, #17191b);
    color: var(--sys-action-primary-text, #ffffff);
}

@media (max-width: 620px) {
    .up-action-actions { flex-direction: column; }
    .up-action-actions .up-action-btn { width: 100%; }
}

/* An action-request message keeps its identity in the requester's thread.
 *
 * The internal marker is stripped from the words, so without this the message reads as an
 * ordinary reply — and once the request has been answered the callout is gone and the thread is
 * the only record that the ticket was ever stopped on it. The chip carries that meaning; the
 * amber edge makes the message findable when scrolling back. */

.up-msg-bubble.is-action-request {
    border: 1px solid var(--ecv-amber-line, rgba(176, 122, 12, .32));
    border-inline-start: 3px solid var(--ecv-amber, #8a5d00);
    background: var(--ecv-amber-wash, rgba(176, 122, 12, .1));
}

.up-msg-tag {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    margin-bottom: 7px;
    padding: 3px 10px;
    border-radius: 999px;
    /* The paired plate, not the --ecv-amber text tint: that inverts with the theme and would
     * put light text on a light plate in dark mode. */
    background: var(--sys-action-warning, #8a5d00);
    color: var(--sys-action-warning-text, #ffffff);
    font-size: 10px;
    font-weight: 800;
    letter-spacing: .1em;
    text-transform: uppercase;
    line-height: 1.5;
}

/* The message body on the amber wash, stated rather than inherited from the plain-bubble rule. */
.up-msg-bubble.is-action-request p {
    color: var(--sys-text-primary, #17191b);
}

/* The help field's dark values, stated explicitly.
 *
 * The token-based rule above resolves correctly when inspected (--ecv-soft is #202325 at that
 * element) yet the field kept computing to #eff0ee in my measurement harness, which I could not
 * reconcile with the served stylesheet. Rather than leave a field that might be unreadable on a
 * dark page, the dark values are stated on the same explicit selectors this codebase already
 * uses for chrome that the token layer could not reach. Belt and braces on a text field is
 * cheap; an invisible one is not. */
html.dark-theme .up-action-help,
[data-theme="dark"] .up-action-help {
    background-color: #181a1c;
}

html.dark-theme .up-action-help textarea,
[data-theme="dark"] .up-action-help textarea {
    background-color: #202325;
    border-color: #2b2e30;
    color: #e9eae8;
}

html.dark-theme .up-action-help textarea::placeholder,
[data-theme="dark"] .up-action-help textarea::placeholder {
    color: #bcc2c5;
}

html.dark-theme .up-action-text,
[data-theme="dark"] .up-action-text {
    background-color: #181a1c;
    color: #e9eae8;
}

/* The cancel link measured 3.91:1 on --text-light; the resting tier is the right weight for a
 * secondary control that is still read. */
.up-action-link {
    color: var(--text-color);
    opacity: .82;
}

.up-action-link:hover { opacity: 1; }

/* The three answers, on the message that asked.
 *
 * Icon-and-label buttons rather than icons alone: an icon on its own asks the reader to guess,
 * and this is the one control on the page whose meaning has to be unambiguous. Done is the
 * filled one because it is the expected outcome; the other two are quiet beside it. */

.up-msg-answers {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-top: 10px;
    padding-top: 9px;
    border-top: 1px solid var(--ecv-amber-line, rgba(176, 122, 12, .26));
}

.up-msg-answer {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    min-height: 30px;
    padding: 0 11px;
    border: 1px solid var(--ecv-amber-line, rgba(176, 122, 12, .4));
    border-radius: 999px;
    background: transparent;
    color: var(--text-color);
    font: inherit;
    font-size: 11.5px;
    font-weight: 700;
    cursor: pointer;
    transition: background-color .16s ease, border-color .16s ease;
}

.up-msg-answer:hover:not(:disabled) {
    background: var(--ecv-amber-wash, rgba(176, 122, 12, .16));
}

.up-msg-answer.is-done {
    background: var(--sys-action-warning, #8a5d00);
    border-color: var(--sys-action-warning, #8a5d00);
    color: var(--sys-action-warning-text, #ffffff);
}

.up-msg-answer.is-done:hover:not(:disabled) { filter: brightness(1.08); }

.up-msg-answer:disabled { opacity: .55; cursor: default; }

.up-msg-answer:focus-visible {
    outline: 2px solid var(--sys-action-primary, #17191b);
    outline-offset: 2px;
}

/* Who said it. Subordinate to the name, but a real piece of information — a reply from a
 * named person with a role reads very differently from a reply from a name. */
.up-msg-role {
    display: inline-flex;
    align-items: center;
    margin-inline-start: 6px;
    padding: 1px 8px;
    border-radius: 999px;
    background: var(--ecv-soft);
    color: var(--text-light);
    font-size: 10.5px;
    font-weight: 700;
    letter-spacing: .04em;
    white-space: nowrap;
}

@media (max-width: 520px) {
    .up-msg-answers { flex-direction: column; align-items: stretch; }
    .up-msg-answer { justify-content: center; }
}

/* ================================================================== *
 * Internal notes — full width, and dressed as the team-only surface
 *
 * It was sitting in one column of the two-column main area, leaving the half beside it empty:
 * the rule that spans the conversation across both columns matched on `#threadForm`, and this
 * card's composer is `#noteForm`. Both are full-width things; only one said so.
 *
 * The styling earns the "team only" label rather than just asserting it. A note is written by
 * one colleague for another, so it reads as a memo — a distinct plate, the author and time above
 * the words, and a composer that is visibly separate from the requester-facing reply box two
 * cards down. Getting those two confused is the one mistake this screen must not invite.
 * ================================================================== */

.ops-detail-main > .ops-detail-card:has(#noteForm),
.ops-detail-main > .ops-detail-card.is-internal {
    grid-column: 1 / -1;
}

/* The team-only plate. A left edge in the warning colour, because the consequence of mistaking
 * this for the requester-facing box is a private remark sent to a customer. */
.ops-detail-card.is-internal {
    border-inline-start: 3px solid var(--sys-action-warning, #8a5d00);
    background-color: var(--ecv-canvas);
}

.ops-detail-card.is-internal > header {
    background-color: var(--ecv-soft);
    border-bottom-color: var(--ecv-line);
}

/* The lock and "TEAM ONLY" carry the whole warning, so they are stated in the warning colour
 * rather than the ordinary eyebrow grey. */
.ops-detail-card.is-internal .ops-eyebrow {
    color: var(--sys-action-warning, #8a5d00);
    font-weight: 800;
    letter-spacing: .14em;
}

.ops-detail-card.is-internal .ops-eyebrow i {
    margin-inline-end: 5px;
}

/* Notes as memos: author and time on one line, the words beneath, on a plate that is a step off
 * the card so a run of them reads as a list rather than a wall. */
.ops-detail-card.is-internal .ops-thread {
    max-height: min(38vh, 380px);
    padding: 12px 14px;
    gap: 8px;
}

.ops-detail-card.is-internal .ops-thread .note {
    max-width: none;
    align-self: stretch;
    border: 1px solid var(--ecv-line);
    border-inline-start: 2px solid var(--sys-action-warning, #8a5d00);
    border-radius: 9px;
    background-color: var(--ecv-paper);
    padding: 10px 12px;
}

.ops-detail-card.is-internal .ops-thread .note header {
    margin-bottom: 5px;
    font-size: 11px;
}

.ops-detail-card.is-internal .ops-thread .note header b {
    color: var(--text-color);
    font-weight: 800;
}

.ops-detail-card.is-internal .ops-thread .note p {
    color: var(--text-color);
    font-size: 12.5px;
    line-height: 1.55;
}

/* The composer, visibly a different thing from the reply box below it. */
.ops-detail-card.is-internal .ops-composer {
    border-top: 1px solid var(--ecv-line);
    background-color: var(--ecv-soft);
}

.ops-detail-card.is-internal .ops-composer label {
    color: var(--text-light);
    font-size: 10.5px;
    font-weight: 800;
    letter-spacing: .12em;
    text-transform: uppercase;
}

.ops-detail-card.is-internal .ops-composer textarea {
    background-color: var(--ecv-paper);
    border-color: var(--ecv-line);
    color: var(--text-color);
    min-height: 74px;
}

.ops-detail-card.is-internal .ops-composer-foot small {
    color: var(--sys-action-warning, #8a5d00);
    font-weight: 700;
}

/* The count on a closed header, in the warning colour so it matches what it is counting. */
.ops-detail-card.is-internal .ops-collapse-count {
    background-color: var(--sys-action-warning, #8a5d00);
    color: var(--sys-action-warning-text, #ffffff);
}

/* Nothing empty here needs a plate of its own. */
.ops-detail-card.is-internal .ops-thread .muted {
    margin: 0;
    padding: 2px 0;
    color: var(--text-light);
    font-size: 12.5px;
}

/* Every composer field on a ticket page takes the paired field tokens — see the note in
 * ecvalla-theme.css for what mixing portal-scoped and theme-scoped tokens did to these. */
.ops-composer textarea,
.ops-detail-card.is-internal .ops-composer textarea,
.up-action-help textarea {
    background-color: var(--sys-field-bg);
    border-color: var(--sys-field-border);
    color: var(--sys-field-text);
}

.ops-composer textarea::placeholder,
.ops-detail-card.is-internal .ops-composer textarea::placeholder,
.up-action-help textarea::placeholder {
    color: var(--sys-field-placeholder);
    opacity: 1;
}

/* Amber as TEXT, not as a plate.
 *
 * "TEAM ONLY" and "Never shown to requester" are words on a card, so they need the text tint
 * (--ecv-amber, which lifts to #e9c46a on ink) — I had used --sys-action-warning, which is the
 * dark PLATE colour and measured 2.83:1 as text on a dark card. The two tokens are opposites
 * and this is the mistake they exist to prevent, made in the other direction. */
.ops-detail-card.is-internal .ops-eyebrow,
.ops-detail-card.is-internal .ops-composer-foot small {
    color: var(--ecv-amber);
}

/* The composer's label is read, so it takes the resting tier rather than the faintest one. */
.ops-detail-card.is-internal .ops-composer label {
    color: var(--text-color);
    opacity: .82;
}

/* The file requirement, stated before it is enforced.
 *
 * A disabled button with no explanation reads as broken. This says what is missing and where to
 * put it, so the person can act instead of guessing why the button will not respond. */
.up-action-required-file {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    margin: 12px 0 0;
    padding: 9px 12px;
    border: 1px dashed var(--ecv-amber-line, rgba(176, 122, 12, .45));
    border-radius: 9px;
    color: var(--ecv-amber);
    font-size: 12px;
    font-weight: 700;
    line-height: 1.5;
}

.up-action-required-file i { margin-top: 2px; }

/* The control sits with the instruction that asks for it, on its own line at the end so the
   sentence is read before the button is offered. Amber like the notice it belongs to, filled
   rather than outlined, because it is the one thing to do here. */
.up-action-required-file {
    flex-wrap: wrap;
}

.up-action-browse {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    margin-inline-start: auto;
    padding: 7px 14px;
    border: 1px solid var(--ecv-amber, #b07a0c);
    border-radius: 999px;
    background: var(--ecv-amber, #b07a0c);
    color: var(--ecv-paper, #fff);
    font: inherit;
    font-size: 11.5px;
    font-weight: 800;
    cursor: pointer;
    white-space: nowrap;
    transition: transform .16s ease, box-shadow .16s ease, opacity .16s ease;
}

.up-action-browse:hover {
    transform: translateY(-1px);
    box-shadow: 0 6px 16px rgba(176, 122, 12, .28);
}

.up-action-browse:active { transform: translateY(0); }

.up-action-browse:focus-visible {
    outline: 2px solid var(--ecv-amber, #b07a0c);
    outline-offset: 2px;
}

.up-action-browse i { font-size: 11px; }

@media (max-width: 560px) {
    .up-action-browse {
        margin-inline-start: 0;
        width: 100%;
        justify-content: center;
    }
}

/* The upload box, as one control.
 *
 * It is a <button> now, so it needs the button reset — a browser gives it its own font, its own
 * background and a border, none of which belong on a full-width panel — and the contents are
 * centred on the box's middle line rather than pinned to the top of it, which is what made the
 * text and the folder chip look like they belonged to different rows.
 *
 * The whole box being the target is the point: the row reads as one object, so it behaves as one.
 */
.up-action-required-file {
    /* Reset the button, keep the panel. */
    width: 100%;
    font: inherit;
    text-align: start;
    cursor: pointer;

    display: flex;
    align-items: center;          /* vertically centred, all of it on one line */
    justify-content: flex-start;
    gap: 10px;
    margin: 12px 0 0;
    padding: 11px 14px;
    min-height: 48px;
    border: 1px dashed var(--ecv-amber-line, rgba(176, 122, 12, .45));
    border-radius: 9px;
    background: transparent;
    color: var(--ecv-amber);
    font-size: 12px;
    font-weight: 700;
    line-height: 1.5;
    transition: background-color .16s ease, border-color .16s ease;
}

/* The paperclip no longer needs nudging: the row centres it. */
.up-action-required-file i { margin-top: 0; }

/* The sentence takes the room, pushing the folder chip to the far edge — so the instruction
 * reads first and the affordance sits where a control is expected. */
.up-action-required-file-text {
    flex: 1 1 auto;
    min-width: 0;
}

/* The whole box responds, so it is obvious the whole box is the target. */
.up-action-required-file:hover {
    border-style: solid;
    background: var(--ecv-amber-wash, rgba(176, 122, 12, .14));
}

.up-action-required-file:focus-visible {
    outline: 2px solid var(--ecv-amber);
    outline-offset: 2px;
}

/* The folder chip is decoration on the container's own action now, not a separate control, so it
 * cannot be clicked or focused independently. */
.up-action-required-file .up-action-browse {
    pointer-events: none;
    flex: 0 0 auto;
}

@media (max-width: 560px) {
    .up-action-required-file {
        flex-wrap: wrap;
        justify-content: center;
        text-align: center;
    }
    .up-action-required-file-text { flex-basis: 100%; }
}

/* ================================================================== *
 * Written by an administrator
 *
 * An administrator's note or reply usually settles something — an approval, a routing decision,
 * an instruction that overrides where the thread was heading. Somebody scanning a long thread has
 * to be able to find those without reading every line.
 *
 * A left edge and a role chip, not a different background: the message is still a message, and
 * recolouring the whole plate would make every administrator remark shout at the same volume as
 * an action request, which is the one thing that genuinely needs to.
 * ================================================================== */

.ops-thread .note.is-admin,
.ops-detail-card.is-internal .ops-thread .note.is-admin {
    border-inline-start: 3px solid var(--sys-action-primary, #17191b);
}

.up-msg-bubble.is-admin {
    border-inline-start: 3px solid var(--sys-action-primary, #17191b);
}

/* The role, said plainly. Inverted so it reads as authority rather than as another grey label —
 * and paired, because --sys-action-primary flips with the theme. */
/* The role beside an author's name. One rule, not two — a second set that happens to agree
 * today is a second set that can disagree tomorrow, which is exactly how the ticket card ended up
 * with four conflicting declarations.
 *
 * Readable on its own before any variant is considered: this pill used to rely on the admin
 * variant to become legible, so a role that did not match that test rendered as a grey smudge. */
.note-role {
    display: inline-flex;
    align-items: center;
    padding: 1px 8px;
    border: 1px solid var(--ecv-line);
    border-radius: 999px;
    background: var(--ecv-soft);
    color: var(--text-color);
    font-size: 10px;
    font-weight: 800;
    letter-spacing: .06em;
    text-transform: uppercase;
    white-space: nowrap;
}

/* Administrator: the inverted plate, declared as a pair so it holds in both themes. */
.note-role.is-admin {
    background: var(--sys-action-primary, #17191b);
    color: var(--sys-action-primary-text, #ffffff);
    border-color: transparent;
}

/* The note header holds a name, a role and a time; it needs to wrap rather than squeeze. */
.ops-thread .note header {
    flex-wrap: wrap;
    row-gap: 3px;
}

/* --- The transfer modal's result and spinner --- */

.ops-transfer-result {
    margin: 0 0 14px;
    padding: 11px 13px;
    border: 1px solid var(--ecv-red-line, rgba(173, 41, 31, .32));
    border-radius: 9px;
    background: var(--ecv-red-wash, rgba(173, 41, 31, .08));
    color: var(--ecv-red);
    font-size: 12.5px;
    line-height: 1.55;
}

.ops-transfer-result.is-ok {
    border-color: var(--ecv-green-line, rgba(19, 129, 59, .32));
    background: var(--ecv-green-wash, rgba(19, 129, 59, .1));
    color: var(--ecv-green);
}

.ops-transfer-success {
    padding-block: var(--ecv-space-8);
    text-align: center;
}

.ops-transfer-success .ops-modal-icon {
    margin-inline: auto;
}

.ops-transfer-success h3 {
    margin-block: var(--ecv-space-4) var(--ecv-space-2);
}

.ops-transfer-success p {
    margin-block: 0 var(--ecv-space-6);
    color: var(--text-light);
    line-height: 1.55;
    overflow-wrap: anywhere;
}

.ops-btn-spinner {
    display: inline-block;
    width: 13px;
    height: 13px;
    margin-inline-end: 7px;
    vertical-align: -2px;
    border: 2px solid currentColor;
    border-top-color: transparent;
    border-radius: 50%;
    animation: cdlg-spin .7s linear infinite;
}

@media (prefers-reduced-motion: reduce) {
    .ops-btn-spinner { animation-duration: 1.6s; }
}

/* The author line: name and role together, time at the end.
 *
 * The header is `justify-content: space-between`, which is right for two children and wrong for
 * three — it pushed the role into the middle of the note, belonging to neither the name it
 * describes nor the timestamp. Grouping the name and its role makes the header two children
 * again, so the pair sits at the start and the time stays at the end.
 */
.note-who {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    min-width: 0;
    flex-wrap: wrap;
}

/* Readable on its own, before any variant is considered.
 *
 * This pill was light text on a light plate — it relied on the admin variant to become legible,
 * so any role that did not match that test rendered as a grey smudge. The base now carries its
 * own contrast and the admin variant is a step up from a readable starting point, not a rescue
 * from an unreadable one. */
/* Same reasoning for the requester-facing thread. */
.up-msg-role {
    background: var(--ecv-soft);
    color: var(--text-color);
    opacity: .92;
}

/* ---------------------------------------------------------------------------
 * The resolution answer
 * ---------------------------------------------------------------------------
 *
 * Reuses .up-action-card wholesale: to a requester this is the same kind of moment as the
 * team asking them something — the ticket is waiting on them and there are two ways out of
 * it — and giving it a second visual language would be a distinction the reader has to
 * decode for no gain.
 *
 * Only the three pieces the card does not already have are defined here.
 */
.up-action-quote {
    margin: 8px 0 0;
    padding: 8px 12px;
    border-inline-start: 3px solid var(--ecv-line, rgba(0, 0, 0, 0.15));
    font-style: italic;
    opacity: 0.85;
}

.up-action-note {
    display: block;
    width: 100%;
    margin-top: 10px;
    padding: 8px 10px;
    border: 1px solid var(--ecv-line, rgba(0, 0, 0, 0.18));
    border-radius: 8px;
    background: transparent;
    color: inherit;
    font: inherit;
    resize: vertical;
}

.up-action-note:focus-visible {
    outline: 2px solid var(--ecv-ink, currentColor);
    outline-offset: 1px;
}

/* A label that exists for a screen reader and not for the eye — the textarea already carries
   a visible placeholder, but a placeholder is not a label. */
.up-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;
}

/* ---------------------------------------------------------------------------
 * Avatars — shared/js/avatar.js
 * ---------------------------------------------------------------------------
 *
 * One appearance for a person across all three portals. Sized entirely by a custom property
 * so a 20px avatar in a table row and a 44px one on an account card are the same component
 * rather than two that drift apart.
 *
 * The photograph is the organisation's own directory photograph. Where there is none — which
 * is four people in five here — the initials get the same care: a stable colour per person,
 * narrow in saturation and lightness so a list of twenty reads as one family and white text
 * stays legible on every one.
 */
.pav {
    --pav-size: 28px;
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: var(--pav-size);
    height: var(--pav-size);
    border-radius: 50%;
    overflow: hidden;
    background: var(--pav-bg, #4a5568);
    color: #fff;
    /* Scales with the circle: initials in a 20px avatar and a 44px one look like the same
       typeface rather than two different decisions. */
    font-size: calc(var(--pav-size) * 0.4);
    font-weight: 700;
    line-height: 1;
    letter-spacing: 0.02em;
    user-select: none;
    vertical-align: middle;
}

.pav img {
    width: 100%;
    height: 100%;
    /* cover, not contain: a directory photograph is rarely square and letterboxing a face
       inside a circle looks like a mistake. */
    object-fit: cover;
    display: block;
}

/* A photograph carries its own edges, so the tinted background behind it is removed once one
   arrives — otherwise a transparent PNG picks up a colour that belongs to the initials. */
.pav.is-photo {
    background: var(--ecv-soft, rgba(128, 128, 128, 0.18));
}

.pav-initials {
    /* Optical centring: cap-height sits slightly high inside a circle. */
    transform: translateY(0.02em);
}

/* Where avatars overlap in a stack, each needs to be cut out of the one behind it. */
.pav-ring {
    box-shadow: 0 0 0 2px var(--ecv-paper, #fff);
}

/* A row of faces — queue members, followers, a thread's participants. */
.pav-stack {
    display: inline-flex;
    align-items: center;
}

.pav-stack .pav + .pav {
    margin-inline-start: calc(var(--pav-size, 28px) * -0.3);
}

/* The name beside the face, which is how most of these are actually used. */
.pav-person {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    min-width: 0;
}

.pav-person > span:last-child {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

@media (prefers-reduced-motion: no-preference) {
    .pav img { transition: opacity 0.18s ease; }
}
