/* SkillBridge - Main Styles
 *
 * Layout, base element styles, responsive rules, and utilities.
 * The :root design tokens this sheet reads live in tokens.css, loaded
 * just before this file (see the app + public base templates).
 */

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

html {
    font-size: var(--font-size-base);
    scrollbar-gutter: stable;
}

body {
    margin: 0;
    font-family: var(--font-family);
    color: var(--color-text);
    background-color: var(--color-page-bg);
    padding-top: var(--safe-top);
}

.global-app-layout {
    min-height: 100vh;
}

.global-app-layout.global-no-sidebar .global-app-main {
    margin-left: 0;
}

.global-app-main {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
    /* clip, NOT hidden: overflow-x hidden makes this element the nearest
       scroll container for descendants, and since .global-app-main itself never
       scrolls (the body does), the sticky .global-app-header silently stops
       sticking and rides off-screen. clip cuts horizontal overflow without
       creating a scroll container, so the header stays frozen. */
    overflow-x: clip;
    margin-left: var(--sidebar-width);
    /* transition margin (not just margin-left) to animate the sidebar collapse.
       The push-drawer resize now animates on .global-app-content, not here — see
       .global-app-content below and the drawer.css PUSH block. */
    transition: margin var(--transition-normal);
}

.global-app-content {
    flex: 1;
    /* Uniform --page-gutter on all four sides: the side padding matches the gap
       under the header instead of being inflated by a centered max-width cap
       that wasted L/R space on wide screens. */
    padding: var(--page-gutter);
    width: 100%;
    /* Animate the push-drawer resize (margin-right + max-width) the same way the
       old .global-app-main margin push animated — see drawer.css PUSH block. */
    transition: margin var(--transition-normal), max-width var(--transition-normal);
}

/* Viewport-fit pages. A page opts in by putting .global-page-fill on its main
   body container (workers table, opportunities table): the layout column locks
   to the viewport, the header card keeps its natural height, and the fill
   container takes the rest, scrolling internally — HubSpot's table treatment,
   where the container always reaches the bottom of the screen (Nick, 09-03).
   Generalized from the opportunities-table rules milestones.css shipped first.

   What the lock does not hold becomes page scroll, and a shell one screen tall
   scrolls its own sticky header off the top (Nick, 09-05). clip, not hidden —
   same reason as the overflow-x line above.

   dvh follows the phone's toolbar as it comes and goes; 100vh is the screen
   with it retracted, so it overflows whenever the toolbar shows. vh stays on
   top as the fallback for browsers without dvh, as in stream.css. Minus
   --safe-top, which body already pads.

   Both min-heights above have to come off, or none of that height work runs:
   used height is the LARGER of height and min-height, so a leftover
   min-height:100vh silently wins and the page scrolls by the notch again
   (measured: --safe-top 40px gave the document back 40px of scroll). The
   min-height:0 chain below earns its keep twice — it is also what shrinks the
   fill container enough to keep the infinite-scroll sentinel inside the clip,
   and the sentinel going out of view fails silently: the list just stops
   loading page 2.

   Browsers without :has() skip the height lock and degrade to today's page
   scroll — everything still readable, nothing clipped. */
.global-app-layout:has(.global-page-fill) {
    min-height: 0;
}

.global-app-main:has(.global-page-fill) {
    min-height: 0;
    height: calc(100vh - var(--safe-top));
    height: calc(100dvh - var(--safe-top));
    overflow: clip;
}

.global-app-main:has(.global-page-fill) .global-app-content {
    display: flex;
    flex-direction: column;
    min-height: 0;
}

/* Everything else on the page — the header card, view-control drawers, the
   scroll sentinel — keeps its natural height and never gets squeezed; only
   .global-page-fill below grows. */
.global-app-main:has(.global-page-fill) .global-app-content > :not(.global-page-fill) {
    flex: 0 0 auto;
}

.global-page-fill {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
}

/* Inside a fill container the grid drops its 70vh fallback cap and takes the
   space flex handed it. */
.global-page-fill .global-grid-wrap {
    flex: 1 1 auto;
    min-height: 0;
    max-height: none;
}

.global-app-header {
    display: flex;
    align-items: center;
    gap: var(--spacing-smd);
    /* Vertical padding is the header's gutter: 6px top/bottom around the 28px
       controls = the 40px --header-height (border-box). Keep these in lockstep —
       see the --header-height comment for why. Horizontal padding is independent. */
    padding: var(--spacing-xsm) var(--spacing-md);
    background: var(--header-bg);
    border-bottom: 1px solid var(--sidebar-border);
    height: var(--header-height);
    position: sticky;
    top: var(--safe-top);
    z-index: 1050;
}

h1, .h1 {
    font-family: var(--font-family-heading);
    font-weight: var(--font-weight-heading);
    font-style: var(--font-style-heading);
    text-transform: var(--text-transform-heading);
    letter-spacing: var(--letter-spacing-heading);
    line-height: var(--line-height-heading);
    font-size: var(--font-size-h1);
}

h2, h3, .h2, .h3 {
    font-family: var(--font-family-subheading);
    font-weight: var(--font-weight-subheading);
    font-style: var(--font-style-subheading);
    text-transform: var(--text-transform-subheading);
    letter-spacing: var(--letter-spacing-subheading);
    line-height: var(--line-height-subheading);
}

h2, .h2 {
    font-size: var(--font-size-h2);
}

h3, .h3 {
    font-size: var(--font-size-h3);
}

/* h4-h6 — body cascade for everything but size. Sizes are pinned on every
   heading level (here and above) so Bootstrap's reboot scale never reaches the
   render; below h4 the scale rejoins the body ladder. */
h4, h5, h6, .h4, .h5, .h6 {
    font-family: var(--font-family);
    font-weight: var(--font-weight-body);
    font-style: var(--font-style-body);
    letter-spacing: var(--letter-spacing-body);
    line-height: var(--line-height-body);
}

h4, .h4 {
    font-size: var(--font-size-h4);
}

h5, .h5 {
    font-size: var(--font-size-lg);
}

h6, .h6 {
    font-size: var(--font-size-md);
}

.global-text-accent {
    font-family: var(--font-accent, inherit);
    font-weight: var(--font-weight-accent);
    font-style: var(--font-style-accent);
    text-transform: var(--text-transform-accent);
    letter-spacing: var(--letter-spacing-accent);
    line-height: var(--line-height-accent);
}

body {
    /* 14px standard body. Set here, not on html — html stays at
       --font-size-base (16px) so the rem scale keeps its anchor. */
    font-size: var(--font-size-md);
    font-weight: var(--font-weight-body);
    font-style: var(--font-style-body);
    letter-spacing: var(--letter-spacing-body);
    line-height: var(--line-height-body);
}

.global-app-header-actions {
    display: flex;
    align-items: center;
    gap: var(--spacing-sm);
    position: relative;
    z-index: 1;
    margin-left: auto;
    /* Never compress the right-hand cluster. The header is a single fixed-height
       row (no wrap), and the payments dashboard packs filter pills (fixed width)
       next to the tab nav in here. If this box is allowed to shrink, it squeezes
       the filter wrapper while its pills stay rigid — so the pills spill out and
       overlap the tabs. Holding it at natural width pushes any deficit onto the
       left search box (.global-search-input-wrapper: flex 0 1 320px, min 160px), which
       shrinks gracefully instead. */
    flex-shrink: 0;
}

/* Header button base — 36px tall, transparent, border, neutral-darken hover.
   Text buttons stay pill-shaped (auto width via padding); the icon-only variant
   below squares this to 36×36. */
.global-header-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    height: 36px;
    min-width: 36px;
    padding: 0 var(--spacing-xsm);
    border-radius: var(--radius-button);
    color: var(--color-text-light);
    background: transparent;
    border: 1px solid var(--color-border);
    cursor: pointer;
    flex-shrink: 0;
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-button);
    line-height: 1;
    text-decoration: none;
    gap: var(--spacing-xs);
    transition: background var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast);
}

.global-header-btn:hover {
    background: var(--color-surface-hover);
}

.global-header-btn i {
    font-size: var(--font-size-base);
}

/* Icon-only variant — 36×36 circle, matched to the
   36px text pill so icon and text buttons align in the header row. */
.global-header-btn-icon {
    padding: 0;
    width: 36px;
    height: 36px;
    border-radius: 50%;
}

/* The word under a bar icon (Menu / Search / Notifications / Support). Phone
   only: the phone block at the end of this file shows it. Off the phone the
   bar stays icons and tooltips. */
.global-header-btn__label {
    display: none;
}

/* The tap-away scrim behind the phone Search sheet. Phone only, same block. */
.global-topbar-search__scrim {
    display: none;
}

/* Circle alias — the icon-button family is a 36px circle. Kept as an alias so the
   templates that still carry .global-header-btn-circle need no markup change; .global-header-btn-icon
   already gives the circle, this just reasserts it for the few standalone uses. */
.global-header-btn-circle {
    width: 36px;
    height: 36px;
    border-radius: 50%;
}

.global-header-btn-active {
    background: var(--color-action);
    color: var(--color-neutral-ink-fg);
    border-color: var(--color-action);
}

/* Pressed AND under the pointer. A click leaves the mouse on the button, and the
   neutral .global-header-btn:hover wash above outranked the pressed fill (a pseudo-class
   beats a bare class): every dashboard's Filter button opened its chip row and then
   sat there white text on pale grey until the mouse moved. Pressed keeps its ink
   and darkens the way a primary button does. */
.global-header-btn-active:hover {
    background: var(--color-action-hover);
    color: var(--color-neutral-ink-fg);
    border-color: var(--color-action-hover);
}

/* Destructive variant: only the hover fill leaves the icon-button family, so a
   delete keeps its cue without becoming a different button. */
.global-header-btn-danger:hover,
.global-header-btn-danger:focus-visible {
    background: var(--color-error);
    color: #fff;
    border-color: var(--color-error);
}

/* Settings button — alias for backwards compat. Same 36×36 circle and
   neutral-darken hover as the shared icon-button family. */
.global-header-settings-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    border-radius: 50%;
    color: var(--color-text-light);
    background: transparent;
    border: 1px solid var(--color-border);
    cursor: pointer;
    flex-shrink: 0;
    transition: background var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast);
}

.global-header-settings-btn:hover {
    background: var(--color-surface-hover);
}

.global-header-settings-btn i {
    font-size: var(--font-size-base);
}

/* ── Standardized header controls ──────────────────────────────────────────
   The header shares the sidebar (brand) surface, so per-org tints made every
   tenant's top bar look different — and washed out on light brands. Instead,
   EVERY control on the header (icon buttons, Bootstrap buttons, selects, the
   sidebar toggle, and the bespoke per-dashboard pickers below) renders from
   ONE fixed palette: a translucent white veil over the brand bar, white text,
   hairline white border, brightening on hover (mirrors the sidebar org-toggle).
   Org-independent by design — the --header-control-* tokens in tokens.css carry
   the exact values and the "no saturate()" note. New header widgets only need
   to opt into these selectors; they do NOT need their own header overrides.

   These lists are short because the bar is a closed set: it renders exactly what
   app_header.html includes — .global-sidebar-toggle, topbar_search, and the bell. So
   adding a class below does nothing unless the markup goes in that include too.
   #5388 is what happens when a name outlives its markup. */

/* One height for every control on the bar. The classes above are 36px on a
   page, but the bar is only 40px tall with 6px gutters, so a 36px control fills
   it edge to edge — the bell's unread badge was being clipped off the top of
   the screen. --filter-ctrl-h is the bar's own control height (28px, set on
   .global-app-header in filter-bar.css) and the number the 40px bar was derived from
   (tokens.css), so read it here. Scoped to .global-app-header, which leaves the same
   classes at 36px on role tiles and the opportunity detail page. The 28px
   fallback keeps the ref whole on pages that link main.css without
   filter-bar.css, like the standalone error pages. */
