
:root {
    --role-tile-height: 52px;
    --role-tile-gap: var(--spacing-xsm);
    /* Roles section holds ~3 tile rows, then scrolls — so the card never grows
       with the role count (same "list scrolls, frame stays put" idea as the
       sidebar nav). 3 rows + 2 gaps + the section's --spacing-smd padding × 2 (1.5rem). */
    --role-section-h: calc(var(--role-tile-height) * 3 + var(--role-tile-gap) * 2 + 1.5rem);
    /* Date banner floor. A dateless card renders the strip too (icon over a blank
       span — the template's config-not-data contract), and this floor is what makes
       the blank strip exactly a filled one's height: one text line at the strip's
       own type (--font-size-sm × the inherited --line-height) + --spacing-sm padding × 2
       + the 1px border-bottom, which border-box counts inside min-height. */
    --card-banner-h: calc(var(--font-size-sm) * var(--line-height) + 1rem + 1px);
    --role-tile-open-bg: #f3f4f6;
    --role-tile-open-border: var(--color-border);
    --role-tile-filled-bg: color-mix(in srgb, var(--color-action-pressed) 10%, white);
    --role-tile-filled-border: color-mix(in srgb, var(--color-action-pressed) 45%, white);
    --role-tile-mine-bg: color-mix(in srgb, var(--color-action) 10%, white);
    --role-tile-mine-border: var(--color-action);
    /* Local stacking INSIDE a card — deliberately not the app-level --z-* ladder,
       which starts at 1020 and would lift a card's internals over the sidebar.
       The roster arrows and the progress panel share this one value on purpose:
       equal z-index means DOM order breaks the tie, and the panel comes later,
       so it covers the arrows whenever it is open. Changing this to two separate
       values re-opens that fight — see the note on .opportunities-card__roles-arrow. */
    --z-card-overlay: 1;
}

/* Card grid layout — 3 columns on desktop (sidebar closed), 2 with sidebar open */
.opportunities-card-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: var(--spacing-md);
}

@media (max-width: 1199.98px) {
    .opportunities-card-grid {
        grid-template-columns: repeat(2, 1fr);
    }
}

@media (max-width: 767.98px) {
    .opportunities-card-grid {
        grid-template-columns: 1fr;
    }
}

/* When sidebar is open, use auto-fill so column count adapts to available space.
   400px minimum is what the roster row needs to stay readable: a full-width row
   carries a role name, whoever is on it, the pay and the location at ~370px of
   usable width, and truncates the name first below that. (It used to be sized for
   a 2-up sub-grid at ~185px per tile — that layout is gone.) */
@media (min-width: 768px) {
    /* Wide screens only: a phone hides the sidebar without collapsing it, so an
       unscoped rule here beat the phone's one-column rule above and a 400px card
       ran off a 393px screen (test_phone_list_pages). */
    .global-app-layout:not(.is-sidebar-collapsed) .opportunities-card-grid {
        grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
    }
}

/* Outer wrapper — establishes the positioning context for the referrer avatar
   WITHOUT clipping its pulse glow. .opportunities-card itself keeps overflow:hidden so
   inner children honor the rounded corners. The grid item is this wrapper. */
.opportunities-card-container {
    position: relative;
    min-width: 0; /* Prevent grid blowout — inherited by .opportunities-card visual. */
}

.opportunities-card {
    background: var(--color-card-bg);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-card);
    box-shadow: var(--shadow-sm);
    overflow: hidden;
    min-width: 0;
    transition: box-shadow 0.15s ease;
}

/* Referral avatar in top-right of every booking card. The avatar lives
   OUTSIDE .opportunities-card so a future overflow:hidden on .opportunities-card can never clip it.
   PLACEMENT ONLY: the circle (radius, overflow clip) and the `img` fill rule are
   `.global-avatar` in components/avatar.css — the raw 999px that used to sit
   here was a second copy of --radius-pill, not a different value. Its tooltip
   is the shared portal bubble (accessibility.js), which needs nothing local. */
