/* File: shared/css/portal-mobile.css
 *
 * The phone. One layer, imported last by portal-chrome.css, that turns the desktop shell
 * into a mobile application: the rail leaves the layout, the top bar becomes a compact
 * header, a bottom bar carries the navigation, and the content is re-proportioned for a
 * 390px screen instead of being a 1440px screen at 27% scale.
 *
 * ============================================================================
 * WHY ONE FILE
 * ============================================================================
 *
 * The alternative was a @media block in each of the eight stylesheets that make up a
 * portal — which is how this codebase previously ended up with three different rail widths
 * and a top bar styled as a sidebar by a later block in the same file. Every rule here is
 * scoped by one of four things: the shell (.prail/.ptopbar/.pcontent), the shared content
 * library (.up-*), the operations workspace (.ops-*), or the department/detail pattern
 * (.dept-* and .tfull). Those four vocabularies cover all three portals, so a fix lands
 * everywhere at once and cannot be applied to one portal and forgotten in another.
 *
 * ============================================================================
 * THE BREAKPOINT, AND WHY IT IS WRITTEN OUT EVERY TIME
 * ============================================================================
 *
 *     @media (max-width: 767.98px), (max-width: 960px) and (max-height: 520px)
 *
 * The first clause is the phone in portrait. The second is the phone in LANDSCAPE — 844×390
 * on a modern handset — which by width alone would get the tablet's icon rail, at the exact
 * moment vertical space is scarcest and a 64px header plus a fixed rail hurt most. Height is
 * the honest signal there.
 *
 * 767.98 rather than 768 so an iPad in portrait, at exactly 768, keeps the icon rail: that
 * device has the width for it and a bottom bar would waste the room. There is therefore no
 * viewport at which both navigations are visible, which is the failure mode the two systems
 * could otherwise produce.
 *
 * CSS has no way to name a media query, so the condition is repeated on each block rather
 * than being kept in a variable. Repetition that a search can find beats a level of
 * indirection that hides which rules are phone-only.
 *
 * ============================================================================
 * DESKTOP IS NOT TOUCHED
 * ============================================================================
 *
 * Every declaration in this file is inside a phone media query, except the token block
 * below — which only DEFINES values and applies none of them. Nothing here can change a
 * desktop or tablet layout.
 */

/* ------------------------------------------------------------------ *
 * Mobile tokens
 *
 * Defined unconditionally so they can be read from anywhere, applied only inside the phone
 * queries. The scale is deliberately tight — 4/6/8/10/12/16/20/24 — because the brief this
 * work answers is as much about removing wasted space as about adding polish: a 32px gap
 * between two sections on a 390px screen costs a fifth of the fold.
 * ------------------------------------------------------------------ */

:root {
    /* Page gutter. 14px is the working figure: it clears the screen's own curvature, keeps
     * a card's border off the bezel, and still leaves 362 of 390px to content. The 24–32px
     * page margins a desktop layout uses would spend 16% of the width on nothing. */
    --m-pad: 14px;
    --m-pad-tight: 12px;

    /* Vertical rhythm. Sections are 16px apart, not 32: one screen should hold a heading,
     * a metric row and the first three rows of a list. */
    --m-gap: 12px;
    --m-gap-section: 16px;
    --m-gap-tight: 8px;

    --m-card-pad: 13px;
    --m-card-pad-tight: 11px;

    /* Controls. 40px is the visible height; the touch target is grown to 44px with padding
     * on the hit area rather than by inflating the control, so a compact bar of filters
     * stays compact while every one of them is still comfortably tappable. */
    --m-control-h: 40px;
    --m-control-h-sm: 34px;
    --m-touch: 44px;

    --m-topbar-h: 52px;
    --m-navbar-h: 58px;

    /* One radius set, five roles. Cards and controls share 10px so a control sitting inside
     * a card does not read as a different material; sheets are 18px because a panel that
     * slides from the edge needs a visibly larger corner to read as a separate surface. */
    --m-radius-control: 10px;
    --m-radius-card: 12px;
    --m-radius-sheet: 18px;
    --m-radius-pill: 999px;

    /* Type. A page title at 19px on a 390px screen fills roughly two thirds of the line,
     * which is what a title should do; the 28–40px the desktop scale reaches would take
     * three lines and push the content below the fold. */
    --m-text-title: 19px;
    --m-text-section: 14px;
    --m-text-card: 13.5px;
    --m-text-body: 13.5px;
    --m-text-meta: 11.5px;
    --m-text-eyebrow: 10px;

    /* Elevation has exactly two jobs on a phone: the bar that floats over content, and the
     * sheet that covers it. Cards get a border, not a shadow — twelve shadowed cards in a
     * scrolling list is what makes an interface look soft and cheap. */
    --m-shadow-bar: 0 -1px 0 var(--m-line, rgba(23, 25, 27, .08)), 0 -8px 24px -12px rgba(23, 25, 27, .18);
    --m-shadow-sheet: 0 -12px 40px -12px rgba(23, 25, 27, .34);

    /* The bottom bar's own height plus the home indicator, exposed as a token so a page can
     * clear it without every page inventing its own number. */
    --m-nav-clearance: calc(var(--m-navbar-h) + env(safe-area-inset-bottom, 0px));

    /* --- The two theme-INVARIANT values in the system -----------------------------------
     *
     * The elevated action is a filled plate: paper on ink in the light theme, ink on paper in
     * the dark one. Either way the plate and the mark on it are OPPOSITES of each other, and
     * that is the one thing that must not follow the theme.
     *
     * --ecv-ink cannot express it. The ECVALLA identity flips ink and paper between themes —
     * in the dark theme --ecv-ink resolves to a near-white — so writing the plate as
     * "white background, --ecv-ink mark" produced a white mark on a white plate: an invisible
     * primary action, which is exactly the bug this pair exists to prevent. These two are
     * therefore fixed palette values rather than theme aliases, and each theme decides which
     * of the two goes where. */
    --m-plate-paper: #ffffff;
    --m-plate-ink: #17191b;
}

/* ------------------------------------------------------------------ *
 * THE BOTTOM BAR
 *
 * Written outside the media query — its geometry does not depend on the viewport — and kept
 * out of the layout on wide screens by the one rule at the end of this block. That way the
 * bar's design lives in one place instead of being half-defined in a media query.
 * ------------------------------------------------------------------ */