.global-app-header .global-header-btn,
.global-app-header .global-header-btn-icon,
.global-app-header .global-sidebar-toggle {
    height: var(--filter-ctrl-h, 28px);
    min-height: var(--filter-ctrl-h, 28px);
    min-width: var(--filter-ctrl-h, 28px);
}

/* The icon-only members are square; .global-header-btn keeps its automatic width so
   text buttons still grow with their label. */
.global-app-header .global-header-btn-icon,
.global-app-header .global-sidebar-toggle {
    width: var(--filter-ctrl-h, 28px);
}

/* The search-reveal icon exists for the compact shell only; the 1023px block
   below turns it on. On a wide bar the field itself is always in view. */
.global-topbar-search__reveal {
    display: none;
}

.global-app-header .global-header-btn i,
.global-app-header .global-sidebar-toggle i {
    font-size: var(--font-size-md);
}

.global-app-header .global-header-btn,
.global-app-header .global-sidebar-toggle {
    background: var(--header-control-bg);
    color: var(--header-control-fg);
    border-color: var(--header-control-border);
}

.global-app-header .global-header-btn:hover,
.global-app-header .global-sidebar-toggle:hover,
.global-app-header .global-header-btn:focus-visible,
.global-app-header .global-sidebar-toggle:focus-visible {
    background: var(--header-control-hover-bg);
    color: var(--header-control-hover-fg);
    border-color: var(--header-control-hover-bg);
}

/* ── Icon glyph centring ─────────────────────────────────────────────────────
   One cause, one cure, one place. Bootstrap Icons ships
   `.bi::before { display: inline-block; line-height: 1; vertical-align: -.125em }`
   — correct for an icon sitting in a line of text, wrong for an icon that IS a
   control's whole content. Flex-centring the button centres the <i>'s BOX
   (measured: the box offset is 0.0px in every family below), but the glyph ink
   still rides ~1-2px high inside that box, because the ::before is an inline
   box hanging off a baseline nothing else in the button shares.

   Making the <i> a flex container hands the ::before to flex centring too, and
   that is the only thing that moves the ink. Measured at 8x zoom against a
   flex-centred reference: the families below started anywhere from +0.5px to
   -2.0px off centre and all of them land at 0.0px (worst residual 0.5px, which
   is half a CSS pixel of rounding in an even-sized box).

   Three things that look like the fix and are not. `vertical-align: 0`,
   `vertical-align: baseline` and `line-height: 1` on the ::before each move the
   ink EXACTLY zero — the app carried the `vertical-align: 0` version on
   .global-icon-btn (230 call sites) for months and it never did anything.

   Two rules decide whether a family belongs here, and both are checkable:

   1. The control is itself a flex/grid container WITH an explicit width and
      height. Then the change is box-neutral twice over: the <i> is already
      blockified to `display: block` by its flex parent, so block -> flex is
      layout-identical, and the button's size is stated rather than derived. On
      a borderless auto-height control, `display: flex` collapses the <i>'s own
      inline strut and the button loses height — measured across the whole
      audit, applying this to every icon button in the app would have resized
      19 of them, by up to 8px. Those are also the borderless ones: no painted
      box for the glyph to sit off-centre in, so nothing there to fix. And
      `inline-flex` is not the way around it — over the same 65 specimens it
      measures worse than flex on both counts (mean |dy| 0.64 vs 0.20, worst
      4.19 vs 2.00) while still moving 14 boxes.

   2. The glyph is the control's ENTIRE content. A button that carries an icon
      NEXT TO text wants Bootstrap's -.125em, which optically seats the glyph on
      the text's baseline; centring it independently pulls it off that text.
      This is why .global-header-btn is deliberately absent (it has a "Top up" usage),
      and why .global-icon-btn is safe despite its one icon+"Edit" call site —
      that selector is scoped to .bi, and the flex parent had already blockified
      it. .assignments-role-row__chip-cancel qualifies because confirm-action.js
      swaps the whole child list out via textContent, taking the <i> with it.

   A new icon-only control with a fixed box belongs on this list; a rule that
   quietly puts one back into a stylesheet of its own is what
   tests/interface/test_icon_glyph_centring.py exists to catch. */
.global-icon-btn .bi,
.global-header-btn-icon i,
.global-header-btn-circle i,
.global-header-settings-btn i,
.global-drawer-close i,
.global-drawer-head__back i,
.global-btn-icon-copy i,
.global-btn-icon-placeholder i,
.global-sidebar-toggle i,
.global-sidebar-brand-logo-placeholder i,
.calendar-popover__close i,
.assignments-role-row__card-arrow i,
.assignments-role-row__chip-cancel i,
.assignments-terms__chip i,
.opportunities-detail__role-icon-btn i,
.opportunities-meeting__action i,
.opportunities-calnav__btn i,
.opportunities-card__milestone-btn i,
.opportunities-card__role-tick i,
.global-toolbar__chip--round i,
.global-filter-builder-row__remove i,
.notifications-drawer__gear i,
.calendar-exc-list__remove i,
.payments-next-payout-banner__icon i,
.workers-photo-hero__edit i,
.webpage-profile-picture__remove-btn i,
.settings-brand-section__info-toggle i,
.calendar-bucket-panel__delete-btn-cta i {
    display: flex;
    align-items: center;
    justify-content: center;
}

/* Three of the families above are used on <a> as well as <button>. Bootstrap's
   Reboot underlines anchors and the app has never reset that globally — the
   underline simply never PAINTED, because the <i> its flex parent had
   blockified swallowed the propagation. A flex <i> draws it: measured at 8x,
   the ink box on an anchor grows from 12x12 (glyph only) to a full 16x16 as
   soon as the rule above applies. The reset has to sit on the anchor itself; a
   propagated text decoration cannot be turned off from a descendant, so
   putting `text-decoration: none` on the <i> would do nothing.
   .opportunities-calnav__btn is the fourth anchor family and already resets its own. */
a.global-icon-btn,
a.global-btn-icon-copy,
a.notifications-drawer__gear {
    text-decoration: none;
}

/* ── Stateful toggles (dim = off, full = on) ─────────────────────────────────
   A .global-header-btn that carries an ON/OFF state — the calendar's Events and Tasks
   filters, the skills-detail edit toggle — opts in with .global-header-btn-toggle.
   When OFF it RECEDES — faded but still clickable; on hover it returns to full
   so it reads as "you can turn me on"; when ON it fills via .global-header-btn-active.
   Plain ACTION buttons (Today, Save, page actions) do NOT carry this class, so
   they keep full strength at rest. Opacity only, so it composes with whatever
   the base rules already set.

   Deliberately NOT scoped to .global-app-header. It was, and that is exactly why the
   dim went dark: blanking the top bar moved every one of these toggles into a
   page toolbar, and the scoped selector stopped matching anything. #4474 gave
   the calendar pair the page-body slot they teleport into; #4479 took the
   skills toggle off {% block header_actions %}. Nothing carries this class
   inside the bar today.

   Two names mean ON, because the toggles wear two button families: .global-header-btn
   fills via .global-header-btn-active, and a .global-icon-btn fills via .is-active
   (components/icon-button.css). A toggle that carried only one of them in this
   :not() would stay faded while it was on.

   Both values below are read off the live page by
   tests/browser/test_calendar_filter_dim_proof.py — the CSS gates check the
   sheet, so they stay green whatever these actually render as. Run it before
   retuning or deleting either. */
.global-header-btn-toggle:not(.global-header-btn-active):not(.is-active) {
    opacity: 0.55;
}
.global-header-btn-toggle:not(.global-header-btn-active):not(.is-active):hover,
.global-header-btn-toggle:not(.global-header-btn-active):not(.is-active):focus-visible {
    opacity: 1;
}

/* Route every real Bootstrap button's weight through the fixed --font-weight-button
   token. Bootstrap already defaults --bs-btn-font-weight to 400, so this is zero
   visual change — its purpose is to make real buttons and the bespoke button-like
   controls (which also read --font-weight-button) share ONE fixed source that no
   tenant brand-config can pull apart.

   The radius line rides the same rule: this sheet loads after Bootstrap, so it
   also beats .btn-sm/.btn-lg's own --bs-btn-border-radius and every button
   size wears the one shared shape. */
.btn {
    --bs-btn-font-weight: var(--font-weight-button);
    --bs-btn-border-radius: var(--radius-button);
}

/* A locked button is never `disabled`: it keeps taking the tap so it can say
   why in a toast. It only looks quiet. */
.btn[aria-disabled="true"] {
    opacity: 0.6;
    cursor: default;
    pointer-events: auto;
}

/* Same for a switch: the primary contact's correspondence toggle is locked on
   and explains itself in a toast, so it keeps the quiet look Bootstrap gave
   :disabled without giving up the tap. */
.form-check-input[aria-disabled="true"] {
    opacity: 0.6;
    cursor: default;
    pointer-events: auto;
}

@media (max-width: 1023px) {
    .global-app-main {
        margin-left: 0;
    }

    .global-sidebar {
        transform: translateX(-100%);
        width: 100vw;
        max-width: 320px;
    }

    .global-sidebar.is-open {
        transform: translateX(0);
    }

    .global-sidebar.is-open + .global-sidebar-scrim {
        opacity: 1;
        pointer-events: auto;
    }

    .global-sidebar-toggle {
        display: inline-flex;
    }

    .global-sidebar-close {
        display: inline-flex;
    }

    /* Compact nav items on mobile to prevent scrollbar */
    .global-sidebar-nav a {
        padding: var(--spacing-xsm) 0;
    }

    .global-sidebar-nav a i,
    .global-sidebar-nav a .bi {
        font-size: var(--font-size-base);
        width: 48px;
    }

    .global-sidebar-icon-rail {
        width: 48px;
    }

    .global-sidebar-profile {
        padding: var(--spacing-xs) 0;
    }

    /* The bar is 56px here (--header-height-touch on :root, tokens.css) and
       every control in it grows to the 44px touch target. Wins over the
       28px --filter-ctrl-h rule above by source order. */
    .global-app-header .global-header-btn-icon,
    .global-app-header .global-sidebar-toggle {
        width: var(--control-h-touch);
        height: var(--control-h-touch);
        min-width: var(--control-h-touch);
        min-height: var(--control-h-touch);
    }

    .global-app-header .global-header-btn {
        height: var(--control-h-touch);
        min-height: var(--control-h-touch);
    }

    /* Search collapses to one icon; the field drops in as a full-width row
       under the bar when tapped (topbar_search.html toggles is-revealed).
       The header is position: sticky, so top: 100% hangs off its bottom edge.
       Qualified with the header so it beats topbar-search.css's 600px rule. */
    .global-app-header .global-topbar-search {
        flex: 0 0 auto;
        min-width: 0;
        max-width: none;
    }

    .global-topbar-search__reveal {
        display: inline-flex;
    }

    .global-topbar-search__form {
        display: none;
        position: absolute;
        top: 100%;
        inset-inline: 0;
        padding: var(--spacing-sm) var(--page-gutter);
        background: var(--header-bg);
    }

    .global-topbar-search.is-revealed .global-topbar-search__form {
        display: flex;
    }

    /* 16px stops iOS zooming the page on focus; 44px is the touch target. */
    .global-topbar-search__form .global-filter-control {
        font-size: var(--font-size-base);
        min-height: var(--control-h-touch);
    }
}