.opportunities-card-container .opportunities-card__referral-avatar {
    position: absolute;
    top: 0.4rem;
    right: 0.4rem;
    z-index: 2;
    width: 1.85rem;
    height: 1.85rem;
    min-width: 1.85rem;
    font-size: var(--font-size-2xs);
    cursor: default;
    pointer-events: auto;
    border: 2px solid var(--color-card-bg);
}

.opportunities-card__booking-code {
    color: #6b7280;
    font-weight: 500;
    font-size: var(--font-size-md);
    margin-right: var(--spacing-2xs);
}

.opportunities-card:hover {
    box-shadow: var(--shadow-md);
}

.opportunities-card__header {
    background: var(--color-card-bg);
    color: #111827;
    padding: var(--spacing-smd) var(--spacing-md);
    border-bottom: 1px solid #f3f4f6;
    border-radius: var(--radius-card) var(--radius-card) 0 0;
}

.opportunities-card__title {
    display: block;
    font-size: var(--font-size-base);
    font-weight: var(--font-weight-medium);
    line-height: 1.3;
    color: #111827;
}

.opportunities-card__subtitle {
    display: block;
    font-size: var(--font-size-sm);
    color: #6b7280;
    line-height: 1.3;
}

/* Date row — centered bold, on a NEUTRAL band.

   The band, its border and its clock were Tailwind indigo literals (#eef2ff,
   #e0e7ff, #6366f1) sitting one row under a header that draws the org's own
   colour, in an app where every tenant picks that colour — the default is wine
   and the org running this grid is green, so indigo matched nothing on either.
   Laurel 08-10: "the background color on the event time... it just doesn't really
   look good or match anything else on the platform at all."

   --color-header-band is the platform's own faint band and --color-border its own
   hairline, so the strip reads as structure — the line between who/what and the
   roster — and leaves colour to the brand and to role status. */
.opportunities-card__date-row {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--spacing-sm);
    padding: var(--spacing-sm) var(--spacing-md);
    min-height: var(--card-banner-h);
    background: var(--color-header-band);
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-medium);
    color: #374151;
    border-bottom: 1px solid var(--color-border);
}

.opportunities-card__date-row i {
    font-size: var(--font-size-md);
    color: var(--color-text-secondary);
    flex-shrink: 0;
}

/* The roster — ONE role per row, always. Fixed height (min == max == 3 rows) so the
   card keeps a uniform height AND never stretches with the role count: extra roles
   scroll inside this section, like the sidebar nav. min-height also reserves the
   footprint for the progress panel that overlays this region (inset: 0).

   Single column is not a setting any more. The 2-up option split a 400px card into
   ~185px tiles, and at that width a row can hold a name or a person, not both — the
   whole reason the tile beneath it was three different shapes. Laurel 08-10: "get rid
   of the 2 column stuff, we are not doing that anymore... which should help us be able
   to have more space for a nicer UI." The card_roles_layout preference went with it.

   Rows are a FIXED track, not minmax(..., auto). With an auto maximum the default
   content distribution stretched short rosters to fill min-height, so a two-role
   card rendered two tall blocks instead of two rows. align-content: start parks the
   rows at the top instead of spreading them. Row text is nowrap+ellipsis below, so
   nothing inside a row can outgrow the track. */
.opportunities-card__roles {
    display: grid;
    grid-template-columns: 1fr;
    grid-auto-rows: var(--role-tile-height);
    align-content: start;
    gap: var(--role-tile-gap);
    padding: var(--spacing-smd);
    min-height: var(--role-section-h);
    max-height: var(--role-section-h);
    overflow-y: auto;
    overscroll-behavior: contain;
    /* Scrollbar hidden; the hover arrows below are the affordance (sidebar nav
       convention). Firefox. */
    scrollbar-width: none;
    transition: opacity 0.2s ease;
}

