/* ---------------------------------------------------------------------------
   Payrolla shared components — the one implementation of the interface
   primitives, loaded by BOTH shells on EVERY page:

     base.html        (operator app — Bootstrap + styles.css)
     client/base.html (tenant portal — portal.css, no Bootstrap)

   This layer exists because there was nowhere for a genuinely shared component
   to live: tokens.css is values only (it says so), styles.css is operator-side,
   portal.css is tenant-side, and dashboard.css is page-scoped to the two
   dashboards — which is where the empty/loading/error states were stranded,
   unusable by the other ~30 authenticated pages that needed them.

   Rules here must work in both shells, so they lean on tokens.css variables and
   never assume Bootstrap. Markup lives in templates/macros/ui.html.
   --------------------------------------------------------------------------- */

/* --- Icons ----------------------------------------------------------------
   The one rule the shared inline-SVG set (macros/ui.html::icon) needs. It used
   to live in dashboard.css, which only the two DASHBOARDS load — fine while the
   icons were a dashboard component, wrong now that the tenant shell draws them
   in its navigation on every page. Here it ships with the macro that emits it. */
.ico { flex: none; vertical-align: middle; }

/* --- Empty / loading / error states -------------------------------------- */

.state {
    align-items: center;
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 26px 18px;
    text-align: center;
}

.state-ico { color: var(--muted); line-height: 0; }
.state-title { font-size: 13.5px; font-weight: 700; margin: 0; }
.state-detail { color: var(--muted); font-size: 12.5px; line-height: 1.55; margin: 0; max-width: 42ch; }

/* --- The state's own button ----------------------------------------------
   A state is a SHARED component, so it owns the whole appearance of the button
   inside it rather than inheriting whichever `.btn` the surrounding shell
   happens to define. It previously set only `margin-top`, and macros/ui.html
   emits `class="btn primary"` — the TENANT portal's vocabulary. The operator
   shell spells its modifiers Bootstrap-style (`.btn-primary`), so `.primary`
   meant nothing there and every empty state on ~30 operator pages rendered its
   one call to action as a default Bootstrap button. The operator DASHBOARD had
   a compensating rule in admin-dashboard.css; no other operator page did.

   Defining it once here fixes all of them and deletes the compensation. Two
   classes of specificity, so it wins over both `.btn` owners regardless of load
   order, and it is Bootstrap-free as this file requires. The look is the
   tenant portal's existing primary button, so nothing changes for tenants. */
.state .btn {
    background: var(--surface);
    /* --line-control, not --line-strong: this outline is the only thing that
       says the state's call to action is a button, and --line-strong is 1.48:1
       on white — below the 3:1 WCAG 1.4.11 requires of a boundary that
       identifies a control. See the token's note in tokens.css. */
    border: 1px solid var(--line-control);
    border-radius: 10px;
    color: var(--ink);
    cursor: pointer;
    display: inline-block;
    font: inherit;
    font-size: 14px;
    font-weight: 700;
    margin-top: 6px;
    padding: 8px 15px;
    text-decoration: none;
    transition: background var(--ease), border-color var(--ease), color var(--ease),
                transform 160ms var(--ease-out);
}

.state .btn:hover { border-color: var(--brand); color: var(--brand); }
/* The press snaps, the release settles — same asymmetry as portal.css's button. */
.state .btn:active { transform: scale(0.97); transition-duration: 60ms; }

.state .btn.primary {
    background: var(--brand-deep);
    border-color: var(--brand-deep);
    color: #fff;
}