@media (min-width: 1024px) {
    .global-sidebar-toggle {
        display: none;
    }

    .global-sidebar-scrim {
        display: none;
    }

    .global-sidebar-collapse-btn {
        display: flex;
    }

    /* Show header on desktop for settings gear and page title */
    .global-app-header {
        display: flex;
        padding-left: var(--spacing-sm);
    }

    /* Inverted corner where the sidebar meets the header: a chrome-colored
       square hanging off the header's bottom-left with a quarter-circle cut,
       so the L-shaped top strip curves into the content instead of meeting
       it at a hard right angle. Rides the sticky header (and the collapsing
       sidebar edge, since the header starts where the sidebar ends), staying
       pinned while content scrolls beneath. Desktop-only — without the
       sidebar there is no inner corner to soften. */
    .global-app-header::before {
        content: "";
        position: absolute;
        /* Pin to the header's VISUAL bottom (Y = --header-height). Subtlety:
           for an absolutely-positioned child, `top:100%` resolves against the
           header's PADDING box, which is 1px shorter than its border-box
           because of the 1px `border-bottom`. So `top:100%` sits at Y=39 on a
           40px header — exactly over that border line. The box then runs down
           (R+1)px to Y = 39 + R + 1 = 40 + R, putting the gradient center at
           the true bottom-right corner (256+R, 40+R). With the circle there,
           the arc's apex lands at Y=40 — flush with the flat header bottom to
           the right, no 1px step. The box's top 1px (Y=39→40) overlays the
           faint-white border-bottom (--sidebar-border) with the wine fill so
           it never paints a white line across the top of the curve.
           (The prior `calc(100% - 1px)` mistook the padding box for the
           border-box and centered the circle 1px high, leaving a visible lip
           where the curve met the flat edge.) */
        top: 100%;
        left: 0;
        width: var(--radius-lg);
        height: calc(var(--radius-lg) + 1px);
        /* The fill is a SOLID, opaque brand color; the quarter-circle is
           carved out with a MASK, not painted with a second gradient color.
           Why a mask: the old fill baked one specific "behind" color into the
           arc's inner stop, so it only matched pages whose content area WAS
           that color — any page with a different background (the org home
           color, the black stream shell, anything future) flashed the stale
           inner color as a white/mismatched square in the corner. A mask
           touches only the alpha channel: the disc is genuinely cut away so
           WHATEVER sits behind this corner shows through unchanged, on every
           page, with zero per-page setup. And because the visible pixels are
           always pure --header-bg (never a color interpolated toward
           `transparent`), the anti-aliased arc has no gray/black halo — the
           exact "white line on the curve" the color-gradient version risked.
           Track --header-bg (not --sidebar-bg): this pseudo hangs off the
           header's bottom edge, and stream.css overrides --header-bg on its
           own, so the corner must follow the header it's attached to.

           Two background layers: on TOP, a 1px --sidebar-border hairline ring
           at exactly the cut radius; below it, the solid --header-bg. The ring
           traces the concave arc, so the divider hairline is continuous around
           the corner — its top meets the header's bottom border, its bottom
           meets the sidebar's vertical ::after hairline (which starts at the
           arc's lower tangent, header-height + radius). Without it the border
           just stopped at the curve and the rounded corner read as unfinished.
           The ring is an ARC (radial), not a straight line through the curve —
           that straight-line version is the ugly "white line on the left of
           the radius" the sidebar::after offset was added to avoid. */
        background:
            radial-gradient(
                circle at 100% 100%,
                transparent var(--radius-lg),
                var(--sidebar-border) var(--radius-lg),
                var(--sidebar-border) calc(var(--radius-lg) + 1px),
                transparent calc(var(--radius-lg) + 1px)
            ),
            var(--header-bg);
        -webkit-mask: radial-gradient(
            circle at 100% 100%,
            transparent var(--radius-lg),
            #000 calc(var(--radius-lg) + 0.5px)
        );
        mask: radial-gradient(
            circle at 100% 100%,
            transparent var(--radius-lg),
            #000 calc(var(--radius-lg) + 0.5px)
        );
        pointer-events: none;
    }

    /* Start the sidebar's right-edge hairline BELOW the inverted corner. The
       base rule (sidebar.css) starts it at the header bottom (Y = header-bottom)
       so the top strip reads seamless on mobile — but on desktop the corner
       fillet above curves down the sidebar edge for --radius-lg px, and a
       straight hairline through that zone paints a faint-white line flanked by
       wine on both sides (sidebar left, fillet right) = a visible line on the
       left of the radius. Pushing the start down by --radius-lg lands it exactly
       at the fillet's tangent point (Y = header-bottom + radius), so the curve
       owns the rounded zone and the hairline picks up cleanly below it. */
    .global-sidebar::after {
        top: calc(var(--header-bottom) + var(--radius-lg));
    }

    /* On desktop the sidebar sits in its fixed column; global-app-main pushes right via
       margin-left so the sidebar never overlays content. Bootstrap modals are
       z-index 1040 (backdrop) / 1050 (dialog). Keeping sidebar at 1060 here
       caused modals to render behind it. 1030 is below the modal stack. */
    .global-sidebar {
        z-index: 1030;
    }
}

/* The focus ring is the platform's, not Bootstrap's: without this the ring a
   focused button wears is Bootstrap's own blue (--bs-btn-focus-shadow-rgb is
   hard-typed per variant, so repointing --bs-primary never reaches it). */
.btn-primary {
    --bs-btn-focus-box-shadow: var(--shadow-focus-ring);
    --bs-btn-bg: var(--brand-primary);
    --bs-btn-border-color: var(--brand-primary);
    --bs-btn-color: var(--brand-primary-fg);
    --bs-btn-hover-bg: var(--brand-secondary);
    --bs-btn-hover-border-color: var(--brand-secondary);
    --bs-btn-hover-color: var(--brand-secondary-fg);
    --bs-btn-active-bg: var(--brand-highlight);
    --bs-btn-active-border-color: var(--brand-highlight);
    --bs-btn-active-color: var(--brand-highlight-fg);
    --bs-btn-disabled-bg: var(--brand-primary);
    --bs-btn-disabled-border-color: var(--brand-primary);
    --bs-btn-disabled-color: var(--brand-primary-fg);
}

/* Neutral-ink primary CTA — public auth, invite, and error surfaces.
   A fixed dark neutral that carries the SAME weight for every tenant (holds
   zero brand or status color), reusing the --color-neutral-ink token set.
   Applied via .global-btn-ink on those specific public CTAs only; the signed-in app
   shell keeps .btn-primary (brand) for org work. The one exception inside the
   shell is the profile-photo nudge strip: a missing profile picture is an
   account-level fact that reads the same on every tenant, so its CTA carries
   the platform ink rather than the org's fill. It travels with the button
   regardless of which public base wraps it, so it survives the pending
   error-page unification onto public.html. */
.global-btn-ink {
    --bs-btn-bg: var(--color-neutral-ink);
    --bs-btn-border-color: var(--color-neutral-ink);
    --bs-btn-color: var(--color-neutral-ink-fg);
    /* Base hover/active use the FLAT ink token, not the color-mix-derived
       press ink. On engines without color-mix (pre-2023 Safari/Chrome/Firefox)
       a var() that resolves to an unsupported color-mix would be invalid at
       computed-value time, blanking --bs-btn-hover-bg to transparent while the
       white hover text stays — invisible text. The flat token degrades safely
       (filled button, no enrichment); the @supports block below restores the
       darker press ink on modern engines. */
    --bs-btn-hover-bg: var(--color-neutral-ink);
    --bs-btn-hover-border-color: var(--color-neutral-ink);
    --bs-btn-hover-color: var(--color-neutral-ink-fg);
    --bs-btn-active-bg: var(--color-neutral-ink);
    --bs-btn-active-border-color: var(--color-neutral-ink);
    --bs-btn-active-color: var(--color-neutral-ink-fg);
    --bs-btn-disabled-bg: var(--color-neutral-ink);
    --bs-btn-disabled-border-color: var(--color-neutral-ink);
    --bs-btn-disabled-color: var(--color-neutral-ink-fg);
}

/* Visible, accessible focus ring. Base ring is the solid ink token (universally
   supported); the transparent outline surfaces a ring under forced-colors /
   Windows High Contrast Mode where box-shadow is dropped. */
.global-btn-ink:focus-visible {
    box-shadow: 0 0 0 0.25rem var(--color-neutral-ink);
    outline: 2px solid transparent;
    outline-offset: 2px;
}

/* Enrichment for engines that support color-mix (2023+): the darker press ink
   on hover/active and the softer translucent focus ring. Gated so old engines
   never substitute an unsupported color-mix value into a real property. */
@supports (color: color-mix(in srgb, red, blue)) {
    .global-btn-ink {
        --bs-btn-hover-bg: var(--color-neutral-ink-hover);
        --bs-btn-hover-border-color: var(--color-neutral-ink-hover);
        --bs-btn-active-bg: var(--color-neutral-ink-hover);
        --bs-btn-active-border-color: var(--color-neutral-ink-hover);
    }
    .global-btn-ink:focus-visible {
        box-shadow: 0 0 0 0.25rem var(--neutral-ink-focus-ring);
    }
}

/* Outline buttons are quiet controls, so they wear the action ink ramp, not
   the tenant's brand: ink label, neutral hairline, and a warm tint wash on
   hover instead of a fill flip — the filled .btn-primary above stays the one
   brand-colored button. This is a one-token default: point these at
   --brand-primary again to restore branded outlines everywhere. */
.btn-outline-primary {
    --bs-btn-focus-box-shadow: var(--shadow-focus-ring);
    --bs-btn-color: var(--color-action);
    --bs-btn-border-color: var(--color-border);
    --bs-btn-hover-bg: var(--color-action-tint);
    --bs-btn-hover-border-color: var(--color-action-hover);
    --bs-btn-hover-color: var(--color-action);
    --bs-btn-active-bg: var(--color-action-tint-pressed);
    --bs-btn-active-border-color: var(--color-action-pressed);
    --bs-btn-active-color: var(--color-action);
}

/* Monochrome brand hover signal.
   When primary and accent are perceptually identical (Fortis-style black
   palette, or any tenant with brand colors that don't differ visibly),
   the standard primary→accent color flip on hover produces no feedback.
   The body.core-brand-monochrome class is set server-side from
   brand_tokens.is_monochrome and engages an alternative signal model:
   an elevation lift and a perceptible-hover background using the
   HSL-shifted brand variant.

   Only the two controls still painting brand need the rescue — the filled
   .btn-primary and the sidebar toggle. Every other control hovers in the
   fixed action ink, which is always distinct from itself. */

body.core-brand-monochrome .btn-primary:hover,
body.core-brand-monochrome .global-sidebar-toggle:hover {
    background: var(--brand-primary-hover);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
    transform: translateY(-1px);
}

body.core-brand-monochrome .btn-primary:active,
body.core-brand-monochrome .btn-primary.active {
    background: var(--brand-primary);
    transform: translateY(0);
    box-shadow: inset 0 2px 4px var(--ink-alpha-15);
}

/* Reduce motion: keep the elevation but drop the translateY. */
@media (prefers-reduced-motion: reduce) {
    body.core-brand-monochrome .btn-primary:hover,
    body.core-brand-monochrome .global-sidebar-toggle:hover {
        transform: none;
    }
}

/* Bootstrap's own idea of "primary" and "link", pointed at ours.

   We theme Bootstrap through custom properties rather than a Sass build, so
   every colour it ships in :root is a colour this app has to consciously aim
   somewhere. The ones below are the ones nobody aimed, and they are upstream
   of more surfaces than their names suggest: --bs-link-color paints .btn-link
   (50 uses across 37 templates), and --bs-primary/-rgb paint .text-primary
   (25), .bg-primary (18), .border-primary, .text-bg-primary, .badge and the
   active row of a dropdown or list group. All of them rendered #0d6efd — a
   blue that belongs to no tenant and to no token here.

   Aimed at the fixed action ink, the same pair `a` uses below, so a link is
   one colour whether it is an anchor, a .btn-link or a .page-link — and the
   same colour on every tenant. The -rgb triples exist because these
   utilities build their own opacity (`rgba(var(--bs-primary-rgb),
   var(--bs-bg-opacity))`) and a hex cannot go there; test_css_standards pins
   each triple to its hex twin so they cannot drift.

   :root, not .btn-link, deliberately: a per-component override fixes one
   component and leaves the next one to be found by eye. */