/* Nobody on this opportunity yet needs no rule of its own: the section above is
   already min-height == max-height, so an empty grid holds the same space as a
   full one. That reserved height is what keeps a role-less card the same size as
   its neighbours, and it is all this case wants — no label, no centring. */

/* Chrome/Safari half of the hidden scrollbar. */
.opportunities-card__roles::-webkit-scrollbar {
    display: none;
}

/* Roster scroll arrows — the sidebar nav pattern (sidebar.css), driven by the
   same skillbridge.hoverScroll component.

   --z-card-overlay is load-bearing in BOTH directions and must stay at 1.
   Raised off auto for the UP arrow specifically: it precedes the scrolled roster
   in the DOM, and the tile that used to fill these rows was position:relative, so
   at auto it painted over the arrow and ate the hover — measured then,
   elementFromPoint at the up arrow's centre returned .opportunities-role-tile--open. The row
   that replaced that tile (.opportunities-card__role-row) is static and no longer competes,
   but the value stays: the DOWN arrow shares this rule, and the progress panel
   that overlays this region uses the same token and comes after these in the DOM,
   so the tie is what keeps the panel on top while it's open — measured too, with
   the panel present both arrow centres return .opportunities-card__progress-panel. Raising
   this past 1 re-opens that fight; dropping it to auto re-opens the first one for
   any row treatment that takes a position. */
.opportunities-card__roles-arrow {
    position: absolute;
    left: 0;
    right: 0;
    height: 28px;
    z-index: var(--z-card-overlay);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 0;
    cursor: pointer;
    color: var(--color-text-muted);
    opacity: 0.7;
    transition: opacity var(--transition-fast);
}

.opportunities-card__roles-arrow:hover {
    opacity: 1;
}

/* The arrow gradients fade INTO the card surface — keep them on the same token
   as .opportunities-card's background or the fade shows a seam. */
.opportunities-card__roles-arrow--up {
    top: 0;
    background: linear-gradient(to bottom, var(--color-card-bg) 40%, transparent);
}

.opportunities-card__roles-arrow--down {
    bottom: 0;
    background: linear-gradient(to top, var(--color-card-bg) 40%, transparent);
}

/* ==========================================================================
   Roster row (opportunities-card__role-row) — one role, one 52px row, one shape
   ==========================================================================

   Card-scoped on purpose. The .role-tile-* block this replaces is still the
   opportunity DETAIL roster's (role-tiles.css serves .opportunities-detail__role-tile too),
   which wants a taller centred tile — restyling those modifiers here would have
   moved that page along with this one.

   The shape is fixed before the content is known: rail, face, name over person,
   facts. Whatever a row has to say lands in one of those slots or is not shown,
   so a roster of eight reads as eight of the same thing. Laurel 08-10: "just
   standardize the height please, so if you are going to put the name under then
   just do that."

   Colour carries exactly one meaning here — status, on the 3px rail and its chip.
   The old tile tinted its whole surface brand-green for every filled role ("why is
   the whole thing green???"), which spent the loudest signal on the most ordinary
   state and left nothing for the roles that actually need a look. */

.opportunities-card__role-row {
    display: flex;
    align-items: center;
    gap: var(--spacing-sm);
    padding-right: var(--spacing-sm);
    border: 1px solid var(--color-border-hairline);
    border-radius: var(--radius-card);
    background: var(--color-surface-subtle);
    /* The row is a fixed 52px grid track; anything that would grow it is clipped
       rather than allowed to push the roster off its 3-row cap. */
    overflow: hidden;
    transition: background-color 0.15s, border-color 0.15s;
}

/* Your own role. The one tint left on the row, and it says whose it is, not what
   state it is in — status stays on the rail. On My Assignments that is normally a
   single row per card, so it reads as "this one is yours" instead of as a colour
   the whole roster shares. */
.opportunities-card__role-row--mine {
    background: var(--role-tile-mine-bg);
    border-color: var(--role-tile-mine-border);
}

