/*
 * The page shell: sidebar rail, top chrome, content column.
 *
 * Authored rather than transplanted - the component library has no sidebar. Values
 * are the Design System's per docs/frontend/design-tokens.md: 28px rows, 16px icons,
 * a 10px gap, an 11px uppercase group label.
 *
 * Loaded after Bootstrap and after _components.css, which is what lets the `a` rules
 * below win on source order.
 *
 * Anchor-shaped components must name BOTH the container and the element:
 * `.app-sidebar a.sidebar-link`, not `.app-sidebar .sidebar-link`.
 *
 * _components.css styles links as `a:not(.btn):hover`, which is (0,2,1) - one notch
 * above a two-class scope. Without the `a`, a component keeps its geometry at rest
 * and loses `display`, `padding`, `border-bottom` and `color` the moment it is
 * hovered. Naming the element ties (0,2,1) and wins on source order, and one
 * selector then governs idle, hover and focus together, so the geometry is
 * unconditional instead of per-state.
 *
 * State rules (`:hover`, `:focus-visible`, `.is-active`) reach (0,3,0) alone and need
 * no `a`. They must set paint properties only - see --transition-paint in
 * _semantic.css.
 */

/* ------------------------------------------------------------------- shell */

/*
 * Two shells, selected by _base.twig. The app shell is a flex row so the rail can be
 * a sticky full-height sibling of the content column; the guest shell (login, signup,
 * password reset) is a plain block.
 */
body.app-shell {
    display: flex;
    align-items: flex-start;
}

body.guest-shell {
    display: block;
}

/*
 * The guest screens' logo bar runs to the page edge rather than sitting inside a
 * centred container.
 *
 * The brand mark is page furniture, like the rail's logo on the authenticated shell,
 * so it belongs at the page's own inset. Inside `.container` it lands wherever the
 * grid's centred max-width happens to start, which on a wide viewport is ~110px in and
 * reads as neither aligned to the edge nor to the card below it.
 *
 * Overridden here rather than by changing the markup, because the same `.container`
 * carries the logo on login, signup and both password-reset screens.
 */
body.guest-shell .app-main > .container {
    max-width: none;
    padding-right: var(--g-space-40);
    padding-left: var(--g-space-40);
}

/*
 * The content column. min-height rather than height so a long screen can grow past
 * the viewport: ginger-admin's `overflow-x: hidden` on html makes it a scroll
 * container, which turns any overflow into a clip rather than a scroll.
 */
.app-main {
    display: flex;
    flex: 1 1 auto;
    flex-direction: column;
    min-width: 0;
    min-height: 100vh;
}

/*
 * The content column fills the shell.
 *
 * With the footer gone `main` is the only thing below the chrome, and .app-main has a
 * 100vh minimum - so without this the surface stops at the last row of content and
 * the brand canvas shows through underneath it.
 */
.app-main > main {
    flex: 1 1 auto;
}

/*
 * The canvas belongs to the rail; the content column is the app surface.
 *
 * The brand assets dictate this. Each brand's `header.logo_name` is drawn for its
 * canvas - nopayn's is a white wordmark on a navy canvas - so the rail that hosts the
 * logo has to BE the canvas or the logo disappears. It also keeps h1 off the canvas:
 * --text-strong is near-black for every brand and would be unreadable on navy.
 *
 * The rule that makes it safe: a dark surface is a canvas, never a component host.
 * Only the rail's nav rows sit on it; every table, badge, card and form sits on
 * --surface-app, which is white for all brands. The guest shell keeps the canvas on
 * <body>, which is why this targets .app-main rather than theme.css.
 */
.app-shell .app-main {
    color: var(--text-on-surface);
    background-color: var(--surface-app);
}

/* ----------------------------------------------------------------- sidebar */

.app-sidebar {
    position: sticky;
    top: 0;
    z-index: 20;
    display: flex;
    flex: 0 0 auto;
    flex-direction: column;
    width: var(--layout-sidebar-width);
    height: 100vh;
    padding: var(--g-space-20) var(--g-space-14);
    overflow: hidden auto;
    color: var(--text-on-canvas);
    background-color: var(--surface-canvas);
    border-right: 1px solid var(--border-default);
    transition: width var(--duration-medium) var(--ease-out);
}

.sidebar-head {
    display: flex;
    flex: 0 0 auto;
    gap: var(--g-space-10);
    align-items: center;
    justify-content: space-between;
    padding: 0 var(--g-space-6) var(--g-space-20);
}

/* theme.css runs the logo at 60px for the old full-width header bar. */
.app-sidebar .logo-icon {
    max-width: 100%;
    max-height: 26px;
}

/* Same `a:not(.btn)` treatment as the rows below; same reason for naming the `a`. */
.app-sidebar a.sidebar-logo {
    display: flex;
    min-width: 0;
    border-bottom: 0;
}

.sidebar-toggle {
    display: flex;
    flex: 0 0 auto;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    padding: 0;
    color: var(--text-subtle);
    background-color: transparent;
    border: 1px solid var(--border-default);
    border-radius: var(--radius-control);
    cursor: pointer;
}

.sidebar-toggle:hover {
    color: var(--text-secondary);
    background-color: var(--surface-muted);
}