:root {
    --bs-primary: var(--color-action);
    --bs-primary-rgb: var(--color-action-rgb);
    --bs-link-color: var(--color-action);
    --bs-link-color-rgb: var(--color-action-rgb);
    --bs-link-hover-color: var(--color-action-hover);
    --bs-link-hover-color-rgb: var(--color-action-hover-rgb);
    /* Bootstrap's tint/shade pair for the same colour. It ships them as three
       more flat hexes (#cfe2ff, #9ec5fe, #052c65) rather than deriving them,
       so repointing --bs-primary above leaves them behind — which is how the
       seven "Write" badges on the payment settings page stayed stock blue
       while the rest of the page moved. A warm tint reads as "subtle" behind
       the ink, the standard hairline holds the border, and the ink itself is
       legible on any app surface. */
    --bs-primary-bg-subtle: var(--color-action-tint-pressed);
    --bs-primary-border-subtle: var(--color-border);
    --bs-primary-text-emphasis: var(--color-action);
    /* Bootstrap builds this one itself out of --bs-primary-rgb, which would
       now be right — but the app's focus ring is its own platform token, not
       the brand, so that it stays a constant signal across every tenant. */
    --bs-focus-ring-color: var(--color-focus-ring-alpha);
}

/* The components Bootstrap does NOT build out of --bs-primary. Each of these
   bakes the literal #0d6efd into its own component property, so repointing the
   root token above does nothing for them and they have to be named. Listed
   together rather than filed next to each component so the whole stock-blue
   surface is one screenful — tests/browser/test_no_stock_bootstrap_blue.py
   walks the same list and fails if a new one appears.

   Almost none of this is on a page today. Counting exact class tokens inside
   class attributes across interface/templates and apps: zero .dropdown-menu,
   zero .dropdown-item, zero .nav-pills, zero .nav-link, zero .progress-bar,
   zero .accordion, and no .list-group-item that is ever .active. .pagination is
   the one that ships (22 uses) and its active-page fill is already brand further
   down — only the focus glow is still Bootstrap's. That is exactly why the rest
   are here: the fix is cheap now and invisible, and the alternative is finding
   them one at a time, in blue, after someone ships a page that uses one. */
.dropdown-menu {
    --bs-dropdown-link-active-bg: var(--color-action);
    --bs-dropdown-link-active-color: var(--color-neutral-ink-fg);
}

.nav-pills {
    --bs-nav-pills-link-active-bg: var(--color-action);
    --bs-nav-pills-link-active-color: var(--color-neutral-ink-fg);
}

.list-group {
    --bs-list-group-active-bg: var(--color-action);
    --bs-list-group-active-border-color: var(--color-action);
    --bs-list-group-active-color: var(--color-neutral-ink-fg);
}

.progress {
    --bs-progress-bar-bg: var(--color-action);
}

.accordion {
    --bs-accordion-btn-focus-box-shadow: var(--shadow-focus-ring);
}

.pagination {
    /* The active-page fill is already brand, further down with the rest of the
       pagination rules; this is only the focus glow, which is a separate
       baked-in property. */
    --bs-pagination-focus-box-shadow: var(--shadow-focus-ring);
}

/* The one that is not a custom property: Bootstrap writes this shadow straight
   into the rule, so there is nothing to repoint and it takes a real override. */
.nav-link:focus-visible {
    box-shadow: var(--shadow-focus-ring);
}

/* Link color for main content: ink, like the text around it — the underline
   Bootstrap already draws is what says "link". One-token default: point these
   back at --brand-primary to restore branded links everywhere. */
a {
    color: var(--color-action);
}

a:hover {
    color: var(--color-action-hover);
}

/* .btn-close is the one control that opts out of the app's focus outline:
   Bootstrap gives it `outline: 0` plus a box-shadow glow, at a specificity
   the global :focus-visible rule cannot reach. Left as a glow on purpose —
   an outline around a 12px X reads as a box, not a ring — but recoloured to
   the platform focus ring so the 20 close buttons in the app stop flashing
   Bootstrap blue at keyboard users. */
.btn-close {
    --bs-btn-close-focus-shadow: var(--shadow-focus-ring);
}

/* Sidebar links: text color depends on the underlying surface.
   - Inactive items sit on --sidebar-bg (= --brand-primary), so they use --sidebar-text.
   - Active items sit on --sidebar-active-bg (= --brand-secondary), so they use --brand-secondary-fg.
   The :not([aria-current="page"]) selector ensures the override below only forces the
   "inactive" text color on inactive links — active links keep the more-specific
   .global-sidebar-nav a[aria-current="page"] rule (which sets brand-secondary-fg). Without this split,
   a tenant with a light primary + dark secondary (Fortis) would render active text
   in the inactive (light-bg) foreground, which collapses to invisibility. */
.global-sidebar a:not([aria-current="page"]),
.global-sidebar a:link:not([aria-current="page"]),
.global-sidebar a:visited:not([aria-current="page"]),
.global-sidebar a:hover:not([aria-current="page"]),
.global-sidebar a:focus:not([aria-current="page"]),
.global-sidebar a:active:not([aria-current="page"]) {
    color: var(--sidebar-text) !important;
    text-decoration: none;
}
.global-sidebar a[aria-current="page"] {
    color: var(--brand-secondary-fg) !important;
    text-decoration: none;
}

.form-control:focus,
.form-select:focus {
    border-color: var(--color-focus-ring);
    box-shadow: 0 0 0 0.2rem var(--color-focus-ring-alpha);
}

/* Checked fill is the fixed action ink, not the tenant's brand: check/radio/
   switch marks are quiet controls and read identically on every org. */
.form-check-input:checked {
    background-color: var(--color-action);
    border-color: var(--color-action);
}

.form-check-input:focus {
    border-color: var(--color-focus-ring);
    box-shadow: 0 0 0 0.2rem var(--color-focus-ring-alpha);
}

/* The bare fill utility. Bootstrap gives it a background and no foreground;
   pair the ink fill with its fixed white so the next bare
   `<div class="bg-primary">` is legible without anyone having to remember.
   Not !important, so an explicit .text-white or .text-dark still wins. */
.bg-primary {
    color: var(--color-neutral-ink-fg);
}

/* .badge fills, pinned to the same fixed pair so the two spellings of
   "primary badge" cannot diverge. */
.badge.bg-primary {
    background-color: var(--color-action) !important;
    color: var(--color-neutral-ink-fg) !important;
}

.badge.text-bg-primary {
    color: var(--color-neutral-ink-fg) !important;
}

.badge.bg-success {
    background-color: var(--color-success) !important;
}

.badge.bg-warning {
    background-color: var(--color-warning) !important;
    color: var(--color-text) !important;
}

.badge.bg-danger {
    background-color: var(--color-error) !important;
}

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

.page-link {
    color: var(--color-action);
}

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

.page-item.active .page-link {
    background-color: var(--color-action);
    border-color: var(--color-action);
}

.navbar-brand {
    color: var(--color-action);
}

.card {
    background: var(--color-card-bg);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-card);
    box-shadow: var(--shadow-sm);
}

.global-list-clickable .list-group-item[data-href] {
    cursor: pointer;
    transition: background-color 0.1s ease;
}

.global-list-clickable .list-group-item[data-href]:hover {
    background-color: var(--color-surface-subtle);
}

.card-header {
    background: transparent;
    border-bottom: 1px solid var(--color-border);
    padding: var(--spacing-md);
    font-weight: var(--font-weight-medium);
}

.card-body {
    padding: var(--spacing-md);
}

.global-stat-card {
    padding: var(--spacing-lg);
    text-align: center;
}

.global-stat-card-value {
    font-size: 2.5rem;
    font-weight: 700;
    color: var(--color-action);
    line-height: 1;
    margin-bottom: var(--spacing-xs);
}

.global-stat-card-label {
    font-size: var(--font-size-md);
    color: var(--color-text-light);
    text-transform: uppercase;
    letter-spacing: 0.05em;
}

.global-action-dropdown {
    position: relative;
    display: inline-block;
}

/* Icon-only 3-dot kebab. Sized off the same padding/line-height as a primary
   button so a row's kebab and its primary action share one control height. */
.global-action-dropdown-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: var(--spacing-sm) var(--spacing-sm);
    background: var(--color-card-bg);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    font-size: var(--font-size-md);
    color: var(--color-text-muted);
    cursor: pointer;
    transition: all var(--transition-fast);
    font-family: var(--font-family);
    line-height: 1.4;
}

.global-action-dropdown-toggle:hover {
    border-color: var(--color-text-muted);
    background: var(--color-grey-50);
    color: var(--color-text);
}

.global-action-dropdown-toggle:focus {
    outline: none;
    border-color: var(--color-focus-ring);
    box-shadow: 0 0 0 3px var(--color-focus-ring-alpha);
}

.global-action-dropdown-toggle--sm {
    padding: var(--spacing-xs) var(--spacing-xsm);
    font-size: var(--font-size-sm);
}

.global-action-dropdown-toggle--outline {
    background: transparent;
}

/* Still worn by the skill-level dropdown (and any other labelled toggle); the
   payments kebab went icon-only and no longer emits these, but they stay for
   the toggles that do. */
.global-action-dropdown-label {
    white-space: nowrap;
}

.global-action-dropdown-chevron {
    width: 1rem;
    height: 1rem;
    opacity: 0.5;
    transition: transform var(--transition-fast);
    flex-shrink: 0;
}

.global-action-dropdown-chevron.global-rotate-180 {
    transform: rotate(180deg);
}

.global-action-dropdown-menu {
    position: absolute;
    top: 100%;
    right: 0;
    margin-top: var(--spacing-xs);
    /* A menu sizes itself to its rows: 10rem is the floor, max-content is the
       ask, and the ceiling keeps it on screen. Without max-content a label
       longer than the floor wraps mid-phrase; without the ceiling an org that
       types a 50-character word for its people (the vocabulary field allows it)
       pushes the panel off the left edge of a phone. Over the ceiling it wraps
       again, which is the right thing to do at that length. */
    min-width: 10rem;
    width: max-content;
    max-width: min(20rem, calc(100vw - 2rem));
    background: var(--color-card-bg);
    border: 1px solid var(--color-grey-200);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-menu);
    z-index: var(--z-dropdown);
    overflow: hidden;
}

.global-action-dropdown-item {
    display: flex;
    align-items: center;
    width: 100%;
    padding: var(--spacing-sm) var(--spacing-smd);
    background: none;
    border: none;
    font-size: var(--font-size-md);
    font-weight: var(--font-weight-body);
    color: var(--color-text);
    text-align: left;
    /* Some rows are links among the buttons, and Bootstrap underlines a link. */
    text-decoration: none;
    cursor: pointer;
    transition: background var(--transition-fast);
    font-family: var(--font-family);
    /* A label can be one unbroken token an org typed itself, and a token with no
       spaces has nowhere to wrap — it would run under the panel's clipped edge.
       anywhere rather than break-word because the label is an anonymous flex
       item, and only anywhere lets it shrink below the token's own width.
       Normal labels are unaffected: max-content ignores soft-wrap opportunities,
       so they still size the panel exactly as before. */
    overflow-wrap: anywhere;
}

/* The one destructive row in a menu (Remove, Delete) reads in the error
   colour — the shared class replaces per-page `text-danger`. */
.global-action-dropdown-item--danger {
    color: var(--color-error-text);
}

.global-action-dropdown-item:hover {
    background: var(--color-grey-100);
}

.global-action-dropdown-item.is-active {
    background: var(--color-action-tint);
    color: var(--color-action);
    font-weight: 500;
}

.global-action-dropdown-item .bi-check2 {
    color: var(--color-success);
}

/* A listed-but-blocked overflow action: greyed, unclickable, with a why-only
   reason line under its label (no hover, no pointer). */
.global-action-dropdown-item--disabled {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--spacing-xs);
    cursor: default;
    opacity: 0.6;
}

.global-action-dropdown-item--disabled:hover {
    background: none;
}

.global-action-dropdown-item__row {
    display: flex;
    align-items: center;
}