/* The name's ::after stretches its link over the row;
   the pill, the "+N" and directions paint above it by DOM order. */
.opportunities-card__role-row[data-linked] {
    position: relative;
}

.opportunities-card__role-link {
    color: inherit;
    text-decoration: none;
}

.opportunities-card__role-link::after {
    content: "";
    position: absolute;
    inset: 0;
}

.opportunities-card__role-row[data-linked] .opportunities-card__role-pay--interactive,
.opportunities-card__role-row[data-linked] .opportunities-card__role-skill--more,
.opportunities-card__role-row[data-linked] a.opportunities-card__role-mode {
    position: relative;
}

.opportunities-card__role-row[data-linked]:hover {
    border-color: var(--color-border);
}

.opportunities-card__role-row--mine[data-linked]:hover {
    background: color-mix(in srgb, var(--color-action) 18%, white);
    border-color: var(--color-action);
}

/* Status, as a rail rather than a fill: the same information the old tile shouted
   with its background, in 3px at the edge where the eye can run down it. */
.opportunities-card__role-rail {
    width: 3px;
    align-self: stretch;
    flex-shrink: 0;
    background: var(--color-border);
}

.opportunities-card__role-row--assigned .opportunities-card__role-rail,
.opportunities-card__role-row--completed .opportunities-card__role-rail {
    background: var(--color-success);
}

.opportunities-card__role-row--invited .opportunities-card__role-rail,
.opportunities-card__role-row--pending .opportunities-card__role-rail {
    background: var(--color-warning);
}

.opportunities-card__role-row--declined .opportunities-card__role-rail,
.opportunities-card__role-row--no_show .opportunities-card__role-rail {
    background: var(--color-error);
}

/* The shift is over but the files are not in — the one row on a finished booking
   that is still asking for something, so it is the one that keeps a colour. Must
   stay BELOW the --completed rule above: same specificity, and this row carries
   both classes, so source order is what decides.

   The same amber as invited/pending on purpose, not by accident: on this rail
   amber has always meant "waiting on somebody", and a fourth hue would make the
   roster a legend to memorise. The two are told apart by the rest of the row —
   an invited row carries its chip and a name-less seat, a files-due row carries
   neither. */
.opportunities-card__role-row--files-due .opportunities-card__role-rail {
    background: var(--color-warning);
}

/* Finished work recedes. Everything on a settled row steps back together — rail
   included — so a roster of eight reads as "these two still want something" at a
   glance instead of eight rows of equal weight. Laurel 08-10: "some kind of
   indicator or something showing whether or not the work has been completed."
   Hover restores it, which is what keeps the fade reading as a decision rather
   than as a row that failed to load. */
.opportunities-card__role-row--settled {
    opacity: 0.62;
}

.opportunities-card__role-row--settled[data-linked]:hover {
    opacity: 1;
}

/* Positioning context for the tick, and the row's left offset with it. This is
   what retired .opportunities-card__role-avatar: that class existed only to hold the
   margin, and the badge overhangs the circle, so the circle can no longer be the
   element that owns the spacing. */
.opportunities-card__role-face {
    position: relative;
    display: inline-flex;
    flex-shrink: 0;
    margin-left: var(--spacing-xsm);
}

/* Done, as a mark on the face. Its own disc rather than bi-check-circle-fill,
   whose knocked-out check would show the avatar through it. */
.opportunities-card__role-tick {
    position: absolute;
    right: -1px;
    bottom: -1px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 14px;
    height: 14px;
    border-radius: var(--radius-pill);
    background: var(--color-success);
    color: var(--color-card-bg);
    font-size: var(--font-size-2xs);
    line-height: 1;
}

/* Name over person. min-width: 0 is what lets the ellipsis below actually fire —
   a flex item defaults to min-content and would push the facts column off the row
   instead of truncating. */
.opportunities-card__role-who {
    display: flex;
    flex-direction: column;
    justify-content: center;
    flex: 1;
    min-width: 0;
}