.pmnav {
    position: fixed;
    inset-inline: 0;
    bottom: 0;
    z-index: 55;
    display: none;
    align-items: stretch;
    justify-content: center;
    gap: 2px;
    height: var(--m-navbar-h);
    padding: 0 6px env(safe-area-inset-bottom, 0px);
    box-sizing: content-box;
    background: var(--m-nav-bg, var(--sys-surface-raised, #fff));
    border-top: 1px solid var(--m-line, var(--border-color, #e5e6e4));
    box-shadow: var(--m-shadow-bar);
}

/* Translucency only where it is real. Without backdrop-filter a semi-transparent bar just
 * shows text through itself, which is worse than an opaque one. */
@supports (backdrop-filter: blur(14px)) {
    .pmnav {
        background: color-mix(in srgb, var(--m-nav-bg, var(--sys-surface-raised, #fff)) 84%, transparent);
        backdrop-filter: saturate(1.5) blur(14px);
    }
}

.pmnav-tab {
    flex: 1 1 0;
    min-width: 0;
    /* Capped, and the row centred, so a phone in LANDSCAPE — 844px wide and 390px tall — does
     * not stretch five tabs to 191px each. A tab is a target, not a column: past about 100px
     * the extra width is dead space between an icon and the next icon, and the group reads
     * better as a centred cluster. Below 500px the cap never binds, so portrait is unchanged. */
    max-width: 104px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;
    /* The hit area is the whole tab — full bar height by an equal share of the width, which
     * on the narrowest supported screen is 62×58px. Item 41 asks for a practical 44px
     * target; this reaches it without the visible icon or label growing at all. */
    min-height: var(--m-touch);
    padding: 5px 2px;
    border: 0;
    background: none;
    font-family: inherit;
    color: var(--m-nav-ink, var(--sys-text-secondary, #6c7174));
    text-decoration: none;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: color 140ms ease;
}

.pmnav-icon {
    position: relative;
    display: grid;
    place-items: center;
    width: 26px;
    height: 22px;
    font-size: 16px;
    line-height: 1;
}

.pmnav-label {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 10px;
    font-weight: 600;
    letter-spacing: .01em;
}

/* Arabic is never letter-spaced, and its glyphs need a touch more height at this size. */
html[dir="rtl"] .pmnav-label {
    letter-spacing: normal;
    font-size: 10.5px;
}

/* --- Active state ---------------------------------------------------
 *
 * Three signals, none of them loud: the ink goes to full strength, the icon sits on a
 * tinted tile, and a 3px marker appears on the bar's top edge above the tab. The corporate
 * accent is whatever the portal's rail uses, so administration's bar and Support's bar are
 * recognisably the same control in the same system, tinted differently — not two different
 * designs. No glow, no gradient, no neon: item 15.
 */

.pmnav-tab.is-active {
    color: var(--m-nav-ink-active, var(--sys-text-primary, #17191b));
}

.pmnav-tab.is-active .pmnav-icon::before {
    content: "";
    position: absolute;
    inset: -5px -3px;
    border-radius: 9px;
    background: var(--m-nav-tile, color-mix(in srgb, currentColor 10%, transparent));
}

.pmnav-tab.is-active::after {
    content: "";
    position: absolute;
    top: 0;
    width: 22px;
    height: 3px;
    border-radius: 0 0 3px 3px;
    background: var(--m-nav-marker, currentColor);
}

.pmnav-tab { position: relative; }

.pmnav-tab:focus-visible {
    outline: 2px solid var(--m-nav-ink-active, var(--sys-text-primary, #17191b));
    outline-offset: -3px;
    border-radius: var(--m-radius-control);
}

/* The press. A 60ms scale on the icon only — enough to feel answered, too short to delay
 * anything, and it cannot move the label or the layout. */
.pmnav-tab:active .pmnav-icon {
    transform: scale(.9);
    transition: transform 60ms ease;
}

/* --- The badge ---------------------------------------------------- */

.pmnav-badge {
    position: absolute;
    top: -3px;
    inset-inline-end: -6px;
    min-width: 15px;
    height: 15px;
    padding: 0 4px;
    box-sizing: border-box;
    display: grid;
    place-items: center;
    border-radius: var(--m-radius-pill);
    background: var(--rail-badge-bg, #a62118);
    color: var(--rail-badge-text, #fff);
    font-size: 9px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}

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

/* --- The elevated action -----------------------------------------
 *
 * Support's New Request. A filled plate, sized so it reads as the one thing to do without
 * becoming a floating button that covers content — which is what the previous .up-fab did,
 * and why it is retired below. It sits IN the bar, so nothing is ever underneath it.
 */

.pmnav-action {
    flex: 0 0 auto;
    align-self: center;
    display: grid;
    place-items: center;
    width: var(--m-touch);
    height: var(--m-touch);
    margin: 0 4px;
    border-radius: 14px;
    background: var(--m-nav-action-bg, var(--sys-action-primary, var(--ecv-ink, #17191b)));
    color: var(--m-nav-action-ink, var(--sys-action-primary-text, #fff));
    font-size: 16px;
    text-decoration: none;
    box-shadow: 0 6px 16px -8px rgba(23, 25, 27, .5);
    transition: transform 120ms ease;
}

.pmnav-action:active { transform: scale(.94); }

.pmnav-action:focus-visible {
    outline: 2px solid var(--m-nav-ink-active, var(--sys-text-primary, #17191b));
    outline-offset: 2px;
}

/* The boot skeleton's empty bar: geometry only, no tiles, so the bottom edge is the right
 * height from first paint and nothing moves when the destinations arrive. */
.pmnav[data-mnav-boot] { pointer-events: none; }

/* --- Above the phone breakpoint the bar does not exist ----------- */

@media (min-width: 768px) and (min-height: 521px) {
    .pmnav,
    .pmsheet { display: none !important; }
}

/* Landscape phones are below the breakpoint by height, so the pair above must not catch
 * them; the query is written as the exact complement of the phone condition. */
@media (min-width: 961px) {
    .pmnav,
    .pmsheet { display: none !important; }
}

/* ------------------------------------------------------------------ *
 * THE MORE SHEET
 *
 * A native <dialog>. Escape, the focus trap, inertness behind it and a real ::backdrop all
 * come from the element; this styles it and nothing more. See the note in
 * shared/js/portal-mobile-nav.js on why none of that is reimplemented.
 * ------------------------------------------------------------------ */

.pmsheet {
    /* The dialog is the full viewport and the panel inside it is the sheet. That is what
     * makes "press outside to dismiss" a single target check rather than a coordinate
     * calculation. */
    position: fixed;
    inset: 0;
    width: 100%;
    max-width: none;
    height: 100%;
    max-height: none;
    margin: 0;
    padding: 0;
    border: 0;
    background: transparent;
    overflow: hidden;
}

.pmsheet::backdrop {
    background: rgba(23, 25, 27, .46);
    opacity: 0;
    transition: opacity 180ms ease;
}

.pmsheet.is-open::backdrop { opacity: 1; }

.pmsheet-panel {
    position: absolute;
    inset-inline: 0;
    bottom: 0;
    display: flex;
    flex-direction: column;
    max-height: min(78svh, 620px);
    padding-bottom: env(safe-area-inset-bottom, 0px);
    background: var(--sys-surface-raised, var(--bg-white, #fff));
    border-top: 1px solid var(--m-line, var(--border-color, #e5e6e4));
    border-radius: var(--m-radius-sheet) var(--m-radius-sheet) 0 0;
    box-shadow: var(--m-shadow-sheet);
    transform: translateY(100%);
    transition: transform 220ms cubic-bezier(.22, 1, .36, 1);
}

.pmsheet.is-open .pmsheet-panel { transform: translateY(0); }

.pmsheet-grip {
    flex: 0 0 auto;
    width: 34px;
    height: 4px;
    margin: 8px auto 2px;
    border-radius: var(--m-radius-pill);
    background: var(--m-line-strong, color-mix(in srgb, var(--text-color, #17191b) 18%, transparent));
}

.pmsheet-head {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    padding: 6px var(--m-pad) 10px;
}

.pmsheet-head strong {
    font-size: var(--m-text-section);
    letter-spacing: -.01em;
    color: var(--text-color);
}

.pmsheet-close {
    display: grid;
    place-items: center;
    width: 34px;
    height: 34px;
    /* The visible control is 34px; the hit area is grown to 44 with a transparent outset so
     * the header stays compact and the target does not. */
    margin: -5px;
    padding: 5px;
    border: 1px solid var(--m-line, var(--border-color, #e5e6e4));
    border-radius: var(--m-radius-pill);
    background: var(--sys-surface-secondary, var(--ecv-soft, #eff0ee));
    color: var(--text-light);
    font-size: 13px;
    cursor: pointer;
}

.pmsheet-close:focus-visible {
    outline: 2px solid var(--text-color);
    outline-offset: 1px;
}

.pmsheet-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
    padding: 0 var(--m-pad) 12px;
}

.pmsheet-group {
    margin: 12px 0 6px;
    font-size: var(--m-text-eyebrow);
    font-weight: 700;
    letter-spacing: .16em;
    text-transform: uppercase;
    color: var(--text-light);
}

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

.pmsheet-group:first-child { margin-top: 4px; }

.pmsheet-rows {
    display: flex;
    flex-direction: column;
    /* One hairline between rows rather than a gap and a border on each: a compact list, not
     * a stack of cards. Item 14. */
    gap: 0;
    border: 1px solid var(--m-line, var(--border-color, #e5e6e4));
    border-radius: var(--m-radius-card);
    overflow: hidden;
}

.pmsheet-row {
    display: flex;
    align-items: center;
    gap: 11px;
    min-height: var(--m-touch);
    padding: 9px 11px;
    background: var(--sys-surface-raised, var(--bg-white, #fff));
    color: var(--text-color);
    text-decoration: none;
    -webkit-tap-highlight-color: transparent;
}

.pmsheet-row + .pmsheet-row {
    border-top: 1px solid var(--m-line, var(--border-color, #e5e6e4));
}

.pmsheet-row:active {
    background: var(--sys-surface-secondary, var(--ecv-soft, #eff0ee));
}

.pmsheet-row.is-active {
    background: var(--m-nav-tile, color-mix(in srgb, var(--text-color, #17191b) 6%, transparent));
}

.pmsheet-row.is-active .pmsheet-row-name { font-weight: 700; }

.pmsheet-row-icon {
    flex: 0 0 30px;
    display: grid;
    place-items: center;
    width: 30px;
    height: 30px;
    border-radius: 9px;
    background: var(--sys-surface-secondary, var(--ecv-soft, #eff0ee));
    color: var(--text-light);
    font-size: 13px;
}

.pmsheet-row.is-active .pmsheet-row-icon {
    background: var(--sys-action-primary, var(--ecv-ink, #17191b));
    color: var(--sys-action-primary-text, #fff);
}

.pmsheet-row-copy {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 1px;
}

.pmsheet-row-name {
    font-size: var(--m-text-card);
    font-weight: 600;
    letter-spacing: -.005em;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.pmsheet-row-sub {
    font-size: var(--m-text-meta);
    color: var(--text-light);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.pmsheet-row-badge {
    flex: 0 0 auto;
    min-width: 19px;
    padding: 2px 6px;
    border-radius: var(--m-radius-pill);
    background: var(--rail-badge-bg, #a62118);
    color: var(--rail-badge-text, #fff);
    font-size: 10px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    text-align: center;
}

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

.pmsheet-row-arrow {
    flex: 0 0 auto;
    font-size: 10px;
    color: var(--text-light);
    opacity: .7;
}

html[dir="rtl"] .pmsheet-row-arrow { transform: scaleX(-1); }

.pmsheet-row:focus-visible {
    outline: 2px solid var(--text-color);
    outline-offset: -2px;
}

@media (prefers-reduced-motion: reduce) {
    .pmsheet-panel,
    .pmsheet::backdrop { transition: none; }
    .pmnav-tab,
    .pmnav-action { transition: none; }
    .pmnav-tab:active .pmnav-icon,
    .pmnav-action:active { transform: none; }
}

/* ================================================================== *
 *
 * THE PHONE
 *
 * Everything below is inside the phone condition described at the top of this file.
 *
 * ================================================================== */

@media (max-width: 767.98px), (max-width: 960px) and (max-height: 520px) {

    /* --- The shell -------------------------------------------------
     *
     * The rail leaves the layout completely. `display: none` rather than a transform,
     * because a transformed fixed element still answers hit tests at its original position
     * and would swallow taps down the leading edge.
     *
     * The two !important declarations are unavoidable and are the only ones in this file.
     * portal-chrome.css asserts `padding-inline-start: var(--rail-w) !important` on the body
     * deliberately — it is defending the rail/content pair against exactly the kind of
     * local tweak that broke it three times before — so removing the inset needs the same
     * weight. The comment there names --rail-w as the sanctioned lever; a phone has no rail
     * at all, so there is no width to set, and overriding the consequence is correct here.
     */
    body.has-portal-chrome {
        padding-inline-start: 0 !important;
        padding-inline-end: 0 !important;
    }

    .prail { display: none; }

    .pmnav { display: flex; }

    /* --- The header ------------------------------------------------
     *
     * 52px, not 64: the mark, the page's name, and the account. That is the whole content
     * of a native application's header and it does not need more room. The subtitle is
     * already suppressed below 720px by portal-chrome.css, which is right — a second line
     * of context on a phone is the desktop's habit, not the phone's.
     */
    .ptopbar {
        gap: 10px;
        min-height: var(--m-topbar-h);
        padding: 0 var(--m-pad-tight);
    }

    /* --- The mark -------------------------------------------------
     *
     * TWO THINGS WERE WRONG HERE.
     *
     * Size: at 22px tall the wordmark is 131px wide — 33% of a 400px header, next to a page
     * title and an account avatar. It was reading as the subject of the header rather than as
     * the mark on it. 15px tall is 89px, about a fifth of the width, which is what a mark in a
     * native application header takes.
     *
     * Colour: the asset is `fill="currentColor"`, but it is loaded through an <img>, and an
     * <img>-loaded SVG cannot see the page's colour — currentColor resolves against the
     * image's own root and paints BLACK. So the mark had no theme treatment at all: correct by
     * accident on the light header, invisible on the dark one.
     *
     * Rather than add another `filter: invert(1)` — which is what the rail and the public bar
     * do, and which can only ever produce pure black or pure white — the mark is painted as a
     * MASK filled with --ecv-ink. That token is #17191b in the light theme and near-white in
     * the dark one, so one declaration is correct in both and the mark is the brand's actual
     * ink rather than an inversion of it.
     */
    .ptopbar-mark {
        display: grid;
        place-items: center;
        flex: 0 0 auto;
        height: 18px;
    }

    .ptopbar-mark img {
        height: 15px;
        width: auto;
    }

    /* Masking is what makes the token apply. Guarded, because if mask were unsupported the
     * mark would vanish entirely — and a missing logo is worse than a black one. Without
     * support the <img> stays visible and the dark theme falls back to the inversion the rest
     * of this codebase uses. */
    @supports (mask-image: url('/shared/assets/ecvalla-logo.svg')) or (-webkit-mask-image: url('/shared/assets/ecvalla-logo.svg')) {
        .ptopbar-mark {
            width: 89px;
            background-color: var(--ecv-ink, #17191b);
            -webkit-mask: url('/shared/assets/ecvalla-logo.svg') no-repeat center / contain;
            mask: url('/shared/assets/ecvalla-logo.svg') no-repeat center / contain;
        }

        /* The image still supplies the intrinsic box the mask is measured against, so it is
         * made transparent rather than removed — display:none would collapse the anchor. */
        .ptopbar-mark img { visibility: hidden; }
    }

    @supports not ((mask-image: url('/shared/assets/ecvalla-logo.svg')) or (-webkit-mask-image: url('/shared/assets/ecvalla-logo.svg'))) {
        [data-theme="dark"] .ptopbar-mark img,
        html.dark-theme .ptopbar-mark img { filter: invert(1); }
    }

    .ptopbar-context { min-width: 0; }

    .ptopbar-title {
        font-size: 14.5px;
        font-weight: 700;
        letter-spacing: -.012em;
        /* A long destination name — "Delegation & Routing" — must shorten, not wrap the
         * header to two lines or push the account off the edge. */
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    /* --- The front doors' bar --------------------------------------
     *
     * The bar on a portal's home page and the questions page. Measured at 400px it was 109px
     * tall across TWO lines: the mark at 155px wide (39% of the bar) took the first line on
     * its own, the language and theme controls sat beside it, and "Sign in" wrapped onto a
     * line of its own underneath — which read as a second, competing header rather than as
     * one bar.
     *
     * Three changes, in the order they matter.
     */

    /* One line, always. The bar's own children shrink instead of wrapping; nothing here is
     * wide enough at 320px to need a second row once the mark is sized for a phone. */
    .pbar {
        flex-wrap: nowrap;
        gap: 8px;
        padding-inline: var(--m-pad-tight);
        /* The desktop bar carries 10px above and below a 26px mark. With a 40px control as
         * the tallest child that came to 61px — taller than the signed-in header it sits
         * beside. 6px brings the two to the same 52px. */
        padding-block: 6px;
        min-height: 52px;
    }

    /* The mark, at the same 15px as the header's. It is the same mark in the same system and
     * it was two different sizes. */
    .pbar-mark {
        height: 15px;
    }

    .pbar-brand {
        flex: 0 1 auto;
        min-width: 0;
        gap: 8px;
    }

    /* The duplicate sign-in.
     *
     * On a home page the hero carries this exact action — same handler, same single-flight
     * lock: auth-screen.js binds every [data-sso-start] on the page, and the bar's copy is
     * the one rendered as a BUTTON precisely because the page is itself the sign-in surface.
     * So the button form is the reliable signal for "the hero already offers this", and it is
     * the one that goes. Two identical calls to action stacked 40px apart is not emphasis.
     *
     * The <a> forms are deliberately left alone: on the questions page the bar's link is the
     * only way in above the fold — its own action sits at the foot of a long page — and for a
     * signed-in visitor the link reads "Open my portal", which is a destination rather than a
     * repeat of the hero. */
    .pbar-action[data-sso-start] {
        display: none;
    }

    .pbar-action {
        flex: 0 1 auto;
        min-width: 0;
        min-height: var(--m-control-h);
        padding-inline: 12px;
        font-size: 12px;
        white-space: nowrap;
    }

    /* 40px visible, with the hit area grown to 44 by a transparent outset — the same trick the
     * sheet's close control uses. Two 44px plates were what pushed the bar to 65px tall; the
     * target is unchanged and the bar comes back to 52. */
    .pbar-btn {
        flex: 0 0 auto;
        box-sizing: border-box;
        width: 40px;
        min-width: 40px;
        height: 40px;
        min-height: 40px;
        padding: 0;
        position: relative;
    }

    .pbar-btn::after {
        content: "";
        position: absolute;
        inset: -2px;
    }

    /* --- Content ---------------------------------------------------
     *
     * The gutter drops to 14px and the bottom clearance becomes exactly the bar plus the
     * home indicator plus 10px of breathing room. It was 90–150px of guessed padding on
     * several screens, which is a blank half-screen at the end of every list.
     */
    .pcontent,
    .up-main,
    .ops-workspace,
    .dept-queue,
    .dept-detail {
        padding-inline: var(--m-pad);
        padding-block-start: 12px;
        padding-block-end: calc(var(--m-nav-clearance) + 10px);
    }

    /* --- Page heads ------------------------------------------------
     *
     * One title, one optional line of context, and the action. Not a hero, a breadcrumb, a
     * page title and a section title all saying the same word — item 44.
     */
    .up-page-head,
    .ops-page-head {
        gap: 3px;
        margin-bottom: 12px;
    }

    .up-page-head h1,
    .ops-page-head h1 {
        font-size: var(--m-text-title);
        line-height: 1.25;
        letter-spacing: -.02em;
    }

    .up-page-head p,
    .ops-page-head p,
    .dept-lede {
        margin: 0;
        font-size: var(--m-text-meta);
        line-height: 1.5;
        color: var(--text-light);
    }

    .dept-lede { margin-bottom: 12px; }

    /* The header already names the page. A second identical H1 immediately below it is the
     * most common piece of duplicated hierarchy in this system, and on a phone it costs a
     * quarter of the fold to say nothing. The page head keeps its lede and its actions. */
    .up-page-head > div > h1:only-child { margin-bottom: 0; }

    /* --- Sections --------------------------------------------------- */

    .up-section,
    .ops-panel + .ops-panel {
        margin-top: var(--m-gap-section);
    }

    .up-section-head,
    .ops-panel-head {
        margin-bottom: var(--m-gap-tight);
    }

    .up-section-head h2,
    .ops-panel-head h2,
    .ops-panel-head h3 {
        font-size: var(--m-text-section);
        letter-spacing: -.01em;
    }

    .up-section-head a,
    .ops-panel-head > a {
        font-size: var(--m-text-meta);
        white-space: nowrap;
    }

    /* --- Cards ------------------------------------------------------ */

    .up-card,
    .ops-panel,
    .ops-queue-panel,
    .ops-detail-card,
    .adm-block,
    .adm-detail,
    .adm-portal-card,
    .adm-dept-card,
    .adm-note,
    .adm-ticket-detail-card,
    .note,
    .dept-description,
    .up-detail,
    .up-tickets-card,
    .up-feedback-card {
        padding: var(--m-card-pad);
        border-radius: var(--m-radius-card);
    }

    /* Cards in a scrolling list carry a border, never a shadow. Twelve elevated surfaces in
     * a column is what makes a list read as soft; one hairline each reads as a document. */
    .up-card,
    .ops-panel,
    .ops-queue-panel,
    .adm-block,
    .adm-detail,
    .adm-portal-card,
    .adm-dept-card {
        box-shadow: none;
    }

    /* --- Metrics ---------------------------------------------------
     *
     * Two per row, down to 320px. One enormous metric per row — which is what an auto-fit
     * grid with a 150px minimum produces at this width — means four screens of scrolling to
     * read four numbers.
     */
    .up-kpis,
    .ops-kpi-grid,
    .dept-metrics,
    .ops-performance-grid,
    .adm-kpis,
    .adm-compact-kpis,
    .adm-bars {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: var(--m-gap-tight);
    }

    .up-kpi,
    .ops-kpi,
    .dept-metrics article,
    .ops-performance-grid article,
    .adm-kpis > *,
    .adm-compact-kpis > * {
        min-height: 0;
        padding: 11px 10px;
        border-radius: var(--m-radius-card);
    }

    /* The dept metric strip was a 1px-gap grid on a border-coloured background, which at two
     * columns leaves a visible cross. Real gaps and real borders instead. */
    .dept-metrics,
    .ops-performance-grid {
        background: none;
        border-bottom: 0;
    }

    .dept-metrics article,
    .ops-performance-grid article {
        border: 1px solid var(--border-color);
        background: var(--card-bg);
    }

    .up-kpi b,
    .ops-kpi span,
    .dept-metrics b,
    .ops-performance-grid span {
        font-size: 9.5px;
        letter-spacing: .1em;
        /* A metric's name must never be clipped: "Waiting for you" is the whole point of
         * the number above it. Two lines is correct here; an ellipsis is not. */
        white-space: normal;
        line-height: 1.25;
    }

    html[dir="rtl"] .up-kpi b,
    html[dir="rtl"] .ops-kpi span,
    html[dir="rtl"] .dept-metrics b {
        letter-spacing: normal;
        text-transform: none;
        font-size: 11px;
    }

    .up-kpi strong,
    .ops-kpi strong,
    .dept-metrics strong {
        margin-top: 4px;
        font-size: 22px;
        line-height: 1;
    }

    /* --- Toolbars and filters --------------------------------------
     *
     * Search takes the full width on its own line; the chips scroll horizontally in their
     * own track rather than wrapping into a four-line block that pushes the list off the
     * screen. The track scrolls, the page does not — item 42 forbids the second.
     */
    .up-toolbar,
    .ops-queue-toolbar,
    .adm-toolbar {
        gap: var(--m-gap-tight);
        margin-bottom: 10px;
    }

    .up-search,
    .ops-search {
        flex: 1 1 100%;
        min-width: 0;
        max-width: none;
        min-height: var(--m-control-h);
    }

    .up-chiprow {
        flex-wrap: nowrap;
        justify-content: flex-start;
        gap: 6px;
        margin-inline: calc(var(--m-pad) * -1);
        padding-inline: var(--m-pad);
        overflow-x: auto;
        scrollbar-width: none;
        overscroll-behavior-x: contain;
        -webkit-overflow-scrolling: touch;
    }

    .up-chiprow::-webkit-scrollbar { display: none; }

    .up-chip {
        flex: 0 0 auto;
        min-height: 32px;
        padding: 0 12px;
        display: inline-flex;
        align-items: center;
        font-size: 12px;
        white-space: nowrap;
    }

    /* The desk's own filter row and view tabs scroll in a track, like the chips above: five
     * named views wrapped onto three lines is most of the space above a queue. */
    .adm-tabs {
        flex-wrap: nowrap;
        gap: 6px;
        margin-inline: calc(var(--m-pad) * -1);
        padding-inline: var(--m-pad);
        overflow-x: auto;
        scrollbar-width: none;
        overscroll-behavior-x: contain;
        -webkit-overflow-scrolling: touch;
    }

    .adm-tabs::-webkit-scrollbar { display: none; }

    .adm-tabs > * {
        flex: 0 0 auto;
        white-space: nowrap;
        min-height: 34px;
    }

    /* A text control is 16px so iOS does not zoom the viewport when it is focused. Anything
     * smaller triggers the zoom, and the zoom leaves the page scrolled sideways — which then
     * looks like the horizontal-overflow bug rather than the keyboard behaviour it is. */
    .up-select,
    .ops-filter-select,
    .ops-status-select,
    .up-search input,
    .ops-search input,
    .ops-input,
    .up-input,
    .ops-modal-input {
        min-height: var(--m-control-h);
        font-size: 16px;
    }

    /* Two filters per row. Five desktop dropdowns in a line is item 25's example of what not
     * to do, and stacking all five is its own waste — a phone holds two comfortably, so
     * five filters are three rows instead of five, and the list starts 80px higher.
     *
     * The !important is aimed at an inline `style="max-width:150px"` on each select in the
     * People and Tickets modules. Those widths are a desktop measure; overriding them here is
     * cheaper and safer than making six modules viewport-aware. */
    .adm-toolbar .up-select,
    .adm-dept-toolbar .up-select,
    .ops-advanced-filters .up-select,
    .ops-filters-inline .ops-filter-select,
    .ops-advanced-filters .ops-filter-select {
        flex: 1 1 calc(50% - 5px);
        min-width: 0;
        max-width: none !important;
    }

    .ops-filters-inline,
    .ops-advanced-filters,
    .adm-toolbar,
    .adm-dept-toolbar {
        flex-wrap: wrap;
        gap: var(--m-gap-tight);
        /* flex-wrap alone was not enough on the department queue. .ops-filters-inline carries
         * "flex: 0 0 auto" from the department identity sheet, so as a flex CHILD it was sized
         * by its content and never allowed to shrink: at 390px it measured 522px, the row was
         * clipped rather than wrapped, and the status filter was partly unreachable on a phone.
         * Wrapping only helps once the container is permitted to be viewport-width, which is
         * what these two declarations grant. min-width:0 is the half people forget — without
         * it a flex item still refuses to go below its content's minimum. */
        flex: 1 1 100%;
        min-width: 0;
    }

    /* The count and the sort control end the toolbar rather than being pushed off it. */
    .adm-toolbar-spacer,
    .ops-toolbar-end { flex-basis: 100%; }

    /* The filter bar goes one control per line on a phone.
     *
     * On the desktop it is a single row whose controls grow to span the panel. Wrapped into a
     * 343px column those same growth ratios pack unevenly — a search on its own line, then a
     * list beside a date field, each a different height — which reads as a broken row rather
     * than a stack. One per line is legible, and each control keeps a full-width touch target.
     *
     * The two date fields are the deliberate exception: .adm-filterbar .up-field is the more
     * specific rule, so From and To stay paired at half width each. A range is one thought,
     * and splitting it across two lines would read as two unrelated filters.
     *
     * The count sits under them all, aligned to the end of the row it summarises. */
    .adm-filterbar > * { flex: 1 1 100%; max-width: none; }
    .adm-filterbar .up-select { flex: 1 1 100%; }
    .adm-filterbar-count { margin-inline-start: 0; text-align: end; }

    .ops-sort,
    .ops-sort select { width: 100%; }

    /* --- Buttons ---------------------------------------------------
     *
     * Compact and strong. Full width only where the button IS the step — a form's submit, a
     * sheet's confirm — and never merely because there is room. Item 19.
     */
    .dept-btn,
    .ops-btn,
    .cdlg-btn {
        min-height: var(--m-control-h);
        padding: 0 14px;
        font-size: 11.5px;
        letter-spacing: .06em;
    }

    .dept-btn-sm {
        min-height: var(--m-control-h-sm);
        padding: 0 11px;
        font-size: 11px;
    }

    html[dir="rtl"] .dept-btn,
    html[dir="rtl"] .ops-btn {
        letter-spacing: normal;
        text-transform: none;
        font-size: 13px;
    }

    /* The floating action button is retired. It covered the last row of every list, it
     * needed 150px of page padding to compensate, and the thing it did now has a permanent
     * home in the bottom bar where nothing is underneath it. */
    .up-fab { display: none !important; }

    /* --- Tables ----------------------------------------------------
     *
     * A seven-column desktop table cannot become seven stacked label/value lines on a
     * phone: that is 200px of vertical space per record and it is what the previous
     * breakpoint produced. It becomes a card with a real hierarchy instead — the first cell
     * is the identity and reads as a title, the rest are compact label/value pairs in two
     * columns, and the label is a caption above its value rather than a 96px column that
     * pushes every value to the middle of the screen.
     *
     * Every td in this system already carries data-label (87 of them), so the label text is
     * the table's own header and cannot drift from it.
     */
    .up-table-wrap {
        overflow: visible !important;
        max-height: none !important;
        height: auto !important;
        border: 0;
        background: none;
    }

    table.up-table,
    table.up-table tbody,
    table.up-table tr,
    table.up-table td {
        display: block;
    }

    table.up-table thead { display: none; }
    table.up-table { white-space: normal; }

    table.up-table tr {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 7px 10px;
        margin-bottom: var(--m-gap-tight);
        padding: var(--m-card-pad-tight);
        border: 1px solid var(--border-color);
        border-radius: var(--m-radius-card);
        background: var(--card-bg, var(--bg-white));
    }

    table.up-table td {
        min-width: 0;
        height: auto;
        padding: 0;
        border: 0;
        text-align: start;
        font-size: var(--m-text-meta);
        white-space: normal;
        overflow: visible;
        text-overflow: clip;
        background: none;
    }

    /* The first two cells are the record's identity — a name and an address, an id and a
     * subject — so they span the card and set its heading. */
    table.up-table td:nth-child(1),
    table.up-table td:nth-child(2) {
        grid-column: 1 / -1;
    }

    table.up-table td:nth-child(1) {
        font-size: var(--m-text-card);
        font-weight: 700;
        letter-spacing: -.01em;
        color: var(--text-color);
    }

    table.up-table td:nth-child(1)::before { display: none; }

    table.up-table td:nth-child(2) {
        margin-top: -4px;
        color: var(--text-light);
    }

    table.up-table td:nth-child(2)::before { display: none; }

    /* The label runs INLINE with its value, not as a caption above it.
     *
     * Stacked, each pair is two lines: 30px for "DEPARTMENT" over "IT". Inline it is one line
     * of about 17px, and a value long enough to need a second line still gets one because the
     * cell wraps normally. Across a seven-field record that is most of a card's height — the
     * difference between two records on a screen and four. */
    table.up-table td[data-label]::before {
        content: attr(data-label);
        display: inline;
        margin-inline-end: 6px;
        font-size: 9px;
        font-weight: 700;
        letter-spacing: .12em;
        text-transform: uppercase;
        color: var(--text-light);
    }

    html[dir="rtl"] table.up-table td[data-label]::before {
        letter-spacing: normal;
        text-transform: none;
        font-size: 10.5px;
    }

    /* Secondary columns.
     *
     * Seven desktop columns compressed into 390px is what item 23 forbids, and a card that
     * lists all seven is the same mistake turned vertical. A table says which of its columns
     * are secondary by marking those cells `data-priority="secondary"`; on a phone they leave
     * the card and stay reachable in the record's own detail view, which every one of these
     * rows already opens on tap.
     *
     * Declared at the SOURCE — one attribute on the cell that holds the value — rather than
     * as a list of nth-child indices here, which would silently address the wrong field the
     * first time a column is inserted. */
    table.up-table td[data-priority="secondary"] { display: none; }

    /* --- The queue table -------------------------------------------
     *
     * .ops-ticket-table is the department desk's queue and it already becomes a card at
     * 850px — but as the same stacked-caption layout, and measured at 390px one ticket came
     * to 302px: two tickets per screen, when item 26 asks for several. The same two moves
     * that fixed .up-table fix this one, applied at the phone breakpoint so the tablet
     * treatment above 768px is left exactly as it was.
     */
    .ops-table-wrap {
        overflow: visible;
    }

    table.ops-ticket-table td[data-priority="secondary"] { display: none; }

    table.ops-ticket-table td {
        height: auto;
        min-height: 0;
        padding: 0;
        border: 0;
        text-align: start;
        vertical-align: baseline;
        /* Same reason as the height above: portal-chrome.css states nowrap at
         * `table.ops-ticket-table td`, so the 850px card rule's `white-space: normal` at one
         * lower specificity never took effect and every subject was ellipsised to a single
         * line — about 40 characters, which is not enough to tell two tickets apart. */
        white-space: normal;
        max-width: none;
    }

    /* Inline labels, not captions. The 850px rule sets these to display:block with a
     * margin — eight fields then cost eight extra lines. */
    table.ops-ticket-table td::before {
        display: inline;
        margin: 0 6px 0 0;
        font-size: 9px;
        letter-spacing: .1em;
    }

    html[dir="rtl"] table.ops-ticket-table td::before {
        margin: 0 0 0 6px;
        letter-spacing: normal;
        text-transform: none;
        font-size: 10.5px;
    }

    /* The reference and the subject are the ticket's identity: the reference alone on the
     * first line, the subject across the card under it. Everything else is a compact pair.
     * The 850px rule already spans the subject; this adds the reference's own line and takes
     * the caption off both, because "TICKET #TCK-8841" says the word twice. */
    table.ops-ticket-table .queue-row {
        gap: 5px 10px;
        margin: 0 0 var(--m-gap-tight);
        padding: var(--m-card-pad-tight);
        border-radius: var(--m-radius-card);
    }

    table.ops-ticket-table td:nth-child(1) {
        grid-column: 1 / -1;
        font-size: var(--m-text-meta);
        font-weight: 700;
    }

    table.ops-ticket-table td:nth-child(1)::before,
    table.ops-ticket-table td:nth-child(2)::before { display: none; }

    table.ops-ticket-table td:nth-child(2) {
        margin-top: -3px;
    }

    table.ops-ticket-table .queue-subject {
        display: block;
        max-width: none;
        margin: 0;
        text-align: start;
        font-size: var(--m-text-card);
        font-weight: 700;
        letter-spacing: -.01em;
        line-height: 1.35;
        /* Two lines of subject, then clipped. A 90-character subject on a phone is four
         * lines that push every other field off the card; two lines identify the ticket.
         * A max-height in em rather than -webkit-line-clamp: the clamp needs
         * display:-webkit-box, and the department identity stylesheet sets this element to
         * display:block at the same specificity, so the clamp was silently inert. */
        max-height: 2.7em;
        overflow: hidden;
    }

    table.ops-ticket-table td,
    table.ops-ticket-table .ops-dept-tag {
        overflow-wrap: anywhere;
    }

    .ops-pagination {
        padding: 8px 0 0;
        font-size: var(--m-text-meta);
    }

    .ops-pagination button {
        min-width: var(--m-touch);
        min-height: var(--m-touch);
    }

    /* A long address is the single most common cause of sideways scroll in this system.
     * Broken at any character rather than allowed to set the card's minimum width. */
    table.up-table td .up-user,
    table.up-table td a[href^="mailto:"],
    .pmsheet-row-sub,
    .up-row-body p,
    .queue-meta {
        overflow-wrap: anywhere;
        word-break: break-word;
    }

    table.up-table .up-sub {
        max-width: none;
        white-space: normal;
    }

    table.up-table .up-table-empty td,
    table.up-table tr.up-table-empty {
        display: block;
        grid-template-columns: none;
        text-align: center;
    }

    /* --- List/detail ------------------------------------------------
     *
     * Two panes become two screens. Already handled at 920px for .up-split; the same is
     * needed for the operations grid, whose side column would otherwise sit under the list.
     */
    .up-split,
    .ops-detail-grid,
    .ops-dashboard-grid,
    .dept-main,
    .adm-split,
    .adm-grid-2,
    .adm-field-grid,
    .adm-field-row,
    .adm-switch-grid,
    .adm-dept-modal-grid,
    .up-detail-facts {
        grid-template-columns: 1fr;
        gap: var(--m-gap);
    }

    .up-detail { position: static; }

    .ops-detail-side { order: -1; }

    /* --- Facts strips ---------------------------------------------- */

    .up-facts,
    .dept-facts,
    .ops-properties,
    .adm-facts {
        gap: 10px;
        margin: 10px 0;
        padding: 11px 12px;
        border-radius: var(--m-radius-card);
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }

    .up-facts dt,
    .dept-facts dt {
        font-size: 9px;
        letter-spacing: .1em;
        white-space: normal;
    }

    .up-facts dd,
    .dept-facts dd {
        margin-top: 2px;
        font-size: 12.5px;
        overflow-wrap: anywhere;
    }

    .dept-facts > div { padding: 10px 11px; }

    /* --- The conversation -------------------------------------------
     *
     * Compact bubbles, and a message may take almost the whole width: a 78% cap on a 390px
     * screen leaves 86px of margin doing nothing while the text inside wraps every four
     * words.
     */
    .up-thread,
    .ops-thread,
    .adm-thread,
    .adm-ticket-conversation,
    body.has-ticket-page #detailBody .up-thread {
        gap: 9px;
        padding: 10px;
        border-radius: var(--m-radius-card);
    }

    .up-msg {
        max-width: 94%;
        gap: 8px;
        font-size: var(--m-text-body);
    }

    .up-msg-bubble,
    .adm-msg {
        padding: 8px 11px;
        border-radius: 13px;
        overflow-wrap: anywhere;
    }

    .up-msg-avatar {
        flex-basis: 28px;
        width: 28px;
        height: 28px;
        font-size: 11px;
    }

    /* A URL or an address inside a message must wrap, not widen the thread. */
    .up-msg p,
    .note p,
    .up-msg a {
        overflow-wrap: anywhere;
        word-break: break-word;
    }

    /* --- Lifecycle -------------------------------------------------- */

    .up-lifecycle {
        margin: 10px 0;
        gap: 0;
        scrollbar-width: none;
    }

    .up-lifecycle::-webkit-scrollbar { display: none; }

    .up-stage { font-size: 10px; }
    .up-stage::after { width: 14px; margin: 0 6px; }

    /* --- Forms -----------------------------------------------------
     *
     * Fields at full width, 10px apart, labels tight to their input, and every text control
     * at 16px so focusing one does not zoom the page. A 20px gap between every field is
     * what makes a six-field form three screens long.
     */
    .up-modal-body,
    .ops-form-group,
    .ops-field-group,
    .ops-dynamic-status-fields,
    .adm-dept-form,
    .dept-note-form,
    .dept-status-form {
        gap: 10px;
    }

    .up-modal-row {
        grid-template-columns: 1fr !important;
        gap: 10px;
    }

    .up-modal-body label,
    .ops-form-group > label,
    .ops-modal-input-label,
    .dept-note-form label,
    .dept-status-form label {
        margin-bottom: 4px;
        font-size: 10px;
        letter-spacing: .1em;
    }

    html[dir="rtl"] .up-modal-body label {
        letter-spacing: normal;
        text-transform: none;
        font-size: 12px;
    }

    input[type="text"],
    input[type="email"],
    input[type="search"],
    input[type="tel"],
    input[type="number"],
    input[type="date"],
    input[type="password"],
    select,
    textarea {
        font-size: 16px;
    }

    textarea,
    .ops-textarea,
    .adm-ticket-composer textarea,
    .dept-note-form textarea {
        min-height: 88px;
        padding: 9px 11px;
    }

    .ops-field-error {
        margin-top: 3px;
        font-size: 11px;
    }

    /* --- Modals and sheets -----------------------------------------
     *
     * One treatment for every overlay in the system, so a confirmation and a form do not
     * arrive as two unrelated designs. A dialog enters from the bottom edge, keeps the
     * sheet's radius on its top corners only, and is as tall as its content up to 92% of
     * the viewport — a complex form gets a full-height sheet, a confirmation stays short.
     * Items 30 and 31.
     */
    /* Every backdrop in all three portals. Listed by name rather than by a shared class,
     * because there is no shared class: the Support portal, administration, the department
     * desk, the confirmation dialog and the departments editor each grew their own. Naming
     * them here is what makes one sheet treatment apply to all of them; giving them a common
     * class would mean editing nine page modules to change a border radius. */
    .up-modal-backdrop,
    .adm-dept-modal-backdrop,
    .adm-team-modal-backdrop,
    .adm-drawer-backdrop,
    .ops-modal,
    .up-feedback-overlay {
        align-items: flex-end;
        padding: 0;
    }

    .up-modal,
    .adm-dept-modal,
    .ops-modal-card,
    .up-feedback-overlay-card {
        width: 100%;
        max-width: none;
        max-height: 92svh;
        border-radius: var(--m-radius-sheet) var(--m-radius-sheet) 0 0;
        padding-bottom: env(safe-area-inset-bottom, 0px);
    }

    .up-modal-head,
    .adm-dept-modal-head,
    .adm-drawer-head,
    .ops-modal-header {
        padding: 13px var(--m-pad) 10px;
    }

    .up-modal-head h2,
    .adm-dept-modal-head h2,
    .ops-modal-title,
    .up-modal-title,
    .cdlg-title {
        font-size: 15px;
        letter-spacing: -.01em;
    }

    .up-modal-body,
    .adm-dept-modal-body,
    .adm-drawer-body,
    .ops-modal-body {
        padding: 0 var(--m-pad) 12px;
        overscroll-behavior: contain;
        -webkit-overflow-scrolling: touch;
    }

    /* The full-bleed action bar cancels the dialog's horizontal padding with a negative margin,
     * and the desktop rule hard-codes 22px because that is the desktop padding. This layer sets
     * the padding to --m-pad, so the bleed over-cancelled by the difference and the bar hung 7px
     * past BOTH viewport edges at 360 — clipped rather than scrollable, so the ends of the bar
     * were simply cut off. Derived from the same variable, so the two cannot drift again. */
    .up-modal-body .up-modal-foot {
        width: calc(100% + (var(--m-pad) * 2));
        margin-inline: calc(var(--m-pad) * -1);
    }

    .up-modal-foot,
    .adm-dept-form-foot,
    .adm-drawer-foot,
    .ops-modal-actions {
        gap: 8px;
        padding: 10px var(--m-pad) 12px;
    }

    /* An overlay's actions are the step, so they take the width — this is the case item 19
     * carves out. Stacked rather than side by side: two half-width buttons at the bottom of a
     * phone are two 45px targets a thumb has to aim between. */
    .up-modal-foot .dept-btn,
    .adm-drawer-foot .dept-btn,
    .ops-modal-actions .ops-btn {
        width: 100%;
    }

    .up-modal-foot,
    .adm-drawer-foot,
    .ops-modal-actions {
        flex-direction: column-reverse;
    }

    /* --- Drawers ---------------------------------------------------
     *
     * A side drawer on a phone is a sheet. The People screen's user drawer comes in from the
     * inline edge on a desktop, which at 390px leaves a 30px sliver of page behind it doing
     * nothing — and puts the drawer's close control at the top of the screen, the furthest
     * point from a thumb.
     *
     * The !important here is not a cascade fight: these drawers set `inset` and `width` as
     * inline styles from their page modules, and an inline style cannot be outranked any
     * other way. The alternative was editing four modules to ask the viewport what shape
     * they should be, which is the layout decision belonging to this file.
     */
    .adm-drawer {
        inset: auto 0 0 0 !important;
        width: 100% !important;
        max-width: none !important;
        max-height: 92svh;
        border-radius: var(--m-radius-sheet) var(--m-radius-sheet) 0 0;
        padding-bottom: env(safe-area-inset-bottom, 0px);
    }

    /* --- Empty, loading and error states ---------------------------
     *
     * Compact. A 200px illustration and a 40px heading on a 390×640 content area is most of
     * the screen spent saying that there is nothing on it. Items 32–34.
     */
    .up-empty,
    .ops-empty,
    .adm-empty,
    .up-thread-empty,
    .up-allcaught {
        padding: 22px 16px;
        border-radius: var(--m-radius-card);
    }

    .up-empty span,
    .up-empty > i,
    .ops-empty i,
    .adm-empty i {
        font-size: 22px;
    }

    .up-empty b,
    .ops-empty b,
    .adm-empty b {
        font-size: 13.5px;
    }

    .up-empty p,
    .ops-empty p,
    .adm-empty p {
        margin-top: 3px;
        font-size: var(--m-text-meta);
        max-width: 34ch;
    }

    /* A skeleton stands in for the component it replaces, at its height, so the shell does
     * not move when the data lands. It is never the whole page: the header and the bottom
     * bar stay painted throughout — that is what shell-boot.js exists for. */
    .up-skeleton,
    .ops-row-skeleton {
        border-radius: var(--m-radius-card);
        min-height: 64px;
    }

    .up-kpis .up-skeleton { min-height: 68px; }

    /* The branded content veil covers the CONTENT pane only, never the shell. It is sized
     * from the pane it sits in, so on a phone it needs the bar's clearance too or its mark
     * lands underneath the navigation. */
    .up-content-loading,
    .dp-content-loading {
        padding-bottom: var(--m-nav-clearance);
    }

    .up-error,
    .ops-field-error,
    .up-action-error,
    .cdlg-error,
    .dept-error {
        padding: 11px 12px;
        border-radius: var(--m-radius-control);
        font-size: 12.5px;
        /* An error carries a reference or an address often enough that it must be allowed to
         * break. A message that widens the page is a second fault on top of the first. */
        overflow-wrap: anywhere;
    }

    .ops-field-error {
        padding: 0;
        margin-top: 3px;
        font-size: 11px;
    }

    /* --- Back navigation --------------------------------------------
     *
     * There are NO breadcrumbs in this system. Item 43 asks for a desktop breadcrumb chain to
     * be collapsed on a phone, and the honest answer after looking is that there is nothing to
     * collapse: every detail view is reached from a list and carries a single back control,
     * which is already the pattern the item is asking for. Writing rules for a component that
     * does not exist would have looked like coverage and been none, so this note stands in its
     * place — if a breadcrumb is ever added, this is where it is handled.
     *
     * What does exist is the ticket workspace's back control. It is the phone's most-used
     * navigation after the bar, so it gets a real target rather than a 12px chevron.
     */
    .adm-ticket-back {
        min-height: var(--m-touch);
        font-size: 12.5px;
    }

    /* --- Charts and the permission matrix --------------------------
     *
     * Administration's charts are viewBox SVGs with preserveAspectRatio, so they already
     * scale to their container and need nothing here beyond the max-width at the end of this
     * block. The permission matrix is the one that does need help.
     */
    .adm-matrix-wrap {
        overflow-x: auto;
        overscroll-behavior-x: contain;
        -webkit-overflow-scrolling: touch;
    }

    .adm-chart {
        min-width: 0;
        max-width: 100%;
    }

    /* The permission matrix is a genuine two-axis table: roles across, resources down. It is
     * the one table in the system that must NOT become cards — reading a permission means
     * reading the intersection, and a card destroys the axis. It scrolls sideways in its own
     * wrapper, which is the correct answer of the four item 23 offers. */
    .adm-matrix,
    .adm-policy-matrix {
        min-width: max-content;
    }

    table.adm-matrix,
    table.adm-matrix tbody,
    table.adm-matrix tr,
    table.adm-matrix td {
        display: revert;
    }

    table.adm-matrix thead { display: revert; }

    /* --- The full-page ticket workspace ---------------------------- */

    .tfull {
        padding-inline: 0;
    }

    .tfull-head h1,
    .up-detail-head h2,
    .dept-detail-head h2 {
        font-size: 16.5px;
        letter-spacing: -.02em;
        overflow-wrap: anywhere;
    }

    .up-subhead,
    .dept-detail h3 {
        margin: 12px 0 6px;
        font-size: 10px;
        letter-spacing: .12em;
    }

    /* --- The front doors' heroes ------------------------------------
     *
     * A DEDICATED MOBILE COMPOSITION, not the desktop hero scaled down. Measured on the
     * Support portal's home page at 390×844 before this block, the desktop hero was:
     *
     *     891px tall            106% of the screen — a full screen of scrolling before
     *                           any content, and the sign-in button at y=397
     *     44px heading          the size item 7 names as the thing not to do on a phone
     *     296px illustration    a third of the screen for an aria-hidden decoration
     *     32px inner gap        the gap item 4 names
     *
     * The three portals name their heroes .uhero / .dhero / .ahero over one shared
     * decorative layer (.phero-*), so one block re-composes all three and they cannot drift.
     *
     * The order the reader gets is the one item 6 asks for: eyebrow, heading, one short line,
     * the action, then the page. Everything below the action stays — it is the answer to "what
     * is this portal" and a front door is where that question is asked — but it starts below
     * the fold instead of pushing the action there.
     */
    .uhero,
    .dhero,
    .ahero {
        min-height: 0;
        padding-block: 18px 24px;
    }

    .uhero-inner {
        gap: 18px;
    }

    .uhero h1,
    .dhero h1,
    .ahero h1 {
        font-size: 26px;
        line-height: 1.15;
        letter-spacing: -.03em;
        margin-block: 6px 8px;
    }

    .uhero-eyebrow,
    .dhero-eyebrow,
    .ahero-eyebrow {
        margin: 0;
        font-size: 9.5px;
        letter-spacing: .18em;
    }

    html[dir="rtl"] .uhero-eyebrow,
    html[dir="rtl"] .dhero-eyebrow,
    html[dir="rtl"] .ahero-eyebrow {
        letter-spacing: normal;
        text-transform: none;
        font-size: 11.5px;
    }

    .uhero-lede,
    .dhero-lede,
    .ahero-lede {
        margin: 0 0 14px;
        font-size: 13.5px;
        line-height: 1.55;
        /* A measure, not a wall. The desktop lede is three sentences; on a phone the first
         * two are what earns the sign-in and the third can be read after it. */
        max-width: 42ch;
    }

    /* The illustration is decoration — every one of these carries aria-hidden, so nothing is
     * being taken away from anybody. On a phone it is a third of the first screen spent on a
     * picture of the product instead of the product, and removing it is what brings the
     * sign-in action above the fold. It returns in full at every width above the phone. */
    .uhero-art,
    .dhero-art,
    .ahero-art {
        display: none;
    }

    /* The waves and glows stay: they are the portal's identity, they cost no vertical space,
     * and they are what stops the compacted hero reading as a bare box. Clipped rather than
     * allowed to set the page's width — the wave artwork is deliberately wider than the
     * viewport so it can travel. */
    .phero-canvas {
        overflow: clip;
    }

    .uhero-actions,
    .dhero-actions,
    .ahero-actions {
        gap: 8px;
    }

    .uhero-btn,
    .dhero-btn,
    .ahero-btn {
        min-height: var(--m-touch);
        padding: 0 18px;
        font-size: 12.5px;
        /* The sign-in action is the step. Full width is right here and only here. */
        width: 100%;
        justify-content: center;
    }

    .uhero-note,
    .dhero-note,
    .ahero-note {
        margin: 10px 0 0;
        font-size: 11.5px;
        line-height: 1.5;
    }

    .uhero-pills {
        gap: 6px;
        margin-block: 12px;
    }

    .upill {
        font-size: 11px;
        padding: 5px 10px;
    }

    /* Three assurances, stacked, tight. Already stacked at 640px by the portal's own
     * stylesheet; this is the spacing, which was inheriting a desktop rhythm. */
    .uhero-assure {
        gap: 7px;
        margin-block-start: 14px;
        font-size: 12px;
    }

    /* The sections below the hero — how it works, where it goes, questions. Their own
     * stylesheets already collapse them to one column at 640px; what they keep is a desktop
     * vertical rhythm, and eight sections at 64px apart is 500px of nothing. */
    .usteps,
    .uroutes,
    .ucaps,
    .ufaq,
    .ucta {
        gap: 10px;
    }

    /* ============================================================== *
     * ADMINISTRATION SCROLLS. ONE SCROLLER: THE DOCUMENT.
     * ============================================================== *
     *
     * The Admin Portal could not be scrolled on a phone at all. Nothing below the fold was
     * reachable — on /admin the last two section cards were simply unreachable, and on the
     * nine authenticated screens the foot of every table was.
     *
     * The cause is not the phone layer. portal-admin/css/admin-portal-identity.css locks the
     * viewport for the whole portal:
     *
     *     html.ap-portal, body.ap-portal { height: 100dvh; overflow: hidden !important; }
     *     .ap-portal .up-shell          { height: calc(100dvh - var(--topbar-h)); overflow: hidden }
     *     .ap-portal .up-main           { height: 100%; overflow-y: auto }
     *
     * That is a deliberate and correct DESKTOP pattern — a command console whose page never
     * moves while its panes scroll independently, with the People grid's sticky table owning
     * its own scroll. It is the wrong pattern on a phone for two reasons. The document is
     * clamped to the viewport, so any content taller than the screen is unreachable rather
     * than merely below the fold — which is what happened on /admin, where the hero and eight
     * section cards come to 1120px in a 1026px box. And the pane heights are computed from the
     * top bar alone, so they know nothing about the 58px bottom bar and would run underneath
     * it even if the page did scroll.
     *
     * So on a phone the chain is unwound and the document becomes the single scroller. That is
     * also simply how a phone behaves: nested scroll regions on a touch screen are the reason
     * a list stops moving halfway and nobody can tell why.
     *
     * The !important declarations here answer !important declarations there, on the exact same
     * properties. Every one is load-bearing.
     */
    html.ap-portal,
    body.ap-portal {
        height: auto !important;
        min-height: 100svh;
        overflow: visible !important;
        overflow-x: clip !important;
    }

    .ap-portal .up-shell {
        display: block;
        height: auto !important;
        max-height: none !important;
        min-height: 0;
        overflow: visible !important;
    }

    .ap-portal .up-main {
        height: auto !important;
        max-height: none !important;
        overflow: visible !important;
        /* The clearance has to live on the element the document actually ends with, now that
         * this one is no longer its own scroll container. */
        padding-block-end: calc(var(--m-nav-clearance) + 10px);
    }

    /* The Tickets list's own flex column. On a desktop it is what hands a bounded height down
     * to the table's scroll container; on a phone the page is the scroller and the rows are
     * cards, so the column has nothing to bound and its overflow: hidden could only clip
     * something — a sticky header, a select's menu. Released with the rest of the chain. */
    .ap-portal .adm-split:has(#ticketListPane),
    .ap-portal .adm-split:has(#ticketListPane) > #ticketListPane {
        display: block;
        height: auto !important;
        max-height: none !important;
        min-height: 0;
        overflow: visible !important;
    }

    /* People & Access is the one screen whose grid was deliberately given its own scroll, and
     * its container carries an inline `overflow: hidden !important` written by people.js. On a
     * phone the identity rows are cards in the page like every other list, so the lock is
     * released here too — otherwise the directory would scroll inside a 400px window inside a
     * page that does not scroll. */
    .ap-portal .up-main:has(#usersTabContainer) {
        display: block;
        overflow-y: visible !important;
    }

    .ap-portal #tabPanel:has(#usersTabContainer),
    .ap-portal #usersTabContainer {
        display: block;
        height: auto !important;
        max-height: none !important;
        min-height: 0;
        overflow: visible !important;
    }

    /* The admin home page is a "viewport-fitted command console" on a desktop: a hero plus
     * eight section cards, sized to fit one screen exactly. On a phone it is a scrolling page,
     * so it stops trying to fit and stops hiding its own overflow. */
    .ahome {
        height: auto !important;
        max-height: none !important;
        overflow: visible !important;
        padding-block-end: 24px;
    }

    /* --- Nothing scrolls sideways ---------------------------------
     *
     * Not a global `overflow: hidden` — that hides defects rather than fixing them, which
     * item 42 forbids. This is the narrow, honest version: a horizontal scroll container
     * may exist, and the page may not. Anything that overflows without asking to is caught
     * by the wrapping rules above, and tools/verify-mobile-shell.js measures every screen at
     * six widths to prove nothing slipped through.
     */
    html, body {
        overflow-x: clip;
        max-width: 100%;
    }

    img, svg, video, canvas, table, pre {
        max-width: 100%;
    }

    /* ------------------------------------------------------------------ *
     * Attachment controls, measured rather than assumed
     *
     * Measured in a browser at 360, 390, 430 and 768 while closing the bug-report and reply
     * attachment work. Page overflow was zero at every width and the modal sat inside the
     * viewport, but three controls in those two flows were too small to hit reliably and one
     * line of text could push past the edge:
     *
     *   * the bug-report modal's own buttons — choose a screenshot, send, cancel — measured
     *     22px tall. They are legacy modal markup styled by ecvalla-identity.css rather than
     *     the button system the reply form uses, whose controls already measure 36px.
     *   * .remove-image-btn, the only way to take a screenshot back off a report before
     *     sending it, measured 23x24. A remove action that cannot be hit is the one control
     *     on this screen that must never be fiddly.
     *   * .up-reply-files sets no wrapping rule, so a long Arabic filename — one unbroken
     *     token, which is exactly what these are — ran 3px past the viewport at 360.
     *
     * 44px is the touch target this project's phone layer uses elsewhere; 32px is the floor
     * its own responsive check measures against. These are raised to 44 for the buttons and
     * 32 for the small round remove control, which keeps it visually a chip.
     * ------------------------------------------------------------------ */

    /* The touch targets themselves are in their own block at the end of this file: they are
     * keyed on the POINTER rather than the width, because a 22px button is no easier to hit on
     * a tablet at 768 than on a phone at 360. */

    /* A filename is one long token with no spaces to break at, so it needs permission to
     * break anywhere. `anywhere` rather than `break-all` so a short name still wraps at a
     * sensible point. */
    .up-reply-files {
        overflow-wrap: anywhere;
        word-break: break-word;
        min-width: 0;
    }

    /* The row the filename line sits in must be allowed to wrap, or the line has nowhere to
     * go however it breaks. */
    .up-reply-actions {
        flex-wrap: wrap;
    }
}

/* ------------------------------------------------------------------ *
 * Portal palettes for the bottom bar
 *
 * The bar borrows each portal's own accent so the three read as one system in three
 * liveries, rather than as one control pasted into three products. Set here, against the
 * body class each portal already carries, because the values are a fact about the bar and
 * belong next to it — not scattered into three identity stylesheets that have drifted
 * before.
 * ------------------------------------------------------------------ */

body.up-portal-user {
    --m-nav-bg: var(--sys-surface-raised, #fff);
    --m-nav-ink: #6c7174;
    --m-nav-ink-active: var(--ecv-ink, #17191b);
    --m-nav-tile: rgba(23, 25, 27, .07);
    --m-nav-marker: var(--ecv-ink, #17191b);
    /* Light theme: the plate is ink, the mark on it is paper. */
    --m-nav-action-bg: var(--m-plate-ink);
    --m-nav-action-ink: var(--m-plate-paper);
}

body.dp-portal {
    --m-nav-bg: var(--sys-surface-raised, #fff);
    --m-nav-ink: #6c7174;
    --m-nav-ink-active: var(--ecv-ink, #17191b);
    --m-nav-tile: rgba(23, 25, 27, .07);
    --m-nav-marker: var(--ecv-ink, #17191b);
}


body.ap-portal {
    --m-nav-bg: var(--sys-surface-raised, #fff);
    --m-nav-ink: #6c7174;
    --m-nav-ink-active: var(--ecv-ink, #17191b);
    --m-nav-tile: rgba(23, 25, 27, .07);
    --m-nav-marker: var(--ecv-ink, #17191b);
}

/* Dark theme. The bar is a raised surface in both themes, so it takes the theme's raised
 * token rather than a second hard-coded colour — the hairline is what separates it from the
 * canvas, and on ink it has to be a light one. */
[data-theme="dark"] body,
[data-theme="dark"] {
    --m-line: rgba(255, 255, 255, .1);
    --m-line-strong: rgba(255, 255, 255, .26);
}

[data-theme="dark"] body.up-portal-user,
[data-theme="dark"] body.dp-portal,

[data-theme="dark"] body.ap-portal {
    --m-nav-ink: rgba(255, 255, 255, .6);
    --m-nav-ink-active: #fff;
    --m-nav-tile: rgba(255, 255, 255, .12);
    --m-nav-marker: #fff;
    /* Dark theme: the plate is paper, the mark on it is ink. The pair is swapped, not
     * re-aliased — see the note on --m-plate-* above for why --ecv-ink cannot be used here. */
    --m-nav-action-bg: var(--m-plate-paper);
    --m-nav-action-ink: var(--m-plate-ink);
}

/* ------------------------------------------------------------------ *
 * Continuity across documents
 *
 * The bar is named for the view transition exactly as the rail and the top bar are, and
 * exactly as deliberately NOT animated: naming it without an animation tells the browser to
 * carry the same box across the navigation untouched. A fade on the bottom bar would be the
 * flash this whole mechanism removes.
 * ------------------------------------------------------------------ */

.pmnav { view-transition-name: ecvalla-mobilenav; }

::view-transition-group(ecvalla-mobilenav) { animation: none; }

::view-transition-old(ecvalla-mobilenav),
::view-transition-new(ecvalla-mobilenav) {
    animation: none;
    mix-blend-mode: normal;
}

/* ------------------------------------------------------------------ *
 * The legacy admin navbar, on a phone
 *
 * `ticket_admin_navbar.html` predates the portal shell and is styled by ecvalla-identity.css
 * rather than by this layer, so nothing here was narrowing it. Measured at 360 and 390, the user
 * profile block ran to 428px — the name, the role and the dropdown arrow all past the edge. The
 * page did not scroll, because an ancestor clips it, which is worse: the control is simply not
 * reachable and nothing indicates that it is there.
 *
 * This screen matters more than its age suggests. It is the only one that mounts the bug-report
 * modal, so it is where somebody goes to report that something is broken — quite possibly from a
 * phone, and quite possibly about this.
 *
 * The name and role are dropped rather than wrapped: the avatar and the arrow are what make the
 * control usable, and two lines of text in a fixed-height bar would push the bar over its own
 * geometry. Nothing is hidden that is not also shown inside the dropdown itself.
 * ------------------------------------------------------------------ */
@media (max-width: 430px) {
    .ticket-admin-navbar {
        gap: 6px;
        padding-inline: 8px;
    }

    .ticket-admin-navbar .app-name,
    .ticket-admin-navbar .user-profile .user-name,
    .ticket-admin-navbar .user-profile .user-role {
        display: none;
    }

    .ticket-admin-navbar .user-profile {
        gap: 4px;
        padding-inline: 4px;
        min-width: 0;
    }

    /* Whatever the bar holds, it stays inside the viewport. */
    .ticket-admin-navbar .navbar-left,
    .ticket-admin-navbar .navbar-right {
        min-width: 0;
        flex-wrap: wrap;
    }
}

/* ------------------------------------------------------------------ *
 * Touch targets in the attachment flows
 *
 * Keyed on `pointer: coarse` as well as width. The phone layer above stops at 767.98px by
 * design, and measurement at 768 showed the bug-report modal's own buttons still at 22px there
 * — a tablet is a touch device with a desktop-width viewport, which is exactly the case a
 * width-only rule misses.
 *
 * Mouse and trackpad rendering is untouched: no rule here applies to a fine pointer.
 *
 * Measured, not assumed. Before: choose-a-screenshot, send and cancel 22px tall, the screenshot
 * remove button 23x24 — and removing a screenshot before sending is the one action on this
 * screen that must not be fiddly. After: 44px on the buttons and 32x32 on the remove chip, at
 * 360, 390, 430 and 768.
 * ------------------------------------------------------------------ */
@media (pointer: coarse), (max-width: 767.98px) {
    #userSelectScreenshotBtn,
    #userSendReportBtn,
    #userBugReportCancelBtn {
        min-height: 44px;
        padding-block: 10px;
    }

    /* Dismissing a dialog is the action somebody takes when they are already frustrated. */
    .up-modal-close {
        min-width: 44px;
        min-height: 44px;
    }

    #userBugTitle {
        min-height: 44px;
    }

    .remove-image-btn {
        min-width: 32px;
        min-height: 32px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }
}

/* ---------------------------------------------------------------------------
 * The queue card, when a cell holds two pills
 * ---------------------------------------------------------------------------
 *
 * .queue-row is `grid-template-columns: repeat(2, minmax(0, 1fr))`. minmax(0, …) lets a
 * column shrink below its content, which is what stops a long subject blowing the card open
 * — and it is also what made this go wrong: the Status cell gained a classification pill
 * beside the status pill, both of which are `white-space: nowrap`, so that cell's min-content
 * width became the width of two pills side by side. The grid gave it the room and squeezed
 * the other column to about forty pixels, at which point every caption wrapped one character
 * per line: "DEPA / RTME / NT".
 *
 * The cell becomes a wrapping flex row, so two pills stack instead of demanding one line, and
 * a floor on the column keeps a caption readable even if a future cell does the same thing.
 */
@media (max-width: 767.98px) {
    /* The whole chain, restated. A <tr> can only be a grid container if its ancestors have
     * stopped being table boxes: leave `table` or `tbody` as table display and the engine
     * re-creates the anonymous table boxes around the row, and the grid items get
     * shrink-to-fit table-cell widths instead of their column. That is why width:100% on the
     * cells was still measuring 18px. */
    table.ops-ticket-table,
    table.ops-ticket-table tbody {
        display: block;
        width: 100%;
        table-layout: auto;
        white-space: normal;
    }

    table.ops-ticket-table .queue-row {
        /* display:grid restated here, and it is the actual fix.
         *
         * portal-chrome.css turns this row into a card at 850px. department-portal-identity.css
         * then sets `.ops-ticket-table .queue-row { display: table-row }` at the same
         * specificity, outside any media query, and loads AFTER it — so the card layout has
         * never once applied on this portal at any width. The queue has always been a table
         * squeezed into 390px; adding a second pill to the Status cell only made the damage
         * visible, by giving one column enough min-content width to starve the other.
         *
         * `table.` in front raises this above both, and portal-mobile.css is the last
         * stylesheet the page loads. */
        display: grid;
        grid-template-columns: repeat(2, minmax(7.5rem, 1fr));
        gap: 9px;
        margin: 8px;
        padding: 13px;
        border: 1px solid var(--ops-line);
        border-radius: 9px;
        text-align: start;
    }

    /* `.queue-row > td` on purpose. department-portal-identity.css gives every column a
     * percentage width via `.ops-ticket-table td:nth-child(n)`, which is specificity (0,2,1)
     * — a plain `table.ops-ticket-table td` is (0,1,1) and loses, which is why width:100%
     * was being ignored and each cell measured 10% of its own grid column. */
    table.ops-ticket-table .queue-row > td {
        display: flex;
        flex-wrap: wrap;
        align-items: baseline;
        gap: 4px;
        /* A <td> promoted to a grid item still carries the table layout's shrink-to-fit
         * width, so each cell sat at 13-40px inside a 175px column and its caption wrapped one
         * character per line. width:100% is what makes the cell actually occupy the column it
         * was given; min-width:0 keeps a long value from pushing the column back open. */
        width: 100%;
        min-width: 0;
        justify-self: stretch;
    }

    /* The caption is written with ::before and must not be pulled onto the pills' line. */
    table.ops-ticket-table .queue-row > td::before {
        flex: 0 0 100%;
    }

    table.ops-ticket-table .queue-row > td .pill {
        max-width: 100%;
        overflow: hidden;
        text-overflow: ellipsis;
    }

    /* The reference and the subject still span the card. */
    table.ops-ticket-table .queue-row > td:nth-child(1),
    table.ops-ticket-table .queue-row > td:nth-child(2) {
        display: block;
        width: 100%;
    }
}