.global-action-dropdown-item__reason {
    font-size: var(--font-size-2xs);
    color: var(--color-text-light);
    line-height: 1.2;
}

/* Outbound brand link row: the platform's own logo on the left, a new-tab
   arrow pushed to the right. The logo is the label (aria-label carries the
   words), so the marks get a fixed height and keep their own width. */
.global-action-dropdown-item--brand {
    justify-content: space-between;
    gap: var(--spacing-smd);
}

.global-action-dropdown-item__brand-img {
    height: 18px;
    width: auto;
    display: block;
}

.global-action-dropdown-item__ext {
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
}

/* Custom single-select (skillbridge.customSelect).
   A styled replacement for a native <select>: a full-width form-control
   toggle plus an own-painted option list. Mirrors .global-action-dropdown's look
   (white menu, soft shadow, ink-tinted active row) but is sized like a
   .global-form-control-modern so it sits flush beside real inputs in the form. */
.global-cs {
    position: relative;
}

/* Editable combobox control: a text <input> (carrying .global-form-control-modern for
   height/border/radius) with the chevron overlaid on the right. The input is a
   real search field — focus opens the list, typing filters it. */
.global-cs-control {
    position: relative;
}

.global-cs-input {
    /* Leave room on the right so typed text never slides under the chevron. */
    width: 100%;
    padding-right: var(--spacing-xl);
    /* An I-beam cursor signals "you can type here", not just "click to open". */
    cursor: text;
}

.global-cs-input:disabled {
    cursor: not-allowed;
    opacity: 0.6;
    background: var(--bs-secondary-bg);
}

/* A combobox whose job is searching (the Ramp vendor box) wears the global
   page-search glyph inside the field (filter-bar.css .global-search-icon), so
   the text starts past it — the same 2.5rem the page search input keeps. */
.global-cs--search .global-cs-input {
    padding-left: var(--spacing-2xl);
}

/* Centered date pickers (shared/components/_date_select.html — the month/day/year
   comboboxes in the Create-Opportunity drawer and any other dateSelect host),
   centered like the header month/year pills. The box is padded the same on
   both sides, so the centered value lands at true center. Both sides are set
   here: .global-form-control-modern comes later in this file and its 1rem
   padding beats the 2rem right padding .global-cs-input asks for, which let
   "September" run under the chevron. 1.75rem a side clears the chevron (it
   ends about 1.6rem in from the right edge) and still fits "September" in a
   drawer's narrow boxes, where 2rem a side cut it short. */
.global-cs-date-row .global-cs-input {
    padding-inline: calc(var(--spacing-lg) + var(--spacing-xs));
    text-align: center;
}

/* Phone: the month takes its own line and the day and year share the next,
   so the longest month ("September") is never cut short. The row's Bootstrap
   utilities fix flex with !important, so each box sizes through width, which
   flex-basis: auto reads. */
@media (max-width: 767.98px) {
    .global-cs-date-row {
        flex-wrap: wrap;
    }

    .global-cs-date-row > .global-cs {
        width: 0;
    }

    .global-cs-date-row > .global-cs:first-child {
        width: 100%;
    }
}

/* The three parts of shared/components/_time_select.html (hour / minute / AM-PM),
   laid out by a class of our own rather than Bootstrap's `.d-flex .gap-2` on the
   row and `.flex-fill` on each part. Those utilities declare `!important`, so a
   host that wants the three parts merged into ONE field cannot override them: the
   working-hours grid's `gap: 0` and `flex: 1 1 0` (features/working-hours.css)
   were dead from the day they were written. The 8px the gap never removed landed
   INSIDE each value's cell — the hairline between parts is a border ON the part,
   so the gap sits inside the cell it appears to separate — and pushed every
   number ~4px left of its centre. These are
   the declarations the utilities produced, so every host renders as it did. */
.global-time-select {
    display: flex;
    gap: var(--spacing-sm);
}

.global-time-select > .global-cs {
    flex: 1 1 auto;
}

/* A part is narrow — an hour cell inside the merged working-hours field measures
   ~35px on a 1280px screen — and .global-cs-menu is pinned to its anchor's width
   (left: 0; right: 0). Take the menu's own 0.25rem padding and .global-cs-item's
   0.625rem sides out of that and the label box was 3px wide: every option was
   built, painted and unreadable. The anchor's width
   stays the FLOOR, so a wide host — the opportunity form, the segment drawer — is
   unchanged and only a menu narrower than its own options grows. */
.global-time-select .global-cs-menu {
    min-width: max(100%, 4rem);
    width: max-content;
}

/* Grown past its anchor, the last part's menu would hang off the right edge of
   the field it belongs to, so that one opens leftward. */
.global-time-select > .global-cs:last-child .global-cs-menu {
    left: auto;
    right: 0;
}

.global-cs-control .global-cs-toggle__chev {
    position: absolute;
    top: 50%;
    right: 0.75rem;
    transform: translateY(-50%);
    /* The chevron is decoration over the input — clicks should land on the
       input (which opens the menu), not get swallowed by the icon. */
    pointer-events: none;
}

.global-cs-control .global-cs-toggle__chev.is-open {
    transform: translateY(-50%) rotate(180deg);
}

/* Picked-option face (opt-in — only drawn when the option carries a photo or
   initials, see _custom_select.html): overlaid on the input's left edge the
   same way the chevron overlays its right edge. pointer-events: none for the
   same reason as the chevron — it's decoration over the input, not a target. */
.global-cs-control__avatar {
    position: absolute;
    top: 50%;
    left: 0.5rem;
    transform: translateY(-50%);
    pointer-events: none;
}

/* Room for the avatar so typed/picked text never renders under it. */
.global-cs-input.global-cs-input--avatar {
    padding-left: var(--spacing-xl);
}

/* Clear button (opt-in — cs.clearable): sits in the chevron's own slot,
   swapped in when a value is picked (see the x-show pair in
   _custom_select.html) rather than stacked beside it, so the 36px control
   never has to fit two icons at once. */
.global-cs-clear {
    position: absolute;
    top: 50%;
    right: 0.5rem;
    transform: translateY(-50%);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: var(--spacing-xs);
    background: none;
    border: none;
    border-radius: var(--radius-sm);
    color: var(--color-text-muted);
    font-size: var(--font-size-xs);
    line-height: 1;
    cursor: pointer;
    transition: background-color var(--transition-fast), color var(--transition-fast);
}

.global-cs-clear:hover {
    background-color: var(--color-grey-50);
    color: var(--color-text);
}

.global-cs-clear:focus-visible {
    outline: 2px solid var(--color-focus-ring);
    outline-offset: 1px;
}

/* Clear button for a whole date or time row (opt-in — cs_clearable on
   _date_select.html / _time_select.html): one round x after the row, shown
   once any part is picked, that empties every part. It sits beside the row,
   not inside a block, because it clears the date, not the month. */
.global-cs-clearable {
    display: flex;
    align-items: center;
    gap: var(--spacing-sm);
}

.global-cs-clearable > .global-cs-date-row,
.global-cs-clearable > .global-time-select {
    flex: 1 1 auto;
    min-width: 0;
}

.global-cs-clearable__btn {
    flex: 0 0 auto;
}

/* The x takes the room "September" needed, so on a wide screen the month
   starts 2rem ahead of the day and year and they share what is left (width is
   the basis .flex-fill reads, as in the phone rule above, which still wins
   there). */
@media (min-width: 768px) {
    .global-cs-clearable > .global-cs-date-row > .global-cs {
        width: 0;
    }

    .global-cs-clearable > .global-cs-date-row > .global-cs:first-child {
        width: 2rem;
    }
}

/* Mirror Bootstrap's .is-invalid red ring on the combobox input. */
.global-cs--invalid .global-cs-input {
    border-color: var(--color-error);
}

.global-cs--invalid .global-cs-input:focus {
    box-shadow: 0 0 0 3px var(--status-danger-bg);
}

.global-cs-toggle__chev {
    flex-shrink: 0;
    opacity: 0.5;
    transition: transform var(--transition-fast);
}

.global-cs-toggle__chev.is-open {
    transform: rotate(180deg);
}

.global-cs-menu {
    position: absolute;
    top: calc(100% + 0.25rem);
    left: 0;
    right: 0;
    z-index: 50;
    /* Tall option lists scroll inside the popup rather than pushing the drawer
       — keeps the menu inside the panel even near the bottom edge. */
    max-height: 16rem;
    overflow-y: auto;
    overscroll-behavior: contain;
    background: var(--color-card-bg);
    border: 1px solid var(--color-grey-200);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-menu);
    padding: var(--spacing-xs);
}

/* Opens upward when the menu would drop past the bottom of whatever clips it —
   customSelect sets csDrop at open time; see dropDirection() in custom-select.js.
   max-height alone does not save a menu near a fold: it caps the height, but a
   256px popup hung off a control 40px from the bottom of the drawer is still
   216px of options painted where nobody can see them. The shadow lifts the other
   way so the menu still reads as sitting above the page, not under it. */
.global-cs-menu--up {
    top: auto;
    bottom: calc(100% + 0.25rem);
    box-shadow: var(--shadow-menu-up);
}

.global-cs-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--spacing-sm);
    width: 100%;
    padding: var(--spacing-sm) var(--spacing-sm);
    background: none;
    border: none;
    border-radius: var(--radius-md);
    font-size: var(--font-size-md);
    font-weight: var(--font-weight-body);
    color: var(--color-text);
    text-align: left;
    cursor: pointer;
    white-space: nowrap;
    transition: background var(--transition-fast);
    font-family: var(--font-family);
}

/* Groups an option's optional avatar with its text so .global-cs-item's own
   space-between still only splits two things — this block and the check —
   instead of spreading a third avatar child away from the label it belongs
   next to. Empty of any avatar (every existing customSelect caller), this is
   a single-child flex row and lays out exactly as .global-cs-item__text did alone. */
/* Multi-pick mode (customSelect({ multiple: true })): each option carries a
   box the pick state fills — display only, the row is the button — and the
   picks stack as chips under the field, between it and where the menu hangs. */
.global-cs-item__box {
    margin: 0;
    flex-shrink: 0;
    pointer-events: none;
}

.global-cs-picks {
    display: flex;
    flex-wrap: wrap;
    gap: var(--spacing-xs);
    margin-top: var(--spacing-xs);
}

.global-cs-item__main {
    display: flex;
    align-items: center;
    gap: var(--spacing-sm);
    min-width: 0;
}

/* The option's own face — .global-cs-item is nowrap, so without this a long label
   could shrink the circle rather than ellipsis first. */
.global-cs-item__avatar {
    flex-shrink: 0;
}

/* Two-line option: the label on top, an optional muted sublabel under it (e.g. a
   vendor name over its email). Callers that pass no sublabel render one line, so
   this wrapper is safe for every existing single-line combobox. */
.global-cs-item__text {
    display: flex;
    flex-direction: column;
    min-width: 0;
    gap: 0.05rem;
}