.sidebar-toggle:focus-visible {
    outline: 2px solid var(--color-primary-hover);
    outline-offset: 1px;
}

/* Rotated by the collapsed state below, so the chevron always points the way out. */
.sidebar-toggle-glyph {
    transition: transform var(--duration-medium) var(--ease-out);
}

.sidebar-nav {
    display: flex;
    flex: 1 1 auto;
    flex-direction: column;
}

.sidebar-group {
    display: flex;
    flex-direction: column;
}

/* Pushes the last group - the account group - to the foot of the rail. */
.sidebar-group-end {
    margin-top: auto;
    padding-top: var(--g-space-20);
}

.sidebar-items {
    display: flex;
    flex-direction: column;
    margin: 0;
    padding: 0;
    list-style: none;
}

.sidebar-group-label {
    margin: 0;
    padding: var(--g-space-14) var(--g-space-12) var(--g-space-6);
    color: var(--text-placeholder);
    font-size: var(--font-size-eyebrow);
    font-weight: var(--font-weight-medium);
    line-height: var(--line-height-tight);
    letter-spacing: var(--tracking-eyebrow);
    text-transform: uppercase;
}

/*
 * The rail rows.
 *
 * Both the container scope AND the `a` are load-bearing; see "anchor-shaped
 * components" at the top of this file. Do not flatten either half.
 */
.app-sidebar a.sidebar-link {
    display: flex;
    gap: var(--g-space-10);
    align-items: center;
    height: 28px;
    margin-bottom: var(--g-space-2);
    padding: 0 var(--g-space-8) 0 var(--g-space-12);
    color: var(--nav-item-color);
    font-size: var(--font-size-body);
    text-decoration: none;
    background-color: transparent;
    border: 1px solid transparent;
    border-radius: var(--radius-control);
    transition: var(--transition-paint);
}

.app-sidebar .sidebar-link:hover {
    background-color: var(--color-primary-alpha-05);
}

/*
 * Neither source defines a focus ring for a sidebar row - see the "Gaps to
 * invent" section of the modernization plan. --color-primary-hover rather than
 * --color-primary because it is the step that clears 4.5:1 on white.
 */
.app-sidebar .sidebar-link:focus-visible {
    outline: 2px solid var(--color-primary-hover);
    outline-offset: 1px;
}

/*
 * Selection is carried by the tinted fill, the hairline and the accented icon,
 * with the label left at the brand's own nav colour. That is the Design System's
 * model rather than Admin's green-label one, and it is the only version that
 * holds up: every brand's accent fails 4.5:1 against a near-white rail, so a
 * label painted in it would be the least readable row on the screen.
 */
.app-sidebar .sidebar-link.is-active {
    font-weight: var(--font-weight-medium);
    background-color: var(--color-primary-alpha-10);
    border-color: var(--color-primary-alpha-20);
}