.opportunities-card__role-name {
    font-size: var(--font-size-xs);
    font-weight: var(--font-weight-medium);
    color: var(--color-text);
    line-height: 1.3;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.opportunities-card__role-line {
    display: flex;
    align-items: center;
    gap: var(--spacing-xsm);
    min-width: 0;
    line-height: 1.3;
}

.opportunities-card__role-person {
    font-size: var(--font-size-2xs);
    color: var(--color-text-muted-cool);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Nobody on it yet — quieter than a name, because an empty seat is a fact, not a
   person. Laurel 08-10: it says "Unassigned", not "Nobody yet".

   It is also the one label on this line that must never clip. A worker's name
   truncates gracefully because the first half still identifies them; "Unassig…"
   is one word losing its ending and reads as a rendering fault. So this holds its
   width and the requirement pills beside it give theirs up instead. */
.opportunities-card__role-person--none {
    color: var(--color-text-muted);
    flex-shrink: 0;
}

/* The chip only appears on a row that wants something: open, invited, pending,
   declined, no-show. Assigned and completed carry a green rail and no chip. */
.opportunities-card__role-chip {
    flex-shrink: 0;
    padding: 0 var(--spacing-xsm);
    border-radius: var(--radius-pill);
    font-size: var(--font-size-2xs);
    line-height: 1.5;
    white-space: nowrap;
    background: var(--color-grey-100);
    color: var(--color-grey-700);
}

.opportunities-card__role-chip--invited,
.opportunities-card__role-chip--pending {
    background: var(--color-warning-bg);
    color: var(--color-warning-text);
}

.opportunities-card__role-chip--declined,
.opportunities-card__role-chip--no_show {
    background: var(--color-error-subtle);
    color: var(--color-error);
}

/* What an empty seat is asking for. Same pill geometry as the status chip and
   deliberately the same neutral fill: a requirement is a fact about the role, not
   a state to act on, so it must not compete with the invited/declined chips that
   are.

   The only thing on this line that gives up width, and the last resort rather
   than the plan: the template ships ONE pill precisely so the common case has
   room to render whole. What this handles is the outlier name — the cap holds it
   to 7rem, and min-width: 0 lets it clip instead of pushing "+2" or the pay off
   the row. The seat label and the "+N" hold their size against it. */
.opportunities-card__role-skill {
    display: inline-block;
    min-width: 0;
    max-width: 7rem;
    padding: 0 var(--spacing-xsm);
    border-radius: var(--radius-pill);
    font-size: var(--font-size-2xs);
    line-height: 1.5;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    background: var(--color-grey-100);
    color: var(--color-grey-700);
}

/* "+2" names the rest rather than being a third requirement — no fill, so it does
   not read as one more thing the seat wants, and the help cursor says the names
   are one hover away instead of one page away. Never shrinks: a clipped count is
   a lie about how much is hidden, where a clipped skill name still reads as the
   name it is. */
.opportunities-card__role-skill--more {
    flex-shrink: 0;
    padding-left: var(--spacing-2xs);
    padding-right: var(--spacing-2xs);
    background: transparent;
    color: var(--color-text-muted);
    cursor: help;
}

/* Right column: the two facts that change a decision — what it pays, and when it
   is due. Right-aligned so the numbers stack into a column the eye can scan
   down, which is the whole point on a booking whose seats differ by date alone.
   Where the seat is moved to the person's line to make room. */
.opportunities-card__role-facts {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    justify-content: center;
    flex-shrink: 0;
    text-align: right;
}

.opportunities-card__role-pay {
    font-size: var(--font-size-xs);
    font-weight: var(--font-weight-medium);
    color: var(--color-text);
    font-variant-numeric: tabular-nums;
    line-height: 1.3;
}

.opportunities-card__role-pay--interactive {
    cursor: pointer;
}

.opportunities-card__role-pay--interactive:hover {
    color: var(--color-action);
}

/* When the seat's own work is due, under the pay. It is the one fact that tells
   two seats of the same role apart, and until now it was on the Billing page and
   nowhere on the booking: twelve rows reading "Video Editor / Unassigned / $200"
   with no way to say which was which. Tabular figures so the dates stack into a
   column under the money that already does. */
.opportunities-card__role-due {
    font-size: var(--font-size-2xs);
    font-weight: var(--font-weight-medium);
    color: var(--color-text-muted-cool);
    font-variant-numeric: tabular-nums;
    line-height: 1.3;
    white-space: nowrap;
}

/* Past its date with the files still out — the same amber the rail has already
   gone, on the fact that says why. */
.opportunities-card__role-due--late {
    color: var(--color-warning-text);
}

/* Where the seat is, on the person's line: the facts column's second slot is the
   due date's now. Never shrinks, so the requirement pill beside it stays the one
   thing on the line that gives up width. */
.opportunities-card__role-mode {
    display: inline-flex;
    align-items: center;
    gap: var(--spacing-2xs);
    flex-shrink: 0;
    font-size: var(--font-size-2xs);
    color: var(--color-text-muted-cool);
    line-height: 1.3;
    white-space: nowrap;
    text-decoration: none;
}

a.opportunities-card__role-mode:hover {
    color: var(--color-action);
}

.opportunities-card__role-mode i {
    font-size: var(--font-size-2xs);
}

.opportunities-property-chip {
    display: inline-flex;
    align-items: center;
    gap: var(--spacing-xs);
    font-size: var(--font-size-2xs);
    padding: var(--spacing-2xs) var(--spacing-sm);
    border-radius: 999px;
    background: var(--bs-gray-100);
    border: 1px solid var(--bs-gray-200);
    white-space: nowrap;
}

.opportunities-property-filter-option {
    display: flex;
    align-items: center;
    gap: var(--spacing-xsm);
    font-size: var(--font-size-sm);
    color: var(--bs-gray-700);
    cursor: pointer;
    padding: var(--spacing-2xs) 0;
}

.opportunities-property-filter-option input[type="checkbox"] {
    cursor: pointer;
    accent-color: var(--color-action);
}

.opportunities-card__progress-toggle {
    display: flex;
    align-items: center;
    gap: var(--spacing-sm);
    width: 100%;
    padding: var(--spacing-sm) var(--spacing-md);
    background: #1e293b;
    color: #fff;
    border: none;
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-button);
    cursor: pointer;
    transition: background-color 0.15s;
    flex-wrap: nowrap;
    overflow: hidden;
}

.opportunities-card__progress-toggle:hover {
    background: #334155;
}

.opportunities-card__stage-name {
    font-weight: var(--font-weight-medium);
    white-space: nowrap;
    flex-shrink: 0;
}

.opportunities-card__stage-dots {
    display: flex;
    align-items: center;
    flex: 1;
    justify-content: flex-end;
    flex-wrap: nowrap;
    min-width: 0;
}

.opportunities-card__dot {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    border: 2px solid var(--color-border);
    background: transparent;
    flex-shrink: 0;
}

.opportunities-card__dot--completed {
    background: #10b981;
    border-color: #10b981;
}

.opportunities-card__dot--active {
    background: var(--color-action);
    border-color: var(--color-action);
    animation: card-dot-pulse 1.5s ease-in-out infinite;
}

.opportunities-card__dot--pending {
    /* transparent with faint border — default state */
}

.opportunities-card__dot-connector {
    width: 12px;
    height: 2px;
    background: var(--color-border);
    flex-shrink: 0;
}

.opportunities-card__dot-connector--completed {
    background: #10b981;
}

.opportunities-card__dot-connector--active {
    background: linear-gradient(90deg, #10b981, var(--color-action));
}

@keyframes card-dot-pulse {
    0%, 100% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.4); }
    50% { box-shadow: 0 0 0 3px rgba(99, 102, 241, 0); }
}