.global-cs-item__label {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.global-cs-item__sub {
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* A group's name over its options (options that carry a group field). Not an
   option: nothing to hover or pick, so it keeps the default cursor. */
.global-cs-group-label {
    padding: var(--spacing-sm) var(--spacing-sm) var(--spacing-xs);
    color: var(--color-text-muted);
    font-weight: var(--font-weight-medium);
    cursor: default;
}

/* The Ramp match panel — wizard step 4 on the connect page.

   A row is a person with their two picks stacked underneath: which Ramp vendor
   they are, and which contact at that vendor gets the money. The picks used to
   sit beside the name at a fixed 300px, which fitted while there was only one of
   them; the second box pushed the row past the 500px side drawer that used to
   host this panel, and the payee picker scrolled off a screen with no horizontal
   scrollbar to bring it back. Stacking is what makes two boxes fit, and the
   stacked fields carry a max-width so the wide wizard card does not stretch a
   combobox across the page. Nothing in here may set a fixed width — a width in
   px is the defect this block was rewritten to remove. */
/* The person filter is the global page search (ADR-131 — wrapper, glyph and
   .global-filter-control all come from filter-bar.css); this wrapper only spaces it
   off the first row. */
.integrations-ramp-match-filter {
    margin: 0 0 var(--spacing-sm);
}
.integrations-ramp-match-row {
    display: flex;
    flex-direction: column;
    gap: var(--spacing-sm);
    min-width: 0;
    padding: var(--spacing-smd) 0;
    border-top: 1px solid var(--color-border);
}
.integrations-ramp-match-row:first-of-type {
    border-top: 0;
}
.integrations-ramp-match-row__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--spacing-sm);
    min-width: 0;
}
.integrations-ramp-match-row__who {
    display: flex;
    align-items: center;
    gap: var(--spacing-sm);
    flex: 1;
    min-width: 0;
}
.integrations-ramp-match-row__person {
    display: flex;
    flex-direction: column;
    min-width: 0;
}
.integrations-ramp-match-row__name {
    font-weight: var(--font-weight-medium);
    color: var(--color-text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.integrations-ramp-match-row__email {
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* The chip keeps its own width whatever the name does, so a long name ellipses
   instead of squeezing the one word that says where this person has got to. */
.integrations-ramp-match-row__chip {
    flex-shrink: 0;
}
.integrations-ramp-match-row__fields {
    display: flex;
    flex-direction: column;
    gap: var(--spacing-sm);
    min-width: 0;
    max-width: 28rem;
}
.integrations-ramp-match-row__field {
    display: flex;
    flex-direction: column;
    gap: var(--spacing-xs);
    min-width: 0;
}
.integrations-ramp-match-row__label {
    color: var(--color-text-muted);
}
/* The payee label says required exactly while a save would refuse the row. */
.integrations-ramp-match-row__label--required {
    color: var(--color-warning-text);
}
.integrations-ramp-match-row__field .global-cs {
    width: 100%;
    min-width: 0;
}
/* The vendor box and this person's own Save, side by side. The box takes the
   room and the button keeps its size — a Save squeezed to a sliver by a long
   vendor name is a button nobody can find. */
.integrations-ramp-match-row__pick {
    display: flex;
    align-items: flex-start;
    gap: var(--spacing-sm);
    min-width: 0;
}
/* The box takes the room up to a search-bar's worth of it — a vendor picker
   stretched across the whole drawer reads as a form field, not a search. */
.integrations-ramp-match-row__pick .global-cs {
    flex: 1;
    width: auto;
    max-width: 20rem;
}
.integrations-ramp-match-row__save {
    flex-shrink: 0;
}
.integrations-ramp-match-row__fact {
    display: flex;
    align-items: baseline;
    gap: var(--spacing-sm);
    flex: 1 1 100%;
    min-width: 0;
}
.integrations-ramp-match-row__fact--foot {
    justify-content: flex-end;
}
.integrations-ramp-match-row__error {
    margin: 0;
    font-size: var(--font-size-sm);
    color: var(--color-error);
}
/* A quiet in-row line for read failures and facts: "we couldn't read who can
   be paid", "only person there". Softer than an error, but still worth words. */
.integrations-ramp-match-row__hint {
    margin: 0;
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
}
.integrations-ramp-match-row__out {
    white-space: nowrap;
}
.integrations-ramp-match__note {
    margin: 0;
    color: var(--color-text-muted);
}
/* Try again rides inside a sentence, so it must sit on its baseline
   instead of the padded island btn-sm puts it on. */
.integrations-ramp-match__inline-link {
    padding: 0;
    font-size: var(--font-size-sm);
    vertical-align: baseline;
}
.integrations-ramp-match__foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--spacing-smd);
    margin-top: var(--spacing-md);
    flex-wrap: wrap;
}
.integrations-ramp-match__foot--end {
    justify-content: flex-end;
}

.global-cs-item:hover {
    background: #f3f4f6;
}

/* Keyboard cursor — the item the arrow keys have landed on. Visually matches
   hover so mouse and keyboard navigation read identically; distinct from
   .is-active, which marks the already-committed selection (ink tint + check).
   @mouseenter keeps this in sync with the pointer. */
.global-cs-item--cursor {
    background: #f3f4f6;
}

.global-cs-item.is-active {
    background: var(--color-action-tint);
    color: var(--color-action);
    font-weight: 500;
}

.global-cs-item__check {
    flex-shrink: 0;
    color: var(--color-success);
}

/* Enter transition for the popup (x-transition classes on .global-cs-menu). */
.global-cs-fade-enter {
    transition: opacity 0.12s ease, transform 0.12s ease;
}

.global-cs-fade-enter-start {
    opacity: 0;
    transform: translateY(-0.25rem);
}

.global-cs-fade-enter-end {
    opacity: 1;
    transform: translateY(0);
}

.global-status-toggle {
    display: flex;
    gap: var(--spacing-xs);
    margin-left: var(--spacing-xs);
}

.global-status-toggle__btn {
    background: transparent;
    border: 1px solid var(--color-border);
    color: var(--color-text-light);
    border-radius: var(--radius-md);
    padding: 0 var(--spacing-smd);
    min-width: 5.5rem;
    text-align: center;
    font-size: var(--font-size-md);
    font-weight: var(--font-weight-button);
    font-family: var(--font-family);
    transition: background var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast);
    height: var(--filter-ctrl-h, 36px);
    line-height: 1;
    box-sizing: border-box;
    cursor: pointer;
}

/* Quiet toggle: a tint wash on hover, the solid action ink only once chosen —
   the same ramp as .btn-outline-primary above. */
.global-status-toggle__btn:hover {
    background: var(--color-action-tint);
    color: var(--color-action);
    border-color: var(--color-action-hover);
}

.global-status-toggle__btn--active {
    background: var(--color-action);
    color: var(--color-neutral-ink-fg);
    border-color: var(--color-action);
}

.global-status-toggle__btn--active:hover {
    background: var(--color-action-hover);
    color: var(--color-neutral-ink-fg);
    border-color: var(--color-action-hover);
}

.global-form-control-modern,
.global-form-select-modern {
    height: 44px;
    padding: var(--spacing-sm) var(--spacing-md);
    border: 1px solid var(--color-border);
    border-radius: 10px;
    background: var(--color-grey-50);
    color: var(--color-text);
    font-size: var(--font-size-lg);
    font-family: var(--font-family);
    transition: background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
}

.global-form-control-modern:hover,
.global-form-select-modern:hover {
    background: var(--color-grey-100);
    border-color: var(--color-text-muted);
}

.global-form-control-modern:focus,
.global-form-select-modern:focus {
    outline: none;
    border-color: var(--color-focus-ring);
    box-shadow: 0 0 0 3px var(--color-focus-ring-alpha);
    background: var(--color-card-bg);
}

.global-form-control-modern::placeholder {
    color: var(--color-text-muted);
}

/* Compact combobox: the control-height pill every filter control wears (.global-filter-select,
   the drawer's row buttons) — page-header pickers, the saved-view drawers' Sort by,
   the permissions person box, the Ramp vendor search. :where() keeps it at one
   class of weight, so a host's own two-class tweak (date-nav centres its text)
   still wins even though main.css loads last. */
:where(.global-cs--compact) .global-cs-input {
    height: var(--filter-ctrl-h, var(--control-h-base));
    min-height: var(--filter-ctrl-h, var(--control-h-base));
    padding: 0 var(--spacing-xl) 0 var(--spacing-smd);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-button);
    background-color: var(--color-card-bg);
    color: var(--color-text);
    font-size: var(--font-size-md);
    line-height: 1;
}

:where(.global-cs--compact) .global-cs-input:hover {
    background-color: var(--color-grey-50);
    border-color: var(--color-text-muted);
}

:where(.global-cs--compact) .global-cs-input:focus {
    outline: none;
    border-color: var(--color-focus-ring);
    box-shadow: var(--shadow-focus-ring);
    background-color: var(--color-card-bg);
}

/* A pick-only combobox (month, year, calendar view): the list is the whole
   input, so it shows a hand, not a text caret. */
:where(.global-cs--pick) .global-cs-input {
    cursor: pointer;
    caret-color: transparent;
}

.global-form-select-modern {
    padding-right: var(--spacing-2xl);
    appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");
    background-position: right 0.75rem center;
    background-repeat: no-repeat;
    background-size: 1.25rem;
}

.form-control,
.form-select {
    border-radius: 8px;
    transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

.form-control:hover,
.form-select:hover {
    border-color: var(--color-text-muted);
}

.form-label {
    font-weight: 500;
    font-size: var(--font-size-md);
    color: var(--color-text);
    margin-bottom: var(--spacing-sm);
}

.form-text {
    font-size: var(--font-size-md);
    color: #6b7280;
    margin-top: var(--spacing-xsm);
}

.form-check-input {
    width: 1.15rem;
    height: 1.15rem;
    border-radius: 4px;
    cursor: pointer;
}

.form-check-label {
    font-size: var(--font-size-lg);
    cursor: pointer;
}

.form-switch .form-check-input {
    width: 2.5rem;
    height: 1.35rem;
    border-radius: 999px;
    background-color: #e5e7eb;
    border: none;
    transition: background-color 0.2s ease;
}

.form-switch .form-check-input:checked {
    background-color: var(--color-action);
}

.form-switch .form-check-input:focus {
    box-shadow: 0 0 0 3px var(--color-focus-ring-alpha);
}

.global-empty-state {
    text-align: center;
    padding: var(--spacing-xl) var(--spacing-md);
}

/* The message and the CTA both carry a bottom margin for when something follows
   them. Whatever ends up last has nothing following it, so its margin is just
   dead space stacked on the panel's own bottom padding — most visible now that
   the standing call is to ship empty states with NO CTA, leaving the message
   last. Zero it on the last child so the panel is padded evenly top and bottom. */
.global-empty-state > :last-child {
    margin-bottom: 0;
}

.global-empty-state-icon {
    font-size: 3rem;
    color: var(--color-text-light);
    margin-bottom: var(--spacing-md);
}

/* --badge: the glyph in a soft ink-tinted disc instead of running bare. For
   the empty states that are the whole page rather than a gap in one — a setup
   screen the worker has to act on, where the muted 3rem glyph reads as an
   absence rather than an invitation. */
.global-empty-state-icon--badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 4rem;
    height: 4rem;
    border-radius: var(--radius-pill);
    background: var(--color-action-selected);
    color: var(--color-action);
    font-size: 1.75rem;
    line-height: 1;
}

.global-empty-state-title {
    font-size: var(--font-size-xl);
    font-weight: var(--font-weight-medium);
    color: var(--color-text);
    margin-bottom: var(--spacing-sm);
}

.global-empty-state-message {
    color: var(--color-text-light);
    margin-bottom: var(--spacing-lg);
    max-width: 400px;
    margin-left: auto;
    margin-right: auto;
}

/* --inline: a compact empty state for inside a card, panel, or list — the
   "nothing here yet" note that shouldn't take the full-page hero treatment.
   Same centered layout, smaller icon / type / padding. */
.global-empty-state--inline {
    padding: var(--spacing-md) var(--spacing-sm);
}

.global-empty-state--inline .global-empty-state-icon {
    font-size: 1.75rem;
    margin-bottom: var(--spacing-sm);
}

.global-empty-state--inline .global-empty-state-title {
    font-size: var(--font-size-md);
}

.global-empty-state--inline .global-empty-state-message {
    font-size: var(--font-size-sm);
    margin-bottom: var(--spacing-md);
}

/* Table empty row: the canonical "no rows yet" line inside a data table. One
   full-width, centered, muted cell — rendered by the _empty_row.html partial.
   A separate root class, NOT a child of .global-empty-state (different DOM shape: a
   <td>, not the <div> hero). Uses --color-text-light — the same token this
   repo's .text-muted utility resolves to (main.css) — so migrating the
   hand-rolled `text-center text-muted` table empties onto this class is a
   visual no-op, and the pending --color-text-light -> grey-600 retoken
   (separate gated PR) lifts the contrast on all of them at once. */