.sidebar-label {
    flex: 1 1 auto;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

/*
 * Icons are masks, not images.
 *
 * ginger-admin renders these same SVGs through `content: url()` on an <img>,
 * which pins them to the fill they were drawn with - Ginger green. Four brands
 * ship from this image, so eight green icons in the rail would be a whitelabel
 * leak introduced by this phase. Masking paints the artwork with a token instead,
 * and the assets are byte-identical to Admin's.
 *
 * Written unprefixed. Vite emits the -webkit- pair for Safari below 15.4 on its
 * own, and stylelint's property-no-vendor-prefix would reject the hand-written
 * version - so do not add it.
 */
.sidebar-icon {
    flex: 0 0 auto;
    width: 16px;
    height: 16px;
    background-color: var(--text-subtle);
    mask-position: center;
    mask-repeat: no-repeat;
    mask-size: contain;
}

.sidebar-link.is-active .sidebar-icon {
    background-color: var(--nav-item-active-color);
}

.sidebar-icon-dashboard {
    mask-image: url("/img/v2/icons/navigation/dashboard.svg");
}

.sidebar-icon-orders {
    mask-image: url("/img/v2/icons/navigation/orders.svg");
}

.sidebar-icon-paymentlinks {
    mask-image: url("/img/v2/icons/navigation/payment_links.svg");
}

.sidebar-icon-payouts {
    mask-image: url("/img/v2/icons/navigation/payouts.svg");
}

.sidebar-icon-invoices {
    mask-image: url("/img/v2/icons/navigation/invoices.svg");
}

.sidebar-icon-apicalls {
    mask-image: url("/img/v2/icons/navigation/API_calls.svg");
}

.sidebar-icon-webhooks {
    mask-image: url("/img/v2/icons/navigation/webhhoks_calls.svg");
}

/*
 * The one rail icon that is not from the navigation set, and it needs its own size.
 *
 * Every icon in `icons/navigation/` is drawn on a 24px canvas with the glyph filling
 * about 71% of it - the padding is baked into the artwork. open-tab.svg is a 12px
 * canvas whose glyph fills 100%. Under `mask-size: contain` in a 16px box that renders
 * 16px of artwork next to everyone else's ~11.3px, so the developer-portal row read as
 * a visibly bigger icon than the eight above it.
 *
 * Matching the proportion rather than the box is the fix. If this icon is ever redrawn
 * on a 24px canvas like the rest, drop this line rather than adjusting the number.
 */
.sidebar-icon-developer-portal {
    mask-image: url("/img/v2/icons/open-tab.svg");
    mask-size: 71%;
}

.sidebar-icon-settings {
    mask-image: url("/img/v2/icons/navigation/merchants.svg");
}

/*
 * Identity and sign-out, pinned to the foot of the rail.
 *
 * .sidebar-nav above this is `flex: 1 1 auto`, so it takes the slack and this sits on
 * the floor without needing a margin of its own.
 */
.sidebar-account {
    display: flex;
    flex: 0 0 auto;
    flex-direction: column;
    gap: var(--g-space-2);
    padding-top: var(--g-space-10);
    border-top: 1px solid var(--border-default);
}

/*
 * Not a control - an identity check. Aligned to the same 12px inset as a rail row's
 * label so the two read as one column, and clipped rather than wrapped: the rail is
 * 232px inside its padding and an address is routinely longer. The full value is in
 * `title`.
 */
.sidebar-account-email {
    padding: 0 var(--g-space-12) var(--g-space-4);
    overflow: hidden;
    color: var(--text-placeholder);
    font-size: var(--font-size-caption);
    white-space: nowrap;
    text-overflow: ellipsis;
}

/*
 * Repaint only. The geometry comes from .sidebar-link, which is where the `a` is
 * named; see the top of this file. Muted rather than nav-coloured, because ending the
 * session is not navigation and should not read as a place you can be.
 */
.sidebar-logout {
    color: var(--text-subtle) !important;
}

.app-sidebar .sidebar-logout:hover {
    color: var(--status-danger-fg) !important;
    background-color: var(--status-danger-bg);
}

.sidebar-logout-glyph {
    flex: 0 0 auto;
}

/* --------------------------------------------------------------- collapsed */

/*
 * The collapsed rail. The class lands on <html> from a blocking script in the
 * head, before first paint, so a persisted preference does not flash the full
 * width on every navigation.
 *
 * Labels are clipped rather than display:none - Admin hides them outright, which
 * takes the accessible name of every row with it.
 */
.sidebar-collapsed .app-sidebar {
    width: var(--layout-sidebar-collapsed);
}

/*
 * The wordmark comes out at 104px rather than being scaled down, which is what
 * ginger-admin does too. Every brand's logo is a wordmark and none of them read at
 * that width; the favicons are square marks but ginger's is a 16px PNG, so there
 * is no per-brand mark to substitute. If one is ever commissioned, this is where
 * it goes.
 */
.sidebar-collapsed .sidebar-head {
    justify-content: center;
    padding-right: 0;
    padding-left: 0;
}

.sidebar-collapsed .sidebar-logo {
    display: none;
}

.sidebar-collapsed .sidebar-toggle-glyph {
    transform: rotate(180deg);
}

.sidebar-collapsed .app-sidebar .sidebar-link {
    justify-content: center;
    padding-right: 0;
    padding-left: 0;
}

/* An address has no icon form, so it is dropped rather than clipped. The logout row
   keeps its glyph through the .sidebar-link rule above. */
.sidebar-collapsed .sidebar-account-email {
    display: none;
}

.sidebar-collapsed .sidebar-label,
.sidebar-collapsed .sidebar-group-label {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    overflow: hidden;
    white-space: nowrap;
    clip-path: inset(50%);
}

/* -------------------------------------------------------------- top chrome */

/*
 * 64px, sticky, white, hairline under it - the Design System's TopChrome. Carries the
 * drawer trigger, the breadcrumb slot, the screen search and the account identity.
 *
 * The breadcrumb slot and the search share this bar and never collide: a screen with
 * a search is an index screen, and index screens render no trail.
 */
.topchrome {
    position: sticky;
    top: 0;
    z-index: 10;
    display: flex;
    flex: 0 0 auto;
    gap: var(--g-space-16);
    align-items: center;
    height: var(--layout-topchrome-height);
    padding: 0 var(--g-space-40);
    background-color: var(--surface-app);
    border-bottom: 1px solid var(--border-default);
}

.topchrome-slot {
    flex: 1 1 auto;
    min-width: 0;
}

/*
 * The drawer trigger. Hidden on desktop, where the rail is always present.
 */
.nav-toggle {
    display: none;
    flex: 0 0 auto;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    padding: 0;
    color: var(--text-secondary);
    background-color: transparent;
    border: 1px solid var(--border-default);
    border-radius: var(--radius-control);
    cursor: pointer;
}

.nav-toggle:focus-visible {
    outline: 2px solid var(--color-primary-hover);
    outline-offset: 1px;
}

/*
 * ginger-admin puts its button classes on <a> as often as on <button>, and the
 * library's own `a:not(.btn)` rule would then draw the brand link underline
 * through them. The `a` in this selector is what ties that specificity; the tie
 * is broken by source order, since this file loads after vendor.css. Do not
 * simplify it to a bare class selector.
 */
a.button__small,
a.button__big {
    text-decoration: none;
    border-bottom: 0;
}

/* --------------------------------------------------------- content column */

/*
 * .page-container comes from the ginger-admin library, which supplies the
 * padding. Only the page-title alignment is added here: theme.css centres h1 for
 * the old full-width centred layout, which is wrong the moment there is a rail.
 */
.page-container h1 {
    text-align: start;
}

/* ----------------------------------------------------------- breadcrumbs */

/*
 * The separator comes from _components.css, which draws
 * /img/v2/icons/breadcrumbs_arrow.svg through
 * `.custom-breadcrumb-item + .custom-breadcrumb-item::before`. That asset is a
 * neutral grey rather than a brand colour, so unlike the navigation icons it does not
 * need masking.
 */
.breadcrumbs {
    display: flex;
    gap: var(--g-space-12);
    align-items: center;
    min-width: 0;
}

.custom-breadcrumb {
    display: flex;
    gap: 0;
    align-items: center;
    min-width: 0;
    margin: 0;
    padding: 0;
    list-style: none;
}

.custom-breadcrumb-item {
    display: flex;
    align-items: center;
    min-width: 0;
}

.custom-breadcrumb-item + .custom-breadcrumb-item::before {
    padding: 0 var(--g-space-10);
    line-height: 1;
}

.custom-breadcrumb-item > * {
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

/* The library's `a:not(.btn)` would otherwise draw the brand link underline in
   the trail; the extra `a` ties the specificity and source order wins it. */
a.sub-header-small {
    color: var(--text-placeholder);
    text-decoration: none;
    border-bottom: 1px solid var(--text-placeholder);
}

a.sub-header-small:hover {
    color: var(--text-secondary);
    border-bottom-color: var(--text-secondary);
}

.custom-breadcrumb-item [aria-current="page"] {
    color: var(--text-secondary);
}

/*
 * The back control. Admin's navigates to the second-to-last crumb rather than
 * calling history.back(), which is why this is an anchor and not a button.
 */
.breadcrumbs a.breadcrumb-back {
    display: flex;
    flex: 0 0 auto;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    padding: 0;
    color: var(--text-secondary);
    border-bottom: 0;
    border-radius: var(--radius-control);
    transition: var(--transition-paint);
}

.breadcrumbs .breadcrumb-back:hover {
    color: var(--color-primary-on-subtle);
    background-color: var(--color-primary-subtle);
}

.breadcrumbs .breadcrumb-back:focus-visible {
    outline: 2px solid var(--color-primary-hover);
    outline-offset: 1px;
}

.breadcrumb-back-icon {
    width: 10px;
    height: 16px;
    background-color: currentcolor;
    mask-image: url("/img/v2/icons/arrow_back.svg");
    mask-position: center;
    mask-repeat: no-repeat;
    mask-size: contain;
}

/* ----------------------------------------------------------- page header */

/*
 * Admin pins .tab-bar to 29px, which fits its 24px h1. The Design System's h1 is
 * 40px, and the precedence rule gives the Design System the value - so the row
 * grows to its content and keeps Admin's alignment and 30px trailing gap.
 */
.page-header .tab-bar {
    gap: var(--g-space-12);
    height: auto;
    min-height: 40px;
    margin-bottom: 0;
}

.page-header .tab-bar .headline {
    min-width: 0;
    margin-bottom: 0;
}

.page-header {
    margin-bottom: var(--g-space-32);
}

.page-header-subtitle {
    margin: var(--g-space-6) 0 0;
}

/* ------------------------------------------------------------------- tabs */

/*
 * The one sub-navigation convention: both sub-navs use .card-tabs, which
 * _components.css already styles. Include _tabs.twig rather than hand-writing it.
 *
 * Two corrections to the library's version. Its .tab-activated is grey
 * (--surface-muted), which reads as disabled rather than selected, so the Design
 * System's tinted fill is used instead. And a tab rendered as a link needs the
 * library's link treatment kept off it.
 *
 * Tabs are the one anchor-shaped component that does NOT need its element named,
 * because _tabs.twig gives each one `.btn` and the library's rule is `a:not(.btn)`.
 */
.card-tabs .tab {
    text-decoration: none;
    border: 0;
    transition: var(--transition-paint);
}

/*
 * `.tab.tab-activated`, not `.tab-activated`. The library's `.card-tabs .tab:hover`
 * is (0,3,0) and a bare `.card-tabs .tab-activated` is (0,2,0), so pointing at the
 * tab you are already on repainted it in the idle hover grey - the selected tab
 * announced itself as unselected for exactly as long as you looked at it. Matching
 * (0,3,0) and winning on source order fixes it, and the state gets a hover of its
 * own so the tab still acknowledges the pointer.
 */
.card-tabs .tab.tab-activated {
    color: var(--color-primary-on-subtle);
    background-color: var(--color-primary-subtle);
}

.card-tabs .tab.tab-activated:hover {
    background-color: var(--color-primary-subtle-hover);
}

.card-tabs .tab:focus-visible {
    outline: 2px solid var(--color-primary-hover);
    outline-offset: 1px;
}

/* ----------------------------------------------------------------- search */

/*
 * The screen search.
 *
 * The outer classes - .closed-search, .search-block, .search-filters,
 * .search-filters-header, .filters-body - come from _components.css and are used
 * as-is. The filter atoms are authored here, because the component library has none.
 *
 * The disclosure is a native <details>, so the three filter states work with
 * scripting off. public/js/app/app.js only adds the outside-click close and the
 * per-chip clear, both enhancements.
 */
.search-form {
    display: flex;
    flex: 0 0 auto;
}

/*
 * The library sizes .search-block at a fixed 360px for Admin's in-flow layout. In
 * the chrome the trigger and the panel want different widths, so the block itself
 * becomes the positioning context and nothing more.
 */
.topchrome .search-block {
    position: relative;
    z-index: 5;
    width: auto;
}

.search-block > summary {
    list-style: none;
    cursor: pointer;
}

.search-block > summary::-webkit-details-marker {
    display: none;
}

.search-block > summary:focus-visible {
    outline: 2px solid var(--color-primary-hover);
    outline-offset: 1px;
}

/* The library's .search-text is a fixed 175px, which truncates mid-word rather
   than at the edge of the control. */
.closed-search .search-text,
.closed-search .search-value {
    flex: 1 1 auto;
    width: auto;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

/* The applied term. .search-text is the library's placeholder treatment; this is
   the same line rendered as a value. */
.closed-search .search-value {
    color: var(--text-default);
    font-size: var(--font-size-body);
}

/*
 * Open replaces closed, in the same place.
 *
 * The trigger keeps its box - `visibility: hidden`, not `display: none` - so the chrome
 * does not reflow when the search opens, and .opened-search overlays exactly that box.
 * Hiding it outright would collapse the block to nothing and shove the account block
 * across; pinning a width instead would mean tracking .closed-search's 250px and its
 * two breakpoint overrides by hand.
 *
 * This is what fixes the two-search-boxes defect: there is only ever one visible search
 * control, and it is in the same position whether the panel is open or shut.
 */
.search-block[open] > summary {
    visibility: hidden;
}

.opened-search {
    position: absolute;
    top: 0;
    right: 0;
    left: 0;
    z-index: 6;
}

/*
 * The filter panel. Admin's is in flow inside a 34px row and overlaps what follows on
 * z-index alone; here it has to be taken out of flow, or a 64px chrome would grow
 * to the height of the filter list.
 *
 * `top: 100%` and no gap: .search-filters carries the library's own `margin-top: 5px`,
 * so adding a gap here as well spaced it twice.
 */
.search-panel {
    position: absolute;
    top: 100%;
    right: 0;
    z-index: 5;
    display: flex;
    flex-direction: column;
    /*
     * Wider than Admin's 360px .search-block. A created-between here is four
     * controls rather than Admin's two datetime-locals, because the API takes date
     * and time separately, and 360 puts the last one on a line of its own.
     */
    width: 480px;
    max-width: calc(100vw - var(--g-space-40));
}

.opened-search input {
    flex: 1 1 auto;
    min-width: 0;
    padding: 0;
    color: var(--text-default);
    background-color: transparent;
    border: 0;
    /* The ring is on .opened-search itself, which is the whole control. */
    outline: 0;
}

/* Admin draws these through `content: url()` on an <img>, which pins them to the
   green they were drawn with. Same masking rule as the rail; see above. */
.search-icon {
    flex: 0 0 auto;
    width: 16px;
    height: 16px;
    background-color: var(--text-subtle);
    mask-image: url("/img/v2/icons/search_icon.svg");
    mask-position: center;
    mask-repeat: no-repeat;
    mask-size: contain;
}

.search-filters-header {
    margin: 0 0 var(--g-space-10);
    letter-spacing: var(--tracking-eyebrow);
    text-transform: uppercase;
}

.search-filters-actions {
    display: flex;
    gap: var(--g-space-10);
    justify-content: flex-end;
    margin-top: var(--g-space-16);
}

/* ------------------------------------------------------------ filter chips */

/*
 * One chip per filter, in three states. Admin swaps between three elements for
 * those; here the control never changes, "set" is decided server-side and "open"
 * is :focus-within. See resources/views/_search_field.twig.
 */
.filter-chip {
    display: inline-flex;
    gap: var(--g-space-5);
    align-items: center;
    min-height: 22px;
    padding: var(--g-space-2) var(--g-space-8);
    background-color: var(--surface-app);
    border: 1px solid var(--border-default);
    border-radius: var(--radius-control);
}

.filter-chip:focus-within {
    border-color: var(--color-primary);
    box-shadow: var(--focus-ring);
}

.filter-chip.is-set {
    background-color: var(--color-primary-subtle);
    border-color: var(--color-primary-alpha-20);
}

.filter-chip .filter-input {
    min-width: 0;
    max-width: 100%;
    padding: 0;
    color: var(--text-muted);
    font-size: var(--font-size-meta);
    line-height: var(--line-height-snug);
    background-color: transparent;
    border: 0;
    /* The ring is drawn by the chip, so the control's own outline is redundant
       rather than removed: :focus-within above is what replaces it. */
    outline: 0;
}

.filter-chip.is-set .filter-input {
    color: var(--color-primary-on-subtle);
}

/*
 * `size` sizes a text chip to its own placeholder, but it is only legal on the
 * textual input types - so the amount and status-code chips take a width here
 * instead. ginger-admin does the same thing at `.filter-amount .input-body input`.
 */
.filter-chip .filter-input[type="number"] {
    width: 7ch;
}

.filter-chip .filter-input::placeholder {
    color: var(--text-subtle);
    opacity: 1;
}

.filter-clear {
    display: flex;
    flex: 0 0 auto;
    align-items: center;
    justify-content: center;
    padding: 0;
    color: var(--color-primary-on-subtle);
    background-color: transparent;
    border: 0;
    cursor: pointer;
}

.filter-clear:focus-visible {
    outline: 2px solid var(--color-primary-hover);
    outline-offset: 1px;
}

.filter-clear-icon {
    width: 10px;
    height: 10px;
    background-color: currentcolor;
    mask-image: url("/img/v2/icons/tag_xmark.svg");
    mask-position: center;
    mask-repeat: no-repeat;
    mask-size: contain;
}

/*
 * A range takes a row of its own and labels itself above its controls. Four
 * controls, a label and a separator do not fit on one line of a popover, and
 * wrapping them inline stranded the separator at the end of a line.
 */
.filter-range {
    display: flex;
    flex: 1 0 100%;
    flex-direction: column;
    gap: var(--g-space-4);
}

.filter-range-fields {
    display: flex;
    flex-wrap: wrap;
    gap: var(--g-space-5);
    align-items: center;
}

.filter-range-label,
.filter-range-separator {
    color: var(--text-subtle);
    font-size: var(--font-size-caption);
}

/* ------------------------------------------------------------- pagination */

/*
 * The results footer. Ported from ginger-admin's elements/Pagination.vue, whose
 * scoped styles - like the filter atoms - never reached _base.css.
 */
.pagination-bar {
    display: flex;
    flex-wrap: wrap;
    gap: var(--g-space-16);
    align-items: center;
    justify-content: space-between;
    margin-top: var(--g-space-20);
}

.pagination-summary {
    margin: 0;
    color: var(--text-muted);
    font-size: var(--font-size-body);
}

.pagination-pages {
    display: flex;
    flex-wrap: wrap;
    gap: var(--g-space-5);
    align-items: center;
    margin-left: auto;
}

/* The current page is a <span> and every other control is an <a>, so this carries
   both forms for the reason given under "anchor-shaped components" at the top of the
   file. Without the `a` member, hovering a page number dropped its 10px of side
   padding and drew the brand underline through it. */
.pagination-pages .pagination-link,
.pagination-pages a.pagination-link {
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 28px;
    height: 28px;
    padding: 0 var(--g-space-10);
    color: var(--text-secondary);
    font-size: var(--font-size-body);
    text-decoration: none;
    background-color: var(--surface-app);
    border-bottom: 0;
    border-radius: var(--radius-control);
    transition: var(--transition-paint);
}

.pagination-pages .pagination-link:hover {
    color: var(--text-strong);
    background-color: var(--surface-muted);
}

.pagination-pages .pagination-link:focus-visible {
    outline: 2px solid var(--color-primary-hover);
    outline-offset: 1px;
}

.pagination-pages .is-current {
    color: var(--text-strong);
    background-color: var(--surface-muted);
}

.pagination-pages .pagination-step:hover {
    color: var(--color-primary-on-subtle);
    background-color: var(--color-primary-subtle);
}

.pagination-icon {
    width: 16px;
    height: 16px;
    background-color: currentcolor;
    mask-position: center;
    mask-repeat: no-repeat;
    mask-size: contain;
}

.pagination-icon-previous {
    mask-image: url("/img/v2/icons/arrow_left.svg");
}

.pagination-icon-next {
    mask-image: url("/img/v2/icons/arrow_right.svg");
}

/* ------------------------------------------------------------- field rows */

/*
 * The Design System's replacement for the <dl class="row"> detail view, which was
 * 133 dt/dd pairs across nine screens laid out on Bootstrap's grid at col-sm-3 /
 * col-sm-9 - a label column that grew with the viewport, so on a wide screen the
 * label sat a third of the page away from its value.
 *
 * A two-column grid instead, with a fixed label column. The pairs are strictly
 * one-to-one in every one of those screens, so auto-placement puts each on its own
 * row without any per-row markup.
 *
 * Cells are stretched rather than centred: the Design System centres a field row,
 * which is identical for the single-line rows that make up almost all of these,
 * and top-aligns the label against a multi-line value, which is what an address or
 * a stack of badges wants.
 */
.field-rows {
    display: grid;
    grid-template-columns: var(--layout-field-label-width) 1fr;
    margin: 0;
}

.field-rows > dt,
.field-rows > dd {
    margin: 0;
    padding: var(--g-space-12) 0;
    font-size: var(--font-size-body);
    font-weight: var(--font-weight-regular);
    border-bottom: 1px solid var(--border-subtle);
}

.field-rows > dt {
    grid-column: 1;
    padding-right: var(--g-space-16);
    color: var(--text-muted);
}

.field-rows > dd {
    grid-column: 2;
    color: var(--text-default);
}

.field-rows > dt:nth-last-child(2),
.field-rows > dd:last-child {
    border-bottom: 0;
}

/* A 200px label column and a 320px viewport do not both fit. */
@media (width < 576px) {
    .field-rows {
        grid-template-columns: 1fr;
    }

    .field-rows > dt,
    .field-rows > dd {
        grid-column: 1;
    }

    .field-rows > dt {
        padding-bottom: 0;
        border-bottom: 0;
    }
}

/* ------------------------------------------------------ permission matrix */

/*
 * One row per path, one column per method. See _permission_matrix.twig for what it
 * replaced and for the hidden `value="0"` companions.
 *
 * theme.css styles bare `th` at 75% size and 0.5 opacity for the old result
 * tables, so every property it sets is restated here rather than half-inherited -
 * a 0.5-opacity path is unreadable, and it is the most important cell in the row.
 */
/*
 * Capped rather than full-bleed. The method columns shrink to their switches and
 * the path column takes the rest, so on a 1440px screen the switches ended up
 * 900px from the path they belong to and the row stopped reading as one thing.
 */
.permission-matrix {
    width: 100%;
    max-width: 720px;
    border-collapse: collapse;
}

.permission-matrix th,
.permission-matrix td {
    padding: var(--g-space-8) var(--g-space-10);
    text-align: left;
    vertical-align: middle;
    border-bottom: 1px solid var(--border-subtle);
}

.permission-matrix thead th {
    color: var(--text-placeholder);
    font-size: var(--font-size-eyebrow);
    font-weight: var(--font-weight-regular);
    letter-spacing: var(--tracking-eyebrow);
    text-transform: uppercase;
    white-space: nowrap;
    opacity: 1;
}

.permission-matrix tbody th {
    color: var(--text-default);
    font-size: var(--font-size-body);
    font-weight: var(--font-weight-regular);
    letter-spacing: normal;
    text-transform: none;
    opacity: 1;
}

.permission-matrix tbody tr:hover {
    background-color: var(--surface-hover);
}

.permission-matrix .permission-cell,
.permission-summary .permission-cell {
    width: 1%;
    text-align: center;
}

/*
 * The read-only twin, _permission_summary.twig. It takes .basic-table for its rules
 * and type and only needs the matrix's two geometry decisions: method columns that
 * shrink to their content, and a capped width so the switches - here, the checks -
 * stay next to the path they belong to instead of 900px away on a wide screen.
 */
.permission-summary {
    max-width: 720px;
}

.permission-summary .permission-granted {
    color: var(--color-primary-on-subtle);
    font-size: var(--font-size-body);
    vertical-align: middle;
}

.permission-summary .permission-cell {
    color: var(--text-subtle);
}

/*
 * Bootstrap's .form-switch indents .form-check by 2.5em and pulls the input back
 * by the same amount, which is how it makes room for a label that follows. These
 * labels are visually hidden, so both halves have to come off or the switch sits
 * outside its own cell.
 */
.permission-matrix .form-check {
    display: flex;
    justify-content: center;
    min-height: 0;
    margin: 0;
    padding-left: 0;
}

.permission-matrix .form-switch .form-check-input {
    margin-left: 0;
}

.permission-matrix tfoot td {
    border-bottom: 0;
}

.permission-note {
    max-width: 640px;
    margin-bottom: var(--g-space-20);
    color: var(--text-muted);
    font-size: var(--font-size-body);
    white-space: pre-line;
}

.permission-empty {
    color: var(--text-subtle);
}

.permission-actions {
    margin-top: var(--g-space-20);
    margin-left: 0;
    justify-content: flex-start;
}

/* ----------------------------------------------------------------- tables */

/*
 * .basic-table is the component library's list table. Use it for any list-shaped
 * block on a detail screen; do not fake columns with <dl class="row"> and col-sm-*.
 *
 * _components.css supplies the type scale and the 34px row. Three things it does not
 * supply, or gets wrong for this app:
 */
.basic-table {
    width: 100%;
    margin-bottom: 0;
    border-collapse: collapse;
}

/*
 * A rule per row, which the library leaves to Bootstrap's `.table` and therefore to
 * whether the caller remembered `.table-borderless`. Field rows already separate
 * their pairs this way, and a detail screen that separates its dl pairs but not its
 * table rows reads as two different components.
 */
.basic-table > thead > tr > th,
.basic-table > tbody > tr > td,
.basic-table > tbody > tr > th {
    padding: var(--g-space-8) var(--g-space-10);
    border-bottom: 1px solid var(--border-subtle);
}

/*
 * A row header is the row's subject - an IBAN, a payment method, an API path - so it
 * reads as a value, not as a bolded label. theme.css scopes the eyebrow treatment to
 * `thead th`, so this only has to undo Bootstrap's own `th { font-weight: 500 }`.
 */
.basic-table > tbody > tr > th {
    color: var(--text-default);
    font-weight: var(--font-weight-regular);
}

/* Flush with the card's own padding at both ends of the row. */
.basic-table > thead > tr > th:first-child,
.basic-table > tbody > tr > td:first-child,
.basic-table > tbody > tr > th:first-child {
    padding-left: 0;
}

.basic-table > thead > tr > th:last-child,
.basic-table > tbody > tr > td:last-child,
.basic-table > tbody > tr > th:last-child {
    padding-right: 0;
}

/* The last row sits on the card's own edge; a rule there is a second border. */
.basic-table > tbody > tr:last-child > td,
.basic-table > tbody > tr:last-child > th {
    border-bottom: 0;
}

/*
 * The row hover tint is kept - tracking one record across eight columns is what it is
 * for - but it goes on the row, not the cell. The library's per-cell version, and the
 * `!important` declarations that come with it, are corrected in theme.css, which is
 * the layer allowed to answer `!important` with `!important`.
 */
.basic-table > tbody > tr:hover {
    background-color: var(--surface-hover);
}

/*
 * Navigable rows.
 *
 * Every index list used to end in a `⋯` column: a 16px icon, at the far right of a
 * 1400px row, as the only way into the record the rest of the row described. The row
 * is the click target now and the column is gone.
 *
 * ginger-admin does the same and does it with `@click` plus `@mousedown` on a bare
 * `<tr>` - no link anywhere in the row. That is the same gap _sidebar.twig records
 * about Admin's navigation, which wires `<div>`s to click handlers that cannot be
 * middle-clicked, opened in a new tab or reached by keyboard. So here the row keeps a
 * real anchor in its identifying cell, and app.js only widens the target to the rest
 * of the row. With scripting off the list still navigates; it just needs the first
 * cell rather than any of it.
 *
 * The pointer cursor is honest on these rows and only on these rows - see the
 * `:not(.row-link)` correction in theme.css for the ones that are not click targets.
 */
.basic-table > tbody > tr.row-link {
    cursor: pointer;
}

/* Keyboard parity: the row that has focus inside it looks like the row under the
   pointer, so tabbing through a list reads the same way as hovering one. */
.basic-table > tbody > tr.row-link:focus-within {
    background-color: var(--surface-hover);
}

.basic-table a.row-link-target:focus-visible {
    outline: 2px solid var(--color-primary-hover);
    outline-offset: 1px;
}

/* Figures line up on the right; so does the header over them. */
.basic-table .text-end {
    text-align: right;
}

/* ------------------------------------------------------------- responsive */

/*
 * Neither source has a mobile layout: the Design System has no media queries at
 * all and Admin lays its pages out in `width: 100vw` wrappers. The portal is
 * responsive today through Bootstrap, so a naive copy would be a regression -
 * hence the drawer, which is new work rather than a port.
 */
@media (width < 992px) {
    .app-sidebar {
        position: fixed;
        top: 0;
        bottom: 0;
        left: 0;
        z-index: 1045;
        width: var(--layout-sidebar-width);
        transform: translateX(-100%);
        transition: transform var(--duration-medium) var(--ease-out);
    }

    /* The collapsed preference is a desktop one; the drawer is always full width. */
    .sidebar-collapsed .app-sidebar {
        width: var(--layout-sidebar-width);
    }

    .sidebar-collapsed .sidebar-head {
        justify-content: space-between;
        padding-right: var(--g-space-6);
        padding-left: var(--g-space-6);
    }

    .sidebar-collapsed .sidebar-logo {
        display: flex;
    }

    .sidebar-collapsed .app-sidebar .sidebar-link {
        justify-content: flex-start;
        padding-right: var(--g-space-8);
        padding-left: var(--g-space-12);
    }

    .sidebar-collapsed .sidebar-label,
    .sidebar-collapsed .sidebar-group-label {
        position: static;
        width: auto;
        height: auto;
        overflow: visible;
        clip-path: none;
    }

    .sidebar-collapsed .sidebar-group-label {
        padding: var(--g-space-14) var(--g-space-12) var(--g-space-6);
    }

    .nav-open .app-sidebar {
        transform: translateX(0);
    }

    .nav-toggle {
        display: inline-flex;
    }

    .nav-scrim {
        position: fixed;
        inset: 0;
        z-index: 1040;
        background-color: var(--scrim);
    }

    .app-main .page-container {
        padding-right: var(--g-space-20);
        padding-left: var(--g-space-20);
    }

    .topchrome {
        padding: 0 var(--g-space-20);
    }

    .closed-search {
        width: 160px;
    }
}

/* Narrower still, and the trigger keeps only its icon. */
@media (width < 576px) {
    .closed-search {
        width: auto;
    }

    .closed-search .search-text {
        position: absolute;
        width: 1px;
        height: 1px;
        padding: 0;
        overflow: hidden;
        white-space: nowrap;
        clip-path: inset(50%);
    }

}

/*
 * The panel stops hanging off the screen.
 *
 * It is anchored to the trigger's right edge, so its left edge lands at
 * `trigger_right - 480`. Once the viewport is narrow enough that the trigger sits less
 * than 480px from the left, that is negative - it measured -63px on a 390px screen - and
 * Admin's `overflow-x: hidden` on html clips it instead of letting it scroll, so the
 * whole left column of filters was unreachable rather than merely awkward.
 *
 * Below this width the panel is fixed to the viewport instead, inset by the same 20px
 * the chrome uses. `position: fixed` resolves against the viewport here: the chrome is
 * `sticky`, and sticky does not create a containing block for fixed descendants.
 *
 * 600px, not the 576px below, and the number is derived rather than chosen: the trigger's
 * right edge is `viewport - 20 - account block`, and the panel needs that to be at least
 * 480 + 20. It measured 497 at 600px and 473 at 576px. **If anything is added to the top
 * chrome the account block grows and this boundary moves** - re-measure it rather than
 * assuming 600 still holds.
 */
@media (width < 600px) {
    .search-panel {
        position: fixed;
        top: var(--layout-topchrome-height);
        right: var(--g-space-20);
        left: var(--g-space-20);
        width: auto;
        max-width: none;
    }
}

/* The drawer slides; a reduced-motion preference means it should not. */
@media (prefers-reduced-motion: reduce) {
    .app-sidebar,
    .sidebar-toggle-glyph {
        transition: none;
    }
}