.opportunities-card__roles-wrap {
    position: relative;
}

.opportunities-card__roles.is-hidden {
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.2s ease;
}

/* Progress panel — overlays the roles area. Shares --z-card-overlay with the
   roster arrows so DOM order decides, and this comes later. */
.opportunities-card__progress-panel {
    position: absolute;
    inset: 0;
    padding: var(--spacing-smd) var(--spacing-md);
    background: #f8fafc;
    overflow-y: auto;
    overscroll-behavior: contain;
    z-index: var(--z-card-overlay);
    display: flex;
    flex-direction: column;
}

/* With the team field switched off for the org there is no roles section under the
   panel, so there is no reserved height to overlay and an absolute panel would have
   nothing to sit on — it flows normally instead. This used to be the no-roles case
   too; since 08-10 a role-less card renders an empty roles region, so the panel has
   a sibling there and keeps overlaying it. */
.opportunities-card__roles-wrap > .opportunities-card__progress-panel:only-child {
    position: relative;
}

/* Transition classes for Alpine x-transition */
.opportunities-progress-enter,
.opportunities-progress-leave {
    transition: opacity 0.25s ease, transform 0.25s ease;
}

.opportunities-progress-enter-from {
    opacity: 0;
    transform: translateY(-8px);
}

.opportunities-progress-enter-to {
    opacity: 1;
    transform: translateY(0);
}