.global-empty-state-row {
    text-align: center;
    color: var(--color-text-light);
    padding: var(--spacing-md);
}

/* Bootstrap paints --bs-table-bg on every cell, and the cells cover the whole
   header band — so a `background` on the thead paints nothing. To tint a band,
   set --bs-table-bg on the thead; the cells inherit it. */
.table {
    --bs-table-bg: var(--color-card-bg);
}

.table > thead th {
    font-weight: var(--font-weight-medium);
    font-size: var(--font-size-xs);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--color-text-light);
    border-bottom-width: 1px;
}

.table-hover > tbody > tr:hover {
    --bs-table-hover-bg: rgba(0, 0, 0, 0.02);
}

.table-responsive {
    border-radius: var(--radius-lg);
    border: 1px solid var(--color-border);
}

/* The loading ring is the shared .global-spinner
   (components/spinner.css). */

.global-skeleton {
    background: linear-gradient(90deg, var(--color-border) 25%, var(--color-page-bg) 50%, var(--color-border) 75%);
    background-size: 200% 100%;
    border-radius: var(--radius-sm);
}

/* The shimmer is motion a reader may have asked never to see; the static
   gradient block still reads as "loading" without it. */
@media (prefers-reduced-motion: no-preference) {
    .global-skeleton {
        animation: skeleton-loading 1.5s ease-in-out infinite;
    }
}

@keyframes skeleton-loading {
    0% { background-position: 200% 0; }
    100% { background-position: -200% 0; }
}

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

.visually-hidden-focusable:not(:focus):not(:focus-within) {
    position: absolute !important;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

:focus-visible {
    outline: 2px solid var(--color-focus-ring);
    outline-offset: 2px;
}

.global-skip-link {
    position: absolute;
    top: -100%;
    left: var(--spacing-md);
    padding: var(--spacing-sm) var(--spacing-md);
    background: var(--color-neutral-ink);
    color: var(--color-neutral-ink-fg);
    border-radius: var(--radius-md);
    z-index: 9999;
    transition: top var(--transition-fast);
}

.global-skip-link:focus-visible {
    top: var(--spacing-md);
}

.text-success { color: var(--color-success); }
.text-warning { color: var(--color-warning); }
.text-info { color: var(--color-info); }
.text-muted { color: var(--color-text-light); }

.global-btn-icon-copy {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    padding: 0;
    border: none;
    border-radius: var(--radius-button);
    background: transparent;
    color: var(--color-text-light);
    cursor: pointer;
    transition: background 0.15s, color 0.15s;
}

.global-btn-icon-copy:hover {
    background: var(--color-action);
    color: var(--color-neutral-ink-fg);
}

.global-btn-icon-copy:focus-visible {
    outline: 2px solid var(--color-focus-ring);
    outline-offset: 2px;
}

.global-scroll-sentinel {
    height: 1px;
    width: 100%;
    visibility: hidden;
}

.global-scroll-status {
    text-align: center;
    padding: var(--spacing-md);
}

@media (min-width: 1024px) {
    .global-sidebar-collapse-btn {
        display: flex;
    }
}

/* Sidebar Collapsed State (Desktop).
   Icon rail pattern: only sidebar width changes.
   Icons sit in fixed 56px columns and never move.
   Text clips naturally via overflow: hidden on .global-sidebar. */

@media (min-width: 1024px) {
    .global-app-layout.is-sidebar-collapsed .global-app-main {
        margin-left: var(--sidebar-collapsed-width);
    }

    .global-app-layout.is-sidebar-collapsed .global-sidebar {
        width: var(--sidebar-collapsed-width);
        overflow: hidden;
    }

    .global-app-layout.is-sidebar-collapsed .global-sidebar-org-toggle,
    .global-app-layout.is-sidebar-collapsed .global-sidebar-org-dropdown {
        display: none;
    }

    /* Footer actions: stack, because the rail is only as wide as one control.
       Side by side the Skool mark and the chevron want 56px + 28px + gap in a
       56px rail, so the second one would be clipped by the sidebar's
       overflow: hidden. Padding drops for the same reason — the Skool mark
       already carries the full 56px rail width. */
    .global-app-layout.is-sidebar-collapsed .global-sidebar-footer-actions {
        flex-direction: column;
        align-items: center;
        justify-content: center;
        padding: 0;
    }

    /* Hide every footer-action child in collapsed mode EXCEPT the chevron
       toggle and the Skool link, which has its own collapsed form (the "sk"
       mark, swapped in by .is-sidebar-collapsed .global-sidebar-skool-rail). Selector
       matches on static aria-label values (NOT Alpine-bound) so the rule
       applies on the very first paint via the synchronously-set
       .is-sidebar-collapsed class. Add a new entry here for any new footer
       button — never use Alpine x-show for collapse visibility (causes a
       flash on refresh while Alpine boots). */
    .global-app-layout.is-sidebar-collapsed .global-sidebar-footer-actions a:not(.global-sidebar-skool),
    .global-app-layout.is-sidebar-collapsed .global-sidebar-footer-actions button[aria-label="Settings"],
    .global-app-layout.is-sidebar-collapsed .global-sidebar-footer-actions button[aria-label="Properties"],
    .global-app-layout.is-sidebar-collapsed .global-sidebar-footer-actions button[aria-label="Availability"] {
        display: none;
    }

    /* Collapsed: settings button aligns with logo center (28px in 56px rail).
       18px = half the 36px button. */
    .global-app-layout.is-sidebar-collapsed .global-header-settings-btn {
        margin-left: calc(28px - 18px - var(--spacing-sm));
    }

    /* Collapsed footer: stack the profile row so the profile button keeps its
       full rail width and the avatar centering carries over unchanged (the
       bell used to share this row; it now lives in the header bar). */
    .global-app-layout.is-sidebar-collapsed .global-sidebar-profile-row {
        flex-direction: column;
        align-items: stretch;
        gap: var(--spacing-xs);
    }

}

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

    .global-toast {
        transform: none;
        opacity: 1;
    }

    .is-toast-show {
        transform: none;
    }

    .is-toast-hiding {
        transform: none;
        opacity: 0;
    }
}

.is-form-loading {
    pointer-events: none;
    opacity: 0.7;
}

.is-form-loading input,
.is-form-loading select,
.is-form-loading textarea {
    background-color: var(--color-page-bg);
}

.global-form-error {
    margin-bottom: var(--spacing-md);
}

.htmx-indicator {
    display: none;
}

.htmx-request .htmx-indicator {
    display: inline-block;
}

.htmx-request {
    pointer-events: none;
    opacity: 0.7;
}

.is-sortable-ghost {
    opacity: 0.4;
    background-color: var(--bs-light);
}

.is-sortable-chosen {
    background-color: var(--color-card-bg);
}

.is-sortable-drag {
    opacity: 0.9;
}

.global-drag-handle {
    cursor: grab;
}

.global-drag-handle:active {
    cursor: grabbing;
}

.global-drag-handle:hover {
    color: var(--color-action) !important;
}

/* Skills dashboard — separated cards.
   A single-column list where each skill is its own .card laid out as ONE
   horizontal row: name | worker count | level badges | menu. */
.global-skills-list {
    display: flex;
    flex-direction: column;
    gap: var(--spacing-smd);
}

/* The Templates page (settings_templates.html) is the global-skills-list's one
   card-shape consumer (page_header.html shape="card", ADR-131 controls-canon
   skin pass) — page-header.css doesn't pad body_template generically, so the
   page-owned wrapper carries the card's own 1rem rhythm here. */
.settings-template-list-card-body {
    padding: var(--spacing-md);
}

/* Properties (settings_properties.html) is a card-shape consumer too (ADR-131
   controls-canon skin pass, Lane 4) — same reason: page-header.css doesn't pad
   body_template generically, so the page-owned wrapper carries the 1rem rhythm. */
.settings-properties-card-body {
    padding: var(--spacing-md);
}

/* Meetings settings list is a card-shape consumer too (ADR-131 controls-canon
   skin pass, Lane 4) — same reason: page-header.css doesn't pad body_template
   generically, so the page-owned wrapper carries the 1rem rhythm. The inner
   .list-group-flush rides flush inside it, same as the intro paragraph. */
.settings-meeting-list-card-body {
    padding: var(--spacing-md);
}

/* Worker roster (workers/list.html) — a toolbar-shape consumer since the
   filters moved into their own controls card: the list body stands on the page
   background as its own .global-card, and this wrapper carries the 1rem rhythm
   page-header.css doesn't pad generically. */
.workers-list-card-body {
    padding: var(--spacing-md);
}

/* File request list (storage/file_request_list.html) is a card-shape consumer
   too (ADR-131 controls-canon skin pass, Lane 5) — same reason: page-header.css
   doesn't pad body_template generically, so the page-owned wrapper carries the
   1rem rhythm. */
.storage-file-request-list-card-body {
    padding: var(--spacing-md);
}

/* Folder template list (storage/folder_template_list.html) is a card-shape
   consumer too (ADR-131 controls-canon skin pass, Lane 5) — same reason:
   page-header.css doesn't pad body_template generically, so the page-owned
   wrapper carries the 1rem rhythm. */
.storage-folder-template-list-card-body {
    padding: var(--spacing-md);
}

/* Remote board (editing/remote_work.html) is a card-shape consumer too
   (ADR-131 controls-canon skin pass, Lane 6) — same reason: page-header.css
   doesn't pad body_template generically, so the page-owned wrapper carries
   the 1rem rhythm. */
.streaming-remote-work-card-body {
    padding: var(--spacing-md);
}

.global-skill-row {
    display: flex;
    /* Bootstrap's base .card sets flex-direction: column; we share the
       .card class, so this MUST be re-asserted or every child stacks. */
    flex-direction: row;
    flex-wrap: nowrap;
    align-items: center;
    gap: var(--spacing-md);
    padding: var(--spacing-smd) var(--spacing-mdlg);
    transition: border-color var(--transition-fast);
}

/* Subtle hover: just a faint border tint, no lift or heavy shadow. */
.global-skill-row:hover {
    border-color: var(--color-grey-400);
}

/* Name takes the slack; everything after it hugs the right edge. */
.global-skill-row__name {
    flex: 1;
    min-width: 0;
    font-size: var(--font-size-lg);
    font-weight: var(--font-weight-medium);
    color: var(--color-text);
    text-decoration: none;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.global-skill-row__name:hover {
    color: var(--color-action);
    text-decoration: underline;
}

/* One global-skills-list row opening onto the rows it owns.
   The Templates page's categories tab: a category row with its service rows
   nested under it. The wrapper each row now sits in is what .global-skills-list gaps,
   so the child block carries its own top margin rather than a second gap. */
.global-tree-toggle {
    flex-shrink: 0;
    padding: 0;
    border: 0;
    background: transparent;
    line-height: 1;
    color: var(--color-text-muted);
}

.global-tree-toggle:hover {
    color: var(--color-text);
}

/* The rail is the ownership: it runs from the parent row down past the last
   child, so which rows belong to which category survives a long list. */
.global-tree-children {
    margin: var(--spacing-sm) 0 0 var(--spacing-lg);
    padding-left: var(--spacing-md);
    border-left: 2px solid var(--color-border);
}

/* Quieter than the row that owns them — a service is read as part of its
   category, not as a peer of one. */
.global-tree-child {
    margin-bottom: var(--spacing-sm);
    padding: var(--spacing-sm) var(--spacing-md);
    background: var(--color-surface-subtle);
}

.global-tree-child .global-skill-row__name {
    font-size: var(--font-size-md);
}

/* Home Page — Hero Slot.
   The org's own logo at the top of the home page, above the Instagram feed
   when there is one. The container is transparent and borderless — the org's
   homepage background colour shows through, and the logo's max-height is the
   only thing that sets the hero's size. */

.core-home-hero {
    /* Borderless full-bleed container. No min-height — the logo's
       viewport-anchored max-height is the actual size driver. Setting
       min-height here was confusing the cascade for square logos. */
    width: 100%;
    margin: 0;
    padding: 0;
    background: transparent;
    border: none;
    border-radius: 0;
    display: flex;
    align-items: center;
    justify-content: center;
}

.core-home-hero__inner {
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--spacing-md);
}