.state .btn.primary:hover { background: var(--brand-dark); border-color: var(--brand-dark); color: #fff; }

.state--empty .state-ico { color: var(--line-strong); }
.state--error .state-ico { color: var(--danger); }

/* Skeleton: matches the height of what it replaces, so an async swap cannot
   shift the layout. Shimmer travels rather than pulsing — a pulsing block at
   0.2Hz is exactly the frequency reduced-motion exists to suppress. */
.state--loading { align-items: stretch; gap: 10px; padding: 4px 0; }

.sk-line {
    background: linear-gradient(90deg, var(--surface-alt) 25%, var(--shell) 37%, var(--surface-alt) 63%);
    background-size: 400% 100%;
    border-radius: var(--radius-sm);
    display: block;
    height: 13px;
}

.sk-line:nth-child(even) { width: 72%; }

@media (prefers-reduced-motion: no-preference) {
    .sk-line { animation: sk-shimmer 1.4s linear infinite; }
}

@keyframes sk-shimmer {
    from { background-position: 100% 0; }
    to { background-position: 0 0; }
}

.sr-only {
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    height: 1px;
    overflow: hidden;
    position: absolute;
    white-space: nowrap;
    width: 1px;
}

/* --- Data table ------------------------------------------------------------
   The one table. Server-paginated, server-filtered, server-sorted — a table that
   hides rows in the browser while the server paginates will confidently show
   "no results" for a row sitting on page 2, which is the defect this replaces.
   Horizontal overflow is owned by the wrapper so a wide table scrolls itself
   instead of the page. */

.dt-wrap { overflow-x: auto; width: 100%; }

.dt {
    border-collapse: collapse;
    font-size: 13.5px;
    width: 100%;
}

.dt th,
.dt td {
    border-bottom: 1px solid var(--line);
    padding: 9px 12px;
    text-align: left;
    vertical-align: middle;
}

/* Column rules. A payroll grid is ten columns of numbers that differ only in
   magnitude — GROSS, PAYE, SSNIT, DEDUCTIONS, NET PAY — and with horizontal
   rules alone the reader has to work out where one column ends and the next
   begins, on every row. The hairline is the same --line as the row rule, so it
   is quiet enough to disappear when reading ACROSS a row and present enough to
   hold a column together when scanning DOWN it. Drawn with `+` rather than a
   blanket border-left so no rule lands on the table's outer edge. */
.dt th + th,
.dt td + td { border-left: 1px solid var(--line); }

.dt thead th {
    background: var(--surface-alt);
    color: var(--muted);
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.02em;
    position: sticky;
    text-transform: uppercase;
    top: 0;
    white-space: nowrap;
    z-index: 1;
}

.dt tbody tr:hover { background: var(--surface-alt); }
.dt .num, .dt .nums { font-variant-numeric: tabular-nums; text-align: right; }

/* --- The actions column ------------------------------------------------------
   Pinned to the right edge, so the row's buttons are the one thing a horizontal
   scroll can never hide.

   This is the rule admin-dashboard.css already states for its own table — "a
   count of work waiting on the operator must not be the thing a scroll hides" —
   applied where it matters more, because here the cell holds Approve and Reject
   rather than a number. The payroll runs list is ten columns wide and clears
   1440px by about thirty pixels; every column added since has been paid for out
   of the right-hand edge, and the right-hand edge is where the buttons are. A
   reviewer who cannot see Approve without scrolling sideways will open the run
   instead, which is the round trip the in-place queue exists to remove.

   Sticky needs an opaque background or the scrolled cells show through it, and
   the shadow is what says "there is more table under this". The divider is a
   shadow rather than the border-left below because Chrome does not paint
   collapsed borders on a sticky cell. */
.dt-actions {
    background: var(--surface);
    box-shadow: inset 1px 0 0 var(--line), -6px 0 10px -8px rgba(15, 23, 42, 0.28);
    position: sticky;
    right: 0;
    text-align: right;
    white-space: nowrap;
}

/* `td` qualified, and that is the whole point of this rule: .dt-actions above
   asks for text-align: right, but `.dt td` sets `left` at one class MORE
   specificity, so it won every time and the buttons sat left under a
   right-aligned "Actions" header. Affects every .dt with an actions column —
   the runs queue and the risk queue as well as the client list. */
.dt td.dt-actions { text-align: right; }

.dt tbody tr:hover .dt-actions { background: var(--surface-alt); }

/* The header cell above it is sticky in BOTH directions — it already sticks to
   the top, and z-index keeps it over the body cells that now share its corner.
   Scoped with :has() because not every last column is an actions column: the
   run detail's rows table ends in Payslip, a plain cell, and pinning its HEADER
   while its body cells scrolled away underneath would be worse than pinning
   neither. (:has() is already load-bearing in admin-dashboard.css.) */
.dt:has(.dt-actions) thead th:last-child {
    position: sticky;
    right: 0;
    z-index: 2;
}

/* Unread notification rows. Lived in portal.css, so the operator inbox — which
   renders the same shared macro — showed no weight difference at all. */
.dt tr.is-unread { font-weight: 600; }

/* A table caption used for the one sentence a reader needs to trust the data in
   it — relocated instructional copy (Task 3.1), not decoration. Left-aligned and
   quiet: it explains the table, it does not title it. */
.dt-note, table > caption.dt-note {
    caption-side: top;
    color: var(--muted);
    font-size: 12.5px;
    line-height: 1.5;
    padding: 0 0 8px;
    text-align: left;
}

/* An identifier cell — a company code, a reference — set in the mono face
   because it is a string you compare character by character rather than read.
   It exists because the markup reached for `<code>`, which Bootstrap paints
   magenta: in the operator shell that put the loudest colour on the page on
   the least important column, and pink is in neither shell's palette. Ink at
   --muted, tracking opened slightly so a run of capitals stays countable. */
.dt-code {
    color: var(--muted);
    font-family: var(--font-sans);
    font-variant-numeric: tabular-nums;
    font-size: 12.5px;
    letter-spacing: 0.02em;
}

/* A cell with nothing in it. Same muted ink as .dt-code so an em dash and an
   identifier do not read as two different kinds of absence. */
.dt-muted { color: var(--muted); }

/* Sortable headers carry the direction in the link itself, so sort state is a
   URL the user can copy, not hidden client state. */
.dt-sort { color: inherit; display: inline-flex; align-items: center; gap: 4px; text-decoration: none; }
.dt-sort:hover { color: var(--brand-deep); }
.dt-sort-ind { opacity: 0.45; }
.dt-sort[aria-sort] .dt-sort-ind, .dt-sort.is-active .dt-sort-ind { opacity: 1; }

/* --- Run status cell ---------------------------------------------------------
   Three lines, three facts: what state the run is in, how far along it is, and
   what comes next (macros/lifecycle.html::status_cell).

   What it replaces: a coloured pill with seven 9px dots crammed under it, the
   dots' labels switched off with `font-size: 0` because a ~110px table column
   had no room for them. That cell asked the reader to know the stage order by
   heart and then count — and never answered the question a queue is scanned
   for, which is what happens if I act on this row.

   Sized to be legible at a glance from a list: the pill is the loudest thing in
   the cell, the bar is a shape cue, and the caption is quiet but readable
   (12px, not the 11px the labels were shrunk to before they were dropped). */

/* 118px is the pill's own natural width at the longest status ("Pending
   Approval"), so the cell costs the table nothing the status word did not
   already cost it. */
.rs { display: grid; gap: 4px; justify-items: start; min-width: 118px; }

.rs-pill {
    border-radius: 999px;
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.01em;
    line-height: 1.35;
    padding: 3px 10px;
    white-space: nowrap;
    /* The default is the `muted` tone; every other tone overrides both. */
    background: var(--surface-alt);
    color: var(--ink-soft);
}

.rs-pill--info { background: var(--brand-soft); color: var(--brand-deep); }
.rs-pill--warn { background: var(--warn-soft); color: #7a4b06; }
.rs-pill--ok { background: var(--green-soft); color: #14532d; }
.rs-pill--brand { background: var(--brand-soft); color: var(--brand-deep); }
.rs-pill--danger { background: var(--danger-soft); color: #b02a37; }

/* One segment per stage that applies to this run, gapped so the count is
   readable as a count and filled so the proportion is readable without one. */
.rs-track { display: flex; gap: 3px; width: 100%; }

.rs-seg {
    background: var(--line);
    border-radius: 2px;
    flex: 1 1 0;
    height: 4px;
    min-width: 8px;
}

.rs-seg--done { background: var(--brand); }
/* The current stage is the one the reader is being asked about, so it is the
   only segment that is neither plainly done nor plainly waiting: brand-coloured
   like the completed run behind it, and ringed so it reads as the edge of the
   progress rather than part of it. */
.rs-seg--current { background: var(--brand); box-shadow: 0 0 0 2px var(--brand-soft); }

.rs-meta, .rs-next { color: var(--muted); font-size: 11.5px; line-height: 1.35; }
/* The next stage is the one thing here a reviewer acts on, so it carries the
   ink — the position above it is context. */
.rs-next { color: var(--ink-soft); font-weight: 600; }

/* --- Pagination ----------------------------------------------------------- */

.dt-foot {
    align-items: center;
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    justify-content: space-between;
    padding: 10px 2px 0;
}

.dt-count { color: var(--muted); font-size: 12.5px; }
.dt-pages { display: flex; gap: 6px; }
.dt-page { border: 1px solid var(--line); border-radius: var(--radius-sm); color: inherit;
           padding: 4px 10px; text-decoration: none; font-size: 12.5px; }
.dt-page:hover { border-color: var(--line-strong); }
.dt-page[aria-disabled="true"] { opacity: 0.45; pointer-events: none; }
.dt-page.is-current { background: var(--brand-soft); border-color: var(--brand); color: var(--brand-deep); font-weight: 700; }

/* --- Decision header -------------------------------------------------------
   Subject, state, the ONE recommended next action, and the few figures that
   justify it. Everything else on a decision page goes one interaction deeper. */

.dh { border-bottom: 1px solid var(--line); display: grid; gap: 14px; padding-bottom: 16px; }
.dh-top { align-items: flex-start; display: flex; flex-wrap: wrap; gap: 12px; justify-content: space-between; }
.dh-subject { display: grid; gap: 4px; }
.dh-title { font-size: 19px; font-weight: 700; margin: 0; }
.dh-sub { color: var(--muted); font-size: 13px; margin: 0; }
.dh-why { color: var(--muted); font-size: 13px; margin: 0; max-width: 62ch; }

/* The figures take the header's full width instead of bunching against its left
   edge. As a flex row with a fixed 22px gap, five figures occupied about a
   third of a desktop header and read as one dense block — WORKERS 33 GROSS PAY
   GH₵ 96,063.63 DEDUCTIONS GH₵ 15,354.73 — leaving the reader to separate the
   labels from the amounts themselves. Equal auto-fit columns give each figure
   its own share of the width, so the grouping is done by space, and the strip
   is scannable left to right. The hairline above sets it off as the header's
   evidence rather than more of its prose. */
.dh-figures {
    border-top: 1px solid var(--line);
    display: grid;
    gap: 16px 24px;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    margin: 0;
    padding-top: 14px;
}
.dh-figure { display: grid; gap: 3px; min-width: 0; }
.dh-figure dt { color: var(--muted); font-size: 11.5px; letter-spacing: 0.02em; text-transform: uppercase; }
.dh-figure dd { font-size: 17px; font-weight: 700; margin: 0; font-variant-numeric: tabular-nums; }

/* --- Action bar ------------------------------------------------------------
   Exactly one visually primary action per screen: the recommended one. Everything
   else is secondary or folded behind "More", so rank is expressed by appearance
   and not left to the reader. */

.ab { align-items: center; display: flex; flex-wrap: wrap; gap: 8px; }
.ab-more { margin-left: auto; position: relative; }
.ab-more > summary { cursor: pointer; list-style: none; }
.ab-more > summary::-webkit-details-marker { display: none; }

/* --- The action bar's own buttons -------------------------------------------
   Same defect as `.state .btn` above, same fix, same reason.

   macros/ui.html::action_bar emits `class="btn"` / `"btn primary"` /
   `"btn danger"` — the TENANT portal's vocabulary, which portal.css defines in
   full. The operator shell's `.btn` is Bootstrap's, whose base class carries
   padding and a pointer and nothing else: no border, no background, no fill.
   So on every operator decision page the ranked actions — Run risk check,
   Release hold, Edit figures, More — rendered as four pieces of bold text in a
   row, with nothing on screen to say they were controls at all.

   The action bar is a SHARED component, so it owns the whole appearance of the
   controls inside it rather than inheriting whichever `.btn` the surrounding
   shell happens to define.

   Two selectors rather than one, because the bar nests: decision_header wraps
   action_bar's own `.ab` in a second one, and a POST action sits inside its own
   <form>. `.ab > .btn` catches the flat case; `.ab :not(.ab-menu) > .btn`
   catches every wrapped one — and excludes the overflow items, which carry
   `.btn` too but are menu rows and keep the flat treatment defined below.

   `.dt-actions` is here for the identical reason: the risk queue's Review link
   is a bare `class="btn"` in a table cell, and it was the same invisible
   control. Bootstrap-variant buttons (`btn-sm btn-outline-primary`, as on the
   Payslip column) are excluded — those already say what they are. */
.ab > .btn,
.ab :not(.ab-menu) > .btn,
.dt-actions .btn:not([class*="btn-"]) {
    align-items: center;
    background: var(--surface);
    /* --line-control, not --line-strong: this outline is the only thing that
       identifies the control, and --line-strong is 1.48:1 on white — below the
       3:1 WCAG 1.4.11 requires. See the token's note in tokens.css. */
    border: 1px solid var(--line-control);
    border-radius: 10px;
    color: var(--ink);
    cursor: pointer;
    display: inline-flex;
    font: inherit;
    font-size: 13.5px;
    font-weight: 700;
    gap: 6px;
    line-height: 1.25;
    padding: 8px 15px;
    text-decoration: none;
    white-space: nowrap;
    transition: background var(--ease), border-color var(--ease), color var(--ease),
                transform 160ms var(--ease-out);
}

.ab > .btn:hover,
.ab :not(.ab-menu) > .btn:hover,
.dt-actions .btn:not([class*="btn-"]):hover { border-color: var(--brand); color: var(--brand); }

/* The press snaps, the release settles — same asymmetry as portal.css's button. */
.ab > .btn:active,
.ab :not(.ab-menu) > .btn:active,
.dt-actions .btn:not([class*="btn-"]):active { transform: scale(0.97); transition-duration: 60ms; }

/* Exactly one primary per bar: the recommended action. */
.ab > .btn.primary,
.ab :not(.ab-menu) > .btn.primary {
    background: var(--brand-deep);
    border-color: var(--brand-deep);
    color: #fff;
}

.ab > .btn.primary:hover,
.ab :not(.ab-menu) > .btn.primary:hover {
    background: var(--brand-dark);
    border-color: var(--brand-dark);
    color: #fff;
}

.ab > .btn.danger,
.ab :not(.ab-menu) > .btn.danger { border-color: #f1c0c5; color: #b02a37; }

.ab > .btn.danger:hover,
.ab :not(.ab-menu) > .btn.danger:hover {
    background: var(--danger-soft);
    border-color: var(--danger);
    color: #b02a37;
}

/* A POST action is wrapped in its own <form> (macros/ui.html::_action_control),
   which is a flex item of the bar and must not add height or margin of its own. */
.ab form { display: flex; margin: 0; }
.ab-menu {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-md, 0 8px 24px rgba(0,0,0,.12));
    display: grid;
    min-width: 210px;
    padding: 6px;
    position: absolute;
    right: 0;
    top: calc(100% + 6px);
    z-index: 20;
}

/* --- Popover entrance (SHARED) ---------------------------------------------
   Both shells load this file, so the keyframe is defined once here and the
   tenant portal's utility menus reference it from portal.css. Keyframe names
   are global to the document regardless of which stylesheet declares them; if
   components.css ever stops loading in a shell, those menus simply appear with
   no animation, which is the old behaviour rather than a break.

   What it buys: these menus are absolutely positioned at `right: 0` under
   their trigger, and they used to appear with no connection to it at all.
   Scaling up from the top-right corner says WHERE the menu came from. The
   translate is 4px, not more — this is a spatial cue, not an entrance.

   150ms because a menu is a menu; the budget for this tier is 150-250ms and
   the fast end of it is right for something opened to be read immediately.

   Open only. `<details>` removes its content the instant it closes, so there
   is no exit to animate without taking the open state over in JavaScript, and
   that trade is not worth making for a menu.

   Reduced motion swaps the KEYFRAME, it does not delete the animation. The
   preference is about vestibular motion — movement and scale — not about
   opacity, and a menu that still fades is easier to follow than one that
   materialises. Same duration, same curve, no transform. (In the operator
   shell styles.css's global killswitch collapses every duration to 0.01ms and
   wins on `!important`, so this fade is what the tenant portal gets; the
   operator shell has made that call globally and this rule does not fight it.) */
.ab-more[open] > .ab-menu {
    animation: ui-pop-in 150ms var(--ease-out) both;
    transform-origin: top right;
}

@media (prefers-reduced-motion: reduce) {
    .ab-more[open] > .ab-menu { animation-name: ui-fade-in; }
}

@keyframes ui-pop-in {
    from { opacity: 0; transform: scale(0.96) translateY(-4px); }
    to { opacity: 1; transform: none; }
}

/* The reduced-motion partner for ui-pop-in. Opacity only — no scale, no
   translate — so the menu still announces itself without moving. */
@keyframes ui-fade-in {
    from { opacity: 0; }
    to { opacity: 1; }
}

.ab-menu a, .ab-menu button {
    background: none; border: 0; border-radius: var(--radius-sm); color: inherit;
    font: inherit; padding: 7px 10px; text-align: left; text-decoration: none; width: 100%;
}
.ab-menu a:hover, .ab-menu button:hover { background: var(--surface-alt); }


/* --- Tabs -------------------------------------------------------------------
   A page whose secondary content is one interaction deeper, not deleted. The
   panels are rendered VISIBLE and the tablist is inert until app.js claims it
   (see macros/ui.html::tabs) — so with no JS the page degrades to the long
   scroll it used to be rather than to four unreachable sections. */

.tabs {
    border-bottom: 1px solid var(--line);
    display: flex;
    gap: 2px;
    margin: 18px 0 0;
    overflow-x: auto;
    scrollbar-width: none;
}
.tabs::-webkit-scrollbar { display: none; }
.tab {
    background: none;
    border: 0;
    border-bottom: 2px solid transparent;
    color: var(--muted);
    cursor: pointer;
    font: inherit;
    font-size: 13.5px;
    font-weight: 600;
    padding: 9px 14px;
    white-space: nowrap;
}
.tab:hover { color: var(--brand-deep); }
.tab[aria-selected="true"] { border-bottom-color: var(--brand); color: var(--brand-deep); }
.tab-count {
    background: var(--surface-alt);
    border-radius: 999px;
    font-size: 11.5px;
    font-variant-numeric: tabular-nums;
    margin-left: 6px;
    padding: 1px 7px;
}
.tab[aria-selected="true"] .tab-count { background: var(--brand-soft); }
.tabpanel { padding-top: 18px; }
.tabpanel[hidden] { display: none; }


/* --- Delivery status ---------------------------------------------------------
   Payslip distribution state, rendered identically by both shells from
   macros/distribution.html. Token-only and Bootstrap-free: the tenant portal
   loads neither Bootstrap nor styles.css, which is the whole reason the two
   portals had grown separate copies of this screen. */

.ds-totals { display: flex; flex-wrap: wrap; gap: 24px; margin: 0 0 16px; }
.ds-total { display: grid; gap: 2px; }
.ds-total dt { color: var(--muted); font-size: 11.5px; letter-spacing: .02em; text-transform: uppercase; }
.ds-total dd { font-size: 20px; font-weight: 700; margin: 0; font-variant-numeric: tabular-nums; }
.ds-total--ok dd { color: var(--green); }
.ds-total--bad dd { color: var(--danger); }

.ds-queue {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    margin-bottom: 16px;
    padding: 14px 16px;
}
.ds-queue-head { align-items: center; display: flex; gap: 10px; justify-content: space-between; margin-bottom: 10px; }
.ds-queue-head h2 { font-size: 14px; font-weight: 700; margin: 0; }
.ds-facts { display: flex; flex-wrap: wrap; gap: 22px; margin: 0; }
.ds-facts > div { display: grid; gap: 2px; }
.ds-facts dt { color: var(--muted); font-size: 11.5px; letter-spacing: .02em; text-transform: uppercase; }
.ds-facts dd { font-size: 13.5px; font-weight: 600; margin: 0; }
.ds-note { color: var(--muted); font-size: 12.5px; margin: 10px 0 0; }
.ds-note--bad { color: var(--danger); }

.ds-pill {
    border-radius: 999px;
    display: inline-block;
    font-size: 11.5px;
    font-weight: 600;
    padding: 2px 9px;
    white-space: nowrap;
    background: var(--surface-alt);
    color: var(--muted);
}
.ds-pill--ok { background: var(--green-soft); color: #14532d; }
.ds-pill--bad { background: color-mix(in srgb, var(--danger) 14%, transparent); color: var(--danger); }
.ds-pill--warn { background: color-mix(in srgb, var(--warn) 16%, transparent); color: var(--warn); }
.ds-pill--info { background: var(--brand-soft); color: var(--brand-deep); }
.ds-pill--muted { background: var(--surface-alt); color: var(--muted); }

.ds-table-wrap { overflow-x: auto; width: 100%; }
.ds-table { border-collapse: collapse; font-size: 13px; min-width: 100%; width: 100%; }
.ds-table th, .ds-table td { border-bottom: 1px solid var(--line); padding: 8px 10px; text-align: left; vertical-align: middle; }
.ds-table thead th { color: var(--muted); font-size: 11.5px; font-weight: 600; letter-spacing: .02em; text-transform: uppercase; white-space: nowrap; }
.ds-table .num { text-align: right; font-variant-numeric: tabular-nums; }
.ds-table tbody tr:last-child td { border-bottom: 0; }
.ds-muted { color: var(--muted); }
.ds-select {
    background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-sm);
    color: inherit; font: inherit; font-size: 12.5px; padding: 3px 6px; max-width: 160px;
}
.ds-select:disabled { opacity: .55; }
.ds-total--warn dd { color: var(--warn); }

/* Settling an unconfirmed send: two secondary buttons in a table cell — the
   action bar's own buttons (so they render in both shells), at table scale. */
.ds-settle { gap: 6px; margin-top: 8px; }
.ds-settle.ab form > .btn { border-radius: 8px; font-size: 12px; padding: 4px 10px; }

/* The confirm step before an SMS or auto send (macros/distribution.html). Its
   one primary button is the action bar's, which already reads in both shells. */
.ds-confirm h2 { font-size: 16px; font-weight: 750; margin: 0 0 4px; }
.ds-confirm-lead { color: var(--muted); font-size: 13.5px; margin: 0 0 16px; }
.ds-confirm-sub { font-size: 14px; font-weight: 700; margin: 22px 0 0; }
.ds-confirm .ds-table-wrap { margin-top: 10px; }
.ds-confirm-actions { margin-top: 20px; }

/* Import preview: the blockers list and the note voice, shared by both portals
   (macros/import_preview.html). */
.ip-note { color: var(--muted); font-size: 12.5px; margin: 8px 0 0; max-width: 78ch; }
.ip-blockers { border-left: 3px solid var(--warn); }
.ip-blockers h2 { font-size: 14px; font-weight: 700; margin: 0 0 8px; }
.ip-blockers ul { margin: 0; padding-left: 18px; }
.ip-blockers li { margin-bottom: 8px; }
.ip-blockers li:last-child { margin-bottom: 0; }

/* --- Reports & exports -------------------------------------------------------
   One report per block: purpose, availability (with a reason when it is not
   available), preview. Shared by both portals via macros/reports.html. */

.rp { border-top: 1px solid var(--line); padding: 16px 0; }
.rp:first-of-type { border-top: 0; }
.rp-head { align-items: center; display: flex; flex-wrap: wrap; gap: 12px; justify-content: space-between; }
.rp-head h2 { font-size: 15px; font-weight: 700; margin: 0; }
.rp-why { color: var(--muted); font-size: 13px; margin: 6px 0 0; max-width: 78ch; }
.rp-blocked { color: var(--warn); font-size: 12.5px; margin: 6px 0 0; max-width: 78ch; }
.rp .dt-wrap, .rp .ds-totals { margin-top: 12px; }


/* --- Panel -------------------------------------------------------------------
   The canonical content card, and the ONLY definition of `.panel` in the
   codebase (Phase 6, Task 6.1).

   It used to be defined twice and mean two different things. styles.css gave
   the operator shell a padded card; portal.css gave the tenant shell the same
   card overridden to `overflow-x: auto; padding: 4px`, because every tenant use
   of it wrapped a table. So the same class name produced an 18px content card
   in one portal and a 4px scroll box in the other — and the Phase 5 shared
   macros, written once for both, landed in whichever box the surrounding shell
   happened to mean. (Concretely: `class="panel ip-blockers"` in the tenant
   import preview rendered a prose list at 4px padding.)

   The table-wrapping meaning has its own name and always did — `.dt-wrap`,
   above. `.panel` is a card. */

.panel {
    background: var(--surface, rgba(255, 255, 255, 0.92));
    border: 1px solid var(--line);
    border-radius: 14px;
    box-shadow: var(--shadow-sm, 0 10px 26px rgba(15, 23, 42, 0.05));
    padding: 18px;
    transition: transform 180ms ease, box-shadow 180ms ease, border-color 180ms ease;
}
.panel:hover {
    border-color: rgba(15, 118, 110, 0.24);
    box-shadow: var(--shadow);
    transform: translateY(-2px);
}


/* --- Analytics card ----------------------------------------------------------
   The grid of chart cards, and a chart card's own header. Moved here from
   styles.css (Phase 6's one-definition rule): the operator stylesheet defined
   them, but the only template that uses them is the TENANT expenses page, which
   never loads styles.css. The result was a chart card with no padding sitting
   under three 18px-padded .stat-cards, and an .analytics-grid that was not a
   grid — the two chart cards stacked full-width instead of sitting side by side.

   Padding matches .stat-card and .panel above (18px) so every card on a page
   shares one inset, whichever shell drew it. */

.analytics-grid {
    display: grid;
    gap: 16px;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    margin-bottom: 26px;
}

.chart-card {
    min-width: 0;          /* charts shrink with the cell instead of overflowing */
    padding: 18px;
}

.chart-card:hover {
    transform: none;       /* a chart is read, not clicked */
}

.chart-head {
    margin-bottom: 14px;
}

.chart-head h2 {
    font-size: 15px;
    font-weight: 750;
    margin: 0;
}

.chart-head small {
    display: block;
    font-size: 12px;
    margin-top: 2px;
}


/* --- Page bar ----------------------------------------------------------------
   A title row: the page (or section) heading, with its actions on the right.

   This is the second half of untangling `.toolbar` (Phase 6, Task 6.1). The
   name meant two different things depending on which shell you were in — in the
   operator console a row of controls (filters, bulk actions, button groups), in
   the tenant portal the PAGE HEADER, because the tenant shell has no
   page_title/page_actions blocks the way base.html does. Ten of the tenant's
   twelve uses were headers; all nine operator uses were control rows.

   `.toolbar` now means the control row, in both shells. This is the header. */

.page-bar {
    align-items: center;
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
    justify-content: space-between;
    margin-bottom: 16px;
}
.page-bar > .page-head, .page-bar h1, .page-bar h2 { margin: 0; }


/* --- Stat card ---------------------------------------------------------------
   A single figure with its label. One definition, one child contract (Phase 6,
   Task 6.1).

   It was defined in both shells with the SAME visual treatment — down to an
   identical decorative gradient blob — but two incompatible child contracts:
   styles.css styled `span` (label) and `strong` (value); portal.css styled
   `.n` (value) and `.l` (label), in the opposite order. A stat card written for
   one portal rendered as unstyled text in the other, which is precisely why the
   Phase 5 shared macros could not use it.

   The surviving contract is <span> label then <strong> value, matching
   .dh-figure's dt/dd above — so a figure reads the same whichever canonical
   component draws it. `.stat-icon` stays in styles.css: it is Bootstrap-icon
   positioning the tenant shell has no use for. */

.stat-card {
    background: var(--surface, rgba(255, 255, 255, 0.92));
    border: 1px solid var(--line);
    border-radius: 14px;
    box-shadow: var(--shadow-sm, 0 10px 26px rgba(15, 23, 42, 0.05));
    overflow: hidden;
    padding: 18px;
    position: relative;
    transition: transform 180ms ease, box-shadow 180ms ease, border-color 180ms ease;
}
.stat-card:hover {
    border-color: rgba(15, 118, 110, 0.24);
    box-shadow: var(--shadow);
    transform: translateY(-2px);
}
.stat-card::after {
    background: linear-gradient(135deg, rgba(15, 118, 110, 0.16), rgba(23, 195, 178, 0.18));
    border-radius: 999px;
    content: "";
    height: 80px;
    position: absolute;
    right: -30px;
    top: -30px;
    width: 80px;
}
.stat-card > span {
    color: var(--muted);
    display: block;
    font-size: 13px;
    margin-bottom: 8px;
    position: relative;
    z-index: 1;
}
.stat-card > strong {
    display: block;
    font-size: 24px;
    font-weight: 800;
    line-height: 1.2;
    position: relative;
    z-index: 1;
}

/* --- Shell chrome: the top navigation bar ---------------------------------
   Horizontal nav, drawn identically by BOTH shells. It was written for the
   tenant portal and lived in portal.css; the operator app now draws the same
   bar in place of its 280px left sidebar, so the rules moved here rather than
   being copied. One bar, one definition — a second copy in styles.css is the
   two-meanings failure tests/test_phase6_vocabulary.py exists to catch.

   Everything below must hold in the operator shell too, which loads Bootstrap.
   That is why the interactive parts state their own background, border and
   display instead of inheriting: Bootstrap's reboot makes `summary` a
   list-item and `button` a bordered control, and both would otherwise land
   inside this bar.

   The bar is a LIGHT surface, not the old sidebar's deep teal. Teal is this
   design's only accent and the Action Required card uses a teal fill to mean
   "a decision is required" (UI_ARCHITECTURE section 6). A permanent full-width
   teal band above that card would spend the same colour on chrome and destroy
   the signal. So: nav is neutral, teal stays signal. Nothing here carries a
   teal fill — the exceptions are the logo artwork (a logotype, which WCAG
   exempts) and the focus ring, which is transient and must be visible. */

.portal-shell {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
}

/* Separation is a hairline, not a shadow: the app reserves its one shadow for
   cards and uses borders for everything nested (tokens.css). --line-control is
   the token for a boundary that must be seen — measured 3.43:1 against the bar
   and 3.17:1 against the page behind it. --line and --line-strong are card
   hairlines and sit at 1.26:1 and 1.48:1 here, which is why neither is used. */
.portal-topbar {
    background: var(--surface);
    border-bottom: 1px solid var(--line-control);
    color: var(--ink);
    position: sticky;
    top: 0;
    z-index: 1040;
}

/* Three columns, not a flex row, and that is what centres the nav.
   `1fr auto 1fr` gives the logo and the utility cluster equal shares of the
   free space, so the auto-width nav between them sits on the BAR's midline —
   not merely midway between its two neighbours, which is what a flex row with
   `margin-inline: auto` produces and which drifts as soon as one side is
   heavier than the other (the operator cluster carries a search field the
   tenant one does not).

   The side columns are plain `1fr`, so they still grow past their share rather
   than clipping their content when the bar gets tight; the nav then leaves
   centre, which is the right way round — chrome that is slightly off-centre
   beats chrome that is cut off. Below the drawer breakpoint this reverts to
   flex, where the hamburger can take its `order: -1`.

   Same ultra-wide ceiling as the content below, so the nav and the page it
   governs share one left edge on a wide monitor. */
.portal-topbar-inner {
    align-items: stretch;
    display: grid;
    gap: 24px;
    grid-template-columns: 1fr auto 1fr;
    margin-inline: auto;
    max-width: 1680px;
    min-height: 64px;
    padding: 0 30px;
    width: 100%;
}

/* The icon is a deep-teal gradient drawn for a light ground, so it needs no
   tile here. The sidebar's white .brand-mark tile existed to lift it off deep
   teal; on white it would be a white square on white. The wordmark stays live
   text bound to app_brand_name rather than the baked lockup SVG, so a
   white-label rename still never has to touch a template. */
.portal-logo {
    align-items: center;
    align-self: center;
    color: var(--brand-deep);
    display: flex;
    gap: 9px;
    justify-self: start;
    text-decoration: none;
}

.portal-logo img { display: block; height: 30px; width: auto; }

.portal-logo-name {
    font-size: 15px;
    font-weight: 700;
    letter-spacing: -0.01em;
}

/* --- Primary nav --------------------------------------------------------- */

.portal-nav {
    align-items: stretch;
    display: flex;
    gap: 2px;
    justify-content: center;
    min-width: 0;
}

/* Full-bar-height items, so the active underline lands on the bar's own bottom
   edge and reads as a tab rather than as a floating rule. The transparent
   border is always present, so becoming active never shifts the row. */
.portal-nav a {
    align-items: center;
    border-bottom: 2px solid transparent;
    color: var(--ink-soft);
    display: flex;
    font-size: 15px;
    gap: 8px;
    padding: 0 12px;
    text-decoration: none;
    transition: background var(--ease), color var(--ease);
    white-space: nowrap;
}

.portal-nav a:hover { background: var(--surface-alt); color: var(--ink); }

/* Three signals, none of them colour alone and none of them teal: the label
   goes to full ink, the weight steps up, and the tab underlines. */
.portal-nav a[aria-current="page"] {
    border-bottom-color: var(--ink);
    color: var(--ink);
    font-weight: 600;
}

.portal-nav .ico { opacity: 0.75; }
.portal-nav a:hover .ico,
.portal-nav a[aria-current="page"] .ico { opacity: 1; }

/* --- Right utility cluster ----------------------------------------------- */

.portal-utility {
    align-items: center;
    display: flex;
    gap: 4px;
    justify-self: end;
    margin-left: auto;
}

.utility-btn {
    align-items: center;
    background: none;
    border: 0;
    border-radius: 10px;
    color: var(--muted);
    cursor: pointer;
    display: inline-flex;
    font: inherit;
    gap: 7px;
    justify-content: center;
    min-height: 38px;
    padding: 0 9px;
    position: relative;
    text-decoration: none;
    transition: background var(--ease), color var(--ease);
}

.utility-btn:hover { background: var(--surface-alt); color: var(--ink); }
.utility-btn[aria-current="page"] { background: var(--surface-alt); color: var(--ink); }

/* --- Global search (operator only) ---------------------------------------
   The tenant portal has no global search, so this renders only where the
   markup asks for it. It sits at the head of the utility cluster rather than
   beside the logo because it is a tool, not an identity — and because the
   cluster is the one part of the bar whose width may vary without moving the
   centred nav off the midline.

   Bootstrap-free on purpose even though only the Bootstrap shell draws it: the
   old field was a `.form-control` tinted for a deep-teal sidebar, and
   inheriting Bootstrap's focus shadow here would put a blue-tinted ring on a
   bar whose focus colour is the brand (see the focus rules below). */
.portal-search {
    align-items: center;
    display: flex;
    position: relative;
}

.portal-search .ico {
    color: var(--muted);
    left: 10px;
    pointer-events: none;
    position: absolute;
}

.portal-search input {
    background: var(--surface-alt);
    border: 1px solid var(--line-control);
    border-radius: var(--radius-pill);
    color: var(--ink);
    font: inherit;
    font-size: 14px;
    height: 38px;
    padding: 0 12px 0 32px;
    transition: border-color var(--ease), background var(--ease);
    width: 190px;
}

.portal-search input::placeholder { color: var(--muted); }

.portal-search input:focus {
    background: var(--surface);
    border-color: var(--brand);
}

/* Ink, not amber. tokens.css reserves --warn and --danger for the Urgent state
   (UI_ARCHITECTURE section 9); a badge that is amber on every render would
   spend that reservation on an ambient count. Notifications are ambient by
   definition (section 3), so the count is legible and quiet rather than
   alarming. */
.utility-badge {
    background: var(--ink);
    border-radius: var(--radius-pill);
    color: var(--surface);
    font-size: 11px;
    font-weight: 700;
    line-height: 1;
    min-width: 17px;
    padding: 3px 5px;
    position: absolute;
    right: 2px;
    text-align: center;
    top: 3px;
}

/* Neutral, not a teal tint: an avatar is identity, and on this bar teal would
   read as the decision signal it is reserved for. */
.account-avatar {
    align-items: center;
    background: var(--surface-alt);
    border: 1px solid var(--line-control);
    border-radius: 50%;
    color: var(--ink);
    display: inline-flex;
    flex: none;
    font-size: 12px;
    font-weight: 700;
    height: 28px;
    justify-content: center;
    letter-spacing: 0.02em;
    width: 28px;
}

.account-name {
    color: var(--ink);
    font-size: 14px;
    font-weight: 600;
    max-width: 18ch;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* --- Utility dropdowns ---------------------------------------------------
   <details>/<summary>, so open, close, focus and keyboard activation all come
   from the element. The inline script in each shell adds only the two
   behaviours it does not give for free: Escape, and click-away. */

.utility-menu { align-items: center; display: flex; position: relative; }

/* `list-style` alone is not enough in the Bootstrap shell: its reboot sets
   `summary { display: list-item }`, which would break the summary out of the
   inline-flex .utility-btn gives it. .utility-btn wins on specificity, and
   this rule keeps the marker off in every engine. */
.utility-menu > summary {
    list-style: none;      /* Firefox */
    user-select: none;
}

.utility-menu > summary::-webkit-details-marker { display: none; }
.utility-menu > summary::marker { content: ""; }

.utility-menu[open] > summary { background: var(--surface-alt); color: var(--ink); }

/* The popover is the one place in this bar a shadow is right: it floats above
   the page rather than sitting nested in it. */
.utility-pop {
    background: var(--surface);
    border: 1px solid var(--line-strong);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    color: var(--ink);
    display: flex;
    flex-direction: column;
    min-width: 200px;
    padding: 6px;
    position: absolute;
    right: 0;
    top: calc(100% + 6px);
    z-index: 1050;
}

/* Settings and account both used to appear with no relationship to the button
   that opened them. They are pinned to `right: 0` under their trigger, so
   growing from the top-right corner is a statement of fact about where they
   came from rather than an effect applied to them.

   `ui-pop-in` and the reasoning behind its values live further up this file.
   It is the same entrance the operator shell's overflow menu uses,
   deliberately, because they are the same gesture. Open only — `<details>`
   drops its content on close, so there is no exit to animate. */
.utility-menu[open] > .utility-pop {
    animation: ui-pop-in 150ms var(--ease-out) both;
    transform-origin: top right;
}

/* Gentler, not gone: reduced motion drops the scale and the lift and keeps the
   fade. */
@media (prefers-reduced-motion: reduce) {
    .utility-menu[open] > .utility-pop { animation-name: ui-fade-in; }
}

.utility-pop a {
    align-items: center;
    border-radius: var(--radius-sm);
    color: var(--ink);
    display: flex;
    font-size: 14px;
    gap: 9px;
    padding: 9px 10px;
    text-decoration: none;
}

.utility-pop a:hover { background: var(--surface-alt); color: var(--brand); }
.utility-pop .ico { color: var(--muted); }
.utility-pop a:hover .ico { color: var(--brand); }

/* The signed-in name labels the menu it sits in; it is not a destination. */
.utility-pop-head {
    border-bottom: 1px solid var(--line);
    color: var(--muted);
    font-size: 12px;
    margin: 2px 0 6px;
    overflow: hidden;
    padding: 4px 10px 8px;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Log out hovers like every other row in this menu. It is REVERSIBLE — you
   sign back in and nothing is lost — and a danger tint on a reversible action
   is what teaches people that red does not reliably mean "this cannot be
   undone". The reservation is being kept for the genuinely unrecoverable
   actions in Employees and Payroll. No red or amber anywhere in this bar. */

/* --brand-soft is 1.17:1 on white — invisible on this bar. --brand measures
   6.14:1 on the bar and 5.67:1 on the page, so the ring is the one place teal
   still appears in the chrome: it is transient, only ever on the focused
   element, and an indicator nobody can see is not an indicator.

   Scoped under .portal-topbar so it wins in the operator shell too, where
   styles.css gives :focus-visible a --brand ring at 2px and the old sidebar a
   --gold one; the bar is neither of those surfaces. */
.portal-topbar .portal-nav a:focus-visible,
.portal-topbar .utility-btn:focus-visible,
.portal-topbar .nav-toggle:focus-visible,
.portal-topbar .portal-logo:focus-visible,
.portal-topbar .portal-search input:focus-visible,
.utility-pop a:focus-visible {
    outline: 3px solid var(--brand);
    outline-offset: 2px;
}

/* --- Mobile nav drawer ---------------------------------------------------
   Sits inside the bar rather than floating over the page, so it scrolls and
   stacks with the rest of the chrome instead of covering content. */
.nav-toggle {
    align-items: center;
    align-self: center;
    background: none;
    border: none;
    border-radius: 10px;
    color: var(--muted);
    cursor: pointer;
    display: none;
    flex: none;
    height: 40px;
    justify-content: center;
    padding: 0;
    width: 40px;
}

.nav-toggle:hover { background: var(--surface-alt); color: var(--ink); }

/* The scrim. It used to appear with an untransitionable `display`, so one
   gesture animated in halves. `visibility` carries the hit-testing the old
   `display: none` provided and flips only at the end of its duration, so the
   way out fades as well as the way in.

   var(--ease) rather than a literal duration, and that is load-bearing twice
   over: it is the exact value the drawer uses, and this file has no
   reduced-motion block for it — tokens.css rewrites the token to `1ms linear`
   under that preference, which is the whole point of it being a token. Do not
   inline 180ms here.

   z-index 1030 is BELOW the bar's 1040, and that is not a preference. The
   drawer IS the desktop nav bar, so it lives inside .portal-topbar; a sticky
   element with a z-index is a stacking context, so the drawer's 1090 resolves
   inside the bar's 1040 and a 1080 scrim would paint straight over the open
   drawer — every item under 50% black. Raising the drawer cannot escape the
   bar's context, and dropping the bar's z-index lets scrolled content paint
   through a sticky header. Lowering the scrim fixes it at the one end with no
   side effect: page content dims, the bar and the drawer that slides out from
   under it stay lit. */
.nav-backdrop {
    background: rgba(2, 6, 23, 0.5);
    inset: 0;
    opacity: 0;
    position: fixed;
    transition: opacity var(--ease), visibility var(--ease);
    visibility: hidden;
    z-index: 1030;
}

.nav-backdrop.show { opacity: 1; visibility: visible; }

/* The primary nav stops fitting one row here and becomes a slide-in drawer.
   The utility cluster stays in the bar — a notification count you have to open
   a drawer to see is not an ambient signal.

   1100px, not the tenant portal's old 900px, and the number is measured rather
   than chosen. Both bars now carry six one-word domain nouns and need about the
   same row — the operator's were "Client Companies", "Payroll Runs", "Expenses
   & Audit" and "Statutory Rates" until they were shortened to fit a bar rather
   than the 280px sidebar they were written for (app/navigation.py). Below this
   width the flex items start shrinking and clipping their own text, and a
   destination you cannot read is worse than one behind a hamburger.

   One breakpoint for both shells, not one each: two shells disagreeing about
   when the same bar becomes the same drawer is exactly the drift this file
   exists to prevent, and the alternative is a second copy of every rule below.
   It is the operator bar that sets the number, because it also carries a 190px
   search field the tenant does not have. If a seventh destination is ever added
   to either bar, re-measure — do not just nudge this up. */
@media (max-width: 1100px) {
    /* Back to flex, so the hamburger can take `order: -1` — a grid item's
       position comes from the track it sits in, not from `order`. */
    .portal-topbar-inner {
        display: flex;
        gap: 12px;
        padding: 0 16px;
    }

    .nav-toggle {
        display: inline-flex;
        order: -1;          /* hamburger first, then the logo */
    }

    /* The drawer is LIGHT, like the bar it replaces. Two reasons: it holds the
       same items, so one surface means one set of colour and weight rules to
       keep true at both breakpoints; and the dark backdrop behind it already
       supplies the separation a dark panel would have been providing. Only the
       active INDICATOR changes axis — an underline cannot read as "current" in
       a vertical list, so it becomes a leading bar on the same ink. */
    .portal-nav {
        align-items: stretch;
        background: var(--surface);
        border-right: 1px solid var(--line-control);
        flex-direction: column;
        gap: 2px;
        height: 100vh;
        justify-content: flex-start;
        left: 0;
        overflow-y: auto;
        padding: 74px 12px 24px;
        position: fixed;
        top: 0;
        transform: translateX(-100%);
        transition: transform var(--ease);
        width: 268px;
        z-index: 1090;
    }

    .portal-nav.open { transform: none; }

    .portal-nav a {
        border-bottom: 0;
        border-left: 3px solid transparent;
        border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
        font-size: 15px;
        min-height: 44px;
        padding: 10px 12px;
    }

    .portal-nav a[aria-current="page"] {
        background: var(--surface-alt);
        border-left-color: var(--ink);
    }

    /* The name is the widest thing in the cluster and the avatar already
       identifies the account. */
    .account-name { display: none; }
}

/* Between the ultra-wide ceiling and the drawer, the search field is the only
   thing keeping the nav off the bar's midline: the grid centres the nav on the
   free space the two side columns share equally, and the operator cluster
   (search + bell + account) outweighs a 91px logo by enough to eat into it.
   140px buys back the difference, so the nav is centred at 1366 as well as at
   1440. It is also why the placeholder is the bare "Search…": the field is the
   one part of this bar that changes width, and a placeholder that fits at
   1680 and is cut off at 1440 reads as a bug rather than as a hint. The
   accessible name on the label still says what is searchable. The tenant bar
   has no search field and is unaffected. */
@media (max-width: 1450px) {
    .portal-search input { width: 140px; }
}

/* The search field is the first thing to go: below this the bar is the
   hamburger, the logo and the two icon buttons, and /search is still one tap
   away from the drawer's Clients page. */
@media (max-width: 720px) {
    .portal-search { display: none; }
}