.opportunities-card__progress-list {
    list-style: none;
    margin: 0;
    padding: 0;
}

.opportunities-card__progress-item {
    display: flex;
    align-items: center;
    gap: var(--spacing-sm);
    padding: var(--spacing-xs) 0;
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
    flex-wrap: nowrap;
}

.opportunities-card__milestone-name {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.opportunities-card__milestone-actions {
    display: flex;
    gap: var(--spacing-xs);
    flex-shrink: 0;
    margin-left: auto;
}

.opportunities-card__milestone-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    border: 1px solid var(--color-border);
    background: var(--color-card-bg);
    color: #6b7280;
    font-size: var(--font-size-2xs);
    cursor: pointer;
    padding: 0;
    transition: all 0.15s ease;
}

.opportunities-card__milestone-btn:hover {
    border-color: var(--color-action);
    color: var(--color-action);
    background: color-mix(in srgb, var(--color-action) 8%, white);
}

.opportunities-card__milestone-btn--skip:hover {
    border-color: var(--color-text-muted);
    color: var(--color-text-muted);
    background: var(--color-grey-50);
}

.opportunities-card__milestone-btn.is-loading {
    opacity: 0.5;
    pointer-events: none;
}

.opportunities-card__milestone-btn.is-done {
    border-color: #10b981;
    background: #10b981;
    color: #fff;
}

.opportunities-card__progress-dot {
    display: inline-block;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: var(--color-border);
    flex-shrink: 0;
}