.core-home-hero__logo {
    /* With the Instagram feed under it the logo is a header, not a hero, so
       it caps at a compact fixed height — a tall or square org logo must
       leave the feed visible on first load rather than filling the viewport
       and pushing it below the fold. `--solo` below is the other case. */
    max-width: 100%;
    max-height: 140px;
    width: auto;
    height: auto;
    object-fit: contain;
}

/* No feed: the logo is the whole home page, so it goes back to the full-page
   size it had before the feed shipped — these are the exact values #4508
   replaced, not a fresh budget. Cap against the VIEWPORT, not the parent — a
   flex parent with no resolved height doesn't propagate one to its children,
   so `max-height: 100%` let square logos overflow. The subtracted pixels are
   rough slack for the chrome above and below, deliberately generous so the
   bottom edge never clips; don't tune them to a token. */
.core-home-hero--solo .core-home-hero__logo {
    max-height: calc(100vh - 240px);
}

@media (max-width: 576px) {
    .core-home-hero__inner {
        padding: var(--spacing-sm);
    }
    .core-home-hero__logo {
        max-height: 96px;
    }
    /* Restated because `.core-home-hero--solo .core-home-hero__logo` outranks the
       single-class rule above no matter the source order. */
    .core-home-hero--solo .core-home-hero__logo {
        max-height: calc(100vh - 200px);
    }
}

.global-hide-spinners::-webkit-outer-spin-button,
.global-hide-spinners::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.global-hide-spinners { -moz-appearance: textfield; }

/* Must load after .card and .global-filter-bar's border shorthand (main.css / filter-bar.css)
   or the thicker high-contrast border loses on source order. */
@media (prefers-contrast: more) {
    .card,
    .global-filter-bar {
        border-width: 2px;
    }
}

/* ── Phone (<768px) ─────────────────────────────────────────────────────────
   The two shared singletons main.css owns, restyled for a thumb. This block
   stays LAST in the file: a base rule written after it wins the cascade back. */
@media (max-width: 767.98px) {
    .global-empty-state {
        padding: var(--spacing-lg) var(--page-gutter);
    }

    /* Swipe strip. Opt-in, phone only: a grid of cards that stacked into a
       tall column becomes ONE sideways row the thumb flicks through, so the
       thing the page is for (the list under the cards) stays above the fold.
       Off the phone the class is inert and the page's own grid rule stands —
       this sheet loads after every feature sheet, so on the phone the flex
       here wins their `display: grid` by source order alone, no `!important`.
       The row bleeds into both page gutters and pads them back, so the first
       card lines up with the page and the last one scrolls to the screen's
       edge. Cards mark themselves with the __item class: an htmx container
       between the strip and its cards is `display: contents`, so a `> *`
       child rule would land on the wrapper, not the cards. */
    .global-swipe-strip {
        display: flex;
        flex-wrap: nowrap;
        gap: var(--spacing-sm);
        margin-inline: var(--page-gutter-bleed);
        padding-inline: var(--page-gutter);
        overflow-x: auto;
        overscroll-behavior-x: contain;
        scroll-snap-type: x mandatory;
        scroll-padding-inline: var(--page-gutter);
        -webkit-overflow-scrolling: touch;
        scrollbar-width: none;
    }

    .global-swipe-strip::-webkit-scrollbar {
        display: none;
    }

    .global-swipe-strip__item {
        flex: 0 0 var(--swipe-card-w);
        min-width: 0;
        scroll-snap-align: start;
    }

    /* The ⋯ menu becomes a bottom sheet: fixed to the viewport's bottom edge,
       full width, 44px rows. It stays in its own tree (Alpine state and the
       rows' hx-* attributes keep working) — no ancestor on this page is
       transformed, so `fixed` resolves against the viewport with no portal.
       Overflow is visible so the ::before scrim below can stand outside it. */
    .global-action-dropdown-menu {
        position: fixed;
        inset: auto 0 var(--bottom-bar-h) 0;
        margin: 0;
        width: auto;
        min-width: 0;
        max-width: none;
        border: 0;
        border-radius: var(--radius-lg) var(--radius-lg) var(--radius-none) var(--radius-none);
        /* The sheet stands on the bar, and the bar pays the home indicator. */
        padding-bottom: 0;
        box-shadow: var(--shadow-modal);
        z-index: var(--z-modal);
        overflow: visible;
    }

    /* The scrim stands ON the sheet's top edge (bottom: 100%) and reaches up
       a full screen, so it dims the page without tinting the sheet and needs
       no z-index of its own. It belongs to the menu, so a tap on it is a
       click outside the toggle — Alpine's click.outside closes the menu —
       and the page under it never sees the tap. */
    .global-action-dropdown-menu::before {
        content: "";
        position: absolute;
        top: auto;
        right: 0;
        bottom: 100%;
        left: 0;
        height: 100vh;
        background: var(--color-scrim);
    }

    .global-action-dropdown-item {
        min-height: var(--control-h-touch);
        padding: var(--spacing-sm) var(--page-gutter);
        font-size: var(--font-size-base);
    }

    /* With overflow visible the first row's hover wash would square off the
       sheet's rounded top; give it the same corners. */
    .global-action-dropdown-item:first-child {
        border-radius: var(--radius-lg) var(--radius-lg) var(--radius-none) var(--radius-none);
    }

    /* iOS zooms into any field under 16px, so every typed field is body size
       on a phone. Buttons and labels keep their own sizes. */
    .form-control,
    .form-select,
    .global-form-control-modern,
    .global-form-select-modern,
    .global-cs-input {
        font-size: var(--font-size-base);
    }

    /* ── The bottom bar ─────────────────────────────────────────────────────
       The app bar is not hidden and not duplicated: .global-app-header ITSELF
       is re-pinned to the bottom edge, so the four controls in it keep the
       Alpine state they already own and the bell drawer, the support panel
       and the search field, which all live inside it, keep rendering.
       tokens.css zeroes --header-height for this query, so --header-bottom
       and --drawer-top already read "just under the notch". The bar's own
       seat is --bottom-bar-h. Same 1050 as the base rule: above the Bootstrap
       offcanvas backdrop (1040) and the sidebar scrim (--z-scrim), so a lit
       button stays tappable while its sheet is up. */
    .global-app-header {
        position: fixed;
        top: auto;
        bottom: 0;
        left: 0;
        right: 0;
        height: var(--bottom-bar-h);
        padding-top: 0;
        padding-bottom: var(--safe-bottom);
        padding-inline: 0;
        gap: 0;
        border-bottom: 0;
        border-top: 1px solid var(--sidebar-border);
        overflow: visible;
    }

    /* Four equal columns. The two right-hand controls sit in a wrapper that
       the desktop bar pushes right; display: contents lifts them out of it so
       every control is a direct flex item and takes a quarter of the bar.
       Order: Menu, Search, Support, Notifications (DOM order, no `order`). */
    .global-app-header .global-app-header-actions {
        display: contents;
    }

    .global-app-header .global-topbar-search,
    .global-app-header .notifications-bell,
    .global-app-header .support-desk {
        display: flex;
        flex: 1 1 0;
        min-width: 0;
        align-self: stretch;
    }

    /* Each control: icon over its word, the full column wide and the touch
       height tall. No circle and no hairline; the column is the target. */
    .global-app-header .global-sidebar-toggle,
    .global-app-header .global-header-btn-icon {
        flex: 1 1 0;
        display: inline-flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        gap: var(--spacing-2xs);
        width: 100%;
        min-width: 0;
        height: auto;
        min-height: var(--control-h-touch);
        padding: var(--spacing-xs) 0;
        border: 0;
        border-radius: var(--radius-none);
        background: transparent;
    }

    .global-app-header .global-sidebar-toggle i,
    .global-app-header .global-header-btn-icon i {
        font-size: var(--font-size-xl);
    }

    .global-header-btn__label {
        display: block;
        font-size: var(--font-size-2xs);
        line-height: 1;
        white-space: nowrap;
    }

    /* The unread dot rides the bell glyph, not the column's corner. */
    .global-app-header .notifications-bell-badge {
        top: var(--spacing-xs);
        right: calc(50% - var(--spacing-md));
    }

    /* The page keeps its last row out from under the bar. */
    .global-app-content {
        padding-bottom: calc(var(--bottom-bar-h) + var(--page-gutter));
    }

    /* The viewport-fit lock (base rule above) stops at the bar, and a locked
       page's content does not ALSO pad for the bar: the lock already left
       the room. */
    .global-app-main:has(.global-page-fill) {
        min-height: 0;
        height: calc(100vh - var(--safe-top) - var(--bottom-bar-h));
        height: calc(100dvh - var(--safe-top) - var(--bottom-bar-h));
    }

    .global-app-main:has(.global-page-fill) .global-app-content {
        padding-bottom: var(--page-gutter);
    }

    /* ── The Menu sheet ─────────────────────────────────────────────────────
       The sidebar rises from the bar instead of sliding in from the left.
       Same class, same is-open, same scrim (--z-scrim, under the bar); only
       the geometry changes. Wins over the <1024 translateX rules above by
       source order. The bar pays --safe-bottom, so the sheet does not. */
    .global-sidebar {
        top: auto;
        bottom: var(--bottom-bar-h);
        left: 0;
        right: 0;
        width: auto;
        max-width: none;
        height: auto;
        max-height: calc(100dvh - var(--header-bottom) - var(--bottom-bar-h));
        overflow-y: auto;
        overscroll-behavior-y: contain;
        padding-top: var(--spacing-sm);
        padding-bottom: 0;
        border-radius: var(--radius-lg) var(--radius-lg) var(--radius-none) var(--radius-none);
        box-shadow: var(--shadow-modal);
        transform: translateY(100%);
        /* translateY(100%) parks the closed sheet exactly over the bar (not
           off the screen the way translateX(-100%) did), where it would catch
           every tap meant for the bar. Hidden until open; visibility waits
           out the slide on close so the sheet is seen leaving. */
        visibility: hidden;
        transition: transform var(--transition-normal), visibility var(--transition-normal);
    }

    .global-sidebar.is-open {
        transform: translateY(0);
        visibility: visible;
    }

    /* ── The Search sheet ───────────────────────────────────────────────────
       The field drops from the TOP of the screen, not the bar: the phone
       keyboard rises over the bottom half the moment the field takes focus,
       and a fixed bottom sheet does not move with it on iOS. Results and
       recents stack under the field inside the same sheet (the desktop
       dropdown hangs below the form; here it is a row of the form). A scrim
       stands between the sheet and the bar so a tap anywhere else closes it;
       it stops at the bar so the lit Search button stays tappable. */
    .global-topbar-search.is-revealed .global-topbar-search__form {
        position: fixed;
        top: var(--safe-top);
        bottom: auto;
        inset-inline: 0;
        flex-wrap: wrap;
        align-items: center;
        max-height: calc(100dvh - var(--safe-top) - var(--bottom-bar-h));
        overflow-y: auto;
        overscroll-behavior-y: contain;
        padding: var(--spacing-sm) var(--page-gutter) var(--spacing-smd);
        background: var(--header-bg);
        border-radius: var(--radius-none) var(--radius-none) var(--radius-lg) var(--radius-lg);
        box-shadow: var(--shadow-modal);
        z-index: var(--z-raised);
    }

    .global-topbar-search__panel {
        position: static;
        flex-basis: 100%;
        width: 100%;
        max-height: none;
        margin-top: var(--spacing-sm);
        border: 0;
        box-shadow: none;
    }

    .global-topbar-search__scrim {
        display: block;
        position: fixed;
        inset: 0 0 var(--bottom-bar-h) 0;
        background: var(--color-scrim);
    }
}