.opportunities-card__milestone--completed { color: #374151; }
.opportunities-card__milestone--completed .opportunities-card__progress-dot { background: #10b981; }

.opportunities-card__milestone--in_progress { color: #374151; }
.opportunities-card__milestone--in_progress .opportunities-card__progress-dot { background: var(--color-action); }

.opportunities-card__milestone--scheduled { color: #374151; }
.opportunities-card__milestone--scheduled .opportunities-card__progress-dot { background: var(--color-warning); }

.opportunities-card__milestone--skipped { color: var(--color-text-muted); text-decoration: line-through; }
.opportunities-card__milestone--skipped .opportunities-card__progress-dot { background: var(--color-border); }

.opportunities-card__milestone--pending { /* default gray */ }

.opportunities-card__progress-empty {
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
    font-style: italic;
    padding: var(--spacing-sm) 0;
}

/* The wrapper is a CSS hook and nothing else, so it steps out of layout. The
   header's toolbar slot (.global-page-header__toolbar, page-header.css) is a
   wrapping flex row, and the shared .global-toolbar inside fills it with
   `flex: 1 1 100%` (toolbar.css) — a rule that only reaches a DIRECT flex
   child. Left as a plain block flex item, this wrapper shrank to its
   content on desktop (559px of a 1175px slot): Search stopped filling the
   line and the gear's margin-left:auto had no slack, so Filter, Sort by and
   the gear bunched up beside Search ("looks fucking different and terrible
   on desktop" — Nick, 09-17). With display: contents the shared row is the
   slot's flex child again, exactly as on the four other dashboards, and the
   descendant selectors below still match through it. */
.opportunities-toolbar-row {
    display: contents;
}

/* ── Phone: the settings/filters drawer opens like desktop, never a sheet ───
   drawer.css turns every .global-drawer-panel into a bottom sheet under
   767.98px (drawer.js tags it .global-drawer-panel--sheet while that query
   matches) — the right call for an ephemeral drawer, wrong for this page,
   whose phone drawers are the desktop's own (Nick, 10-01: "much more similar
   to the desktop").

   .opportunities-drawer-panel marks the dock panels in _opps_view_controls.html
   (the settings drawer plus its Filters / Who sees it / kind-settings /
   card-settings steps). Restating the box below drops the sheet's bottom-anchored
   inset/transform back to the OVERLAY-mode (<1200px) shape drawer.css already
   ships for tablet width — same right edge, same slide-in-from-the-right
   transform, just narrowed to the phone's own width (min(--drawer-width,
   100%)) and still clearing the bottom bar (--bottom-bar-h, tokens.css) so it
   never covers it. Equal specificity to the .global-drawer-panel rules it
   replaces, so this only wins by loading after drawer.css (base.html) — the
   other four dashboards sharing the same dock shell never carry this class
   and keep their sheet. */
@media (max-width: 767.98px) {
    .opportunities-drawer-panel {
        inset: var(--drawer-top) 0 var(--bottom-bar-h) auto;
        width: min(var(--drawer-width), 100%);
        margin-inline: 0;
        max-height: none;
        border-radius: var(--radius-none);
        transform: translateX(100%);
        /* No visibility rule here on purpose: drawer.css's own phone block
           already carries .global-drawer-panel { visibility: hidden } (closed)
           and .is-open { visibility: visible } — the same hidden-until-open
           behavior the overlay-mode (<1200px) shape uses via transform alone,
           and test_the_page_fits_a_phone's closed-sheet check reads it. Only
           the axis changes (translateX here vs. drawer.css's translateY), so
           layering under the existing visibility rule is exactly right. */
    }

    .opportunities-drawer-panel.is-open {
        transform: translateX(0);
    }
}

/* ── Phone: no bottom-sheet grab handle on a drawer that opens from the
   right ── drawer.css's own phone block gives every .global-drawer-head a
   ::before grab-handle pill plus extra padding-top to make room for it
   (drawer.css, its "drag-handle pill" rule) — sheet-only chrome for a
   panel a user drags DOWN to dismiss. This page's settings/filters drawer
   slides in from the right instead (the box above), so a drag-down handle
   promises a gesture the panel does not have; it read as debris left over
   from the bottom sheet. Hide the pill and drop the reserved space back to
   the drawer's normal padding-top (var(--spacing-md) — the same value
   drawer.css's own non-phone .global-drawer-head rule uses), so the head
   reads exactly like the desktop drawer's.

   Scoped to .opportunities-drawer-panel .global-drawer-head (two classes)
   so it outranks drawer.css's bare .global-drawer-head phone rule (one
   class) regardless of link order — the same reason the box above restates
   its own properties instead of leaning on load order alone. */
@media (max-width: 767.98px) {
    .opportunities-drawer-panel .global-drawer-head {
        padding-top: var(--spacing-md);
    }

    .opportunities-drawer-panel .global-drawer-head::before {
        display: none;
    }
}

