/* ============================================================
   Reset & base
   ============================================================ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

/* No scrollbar-gutter reservation, no forced scrollbar — the document
   shows a scrollbar only when content actually overflows. On short
   pages .site-header reaches the viewport edge; on long pages the
   scrollbar appears and the header butts up to it. Going from short
   to long (e.g. opening a modal that locks body scroll) causes a
   horizontal shift of one scrollbar width; modals that mind this can
   compensate via their own lock styles. */
html { overflow-y: auto; }

body {
    font-family: system-ui, -apple-system, sans-serif;
    font-size: 14px;
    line-height: 1.5;
    color: #1f2937;
    background: #f3f4f6;
}

a { color: #2563eb; text-decoration: none; }
a:hover { text-decoration: underline; }

/* ============================================================
   Density spacing tokens — page chrome
   ============================================================
   The per-user Comfortable / Condensed toggle (see the "Table density"
   block further down) governs PAGE CHROME as well as table cells: the
   layout-holding spacing below is declared through these tokens, and the
   condensed override tightens them all at once. Comfortable (attribute
   absent) resolves every token to today's exact literal, so default
   rendering is byte-identical; every consuming declaration also carries
   the same literal as its var() fallback, so a rule is correct even in
   isolation.

   Deliberately COARSE: four tokens for layout spacing (page padding,
   card/section padding + big block margins, section gaps, form-row
   rhythm) — not a token per measurement. Fine positioning, borders,
   small nudges and CONTROL padding (buttons, inputs, menu items) stay
   literal: density moves layout whitespace, never touch-target size.

   Projects opt their own chrome in by consuming the tokens
   (`padding: var(--bspkfw-space-card, 24px);`) — they then tighten in
   step with framework chrome, for free. A project that wants the OLD
   table-only condensed look re-pins the tokens to their comfortable
   values in its own stylesheet:

       :root[data-table-density="condensed"] {
           --bspkfw-space-page: 32px 24px;  --bspkfw-space-card: 24px;
           --bspkfw-space-section-gap: 20px; --bspkfw-space-row: 16px;
       }
   */
:root {
    --bspkfw-space-page:        32px 24px;  /* <main> outer padding */
    --bspkfw-space-card:        24px;       /* card / .page-section padding, page-header + fieldset margins, modal x-padding */
    --bspkfw-space-section-gap: 20px;       /* .page-sections gap, section-desc + tab-bar margins */
    --bspkfw-space-row:         16px;       /* form rows / stacked blocks (.form-group, .alert, em-groups) */

    /* Modal stacking scale. Two `.modal`s at the same z-index stack by DOM
       order — pure luck — so a modal that OPENS FROM another modal (the
       photo cropper launched inside an edit modal) takes the nested rank.
       The next nested-modal case takes the same token rather than minting
       another magic number. */
    --bspkfw-z-modal:        100;
    --bspkfw-z-modal-nested: 110;
}
:root[data-table-density="condensed"] {
    --bspkfw-space-page:        16px 20px;
    --bspkfw-space-card:        14px;
    --bspkfw-space-section-gap: 12px;
    --bspkfw-space-row:         10px;
}

/* ============================================================
   Form-field tokens — .form-control AND the Kselect widget
   ============================================================
   One palette for "things that look like a form field", so a theme can
   restyle inputs and the searchable-select widget together instead of
   chasing two vocabularies. Every token defaults to the exact literal
   .form-control already used, and every consuming declaration repeats
   that literal as its var() fallback — so an untouched install renders
   byte-identically and a rule stays correct in isolation. Same
   discipline as the density tokens above.

   WHY THIS EXISTS: kselect ships ~44 of its own --ks-* custom
   properties with hardcoded light values. Before this block, the
   midnight theme darkened .form-control and overrode none of them, so
   the enhanced control and its body-level dropdown rendered white on a
   dark page. Mapping --ks-* through these tokens (below) means a theme
   sets ONE palette and both surfaces follow.

   A theme overrides the --bspkfw-field-* tokens and never touches
   --ks-* directly (see public_html/themes/midnight/theme.css).

   Load-order note: kselect 1.7.0 declares its defaults at
   :where(:root) — zero specificity — so this :root block wins even
   though kselect.min.css is linked after base.css and after the theme
   (LayoutRenderer). On kselect < 1.7.0 it would have lost, and the
   mapping had to live on `body` to win by proximity instead. */
:root {
    --bspkfw-field-bg:              #fff;
    --bspkfw-field-text:            #1f2937;
    --bspkfw-field-border:          #d1d5db;
    --bspkfw-field-border-focus:    #2563eb;
    --bspkfw-field-focus-ring:      rgba(37,99,235,0.12);
    --bspkfw-field-placeholder:     #6b7280;
    --bspkfw-field-muted:           #6b7280;   /* group labels, badges, arrow, clear */
    --bspkfw-field-surface:         #f9fafb;   /* dropdown search bar, group headers */
    --bspkfw-field-hover:           #f3f4f6;   /* option row hover / keyboard focus */
    --bspkfw-field-accent:          #2563eb;   /* selected ink, checkbox, tag text */
    --bspkfw-field-accent-soft:     #eff6ff;   /* tag fill, selected-row fill */
    --bspkfw-field-accent-border:   #bfdbfe;
    --bspkfw-field-disabled-bg:     #f3f4f6;
    --bspkfw-field-disabled-fg:     #9ca3af;
    --bspkfw-field-disabled-border: #e5e7eb;
    --bspkfw-field-radius:          6px;
    --bspkfw-field-invalid:         #f87171;
    --bspkfw-field-invalid-ring:    rgba(239,68,68,0.12);

    /* Kselect ← framework. Two deliberate departures from kselect's own
       defaults, both for consistency rather than taste: its accent is
       indigo (#6366f1) where every other focus ring, link and primary
       button here is blue (#2563eb), and its font stack is a hardcoded
       'Inter' where the rest of the page inherits system-ui. Geometry is
       pulled back to .form-control's 6px so an enhanced select and a
       plain one sit level in the same form. */
    --ks-font-family:               inherit;
    --ks-font-size:                 14px;
    --ks-color-bg:                  var(--bspkfw-field-bg, #fff);
    --ks-color-text:                var(--bspkfw-field-text, #1f2937);
    --ks-color-border:              var(--bspkfw-field-border, #d1d5db);
    --ks-color-border-focus:        var(--bspkfw-field-border-focus, #2563eb);
    --ks-color-placeholder:         var(--bspkfw-field-placeholder, #6b7280);
    --ks-color-disabled-bg:         var(--bspkfw-field-disabled-bg, #f3f4f6);
    --ks-color-disabled-text:       var(--bspkfw-field-disabled-fg, #9ca3af);
    --ks-color-disabled-border:     var(--bspkfw-field-disabled-border, #e5e7eb);
    --ks-color-option-hover:        var(--bspkfw-field-hover, #f3f4f6);
    --ks-color-option-selected:     var(--bspkfw-field-accent, #2563eb);
    --ks-color-option-selected-bg:  var(--bspkfw-field-accent-soft, #eff6ff);
    --ks-color-option-selected-fg:  var(--bspkfw-field-accent, #2563eb);
    --ks-color-group-header:        var(--bspkfw-field-surface, #f9fafb);
    --ks-color-group-label:         var(--bspkfw-field-muted, #6b7280);
    --ks-color-search-bg:           var(--bspkfw-field-surface, #f9fafb);
    --ks-color-tag-bg:              var(--bspkfw-field-accent-soft, #eff6ff);
    --ks-color-tag-text:            var(--bspkfw-field-accent, #2563eb);
    --ks-color-tag-border:          var(--bspkfw-field-accent-border, #bfdbfe);
    --ks-color-tag-remove:          var(--bspkfw-field-accent, #2563eb);
    --ks-color-tag-remove-hover:    var(--bspkfw-field-invalid, #f87171);
    --ks-color-clear:               var(--bspkfw-field-muted, #6b7280);
    --ks-color-arrow:               var(--bspkfw-field-muted, #6b7280);
    --ks-color-arrow-open:          var(--bspkfw-field-border-focus, #2563eb);
    --ks-color-checkbox-border:     var(--bspkfw-field-border, #d1d5db);
    --ks-color-checkbox-checked:    var(--bspkfw-field-accent, #2563eb);
    --ks-color-limit-badge-bg:      var(--bspkfw-field-surface, #f9fafb);
    --ks-color-limit-badge-text:    var(--bspkfw-field-muted, #6b7280);
    --ks-radius-control:            var(--bspkfw-field-radius, 6px);
    --ks-radius-dropdown:           var(--bspkfw-field-radius, 6px);
    /* Left at kselect's defaults deliberately: --ks-color-limit-reached-*
       (an amber warning state, semantic rather than themed),
       --ks-color-option-selected-text / -selected-row / -checkbox-tick
       (derived from the tokens above by kselect itself), --ks-radius-tag,
       --ks-radius-checkbox, --ks-shadow-dropdown, --ks-transition,
       --ks-z-index, --ks-line-height. */
}

/* ============================================================
   Site chrome
   ============================================================ */
.site-header {
    background: #1e293b;
    padding: 0 24px;
    height: 52px;
    display: flex;
    align-items: center;
    justify-content: space-between;
}

.site-title {
    color: #f8fafc;
    font-size: 16px;
    font-weight: 600;
    text-decoration: none;
    /* The header's only shrinkable item. Everything to its right (region
       pill, bell, user menu / guest auth affordances) is fixed-size, so
       without this a long brand + a busy right-hand cluster pushes the
       chrome off the side of a phone rather than truncating. Inert whenever
       the title fits — which is every install whose brand isn't already too
       wide for the viewport. */
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.site-nav-right {
    display: flex;
    align-items: center;
    gap: 12px;
    /* Never squeezed — the title above absorbs the shortfall instead. */
    flex-shrink: 0;
}

/* Optional project main-nav slot (Project::topNavItems()). Rendered between
   the site-title and the right-side chrome; absent entirely unless the
   project opts in. margin-right:auto keeps the links beside the title and
   pushes .site-nav-right to the far right. Projects override freely. */
.site-nav-main {
    display: flex;
    align-items: center;
    gap: 4px;
    margin-left: 24px;
    margin-right: auto;
}

.site-nav-main-item {
    color: #cbd5e1;
    text-decoration: none;
    font-size: 14px;
    font-weight: 500;
    padding: 6px 10px;
    border-radius: 6px;
    line-height: 1;
}

.site-nav-main-item:hover,
.site-nav-main-item:focus-visible {
    color: #f8fafc;
    background: #334155;
}

/* Current/active section — set by Project::topNavItems() items with
   'active' => true. Theme-neutral: brighter text, bolder weight, and an
   accent underline drawn in currentColor (no layout shift, no fixed colour),
   so it stays visible against a project-restyled nav and is overridable. */
.site-nav-main-item.is-active {
    color: #f8fafc;
    font-weight: 700;
    text-decoration: underline;
    text-decoration-thickness: 2px;
    text-underline-offset: 6px;
}

/* Dropdown groups in the project main-nav — topNavItems() items with
   `children` (LayoutRenderer::renderTopNav + /js/site-nav.js). The nav and
   each group stretch to the full header height so the flyout panel sits
   flush under the header with NO hover dead-zone between trigger and panel
   (a gap would drop :hover mid-crossing and flicker the menu shut). Items
   stay vertically centered via the existing align-items, so a flat nav is
   visually unchanged. Hover-open is pointer-device-only (hover:hover);
   click/touch + keyboard toggle `.is-open` via site-nav.js. */
.site-nav-main { align-self: stretch; }
.site-nav-main-group {
    position: relative;
    display: flex;
    align-items: center;
    align-self: stretch;
}
/* Trigger may be a <button> (parent without href) — strip the UA button
   chrome so it reads identically to the <a> items beside it. */
.site-nav-main-trigger {
    background: transparent;
    border: 0;
    font: inherit;
    font-size: 14px;
    font-weight: 500;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
}
.site-nav-main-trigger.is-active { font-weight: 700; }
.site-nav-caret {
    font-size: 9px;
    line-height: 1;
    margin-left: 5px;
    transition: transform 120ms ease;
}
.site-nav-main-group.is-open .site-nav-caret { transform: rotate(180deg); }

.site-nav-dropdown {
    display: none;
    position: absolute;
    top: 100%;
    left: 0;
    min-width: 180px;
    max-width: calc(100vw - 16px);
    background: #fff;
    border: 1px solid #e5e7eb;
    border-radius: 0 0 8px 8px;
    box-shadow: 0 6px 20px rgba(0,0,0,0.12);
    padding: 4px 0;
    z-index: 200;
}
.site-nav-main-group.is-open > .site-nav-dropdown { display: block; }
@media (hover: hover) {
    .site-nav-main-group:hover > .site-nav-dropdown { display: block; }
}
.site-nav-dropdown-item {
    display: block;
    padding: 8px 14px;
    color: #374151;
    text-decoration: none;
    font-size: 14px;
    white-space: nowrap;
}
.site-nav-dropdown-item:hover,
.site-nav-dropdown-item:focus-visible { background: #f3f4f6; color: #111827; }
.site-nav-dropdown-item.is-active { font-weight: 700; color: #111827; }

/* Small-screen strategy: when the nav's items outgrow the header, the nav
   ITSELF scrolls and the page never does. site-nav.js measures on load /
   resize / font-load and toggles `.is-overflowing` — engagement is by
   MEASUREMENT, not a breakpoint, because the overflow point is a function
   of how many top-level menus the project defines (a twelve-menu project
   overflows at ~1370px; a four-menu one may never). A nav that fits keeps
   today's rules byte-for-byte: no scroll container, no clipping context.
   `min-width: 0` lets the flex item shrink below its content so the
   scrolling is contained; children keep their natural widths. The
   scrollbar is hidden (chip-row idiom — touch scrolls by momentum,
   desktop by trackpad / shift-wheel, and keyboard focus auto-scrolls
   items into view), so engaging the mode causes no height shift. */
.site-nav-main.is-overflowing {
    min-width: 0;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
}
.site-nav-main.is-overflowing::-webkit-scrollbar { display: none; }
.site-nav-main.is-overflowing > .site-nav-main-item,
.site-nav-main.is-overflowing > .site-nav-main-group { flex-shrink: 0; }
.site-nav-main.is-overflowing .site-nav-main-item { white-space: nowrap; }
/* A scroll container clips absolutely-positioned descendants — the same
   trap as `overflow: hidden` on a table wrapper clipping the row-actions
   popup, and the same escape: in overflow mode the dropdown is
   `position: fixed`, anchored to its group's rect by site-nav.js at open
   time (viewport coordinates are immune to every ancestor scroll/clip).
   The static `top: 100%` / `left: 0` are neutralised because they mean
   something entirely different against the viewport. */
.site-nav-main.is-overflowing .site-nav-dropdown {
    position: fixed;
    top: auto;
    left: auto;
}

/* Count pill after a nav label (optional `badge` key on any topNavItems()
   item; a group's trigger shows its own badge + the sum of its children's,
   so a collapsed group still surfaces the total). */
.site-nav-badge {
    display: inline-block;
    min-width: 18px;
    padding: 1px 5px;
    margin-left: 6px;
    border-radius: 999px;
    background: #dc2626;
    color: #fff;
    font-size: 11px;
    font-weight: 600;
    line-height: 16px;
    text-align: center;
}

/* Header region picker — single glyph (flag or globe) opens the language /
   locale / currency picker. */
.region-trigger {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: transparent;
    border: 1px solid #334155;
    border-radius: 999px;
    width: 32px;
    height: 32px;
    color: #cbd5e1;
    font-family: inherit;
    cursor: pointer;
    line-height: 1;
    padding: 0;
}
.region-trigger:hover { background: #334155; color: #f1f5f9; }
.region-trigger-icon  { font-size: 16px; line-height: 1; }

/* Header auth affordance for SIGNED-OUT visitors — occupies the same
   .site-nav-right slot the user menu fills when authed. Metrics deliberately
   match .region-trigger (32px pill, same border/ink) so the cluster lines up
   whichever combination renders.

   .header-login is the framework's own quiet "Sign in" link: a returning user
   is looking for it, so it shouldn't compete with the page's own acquisition
   CTA. .header-cta is the shared filled variant for a project's
   Project::guestHeaderActions() button (typically "Sign up") — one class so
   every consumer's guest CTA looks the same instead of each project inventing
   its own. flex-shrink:0 keeps both at natural size in the flex cluster. */
.header-login,
.header-cta {
    display: inline-flex;
    align-items: center;
    height: 32px;
    padding: 0 12px;
    border: 1px solid #334155;
    border-radius: 999px;
    font-size: 13px;
    font-weight: 500;
    line-height: 1;
    text-decoration: none;
    white-space: nowrap;
    flex-shrink: 0;
}
.header-login { color: #cbd5e1; background: transparent; }
/* text-decoration repeated on :hover — the global `a:hover {underline}` rule
   out-specifies the base class selector above, and an underlined pill is not
   a pill. */
.header-login:hover { background: #334155; color: #f1f5f9; text-decoration: none; }
.header-cta { color: #ffffff; background: #2563eb; border-color: #2563eb; }
.header-cta:hover { background: #1d4ed8; border-color: #1d4ed8; color: #ffffff; text-decoration: none; }
.header-login:focus-visible,
.header-cta:focus-visible { outline: 2px solid #60a5fa; outline-offset: 2px; }

/* Header user menu — single avatar trigger that opens a dropdown.
   display:flex on the wrapper takes the trigger out of inline line-box
   layout — without it the inherited body line-height (1.5) creates a
   strut around the inline-block button and shifts the avatar a few
   pixels off the .region-trigger sitting next to it. */
.user-menu { position: relative; display: flex; align-items: center; }
.user-menu-trigger {
    background: transparent;
    border: none;
    padding: 0;
    cursor: pointer;
    line-height: 0;
    border-radius: 50%;
    outline-offset: 2px;
}
.user-menu-trigger:focus-visible { outline: 2px solid #60a5fa; }
.user-menu-photo {
    display: block;
    width: 32px;
    height: 32px;
    border-radius: 50%;
    object-fit: cover;
    border: 1px solid #334155;
}
.user-menu-photo--placeholder {
    background: #334155 url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><circle cx='16' cy='12' r='6' fill='%2394a3b8'/><path d='M4 30c0-7 5-12 12-12s12 5 12 12' fill='%2394a3b8'/></svg>") center/cover no-repeat;
}

.user-menu-dropdown {
    position: absolute;
    top: calc(100% + 8px);
    right: 0;
    min-width: 180px;
    background: #fff;
    border: 1px solid #e5e7eb;
    border-radius: 8px;
    box-shadow: 0 6px 20px rgba(0,0,0,0.12);
    padding: 4px 0;
    z-index: 200;
}
.user-menu-header {
    padding: 8px 14px;
    color: #6b7280;
    font-size: 12px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    border-bottom: 1px solid #f3f4f6;
    margin-bottom: 4px;
}
.user-menu-item {
    display: block;
    padding: 8px 14px;
    color: #374151;
    text-decoration: none;
    font-size: 14px;
}
.user-menu-item:hover { background: #f3f4f6; color: #111827; }
/* Button-flavoured menu items (theme picker, density toggle) — strip the
   UA button chrome so they read identically to the <a> items. */
button.user-menu-item {
    width: 100%;
    text-align: left;
    background: transparent;
    border: 0;
    cursor: pointer;
    font: inherit;
    font-size: 14px;
}
/* Toggle-style menu items (density) — pressed state gets a checkmark. */
.user-menu-item[aria-pressed="true"]::after {
    content: " ✓";
    color: #2563eb;
}
.user-menu-item--danger { color: #dc2626; }
.user-menu-item--danger:hover { background: #fef2f2; color: #b91c1c; }

/* Section divider in the user-menu dropdown — emitted between project-supplied
   items (see PageHandler::renderProjectMenuItems) and the framework's
   Profile/Settings/Logout block, and again before Logout. */
.user-menu-divider {
    height: 1px;
    background: #e5e7eb;
    margin: 4px 0;
}

/* Submenu rows (Framework / System / project-defined). Inline expansion —
   clicking the trigger reveals indented items below it without losing the
   parent dropdown. Uses a button (not <a>) because the trigger itself
   doesn't navigate. */
.user-menu-submenu-trigger {
    width: 100%;
    text-align: left;
    background: transparent;
    border: 0;
    cursor: pointer;
    font: inherit;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
}
.user-menu-submenu-chevron {
    color: #9ca3af;
    font-size: 16px;
    transition: transform 120ms ease;
}
.user-menu-submenu-trigger[aria-expanded="true"] .user-menu-submenu-chevron {
    transform: rotate(90deg);
}
.user-menu-submenu-items {
    background: #f9fafb;
    border-top: 1px solid #f3f4f6;
    border-bottom: 1px solid #f3f4f6;
}
.user-menu-submenu-item {
    padding-left: 28px;
}

/* Notifications bell — top-bar trigger + dropdown.
   Greyed out by default; coloured + red badge when has-unread. */
.notify-bell {
    position: relative;
    background: transparent;
    border: 0;
    padding: 6px 8px;
    color: #94a3b8;            /* greyed default */
    cursor: pointer;
    font-size: 18px;
    line-height: 1;
}
.notify-bell:hover { color: #cbd5e1; }
.notify-bell.has-unread { color: #f1f5f9; }
.notify-bell-badge {
    position: absolute;
    top: 2px;
    right: 0;
    min-width: 16px;
    height: 16px;
    padding: 0 4px;
    background: #dc2626;       /* red dot */
    color: #fff;
    font-size: 10px;
    font-weight: 700;
    line-height: 16px;
    text-align: center;
    border-radius: 10px;
    box-sizing: border-box;
}
.notify-dropdown {
    position: absolute;
    top: calc(100% + 8px);
    right: 0;
    width: 360px;
    max-width: calc(100vw - 32px);
    background: #fff;
    border: 1px solid #e5e7eb;
    border-radius: 8px;
    box-shadow: 0 6px 20px rgba(0,0,0,0.12);
    z-index: 200;
}
.notify-dropdown-header {
    padding: 10px 14px;
    color: #6b7280;
    font-size: 12px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    border-bottom: 1px solid #f3f4f6;
}
.notify-dropdown-list { max-height: 60vh; overflow-y: auto; }
.notify-dropdown-empty {
    padding: 24px 14px;
    text-align: center;
    color: #6b7280;
    font-size: 14px;
}
.notify-item {
    display: block;
    padding: 10px 14px;
    border-bottom: 1px solid #f3f4f6;
    color: #374151;
    text-decoration: none;
    font-size: 14px;
}
.notify-item:hover { background: #f3f4f6; color: #111827; }
.notify-item:last-child { border-bottom: 0; }
.notify-item.unread { background: #eef2ff; }
.notify-item-meta {
    display: flex;
    justify-content: space-between;
    gap: 8px;
    font-size: 12px;
    color: #6b7280;
    margin-top: 2px;
}
.notify-dropdown-footer {
    padding: 10px 14px;
    text-align: center;
    border-top: 1px solid #f3f4f6;
}
.notify-dropdown-footer a {
    color: #4f46e5;
    font-size: 13px;
    font-weight: 600;
    text-decoration: none;
}

/* User-menu per-object badges (unread counts beside menu entries). */
.user-menu-badge {
    display: inline-block;
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    margin-left: 6px;
    background: #dc2626;
    color: #fff;
    font-size: 11px;
    font-weight: 700;
    line-height: 18px;
    text-align: center;
    border-radius: 9px;
    vertical-align: middle;
}

/* Manage-page banner — "{n} notifications for {plural}" link. */
.notify-manage-banner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 10px 14px;
    margin-bottom: 16px;
    background: #fef3c7;
    border: 1px solid #fde68a;
    border-radius: 8px;
    color: #78350f;
    font-size: 14px;
}
.notify-manage-banner a {
    color: #78350f;
    font-weight: 600;
    text-decoration: underline;
}

main { padding: var(--bspkfw-space-page, 32px 24px); max-width: 1200px; margin: 0 auto; }

/* ============================================================
   Page header
   ============================================================ */
.page-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: var(--bspkfw-space-card, 24px);
}

.page-header h1 { font-size: 22px; font-weight: 600; }

/* The action bar WRAPS when it runs out of room — five controls on a
   phone become two right-anchored rows instead of forcing the page
   wider than the viewport (every manage page carries this markup, so
   without wrap every managed table scrolled sideways on phones even
   with the nav's own overflow treatment engaged). min-width: 0 lets
   the flex item shrink below its content so it never defines the
   header's minimum; justify-content only acts once wrapping makes the
   box wider than a row, so a bar that fits renders byte-identically. */
.header-actions {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 8px;
    min-width: 0;
}

/* ============================================================
   Buttons
   ============================================================ */
.btn {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 7px 14px;
    border: 1px solid #d1d5db;
    border-radius: 6px;
    background: #fff;
    color: #374151;
    font-size: 13px;
    font-weight: 500;
    /* Pin line-height + font so `<a class="btn">` and `<button class="btn">`
       compute the same content-box height. Without these, a <button> uses the
       UA control font + line-height:normal (~1.2) and renders a few px shorter
       than an <a> (which inherits the body 1.5), so mixed link/action rows
       don't line up. 1.5 matches the body default the anchors already use, so
       anchor heights are unchanged — only the <button> grows to match. */
    line-height: 1.5;
    font-family: inherit;
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s;
}

.btn:hover { background: #f9fafb; border-color: #9ca3af; }
.btn:active { background: #f3f4f6; }
.btn[hidden] { display: none; }

.btn-primary {
    background: #2563eb;
    border-color: #2563eb;
    color: #fff;
}
.btn-primary:hover { background: #1d4ed8; border-color: #1d4ed8; }

.btn-danger {
    background: #fff;
    border-color: #fca5a5;
    color: #dc2626;
}
.btn-danger:hover { background: #fef2f2; border-color: #f87171; }

.btn-sm { padding: 4px 10px; font-size: 12px; }

/* Form::actionButton() wrapper — a <form> is display:block by default,
   which would break row layouts (a Void button inside a <td> pushing its
   row open); inline-block keeps the whole control sitting in text flow
   exactly like a bare <button> would. Driven by action-form.js. */
.bspkfw-action-form { display: inline-block; margin: 0; }

/* `.is-submitting` is added to a submit button by `Form`'s auto-generated
   script for the duration of the in-flight network call. The button is
   ALSO `disabled` during that window (the actual fix for double-fire);
   this class hides the label + overlays a centered spinner so the
   user gets a non-intrusive "in progress" cue WITHOUT the button's
   box changing size. The spinner color comes from a per-variant
   custom property (--bspkfw-spinner-color) so it stays legible against
   any background — variants below set it; unknown variants fall back
   to medium grey. */
.is-submitting {
    position: relative;
    color: transparent !important;
    text-shadow: none !important;
    cursor: wait;
}
.is-submitting > * { visibility: hidden; }      /* hide any nested icons / spans */
.is-submitting::after {
    content: "";
    position: absolute;
    left: 50%;
    top: 50%;
    width: 14px;
    height: 14px;
    margin: -7px 0 0 -7px;                       /* center on the button */
    border: 2px solid var(--bspkfw-spinner-color, #6b7280);
    border-top-color: transparent;
    border-radius: 50%;
    opacity: 0.85;
    animation: bspkfw-submitting-spin 0.65s linear infinite;
}
.btn-primary { --bspkfw-spinner-color: #ffffff; }
.btn-danger  { --bspkfw-spinner-color: #dc2626; }
@keyframes bspkfw-submitting-spin { to { transform: rotate(360deg); } }

/* ============================================================
   Data table
   ============================================================ */
.data-table {
    width: 100%;
    border-collapse: collapse;
    background: #fff;
    border-radius: 8px;
    overflow: hidden;
    box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}

.data-table th {
    background: #f9fafb;
    padding: 10px 14px;
    text-align: left;
    font-weight: 600;
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: #6b7280;
    border-bottom: 1px solid #e5e7eb;
}

.data-table td {
    padding: 10px 14px;
    border-bottom: 1px solid #f3f4f6;
    color: #374151;
}

.data-table td:not(.row-actions) {
    max-width: 280px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.data-table tr:last-child td { border-bottom: none; }
.data-table tr:hover td { background: #f9fafb; }

/* ============================================================
   Horizontally scrolling tables — edge cues (.bspkfw-tscroll)
   ============================================================

   A wide table in a scroll container works, but gives the reader no sign
   that anything is off-screen: the page doesn't look broken, so nobody
   thinks to scroll. Markup comes from `Bspkfw\Core\Html::scrollableTable()`
   (or is hand-written with the same classes), behaviour from
   /js/table-scroll.js, which sets `data-at` on the wrapper:

       none    the content fits — NO cue at all (a permanent cue on a
               table that doesn't scroll is worse than none)
       start   scrolled fully left  — content hidden to the RIGHT
       middle  content hidden BOTH ways
       end     scrolled fully right — content hidden to the LEFT

   WHY THE CUE IS A SIBLING OF THE SCROLLER AND NOT ITS BACKGROUND: the
   classic "scrolling shadows" trick (cover + shadow layers with
   `background-attachment: local/scroll`) paints on the scroller's own
   background — which is BEHIND its children — and `.data-table` sets an
   opaque background with opaque cells over it. All four layers compute
   correctly and NOTHING appears on screen. The cue therefore lives
   outside the scroller, in a `position: relative` wrapper, painted over
   the table. Styling the scrollbar doesn't work either: on overlay-
   scrollbar platforms it occupies no layout space and fades out, leaving
   no standing hint. Don't re-derive either of those.

   `pointer-events: none` so a cue never eats a click on the cell beneath
   it, and no transition by default — the cue tracks a scroll position, so
   animating it would lag the thing it describes (and would need a
   `prefers-reduced-motion` escape). Tune per project with the custom
   properties rather than re-writing the rules. */
.bspkfw-tscroll {
    position: relative;
}

.bspkfw-tscroll-view {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
}

/* The view is focusable (role="region" + tabindex) so a keyboard user can
   reach and scroll it — table-scroll.js drops the tabindex again whenever
   the content fits, so a table that doesn't scroll is not a tab stop. */
.bspkfw-tscroll-view:focus-visible {
    outline: 2px solid #60a5fa;
    outline-offset: 2px;
}

.bspkfw-tscroll-cue {
    position: absolute;
    top: 0;
    bottom: 0;
    width: var(--bspkfw-tscroll-cue-width, 26px);
    pointer-events: none;
    opacity: 0;
    /* Above the table AND above a sticky header / sticky first column,
       which is deliberate: those scroll horizontally with everything
       else, so the edge they sit at has hidden content too. */
    z-index: 3;
}

/* `.data-table` is a rounded card, so square-cornered cues would paint
   over the curve. Inherit the same radius on the outer corners only. */
.bspkfw-tscroll-cue[data-edge="left"] {
    left: 0;
    border-radius: var(--bspkfw-tscroll-cue-radius, 8px) 0 0 var(--bspkfw-tscroll-cue-radius, 8px);
    background: linear-gradient(to right,
        var(--bspkfw-tscroll-cue-color, rgba(15, 23, 42, 0.20)),
        var(--bspkfw-tscroll-cue-fade, rgba(15, 23, 42, 0)));
}

.bspkfw-tscroll-cue[data-edge="right"] {
    right: 0;
    border-radius: 0 var(--bspkfw-tscroll-cue-radius, 8px) var(--bspkfw-tscroll-cue-radius, 8px) 0;
    background: linear-gradient(to left,
        var(--bspkfw-tscroll-cue-color, rgba(15, 23, 42, 0.20)),
        var(--bspkfw-tscroll-cue-fade, rgba(15, 23, 42, 0)));
}

.bspkfw-tscroll[data-at="start"]  > .bspkfw-tscroll-cue[data-edge="right"],
.bspkfw-tscroll[data-at="end"]    > .bspkfw-tscroll-cue[data-edge="left"],
.bspkfw-tscroll[data-at="middle"] > .bspkfw-tscroll-cue { opacity: 1; }

/* Paper has no scroll position: drop the cue and let the whole table
   print instead of clipping it at the viewport width it happened to
   have on screen. */
@media print {
    .bspkfw-tscroll-cue  { display: none; }
    .bspkfw-tscroll-view { overflow-x: visible; }
}

/* Table density — the per-user Comfortable / Condensed toggle (manage
   toolbar + user menu) persists to user_settings.tableDensity and is
   server-rendered as data-table-density on <html>, so condensed spacing
   paints on first render (no flash of comfortable). Keyed on the ROOT
   attribute — a public contract — so ANY table opts in with one selector;
   projects add
       :root[data-table-density="condensed"] .my-table th,
       :root[data-table-density="condensed"] .my-table td { padding: …; }
   and inherit the same toggle + persistence for free. Comfortable is the
   default (attribute absent) and needs no rules. The same attribute also
   drives the PAGE-CHROME spacing tokens near the top of this file
   (--bspkfw-space-*), so condensed tightens cards / sections / form rows
   / modals as well as these cells. Spacing only — colours, zebra, hover,
   selection styling all cascade unchanged.
   The condensed values are overridable per project via the --bspkfw-
   custom properties below (set on :root); the var() fallbacks are the
   framework defaults, so unset = unchanged. */
:root[data-table-density="condensed"] .data-table th {
    padding: var(--bspkfw-data-table-condensed-th-padding, 5px 10px);
}
:root[data-table-density="condensed"] .data-table td {
    padding: var(--bspkfw-data-table-condensed-td-padding, 4px 10px);
    line-height: var(--bspkfw-data-table-condensed-line-height, 1.25);
}

.row-actions { width: 1%; white-space: nowrap; }
.row-actions-buttons .btn + .btn { margin-left: 6px; }

/* Project-declared row actions (BaseObject::rowActions()) — LINKS, not
   buttons: a row action goes to a page, so it middle-clicks / opens in a new
   tab like any other link. They wear the built-ins' own classes (.btn.btn-sm
   in the button row, .row-action-item in the ⋮ menu), so the only thing to
   undo is the underline the global `a:hover` would otherwise add. */
.row-actions-buttons a.btn:hover,
a.row-action-item:hover { text-decoration: none; }
.bspkfw-row-action-icon { font-style: normal; line-height: 1; }

/* Bulk-select column — leftmost cell of every manage table. The <th> hosts
   a "select all visible" checkbox plus a 3-way view filter (all / checked /
   unchecked). Selections survive sort / search / pagination because they
   live in JS state (selectedIds Set), not on the DOM. */
.bspkfw-select-col {
    width: 1%;
    white-space: nowrap;
    padding-left: 12px;
    padding-right: 8px;
}
.bspkfw-select-col input[type="checkbox"] {
    cursor: pointer;
    margin: 0;
    vertical-align: middle;
}
.bspkfw-select-header {
    display: inline-flex;
    align-items: center;
    gap: 6px;
}
.bspkfw-view-filter {
    background: transparent;
    border: 1px solid transparent;
    border-radius: 4px;
    padding: 1px 4px;
    cursor: pointer;
    font-size: 14px;
    line-height: 1;
    color: #6b7280;
    transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.bspkfw-view-filter:hover {
    background: #f3f4f6;
    border-color: #d1d5db;
    color: #111827;
}
.bspkfw-view-filter:focus-visible {
    outline: 2px solid #60a5fa;
    outline-offset: 1px;
}
.bspkfw-view-filter[data-view-mode="checked"],
.bspkfw-view-filter[data-view-mode="unchecked"] {
    /* Active (non-default) modes get a tinted ring so it's obvious the list
       is filtered — easy to overlook otherwise. */
    color: #2563eb;
    border-color: #bfdbfe;
    background: #eff6ff;
}

/* Row highlight for ticked rows — subtle so it doesn't fight the hover
   background, but enough to scan a page at a glance. */
.data-table tr.bspkfw-row-selected td       { background: #f5f8ff; }
.data-table tr.bspkfw-row-selected:hover td { background: #eef2ff; }

/* m2m link columns — comma-separated label list, never sortable. Align
   with neighbour cells; allow the label list to wrap on narrow tables
   instead of forcing horizontal scroll. */
.data-table .bspkfw-link-col {
    white-space: normal;
    max-width: 240px;
}

/* Inline-edit — clicking a data cell turns it into a tiny form input plus
   save/cancel icons (or just an input in auto-save mode). The .data-table
   td:not(.row-actions) rule clamps width + nowrap, which we need to relax
   while editing so the input has room to breathe. */
.data-table td .bspkfw-cell-edit {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    width: 100%;
}
.data-table td:has(> .bspkfw-cell-edit) {
    max-width: none;
    white-space: normal;
}
.bspkfw-cell-edit > .form-control {
    flex: 1 1 auto;
    min-width: 0;
    padding: 3px 6px;
    height: 28px;
    font-size: 13px;
}

/* m2m inline-edit — Kselect-decorated <select multiple>. Two fixes vs the
   default Kselect look:
   - .ks-wrapper inside an inline-flex parent has a baseline of its first
     line, which leaves the control visually anchored at the bottom of
     the cell. align-self: center forces it back to the row's centre.
   - When the dropdown opens, Kselect adds a focus-coloured 1.5px border
     + 3px glow on .ks-control to read as "active". Inside a tight
     table cell that double-bordered look fights the cell's own framing,
     so we hide it — the dropdown's own border carries the visual.
   Note: Kselect *copies* the underlying <select>'s class names onto its
   own .ks-wrapper unless `copyClassName: false` is passed. Every framework
   call site now passes it (core/Form/KselectInit.php for the PHP-rendered
   fields; edit-modal.js and manage.js for the JS-built pickers), so the
   wrapper carries only Kselect's own classes and .form-control no longer
   paints a second box around .ks-control. Two consequences worth keeping
   in mind here: the inline-edit multi-select stays classless anyway (see
   buildInlineLinkEditor — the cell wants the barest chrome), and a class
   selector must still never try to display:none the underlying select,
   because it would also hide the wrapper. Kselect's own inline
   style.display='none' handles hiding the raw <select> just fine. */
.bspkfw-cell-edit .ks-wrapper {
    flex: 1 1 auto;
    min-width: 0;
    align-self: center;
}
.bspkfw-cell-edit .ks-wrapper.ks-open > .ks-control {
    border-color: transparent;
    box-shadow: none;
}

/* Validation state on a Kselect-enhanced field. `.is-invalid` normally styles
   the <select> itself, but Kselect hides that element, so edit-modal.js puts
   the class on the wrapper too (see showFieldError) and the visible chrome is
   restyled here. Kselect ships no .is-invalid styling of its own — it's our
   class — so this rule is the only thing that makes a failed required select
   look failed. Mirrors .form-control.is-invalid's colours. */
.ks-wrapper.is-invalid > .ks-control {
    border-color: var(--bspkfw-field-invalid, #f87171);
    box-shadow: 0 0 0 3px var(--bspkfw-field-invalid-ring, rgba(239,68,68,0.12));
}
.bspkfw-cell-edit > textarea.form-control {
    min-height: 50px;
    resize: vertical;
}
.bspkfw-cell-save, .bspkfw-cell-cancel {
    width: 22px;
    height: 22px;
    padding: 0;
    border: 1px solid transparent;
    border-radius: 3px;
    background: transparent;
    cursor: pointer;
    font-size: 14px;
    line-height: 1;
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.bspkfw-cell-save  { color: #16a34a; }
.bspkfw-cell-save:hover  { background: #f0fdf4; border-color: #86efac; }
.bspkfw-cell-cancel { color: #6b7280; }
.bspkfw-cell-cancel:hover { background: #f3f4f6; border-color: #d1d5db; }
.bspkfw-cell-dirty {
    /* Subtle marker on cells the user just changed but hasn't yet flushed
       through Save All — gone once the list re-fetches. */
    box-shadow: inset 3px 0 0 #f59e0b;
}

/* Save all — fixed-position bar at bottom-right, only present when there
   are pending edits. Stays out of the way until the user has something
   to save. */
.bspkfw-save-all-bar {
    position: fixed;
    right: 24px;
    bottom: 24px;
    z-index: 200;
}
.bspkfw-save-all-btn {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

/* First-time prompt — small modal asking the user whether to enable
   inline edit on this object, with a sub-checkbox for auto-save and a
   note about the profile-page settings. */
.bspkfw-inline-prompt .modal-box--narrow {
    max-width: 480px;
}
.bspkfw-inline-prompt .checkbox-label {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    margin: 12px 0 0;
}
.bspkfw-iep-autosave-hint {
    margin: 6px 0 0 24px;
    color: #6b7280;
    font-size: 13px;
    line-height: 1.5;
}
.bspkfw-iep-note {
    margin-top: 16px;
    padding-top: 12px;
    border-top: 1px solid #f3f4f6;
    color: #6b7280;
    font-size: 12px;
}

/* Profile page → Inline edit settings table. Compact like the
   communications-prefs table (same shape, three columns). */
.inline-edit-pref-table {
    width: 100%;
    border-collapse: collapse;
    margin-top: 12px;
}
.inline-edit-pref-table th,
.inline-edit-pref-table td {
    padding: 8px 12px;
    text-align: left;
    border-bottom: 1px solid #f3f4f6;
}
.inline-edit-pref-table th {
    font-size: 12px;
    font-weight: 600;
    color: #6b7280;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
.inline-edit-pref-table th:nth-child(2),
.inline-edit-pref-table th:nth-child(3),
.inline-edit-pref-table td:nth-child(2),
.inline-edit-pref-table td:nth-child(3) {
    text-align: center;
    width: 1%;
    white-space: nowrap;
}

/* "With selected" page-header dropdown. Only rendered when the user has
   bulk-edit or bulk-delete permission and the table isn't readonly.
   Trigger is disabled when the selection is empty so the affordance is
   visible (and discoverable) without ever firing destructively. */
.bspkfw-with-selected {
    position: relative;
    display: inline-block;
}
.bspkfw-with-selected-trigger:disabled {
    opacity: 0.5;
    cursor: not-allowed;
    pointer-events: auto; /* keep tooltip-on-hover working */
}
.bspkfw-with-selected-arrow {
    margin-left: 6px;
    font-size: 10px;
    line-height: 1;
}
.bspkfw-with-selected-menu {
    position: absolute;
    top: calc(100% + 4px);
    right: 0;
    min-width: 160px;
    background: #fff;
    border: 1px solid #e5e7eb;
    border-radius: 6px;
    box-shadow: 0 4px 12px rgba(0,0,0,0.1);
    z-index: 100;
    padding: 4px 0;
}
.bspkfw-with-selected-menu[hidden] { display: none; }
.bspkfw-with-selected-item {
    display: block;
    width: 100%;
    text-align: left;
    background: transparent;
    border: none;
    cursor: pointer;
    font-size: 14px;
    padding: 8px 14px;
    color: #374151;
}
.bspkfw-with-selected-item:hover { background: #f3f4f6; }
.bspkfw-with-selected-item:focus-visible {
    outline: 2px solid #60a5fa;
    outline-offset: -2px;
}
.bspkfw-with-selected-item--danger        { color: #dc2626; }
.bspkfw-with-selected-item--danger:hover  { background: #fef2f2; color: #b91c1c; }

/* Compact mode: hide the horizontal button row and show the three-dot menu instead. */
.row-actions-menu { position: relative; display: none; }
.data-table.compact-actions .row-actions-buttons { display: none; }
.data-table.compact-actions .row-actions-menu     { display: inline-block; }
/* Standalone (non-manage-grid) use — the neutral opt-in that makes the menu
   visible WITHOUT a .data-table.compact-actions ancestor. Behaviour comes from
   the always-loaded js/row-actions-menu.js; markup via RowActions::menu(). */
.row-actions-menu--standalone { display: inline-block; }

.row-actions-menu-trigger {
    background: transparent;
    border: 1px solid transparent;
    border-radius: 4px;
    cursor: pointer;
    font-size: 18px;
    line-height: 1;
    padding: 4px 8px;
    color: #6b7280;
}
.row-actions-menu-trigger:hover { background: #f3f4f6; color: #111827; }
.row-actions-menu.open .row-actions-menu-trigger { background: #f3f4f6; border-color: #d1d5db; }

.row-actions-menu-list {
    /* Positioned via JS at open time using fixed-positioning so it can escape the
       table's `overflow: hidden` (used for rounded corners). */
    position: fixed;
    background: #fff;
    border: 1px solid #e5e7eb;
    border-radius: 6px;
    box-shadow: 0 4px 12px rgba(0,0,0,0.1);
    z-index: 100;
    min-width: 130px;
    padding: 4px 0;
}
.row-action-item {
    display: block;
    width: 100%;
    text-align: left;
    background: transparent;
    border: none;
    cursor: pointer;
    font-size: 14px;
    padding: 8px 14px;
    color: #374151;
}
.row-action-item:hover { background: #f3f4f6; }
.row-action-item--danger        { color: #dc2626; }
.row-action-item--danger:hover  { background: #fef2f2; }

.badge-super-admin {
    display: inline-block;
    color: #d97706;
    font-size: 11px;
    vertical-align: middle;
    margin-left: 4px;
    line-height: 1;
}

/* ============================================================
   Forms
   ============================================================ */
.form-group { margin-bottom: var(--bspkfw-space-row, 16px); }

/* Bspkfw\Core\Form\Fieldset wraps grouped fields in <fieldset>. The
   browser's default styling is fine but a little tight; ease the
   contents off the border so the inner fields don't visually
   collide with the legend or the edge. */
.bspkfw-fieldset {
    padding: 16px 20px 4px;
    margin: 0 0 var(--bspkfw-space-card, 24px);
    border: 1px solid #e5e7eb;
    border-radius: 8px;
}
.bspkfw-fieldset > legend {
    padding: 0 8px;
    font-weight: 600;
    color: #1f2937;
}

label {
    display: block;
    font-weight: 500;
    margin-bottom: 5px;
    color: #374151;
}

.required { color: #dc2626; margin-left: 2px; }

.form-control {
    width: 100%;
    padding: 8px 11px;
    border: 1px solid var(--bspkfw-field-border, #d1d5db);
    border-radius: var(--bspkfw-field-radius, 6px);
    font-size: 14px;
    font-family: inherit;
    color: var(--bspkfw-field-text, #1f2937);
    background: var(--bspkfw-field-bg, #fff);
    transition: border-color 0.15s, box-shadow 0.15s;
}

.form-control:focus {
    outline: none;
    border-color: var(--bspkfw-field-border-focus, #2563eb);
    box-shadow: 0 0 0 3px var(--bspkfw-field-focus-ring, rgba(37,99,235,0.12));
}

.form-control.is-invalid {
    border-color: var(--bspkfw-field-invalid, #f87171);
    box-shadow: 0 0 0 3px var(--bspkfw-field-invalid-ring, rgba(239,68,68,0.12));
}

/* Number input — full-height custom stepper on the right edge.
   The native browser spinner is hidden; clicks on the buttons call
   input.stepUp() / stepDown() (wired by /js/number-stepper.js). */
.number-input {
    position: relative;
    display: block;
}
.number-input input[type="number"] {
    -moz-appearance: textfield;
    padding-right: 36px;
}
.number-input input[type="number"]::-webkit-inner-spin-button,
.number-input input[type="number"]::-webkit-outer-spin-button {
    -webkit-appearance: none;
    margin: 0;
}
.number-input-stepper {
    position: absolute;
    top:    1px;
    right:  1px;
    bottom: 1px;
    width:  28px;
    display: flex;
    flex-direction: column;
    border-left: 1px solid #d1d5db;
    border-radius: 0 5px 5px 0;
    overflow: hidden;
    background: #f9fafb;
}
.number-input-up,
.number-input-down {
    flex: 1 1 50%;
    background: transparent;
    border: none;
    cursor: pointer;
    color: #6b7280;
    font-size: 9px;
    line-height: 1;
    padding: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    user-select: none;
}
.number-input-up    { border-bottom: 1px solid #d1d5db; }
.number-input-up:hover,
.number-input-down:hover { background: #f3f4f6; color: #111827; }
.number-input-up:active,
.number-input-down:active { background: #e5e7eb; }
/* Hide stepper when the input is disabled. */
.number-input input:disabled ~ .number-input-stepper { opacity: 0.4; pointer-events: none; }

.field-error { color: #dc2626; font-size: 12px; margin-top: 4px; }

/* PasswordInput ['reveal' => true] — the show/hide eye toggle sits inside
   the control (absolutely positioned over the input's right padding).
   NOT the encrypted-column "Reveal" feature — this only flips visibility
   of what the user just typed. */
.form-reveal-wrap { position: relative; }
.form-reveal-wrap .form-control { padding-right: 42px; }
.form-reveal-toggle {
    position: absolute;
    top: 50%;
    right: 6px;
    transform: translateY(-50%);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    padding: 0;
    background: none;
    border: none;
    border-radius: 4px;
    color: #6b7280;
    cursor: pointer;
}
.form-reveal-toggle:hover { color: #111827; }
.form-reveal-toggle:focus-visible { outline: 2px solid #2563eb; outline-offset: 1px; }
.form-reveal-toggle svg { width: 20px; height: 20px; }
.form-reveal-toggle .reveal-eye-off { display: none; }
.form-reveal-toggle.is-revealed .reveal-eye { display: none; }
.form-reveal-toggle.is-revealed .reveal-eye-off { display: block; }

/* Non-blocking advisory (Field option `asyncAdvisor`) — a live server-checked
   warning the user is free to ignore. Deliberately amber, NOT the red of
   .field-error: the colour is the whole signal that this does not block
   submit. Sized/spaced like .field-error and .form-help so the three
   below-input channels line up. */
.form-advisory {
    color: #b45309;
    font-size: 12px;
    margin-top: 4px;
    display: flex;
    align-items: baseline;
    gap: 5px;
}
.form-advisory::before {
    content: "\26A0";     /* ⚠ — decorative; the text carries the meaning */
    flex: none;
}
.form-advisory[hidden] { display: none; }   /* beats the flex display above */

/* Postcode-autofill alternate localities ("Or: Hollywood / Studio City…") —
   rendered by postal-autofill.js under the city input when a postcode maps
   to more than one place. Sized/spaced like .form-help / .field-error so
   the below-input channels line up; the buttons read as inline links. */
.bspkfw-postal-alts {
    font-size: 12px;
    color: #6b7280;
    margin-top: 4px;
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 6px;
}
.bspkfw-postal-alts button {
    background: none;
    border: none;
    padding: 0;
    font: inherit;
    color: #2563eb;
    cursor: pointer;
    text-decoration: underline;
}
.bspkfw-postal-alts button:hover { color: #1d4ed8; }

.form-control--mono { font-family: ui-monospace, monospace; font-size: 12px; }

.form-control--readonly {
    background: var(--bspkfw-field-surface, #f9fafb);
    color: var(--bspkfw-field-muted, #6b7280);
    cursor: default;
    border-color: var(--bspkfw-field-disabled-border, #e5e7eb);
}

.form-control:disabled,
select.form-control:disabled,
input.form-control[readonly] {
    background: var(--bspkfw-field-disabled-bg, #f3f4f6);
    color: var(--bspkfw-field-disabled-fg, #9ca3af);
    border-color: var(--bspkfw-field-disabled-border, #e5e7eb);
    cursor: not-allowed;
    opacity: 1;
}

textarea.form-control { resize: vertical; }

select.form-control { padding-right: 28px; cursor: pointer; }
.form-control--inline { width: auto; }

.checkbox-label { display: flex; align-items: center; gap: 8px; font-weight: normal; }
.checkbox-label input { width: auto; }

.form-actions {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
    padding-top: var(--bspkfw-space-row, 16px);
    border-top: 1px solid #f3f4f6;
    margin-top: 8px;
}

.form-captcha {
    margin: 16px 0 4px;
    display: flex;
    justify-content: flex-start;
}

/* ============================================================
   Modal
   ============================================================ */
.modal {
    position: fixed;
    inset: 0;
    z-index: var(--bspkfw-z-modal, 100);
    display: flex;
    align-items: center;
    justify-content: center;
}

.modal[hidden] { display: none; }

/* The photo cropper is always opened FROM something — /account today, an
   edit modal's file column via onCropped tomorrow — and there is no flow
   in which it should sit below another layer, so the nested rank is its
   permanent rank, not an option. (At the shared .modal z-index, DOM order
   decided: an edit modal appended at open time rendered ON TOP of the
   cropper it launched, leaving a Save button that couldn't be clicked.)
   Its backdrop stacks with it, so the layer beneath stays inert. */
#bspkfw-photo-cropper-modal { z-index: var(--bspkfw-z-modal-nested, 110); }

.modal-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(0,0,0,0.45);
}

.modal-box {
    position: relative;
    z-index: 1;
    background: #fff;
    border-radius: 10px;
    box-shadow: 0 20px 40px rgba(0,0,0,0.18);
    width: 520px;
    max-width: calc(100vw - 32px);
    max-height: calc(100vh - 64px);
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

/* Wide variant — base .modal-box's max-width keeps it inside narrow viewports. */
.modal-box--wide { width: min(85vw, 1000px); }

/* ============================================================
   Permissions matrix
   ============================================================ */
.perm-matrix {
    width: 100%;
    border-collapse: collapse;
    font-size: 13px;
}

.perm-matrix th,
.perm-matrix td {
    padding: 8px 12px;
    border-bottom: 1px solid #e5e7eb;
    text-align: center;
    white-space: nowrap;
}

.perm-matrix th:first-child,
.perm-matrix td:first-child { text-align: left; }

.perm-matrix thead th {
    background: #f9fafb;
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: #6b7280;
}

.perm-level-name { font-weight: 500; }

.perm-matrix tbody tr:last-child td { border-bottom: none; }

/* Row and column toggle controls */
.perm-row-toggle,
.perm-col-toggle {
    display: flex;
    align-items: center;
    justify-content: center;
    margin-right: 8px;
    cursor: pointer;
}

.perm-row-toggle input[type="checkbox"],
.perm-col-toggle input[type="checkbox"] {
    cursor: pointer;
}

.perm-row-toggle input[type="checkbox"]:indeterminate,
.perm-col-toggle input[type="checkbox"]:indeterminate {
    background-color: #d1d5db;
}

.perm-col-label {
    display: inline-block;
}

.perm-level-label {
    display: inline-block;
}

.modal-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 12px var(--bspkfw-space-card, 24px);
    flex-shrink: 0;
    background: #f9fafb;
    border-bottom: 1px solid #f3f4f6;   /* mirrors .modal-footer's top border */
    cursor: move;             /* hint that the modal is draggable by its header */
    user-select: none;        /* avoid text selection while grabbing */
}
.modal-header .modal-close,
.modal-header button,
.modal-header a,
.modal-header input,
.modal-header select,
.modal-header label {
    cursor: pointer;          /* keep interactive controls inside the header clickable */
}
.modal-box.is-dragging {
    transition: none;         /* avoid any transition smoothing during the drag */
}

.modal-scroll {
    flex: 1;
    overflow-y: auto;
    padding: 12px var(--bspkfw-space-card, 24px);
    min-height: 0;
}

.modal-footer {
    flex-shrink: 0;
    display: flex;
    justify-content: flex-end;
    gap: 8px;
    padding: var(--bspkfw-space-row, 16px) var(--bspkfw-space-card, 24px);
    border-top: 1px solid #f3f4f6;
}

.modal-header h2 { font-size: 18px; font-weight: 600; }

/* EditModal carousel chrome — only visible when openSequence() is driving
   more than one step. The step indicator sits next to the title; the
   Skip-rest button sits at the far left of the footer (margin-right: auto
   pushes Cancel/Save to the right). Both are `[hidden]` by default. */
.bspkfw-em-step {
    margin-left: 12px;
    font-size: 13px;
    color: #6b7280;
    font-weight: normal;
    align-self: center;
}
.bspkfw-em-step[hidden] { display: none; }
.bspkfw-em-skip-rest {
    margin-right: auto;
    background: transparent;
    border: 1px dashed #d1d5db;
    color: #6b7280;
}
.bspkfw-em-skip-rest:hover { background: #f9fafb; border-color: #9ca3af; color: #111827; }
.bspkfw-em-skip-rest[hidden] { display: none; }

/* EditModal bulk mode — "Editing N Plural" title button + expandable
   list of selected records, plus per-field bulk-toggle styling. */
.bspkfw-em-bulk-title-btn {
    background: transparent;
    border: none;
    padding: 0;
    font: inherit;
    color: inherit;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: 6px;
}
.bspkfw-em-bulk-title-btn:hover { text-decoration: underline; }
.bspkfw-em-bulk-title-btn:focus-visible {
    outline: 2px solid #60a5fa;
    outline-offset: 2px;
    border-radius: 2px;
}
.bspkfw-em-bulk-arrow {
    font-size: 12px;
    line-height: 1;
    color: #6b7280;
    transition: transform var(--ks-transition, 0.18s ease);
}
.bspkfw-em-bulk-title-btn[aria-expanded="true"] .bspkfw-em-bulk-arrow {
    transform: rotate(180deg);
}
.bspkfw-em-bulk-objects {
    border-bottom: 1px solid #f3f4f6;
    background: #f9fafb;
    max-height: 200px;
    overflow-y: auto;
    flex-shrink: 0;
}
.bspkfw-em-bulk-objects[hidden] { display: none; }
.bspkfw-em-bulk-objects-list {
    margin: 0;
    padding: 8px 24px;
    list-style: none;
    font-size: 13px;
    color: #374151;
}
.bspkfw-em-bulk-objects-list li {
    padding: 3px 0;
    border-bottom: 1px solid #e5e7eb;
}
.bspkfw-em-bulk-objects-list li:last-child { border-bottom: none; }

/* Per-row bulk toggle — small checkbox prepended to each editable field
   group + each m2m / tags section heading. The row is greyed out when
   the toggle is off; CSS pointer-events:none handles the Kselect case
   (no runtime disable API). At submit time the toggle's checked state
   is the source of truth — we don't read input.disabled. */
.bspkfw-em-bulk-toggle {
    margin-right: 8px;
    cursor: pointer;
    vertical-align: middle;
    flex-shrink: 0;
}
.bspkfw-em-bulk-row { position: relative; }
.bspkfw-em-bulk-row.form-group { display: flex; flex-wrap: wrap; align-items: baseline; }
.bspkfw-em-bulk-row.form-group > label,
.bspkfw-em-bulk-row.form-group > .form-control,
.bspkfw-em-bulk-row.form-group > .field-error,
.bspkfw-em-bulk-row.form-group > .form-control--readonly {
    flex-basis: 100%;
}
.bspkfw-em-bulk-row.form-group > .bspkfw-em-bulk-toggle { flex-basis: auto; }
.bspkfw-em-bulk-disabled .form-control,
.bspkfw-em-bulk-disabled label,
.bspkfw-em-bulk-disabled .db-link-heading,
.bspkfw-em-bulk-disabled .ks-control,
.bspkfw-em-bulk-disabled .ks-wrapper,
.bspkfw-em-bulk-disabled .file-upload {
    opacity: 0.5;
}
.bspkfw-em-bulk-disabled .ks-wrapper,
.bspkfw-em-bulk-disabled .file-upload {
    pointer-events: none;
}
.bspkfw-em-bulk-disabled .bspkfw-em-bulk-toggle {
    /* The toggle itself stays clickable so the user can re-enable. */
    opacity: 1;
    pointer-events: auto;
}

/* EditModal — per-record readonly (record.__readonly_fields, see
   BaseObject::readOnlyFieldsFor). edit-modal.js locks the inputs
   (readOnly / disabled); this greys the row and inert-ifies the
   file-upload surface, which has no disable API, mirroring the
   bulk-disabled treatment above. Softer opacity than bulk-disabled —
   the value is still meaningful content, just uneditable.

   Kselect is no longer in that "no disable API" category: since 1.7.0 it
   re-reads the <select>'s `disabled` on refresh (via its autoSync observer),
   so lockFieldUi's `inp.disabled = true` genuinely disables the widget —
   .ks-disabled styling, tabindex="-1", aria-disabled. That closed a real
   hole: pointer-events alone blocked the mouse but left the control
   tab-reachable and operable, so "greyed out" was a lie to a keyboard user.
   The .ks-wrapper pointer-events rules below (and in the bulk block above)
   are therefore now belt-and-braces rather than the mechanism, and are kept
   deliberately: they still cover any future path that greys a row without
   setting `disabled`, and they close the async gap between the property
   write and the observer callback. Safe to drop if that ever needs
   simplifying — verify a bulk toggle and a readonly field by keyboard. */
.bspkfw-em-record-readonly label,
.bspkfw-em-record-readonly .ks-control,
.bspkfw-em-record-readonly .ks-wrapper,
.bspkfw-em-record-readonly .file-upload {
    opacity: 0.7;
}
.bspkfw-em-record-readonly .ks-wrapper,
.bspkfw-em-record-readonly .file-upload {
    /* NOT .file-upload-existing — its view links stay clickable; the
       per-file remove buttons are disabled individually by JS. */
    pointer-events: none;
}

/* ------------------------------------------------------------------
   EditModal — field groups + responsive two-column layout.
   Server-rendered by BaseObject::renderGroupedFieldsHtml() when the
   object declares formFieldGroups(); flat (ungrouped) modals never
   carry any of these classes and are untouched. Groups are native
   <details> disclosures (CSP-safe — no JS for the toggle); the
   legend-less --lead section holds fields not claimed by a group.
   ------------------------------------------------------------------ */
.bspkfw-edit-modal.bspkfw-em-grouped .modal-box {
    width: 760px;
    max-width: calc(100vw - 32px);
}
.bspkfw-em-group {
    border: 1px solid #e5e7eb;
    border-radius: 8px;
    padding: 12px 16px 0;
    margin: 0 0 var(--bspkfw-space-row, 16px);
}
.bspkfw-em-group[hidden] { display: none; }
.bspkfw-em-group--lead {
    border: none;
    border-radius: 0;
    padding: 0;
}
.bspkfw-em-group > .bspkfw-em-group-legend {
    cursor: pointer;
    font-weight: 600;
    color: #1f2937;
    margin: 0 0 12px;
    user-select: none;
}
details.bspkfw-em-group:not([open]) { padding-bottom: 12px; }
details.bspkfw-em-group:not([open]) > .bspkfw-em-group-legend { margin-bottom: 0; }
.bspkfw-em-group-body {
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: 16px;
    /* Vertical rhythm stays the rows' own .form-group margin-bottom —
       no row-gap, so single-column stacking matches the flat modal. */
}
/* Long inputs span both columns — type-derived (textarea / markdown /
   html / json / file) or an explicit 'width' => 'full' descriptor key. */
.bspkfw-em-group-body > .form-group--full { grid-column: 1 / -1; }
@media (max-width: 700px) {
    .bspkfw-em-group-body { grid-template-columns: 1fr; }
}

.modal-close {
    background: none;
    border: none;
    font-size: 20px;
    color: #9ca3af;
    cursor: pointer;
    line-height: 1;
    padding: 2px 6px;
    border-radius: 4px;
}

.modal-close:hover { color: #374151; background: #f3f4f6; }

/* ============================================================
   Modal tabs
   ============================================================ */
.modal-tab-bar {
    display: flex;
    gap: 4px;
    border-bottom: 1px solid #e5e7eb;
    margin-bottom: var(--bspkfw-space-section-gap, 20px);
}

.tab-btn {
    padding: 6px 14px 10px;
    background: none;
    border: none;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
    color: #6b7280;
    font-size: 13px;
    font-weight: 500;
    cursor: pointer;
    transition: color 0.15s, border-color 0.15s;
}

.tab-btn:hover { color: #374151; }
.tab-btn.active { color: #2563eb; border-bottom-color: #2563eb; }

.tab-hidden { display: none !important; }

.checkbox-list {
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 4px 0;
}

.db-link-section {
    margin-top: var(--bspkfw-space-section-gap, 20px);
    padding-top: var(--bspkfw-space-row, 16px);
    border-top: 1px solid #e5e7eb;
}
.db-link-heading {
    font-size: 14px;
    font-weight: 600;
    color: #374151;
    margin: 0 0 8px;
}

/* ============================================================
   Alerts & state
   ============================================================ */
.alert {
    padding: 10px 14px;
    border-radius: 6px;
    font-size: 13px;
    margin-bottom: var(--bspkfw-space-row, 16px);
}

.alert-error { background: #fef2f2; color: #991b1b; border: 1px solid #fecaca; }
.alert-success { background: #f0fdf4; color: #166534; border: 1px solid #bbf7d0; }
.alert-warning { background: #fffbeb; color: #92400e; border: 1px solid #fde68a; }

.empty-state, .loading { color: #9ca3af; padding: 32px 0; text-align: center; }

/* Loading spinner — see core/Spinner.php and public_html/js/spinner.js. The
   overlay variant absolute-positions itself inside any `position: relative`
   host (or one Spinner.show() temporarily makes relative), so callers like
   the manage table can grey out the existing rows while a refresh fetches.
   Project-specific styling can override these rules wholesale by shipping
   a custom `public_html/css/base.css` (or a separate stylesheet); the
   class names are the contract between PHP / JS and CSS. */
.bspkfw-spinner-overlay {
    position: absolute;
    inset: 0;
    background: rgba(255, 255, 255, 0.65);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 50;
    min-height: 120px;
    /* No `pointer-events:none` — we want the overlay to swallow clicks
       while the underlying content is in a transient loading state. */
}
.bspkfw-spinner { display: inline-block; line-height: 0; }
.bspkfw-spinner-ring {
    width: 32px;
    height: 32px;
    border: 3px solid rgba(99, 102, 241, 0.2);
    border-top-color: #6366f1;
    border-radius: 50%;
    animation: bspkfw-spinner-rotate 0.8s linear infinite;
}
@keyframes bspkfw-spinner-rotate {
    to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
    .bspkfw-spinner-ring { animation-duration: 2.4s; }
}

.spinner {
    display: inline-block;
    width: 18px;
    height: 18px;
    border: 2px solid #d1d5db;
    border-top-color: #2563eb;
    border-radius: 50%;
    vertical-align: middle;
    margin-left: 10px;
    animation: spinner-rotate 0.8s linear infinite;
}
.spinner[hidden] { display: none; }

/* File upload field — picker, file list, progress bar */
.file-upload-list {
    list-style: none;
    padding: 0;
    margin: 8px 0 0;
}
.file-upload-item {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 6px 10px;
    background: #f9fafb;
    border: 1px solid #e5e7eb;
    border-radius: 6px;
    margin-top: 6px;
    font-size: 13px;
}
.file-upload-name {
    flex: 1 1 auto;
    word-break: break-all;
    color: #374151;
}
.file-upload-meta {
    color: #6b7280;
    font-size: 12px;
    flex-shrink: 0;
}
.file-upload-remove {
    background: transparent;
    border: none;
    cursor: pointer;
    color: #6b7280;
    font-size: 18px;
    line-height: 1;
    padding: 0 6px;
    flex-shrink: 0;
}
.file-upload-remove:hover { color: #dc2626; }
.file-upload-progress {
    flex: 1 1 50%;
    height: 6px;
    background: #e5e7eb;
    border-radius: 3px;
    overflow: hidden;
}
.file-upload-progress-bar {
    height: 100%;
    width: 0;
    background: #2563eb;
    transition: width 0.15s linear;
}
.file-upload-item--uploading .file-upload-name {
    flex: 0 1 30%;
    color: #6b7280;
    font-style: italic;
}

/* "Current files" block rendered above the picker on the edit modal — already-
   saved basenames with download links and ✕ buttons that hit the framework's
   /files/ DELETE endpoint. */
.file-upload-existing {
    margin-bottom: 8px;
}
.file-upload-existing-list {
    list-style: none;
    padding: 0;
    margin: 0;
}
.file-upload-item--existing {
    background: #f3f6fa;
    border-color: #d6deeb;
}
.file-upload-item--existing .file-upload-name {
    color: #1e40af;
    text-decoration: none;
}
.file-upload-item--existing .file-upload-name:hover {
    text-decoration: underline;
}
/* Stored file a queued upload will displace on save (maxFiles cap with
   onExceed 'replace' — EditModal marks the oldest overflow rows so the swap
   is visible before Save, not discovered after). */
.file-upload-item--replacing {
    opacity: 0.6;
}
.file-upload-item--replacing .file-upload-name {
    text-decoration: line-through;
    color: #6b7280;
}
.file-upload-item--replacing .file-upload-name:hover {
    text-decoration: line-through underline;
}
.file-upload-replacing-tag {
    flex-shrink: 0;
    font-size: 12px;
    color: #92400e;
    background: #fef3c7;
    border: 1px solid #fde68a;
    border-radius: 4px;
    padding: 1px 6px;
}

/* Thumbnail preview for an already-stored image in a managed-edit FileUpload. */
.file-upload-thumb {
    width: 40px;
    height: 40px;
    object-fit: cover;
    border-radius: 4px;
    border: 1px solid #d6deeb;
    flex-shrink: 0;
    background: #fff;
}

/* View modal — file column download list. */
.view-file-list {
    list-style: none;
    padding: 0;
    margin: 0;
}
.view-file-list li {
    padding: 2px 0;
}
.view-file-list a {
    color: #1e40af;
    text-decoration: none;
}
.view-file-list a:hover {
    text-decoration: underline;
}

/* Gallery layout — used when one or more files in a list are images. Thumbs
   wrap into rows; non-image items still flow as text links inside the same
   list (occupying their natural width). */
.view-file-list--gallery {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: center;
}
.view-file-list--gallery .view-file-item {
    padding: 0;
}
.view-file-list--gallery .view-file-item--thumb {
    flex: 0 0 auto;
}

.view-file-thumb {
    display: block;
    width: 88px;
    height: 88px;
    padding: 0;
    border: 1px solid #d6deeb;
    border-radius: 6px;
    background: #f3f6fa;
    cursor: zoom-in;
    overflow: hidden;
    transition: transform .12s ease, box-shadow .12s ease, border-color .12s ease;
}
.view-file-thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}
.view-file-thumb:hover {
    transform: translateY(-1px);
    border-color: #94a3b8;
    box-shadow: 0 4px 10px rgba(0,0,0,0.08);
}
.view-file-thumb:focus-visible {
    outline: 2px solid #2563eb;
    outline-offset: 2px;
}

/* Lightbox — fullscreen overlay shown when a thumbnail is clicked. Lives
   directly under <body>; ignores the rest of the page's stacking. */
body.image-lightbox-open { overflow: hidden; }

.image-lightbox {
    position: fixed;
    inset: 0;
    z-index: 9999;
    display: flex;
    align-items: center;
    justify-content: center;
}
.image-lightbox[hidden] { display: none; }
.image-lightbox-backdrop {
    position: absolute;
    inset: 0;
    background: rgba(15, 23, 42, 0.92);
    cursor: zoom-out;
}
.image-lightbox-stage {
    position: relative;
    z-index: 1;
    max-width: 92vw;
    max-height: 92vh;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 12px;
    padding: 0 64px;
}
.image-lightbox-image {
    max-width: 100%;
    max-height: 80vh;
    object-fit: contain;
    border-radius: 6px;
    box-shadow: 0 12px 40px rgba(0,0,0,0.5);
    background: #fff;
}
.image-lightbox-caption {
    color: #f1f5f9;
    font-size: 13px;
    display: flex;
    gap: 12px;
    align-items: baseline;
}
.image-lightbox-name    { font-weight: 500; word-break: break-all; }
.image-lightbox-counter { color: #94a3b8; font-variant-numeric: tabular-nums; }

.image-lightbox-close,
.image-lightbox-prev,
.image-lightbox-next {
    position: absolute;
    z-index: 2;
    background: rgba(255,255,255,0.12);
    color: #f8fafc;
    border: 1px solid rgba(255,255,255,0.18);
    border-radius: 50%;
    width: 44px;
    height: 44px;
    font-size: 22px;
    line-height: 1;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: background .12s ease;
}
.image-lightbox-close:hover,
.image-lightbox-prev:hover,
.image-lightbox-next:hover { background: rgba(255,255,255,0.24); }
.image-lightbox-close { top: 18px; right: 18px; font-size: 26px; }
.image-lightbox-prev  { left: 18px; top: 50%; transform: translateY(-50%); }
.image-lightbox-next  { right: 18px; top: 50%; transform: translateY(-50%); }
.image-lightbox-prev[hidden], .image-lightbox-next[hidden] { display: none; }

@media (max-width: 540px) {
    .image-lightbox-stage { padding: 0 12px; }
    .image-lightbox-prev, .image-lightbox-next { width: 36px; height: 36px; font-size: 20px; }
    .image-lightbox-close { top: 12px; right: 12px; }
}

.spinner-inline {
    width: 14px;
    height: 14px;
    border-width: 2px;
    margin: 0;
    vertical-align: -3px;
}
@keyframes spinner-rotate { to { transform: rotate(360deg); } }

/* ============================================================
   Login page
   ============================================================ */
.login-wrap {
    /* 52px = .site-header height, 64px = <main>'s 32px top + 32px bottom
       padding. Without both subtractions, the inner box overflows by
       exactly main's vertical padding and the page grows a scrollbar
       even when there's nothing to scroll to. */
    min-height: calc(100vh - 52px - 64px);
    display: flex;
    align-items: center;
    justify-content: center;
}

.login-box {
    background: #fff;
    border-radius: 10px;
    box-shadow: 0 4px 16px rgba(0,0,0,0.1);
    padding: 36px;
    width: 360px;
    max-width: calc(100vw - 48px);
}

.login-box h1 { font-size: 20px; font-weight: 600; margin-bottom: 24px; text-align: center; }

.btn-block { width: 100%; justify-content: center; margin-top: 8px; }

.login-footer { margin-top: 20px; text-align: center; font-size: 13px; color: #6b7280; }
.forgot-link { text-align: right; font-size: 13px; margin-top: -4px; margin-bottom: 4px; }

.alert-list { margin: 0; padding-left: 20px; }
.alert-list li + li { margin-top: 4px; }

.field-hint { font-weight: normal; color: #9ca3af; font-size: 12px; }

/* ============================================================
   Landing hero
   ============================================================ */
.hero {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    /* Account for <main>'s 64px vertical padding the same way .login-wrap
       does — without that, the page grows a 64px-tall scrollbar tail. */
    min-height: calc(100vh - 52px - 64px);
    text-align: center;
    padding: 48px 24px;
}

.hero h1 { font-size: 42px; font-weight: 700; letter-spacing: -0.02em; margin-bottom: 16px; }

.hero-sub { font-size: 18px; color: #6b7280; max-width: 480px; margin-bottom: 36px; line-height: 1.6; }

.hero-actions { display: flex; gap: 12px; }

/* ============================================================
   Dialog (alert / confirm / prompt)
   ============================================================ */
.dialog-box {
    max-width: 400px;
    padding: 28px 24px 20px;
}

.dialog-msg {
    margin-bottom: 20px;
    line-height: 1.6;
    color: #1f2937;
    font-size: 14px;
    white-space: pre-wrap;
}

.dialog-prompt-input {
    margin-bottom: 16px;
}

.dialog-btns {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
}

/* ============================================================
   Notifications
   ============================================================ */
#notify-container {
    position: fixed;
    top: 16px;
    right: 16px;
    z-index: 200;
    display: flex;
    flex-direction: column;
    gap: 8px;
    pointer-events: none;
}

/* Scoped to #notify-container so the toast's `opacity: 0` (slide-in
   start state) doesn't bleed onto the bell dropdown's `.notify-item`
   rows — they share the class name but live in `.notify-dropdown-list`,
   not here, and never get the `.notify-visible` toggle. */
#notify-container .notify-item {
    pointer-events: all;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 12px 16px;
    border-radius: 8px;
    box-shadow: 0 4px 12px rgba(0,0,0,0.12);
    font-size: 14px;
    font-weight: 500;
    max-width: 320px;
    opacity: 0;
    transform: translateX(20px);
    transition: opacity 0.2s ease, transform 0.2s ease;
}

#notify-container .notify-item.notify-visible { opacity: 1; transform: translateX(0); }

.notify-success { background: #f0fdf4; color: #166534; border: 1px solid #bbf7d0; }
.notify-error   { background: #fef2f2; color: #991b1b; border: 1px solid #fecaca; }
.notify-info    { background: #eff6ff; color: #1e40af; border: 1px solid #bfdbfe; }
.notify-warning { background: #fffbeb; color: #92400e; border: 1px solid #fde68a; }

.notify-msg { flex: 1; }

.notify-close {
    background: none;
    border: none;
    cursor: pointer;
    font-size: 18px;
    line-height: 1;
    color: inherit;
    opacity: 0.5;
    padding: 0 2px;
    flex-shrink: 0;
}

.notify-close:hover { opacity: 1; }

/* Visually-hidden but still in the accessibility tree — the standard
   "screen-reader only" clip recipe. Foundational a11y primitive: skip
   links, icon-only control names, live-region labels (notify.js's mirrors
   use .sr-only), etc. `.sr-only` is always hidden; `.sr-only-focusable`
   is hidden until focused (or it contains focus), then paints normally.
   NOT display:none / visibility:hidden / width:0 — those drop the node
   from the a11y tree or truncate the announcement. */
.sr-only,
.sr-only-focusable:not(:focus):not(:focus-within) {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    border: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
}

/* The focusable variant, while focused: a normal, clearly-visible control.
   Sensible defaults (surface bg, dark text, the framework's primary-blue
   :focus-visible outline) so a bare `<a class="sr-only-focusable">` looks
   right with no per-project styling; consumers add positioning (e.g. a
   skip link pins itself to the top-left). */
.sr-only-focusable:focus,
.sr-only-focusable:focus-within {
    padding: 8px 16px;
    background: #ffffff;
    color: #1f2937;
    border-radius: 6px;
    outline: 2px solid #2563eb;
    outline-offset: 2px;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
    z-index: 1000;
}

/* ============================================================
   Enhanced table (search, sort, responsive)
   ============================================================ */

/* width: auto lets the table overflow its container when content is too wide,
   which is required for the JS overflow check to work. min-width: 100% ensures
   it still fills the container when there is enough space. */
table.enhanced-table { width: auto; min-width: 100%; }

.enhanced-table-toolbar { margin-bottom: 12px; }

.enhanced-table-search { max-width: 280px; }

.col-sortable { cursor: pointer; user-select: none; }
.col-sortable:hover { background: #f3f4f6; color: #374151; }

.col-sortable::after { content: '\00a0\2195'; opacity: 0.35; font-size: 0.85em; }
.col-sortable[aria-sort="ascending"]::after  { content: '\00a0\2191'; opacity: 1; color: #2563eb; }
.col-sortable[aria-sort="descending"]::after { content: '\00a0\2193'; opacity: 1; color: #2563eb; }

/* Expand button — prepended to the first cell when columns are hidden */
.et-expand-btn {
    position: relative;
    display: inline-block;
    width: 18px;
    height: 18px;
    flex-shrink: 0;
    vertical-align: middle;
    margin-right: 5px;
    padding: 0;
    border: 1px solid #d1d5db;
    border-radius: 4px;
    background: #fff;
    /* hide the + / − character so we can draw a perfectly centered glyph instead */
    font-size: 0;
    color: transparent;
    cursor: pointer;
}
.et-expand-btn::before,
.et-expand-btn::after {
    content: '';
    position: absolute;
    left: 50%;
    top: 50%;
    background: #6b7280;
    transform: translate(-50%, -50%);
}
.et-expand-btn::before { width: 8px; height: 2px; }
.et-expand-btn::after  { width: 2px; height: 8px; }
.et-expand-btn[aria-expanded="true"]::after { display: none; }
.et-expand-btn:hover { background: #f3f4f6; }
.et-expand-btn:hover::before,
.et-expand-btn:hover::after { background: #374151; }

/* Detail row — the temporary row injected below an expanded row */
.et-detail-cell {
    background: #f8fafc;
    padding: 10px 14px 12px 32px;
    border-bottom: 1px solid #e5e7eb;
}

.et-detail-list {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 6px 16px;
    margin: 0;
}

.et-detail-list dt {
    font-size: 11px;
    font-weight: 600;
    color: #6b7280;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    white-space: nowrap;
    align-self: baseline;
}

.et-detail-list dd { margin: 0; color: #374151; font-size: 13px; }

/* ============================================================
   View modal
   ============================================================ */
.view-field-list {
    display: flex;
    flex-direction: column;
    gap: 12px;
    margin: 0;
    padding: 4px 0;
}

.view-field {
    display: grid;
    grid-template-columns: 160px 1fr;
    gap: 4px 16px;
    align-items: baseline;
}

.view-field dt {
    font-size: 11px;
    font-weight: 600;
    color: #6b7280;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    align-self: baseline;
    padding-top: 2px;
}

.view-field dd {
    margin: 0;
    color: #111827;
    font-size: 14px;
    word-break: break-word;
}

.view-null { color: #9ca3af; font-style: italic; }

/* Boolean (checkbox-typed) cell values — manage list + view modal.
   The "off" state reuses .view-null so it matches empty cells. */
.bspkfw-bool { font-style: normal; }
.bspkfw-bool-yes { color: #16a34a; font-weight: 600; }

/* Cell decorations (BaseObject::cellDecorations) — a small mark rendered
   BESIDE a grid cell's value. Deliberately colourless: the object's own
   `class` decides how the mark looks, and inheriting currentColor means an
   undressed mark still reads correctly on every theme. */
.bspkfw-cell-mark {
    display: inline-block;
    font-style: normal;
    font-size: 0.95em;
    line-height: 1;
    white-space: nowrap;
}
.bspkfw-cell-mark--after  { margin-left: 0.35em; }
.bspkfw-cell-mark--before { margin-right: 0.35em; }

.view-link-list {
    margin: 2px 0 0;
    padding-left: 18px;
    color: #111827;
}
.view-link-list li { padding: 1px 0; }

.duration-ago { color: #9ca3af; font-size: 0.85em; }

.view-field--block {
    grid-template-columns: 1fr;
}

.view-json-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 13px;
    margin-top: 2px;
}

.view-json-table th,
.view-json-table td {
    padding: 5px 8px;
    border: 1px solid #e5e7eb;
    text-align: left;
    vertical-align: top;
}

.view-json-table thead th {
    background: #f3f4f6;
    font-weight: 600;
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: #6b7280;
}

.view-json-table tbody th {
    background: #f9fafb;
    font-weight: 600;
    color: #374151;
    width: 140px;
    white-space: nowrap;
}

@media (max-width: 480px) {
    .view-field {
        grid-template-columns: 1fr;
    }
}

/* ============================================================
   Home page
   ============================================================ */
.home-list {
    list-style: none;
    margin-top: var(--bspkfw-space-section-gap, 20px);
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.home-list a {
    display: inline-block;
    padding: 10px 16px;
    background: #fff;
    border: 1px solid #e5e7eb;
    border-radius: 6px;
    font-weight: 500;
}

.home-list a:hover { border-color: #2563eb; background: #eff6ff; text-decoration: none; }

/* ============================================================
   View modal user manager
   ============================================================ */
.vm-user-manager {
    border-top: 1px solid #e5e7eb;
    padding-top: 16px;
    margin-top: 16px;
}

.vm-user-heading {
    font-size: 12px;
    font-weight: 600;
    color: #6b7280;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    margin-bottom: 8px;
}

.vm-user-list {
    list-style: none;
    margin-bottom: 12px;
}

.vm-user-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 6px 0;
    border-bottom: 1px solid #f3f4f6;
}

.vm-user-row:last-child { border-bottom: none; }

.vm-add-user {
    display: flex;
    gap: 8px;
    margin-top: 8px;
}

.vm-add-user .form-control { flex: 1; }

/* ============================================================
   Profile page
   ============================================================ */
.profile-page { max-width: 680px; }

/* Profile photo widget — sits above the email/display-name form */
.profile-photo-widget {
    display: flex;
    align-items: center;
    gap: 16px;
    margin-bottom: 16px;
}
.profile-photo-preview {
    width: 96px;
    height: 96px;
    border-radius: 50%;
    overflow: hidden;
    background: #e5e7eb url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 96 96'><circle cx='48' cy='36' r='18' fill='%239ca3af'/><path d='M16 88c0-18 14-32 32-32s32 14 32 32' fill='%239ca3af'/></svg>") center/cover no-repeat;
    flex-shrink: 0;
    border: 1px solid #e5e7eb;
}
.profile-photo-preview img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}
.profile-photo-widget.has-no-photo .profile-photo-preview img { display: none; }
.profile-photo-actions { flex: 1; }

/* Cropper modal */
.cropper-stage {
    padding: 20px;
    display: flex;
    justify-content: center;
}
.cropper-viewport {
    width: 320px;
    max-width: 100%;       /* shrink to fit a narrow modal on small screens */
    aspect-ratio: 1 / 1;   /* stay square (circular) at any rendered width */
    overflow: hidden;      /* belt-and-braces; clip-path does the real work */
    position: relative;
    background: #111827;
    border-radius: 50%;    /* shapes the 1px border */
    /* The preview <img> is driven by an inline transform (photo-cropper.js),
       which promotes it to its own compositing layer and escapes the parent's
       rounded-corner overflow clip in Chromium/WebKit — especially once the
       box is sized by aspect-ratio rather than a fixed height. clip-path
       clips composited/transformed descendants reliably, keeping the circle. */
    clip-path: circle(50%);
    cursor: grab;
    touch-action: none;
    user-select: none;
}
.cropper-viewport:active { cursor: grabbing; }
.cropper-image {
    position: absolute;
    top: 0;
    left: 0;
    transform-origin: 0 0;
    pointer-events: none;
    user-select: none;
    max-width: none;
    max-height: none;
}
.cropper-controls {
    padding: 0 24px 12px;
}
.cropper-toolbar {
    display: flex;
    align-items: center;
    gap: 12px;
}
.cropper-toolbar .cropper-zoom-label { flex: 1; }
.cropper-rotate {
    display: flex;
    gap: 6px;
}
/* When JS hides the rotate group (rotate: false), the zoom label's
   flex: 1 reclaims the row — layout identical to the pre-rotate modal. */
.cropper-rotate[hidden] { display: none; }
.cropper-zoom-label {
    display: flex;
    align-items: center;
    gap: 12px;
    font-size: 13px;
    color: #374151;
}
.cropper-zoom-label input[type="range"] { flex: 1; }

.page-sections {
    display: flex;
    flex-direction: column;
    gap: var(--bspkfw-space-section-gap, 20px);
}

.page-section {
    background: #fff;
    border: 1px solid #e5e7eb;
    border-radius: 8px;
    padding: var(--bspkfw-space-card, 24px);
}

.page-section-title {
    font-size: 15px;
    font-weight: 600;
    margin-bottom: 4px;
}

.page-section-desc {
    font-size: 13px;
    color: #6b7280;
    margin-bottom: var(--bspkfw-space-section-gap, 20px);
}

.page-section-desc:last-child { margin-bottom: 0; }

.page-section--danger { border-color: #fca5a5; }
.page-section--danger .page-section-title { color: #dc2626; }

.page-section.collapsible > .page-section-title {
    cursor: pointer;
    user-select: none;
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 4px;
}

.page-section.collapsible > .page-section-title::after {
    content: '▾';
    display: inline-block;
    color: #9ca3af;
    font-size: 12px;
    flex-shrink: 0;
    transition: transform 0.15s;
}

.page-section.collapsible.collapsed { cursor: pointer; }
.page-section.collapsible.collapsed > .page-section-title { margin-bottom: 0; }
.page-section.collapsible.collapsed > .page-section-title::after { transform: rotate(-90deg); }
.page-section.collapsible.collapsed > *:not(.page-section-title) { display: none; }

/* ============================================================
   Communications preferences table
   ============================================================ */
.comms-pref-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 14px;
    margin-bottom: 20px;
}

.comms-pref-table th,
.comms-pref-table td {
    padding: 10px 12px;
    border-bottom: 1px solid #e5e7eb;
    vertical-align: middle;
}

.comms-pref-table thead th {
    background: #f9fafb;
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: #6b7280;
    text-align: center;
}

.comms-pref-table thead th:first-child { text-align: left; }

.comms-pref-table tbody tr:last-child td { border-bottom: none; }

.comms-pref-check { text-align: center; width: 100px; }

.comms-pref-name { text-align: left; }

.comms-pref-label {
    display: block;
    font-weight: 500;
}

.comms-pref-desc {
    display: block;
    font-size: 12px;
    color: #6b7280;
    margin-top: 2px;
}

.comms-channels-bar { margin-bottom: 16px; }

.comms-ch-edit {
    cursor: pointer;
    user-select: none;
}
.comms-ch-edit:hover { background: #f3f4f6; }

.comms-pref-head-label { display: block; }

/* Per-channel "select / unselect all" checkbox in the comms-prefs header. */
.comms-select-all { cursor: pointer; margin: 4px 0 0; }

/* Inline field help (Field option `help`, and the descriptor key of the same
   name). `display: block` is load-bearing, not cosmetic: this is a <small>,
   and vertical margins are inert on a non-replaced inline element, so without
   it the 4px below rounds to nothing and the help sits flush against the
   input. Every sibling below-input channel already sidesteps this by being a
   block/flex node (.field-error, .form-advisory, .bspkfw-postal-alts). Colour
   routes through the shared field palette — the token's :root default IS this
   rule's former literal, so a light theme is byte-identical while midnight /
   sepia stop rendering grey-on-dark. */
.form-help {
    display: block;
    font-size: 12px;
    color: var(--bspkfw-field-muted, #6b7280);
    margin-top: 4px;
}

/* "Show all / Show fewer" toggle under a default-group-filtered select
   (optgroup-filter.js — see Form\Select's defaultGroups option). */
.bspkfw-og-toggle {
    display: block;
    background: none;
    border: none;
    padding: 0;
    margin-top: 4px;
    font-size: 12px;
    color: #2563eb;
    cursor: pointer;
}
.bspkfw-og-toggle:hover { text-decoration: underline; }

/* ============================================================
   Badges
   ============================================================ */
.badge {
    display: inline-block;
    padding: 2px 8px;
    border-radius: 10px;
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.03em;
}
.badge-success { background: #dcfce7; color: #166534; }
.badge-info    { background: #dbeafe; color: #1e40af; }
.badge-warning { background: #fef9c3; color: #854d0e; }
.badge-danger  { background: #fee2e2; color: #991b1b; }
.badge-neutral { background: #f3f4f6; color: #374151; }

/* ------------------------------------------------------------------
   Stat tiles — a responsive grid of labelled count cards. Shared so
   any dashboard-style page (jobs queue, reports summaries, …) renders
   the same treatment. Tones reuse the .badge colour palette.
   ------------------------------------------------------------------ */
.stat-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
    gap: 12px;
}
.stat-card {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 14px 16px;
    border: 1px solid #e5e7eb;
    border-left-width: 4px;
    border-radius: 8px;
    background: #fff;
}
.stat-card-value { font-size: 26px; font-weight: 700; line-height: 1; }
.stat-card-label { font-size: 12px; color: #6b7280; text-transform: uppercase; letter-spacing: 0.03em; }
.stat-card--success { border-left-color: #16a34a; }
.stat-card--info    { border-left-color: #2563eb; }
.stat-card--warning { border-left-color: #ca8a04; }
.stat-card--danger  { border-left-color: #dc2626; }
.stat-card--neutral { border-left-color: #9ca3af; }
.stat-oldest { margin-top: 12px; font-size: 14px; }
.stat-oldest .stat-card-label { display: inline; }
.stat-oldest--stale { color: #dc2626; }

/* Monospace error cell — wraps long messages, preserves whitespace,
   tinted so a failed-job error reads as "this is the problem". */
.job-error {
    display: block;
    max-width: 480px;
    white-space: pre-wrap;
    word-break: break-word;
    color: #991b1b;
    font-size: 12px;
}
.job-error em { color: #9ca3af; font-style: italic; }

/* ============================================================
   Table toolbar, sortable headers, and pagination
   ============================================================ */
.table-toolbar { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; flex-wrap: wrap; }
.table-search  { display: flex; align-items: center; gap: 6px; flex: 1; min-width: 0; }
.table-search input { flex: 1; min-width: 0; }
.table-page-size { display: flex; align-items: center; gap: 6px; font-size: 0.875rem; white-space: nowrap; margin-bottom: 0; }

/* Density toggle button (manage toolbar) — a two-state aria-pressed
   toggle; pressed = condensed. Mirrors the .bspkfw-view-filter treatment:
   quiet at rest, tinted ring when the non-default state is active. */
.table-density-toggle {
    display: inline-flex;
    align-items: center;
    background: transparent;
    border: 1px solid #d1d5db;
    border-radius: 4px;
    padding: 6px 8px;
    cursor: pointer;
    color: #6b7280;
    transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.table-density-toggle:hover {
    background: #f3f4f6;
    color: #111827;
}
.table-density-toggle:focus-visible {
    outline: 2px solid #60a5fa;
    outline-offset: 1px;
}
.table-density-toggle[aria-pressed="true"] {
    color: #2563eb;
    border-color: #bfdbfe;
    background: #eff6ff;
}

/* Quick-filter chip row (object_config.quick_filters) — one group per
   declared column, rendered by manage.js between the toolbar and the
   table. Chips are aria-pressed toggles; active = the same tinted-ring
   treatment as the density toggle / view-filter buttons. */
.table-quick-filters {
    display: flex;
    align-items: center;
    gap: 6px 16px;
    flex-wrap: wrap;
    margin-bottom: 10px;
}
.table-qf-group {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
}
.table-qf-label {
    font-size: 0.8125rem;
    color: #6b7280;
    white-space: nowrap;
}
.table-qf-chip {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    background: transparent;
    border: 1px solid #d1d5db;
    border-radius: 999px;
    padding: 3px 12px;
    font-size: 0.8125rem;
    line-height: 1.4;
    color: #374151;
    cursor: pointer;
    white-space: nowrap;
    transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.table-qf-chip:hover {
    background: #f3f4f6;
    color: #111827;
}
.table-qf-chip:focus-visible {
    outline: 2px solid #60a5fa;
    outline-offset: 1px;
}
.table-qf-chip.is-active {
    color: #2563eb;
    border-color: #bfdbfe;
    background: #eff6ff;
}
.table-qf-count {
    font-size: 0.75rem;
    color: #6b7280;
}
.table-qf-chip.is-active .table-qf-count { color: #2563eb; opacity: 0.75; }

th.sortable { cursor: pointer; user-select: none; white-space: nowrap; }
th.sortable:hover { background: rgba(0,0,0,0.04); }
.sort-icon { opacity: 0.3; font-size: 0.8em; margin-left: 4px; }
th.sort-asc .sort-icon, th.sort-desc .sort-icon { opacity: 1; }
th.sort-asc .sort-icon::before  { content: '↑'; }
th.sort-desc .sort-icon::before { content: '↓'; }
th.sortable:not(.sort-asc):not(.sort-desc) .sort-icon::before { content: '↕'; }

.table-pagination { display: flex; align-items: center; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
.pg-info  { flex: 1; text-align: center; color: #6b7280; font-size: 0.875rem; }
.pg-count { font-size: 0.875rem; color: #6b7280; }

/* ============================================================
   Schema modal
   ============================================================ */
.sm-hint { font-weight: normal; font-size: 0.8em; color: #6b7280; }

.sm-meta {
    flex-shrink: 0;
    padding: 0 24px;
}

.sm-field-actions {
    flex-shrink: 0;
    display: flex;
    gap: 6px;
    padding: 10px 24px;
    border-top: 1px solid #f3f4f6;
}

.sm-row-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }

.sm-separator { border: none; border-top: 1px solid #e5e7eb; margin: 16px 0; }

.sm-fields-header { font-size: 0.875rem; font-weight: 600; margin-bottom: 8px; color: #374151; }

.sm-fields-table { table-layout: fixed; }
.sm-fields-table .sm-col-drag   { width: 32px; text-align: center; }
.sm-fields-table .sm-col-null   { width: 80px; text-align: center; }
.sm-fields-table .sm-col-vis    { width: 52px; text-align: center; }
.sm-fields-table .sm-col-remove { width: 36px; text-align: center; }
.sm-remove-btn {
    width: 26px;
    height: 26px;
    padding: 0;
    line-height: 1;
    font-size: 16px;
    color: #ef4444;
    background: none;
    border: 1px solid transparent;
    border-radius: 4px;
    /* `.btn` is inline-flex with align-items:center but no horizontal
       centering — the × sits flush-left on hover, looking off. Override
       to centre both axes inside the 26×26 box. */
    justify-content: center;
}
.sm-remove-btn:hover { background: #fee2e2; border-color: #fca5a5; }
.sm-fields-table td { vertical-align: middle; padding: 5px 6px; }

.sm-field-row:has(input:disabled) { background: #f9fafb; color: #9ca3af; }
.sm-field-row:has(input:disabled) td { color: #9ca3af; }
.sm-drag-handle { cursor: grab; color: #9ca3af; font-size: 1.1rem; display: inline-block; padding: 0 4px; }
.sm-drag-row { cursor: default; }
.sm-dragging  { opacity: 0.4; }
.sm-drag-over { outline: 2px dashed #2563eb; outline-offset: -2px; }
.sm-fk-type { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; padding: 2px 0; }
.sm-fk-badge  { background: #ede9fe; color: #7c3aed; border-radius: 4px; padding: 1px 7px; font-size: 12px; font-weight: 500; }
.sm-m2m-badge { background: #d1fae5; color: #065f46; border-radius: 4px; padding: 1px 7px; font-size: 12px; font-weight: 500; }
.sm-virtual-name { font-weight: 500; color: #374151; padding: 6px 0; display: inline-block; }
.sm-links-note { font-size: 0.78rem; color: #6b7280; margin: 6px 0 0; }

/* Communication message preview modal */
.comm-tabs { }
.comm-tab-nav { display: flex; gap: 2px; border-bottom: 1px solid #e5e7eb; margin-bottom: 16px; }
.comm-tab-btn { background: none; border: none; border-bottom: 2px solid transparent; padding: 8px 16px; cursor: pointer; font-size: 14px; color: #6b7280; margin-bottom: -1px; border-radius: 4px 4px 0 0; transition: color .15s; }
.comm-tab-btn:hover { color: #111827; background: #f9fafb; }
.comm-tab-btn.active { color: #4f46e5; border-bottom-color: #4f46e5; font-weight: 500; }
.comm-tab-panel { display: none; }
.comm-tab-panel.active { display: block; }
.cmm-meta { border: 1px solid #e5e7eb; border-radius: 6px; overflow: hidden; margin-bottom: 12px; }
.cmm-meta-row { display: flex; border-bottom: 1px solid #e5e7eb; }
.cmm-meta-row:last-child { border-bottom: none; }
.cmm-meta-label { width: 80px; padding: 7px 12px; font-size: 13px; font-weight: 600; color: #6b7280; background: #f9fafb; flex-shrink: 0; }
.cmm-meta-value { padding: 7px 12px; font-size: 13px; }
.cmm-email-body { border: 1px solid #e5e7eb; border-radius: 6px; overflow: hidden; }
.cmm-iframe { width: 100%; min-height: 200px; height: 360px; border: none; display: block; background: #fff; }
.cmm-slack-msg { background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 6px; padding: 12px 16px; font-size: 14px; white-space: pre-wrap; word-break: break-word; }
.comm-sd-section { border-radius: 6px; overflow: hidden; margin-bottom: 8px; border: 1px solid #e5e7eb; }
.comm-sd-section:last-child { margin-bottom: 0; }
.comm-sd-header { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; cursor: pointer; user-select: none; background: #f9fafb; }
.comm-sd-header:hover { filter: brightness(0.97); }
.comm-sd-title { font-size: 13px; font-weight: 500; }
.comm-sd-chevron { font-size: 11px; color: #9ca3af; transition: transform .15s; }
.comm-sd-section.open .comm-sd-chevron { transform: rotate(180deg); }
.comm-sd-body { padding: 10px 12px; }
.comm-sd-rows { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; font-size: 13px; margin: 0; }
.comm-sd-row { display: contents; }
.comm-sd-rows dt { font-weight: 600; color: #6b7280; }
.comm-sd-rows dd { margin: 0; }
.comm-sd-success { border-color: #bbf7d0; }
.comm-sd-success .comm-sd-header { background: #f0fdf4; }
.comm-sd-failed { border-color: #fecaca; }
.comm-sd-failed .comm-sd-header { background: #fef2f2; }
.comm-sd-sending { border-color: #bfdbfe; }
.comm-sd-sending .comm-sd-header { background: #eff6ff; }
.comm-sd-neutral .comm-sd-header { background: #f9fafb; }

/* ============================================================
   Tag chips — styled by per-category `style` blob (inline). The
   chip class only sets layout / size; colour comes from inline
   style on each chip so each tag-category can theme its own.
   ============================================================ */
.bspkfw-tag-chip {
    display: inline-block;
    padding: 2px 8px;
    border-radius: 999px;
    background: #e5e7eb;
    color: #374151;
    font-size: 12px;
    line-height: 1.5;
    white-space: nowrap;
    vertical-align: baseline;
}
.bspkfw-tag-chip + .bspkfw-tag-chip { margin-left: 4px; }

/* Manage-table cell: "n tags" trigger + popover. position:fixed on the
   popover so the table's overflow:hidden doesn't clip it (mirrors the
   existing row-actions-menu pattern). */
.bspkfw-tag-multi {
    background: transparent;
    border: 1px solid #d1d5db;
    border-radius: 999px;
    padding: 1px 9px;
    font-size: 12px;
    color: #374151;
    cursor: pointer;
}
.bspkfw-tag-multi:hover { background: #f3f4f6; }
.bspkfw-tag-popover {
    position: fixed;
    background: #fff;
    border: 1px solid #e5e7eb;
    border-radius: 6px;
    box-shadow: 0 4px 12px rgba(0,0,0,0.12);
    padding: 8px 10px;
    z-index: 250;
    min-width: 160px;
    max-width: 360px;
    line-height: 1.9;
}
.bspkfw-tag-popover[hidden] { display: none; }

/* Edit-modal picker: a Kselect <select multiple>. Each option is rendered by
   Kselect as ".ks-option-text > .bspkfw-tag-chip" (Kselect runs innerHTML on
   our chip-HTML option text, allowHtml: true). The chip already carries its
   own colour via the per-category inline style; we just nudge spacing here. */
.bspkfw-tag-picker-select { /* native select stays display:none — Kselect handles UI */ }
.tags-picker-container .ks-option-text .bspkfw-tag-chip { vertical-align: middle; }

/* Sticky "+ Add new tag" footer mounted inside the open Kselect dropdown.
   `position: sticky` + `bottom: 0` pins it to the viewport of the existing
   .ks-dropdown / .ks-mobile-options-wrap scroll container so it stays
   visible while the user scrolls the option list. No Kselect-file edits. */
.bspkfw-tag-add-footer {
    position: sticky;
    bottom: 0;
    background: var(--ks-color-bg, #fff);
    border-top: 1px solid #e5e7eb;
    padding: 8px 10px;
    z-index: 1;
}
.bspkfw-tag-add-btn {
    width: 100%;
    padding: 6px 10px;
    background: transparent;
    border: 1px dashed #c7d2fe;
    border-radius: 6px;
    color: #4f46e5;
    font-size: 13px;
    font-weight: 500;
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s;
}
.bspkfw-tag-add-btn:hover  { background: #eef2ff; border-color: #818cf8; }
.bspkfw-tag-add-btn:focus-visible {
    outline: 2px solid #6366f1;
    outline-offset: 2px;
}
.bspkfw-tag-add-form {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 6px;
}
.bspkfw-tag-add-form .bspkfw-tag-add-cat,
.bspkfw-tag-add-form .bspkfw-tag-add-name {
    height: 32px;
    padding: 4px 8px;
    font-size: 13px;
}
.bspkfw-tag-add-form .bspkfw-tag-add-actions {
    grid-column: 1 / span 2;
    display: flex;
    justify-content: flex-end;
    gap: 6px;
}
.bspkfw-tag-add-form .bspkfw-tag-add-error {
    grid-column: 1 / span 2;
    margin: 0;
    font-size: 12px;
    padding: 6px 8px;
}
.bspkfw-tag-add-empty {
    color: #6b7280;
    font-size: 12px;
    text-align: center;
    padding: 4px 0;
}

/* -----------------------------------------------------------------
   Import / Export wizard (import-export.js)
   Reuses .modal / .modal-box / .form-group / .data-table from the
   shared modal styles above; only the wizard-specific bits live here.
   ----------------------------------------------------------------- */
.bspkfw-iew-modal .modal-box {
    width: 720px;
    max-width: 95vw;
}
.bspkfw-iew-form  { display: flex; flex-direction: column; gap: 12px; }
.bspkfw-iew-help  { color: #6b7280; font-size: 13px; margin: 6px 0 12px; }
.bspkfw-iew-template { margin: 12px 0; }
.bspkfw-iew-template-help { color: #6b7280; font-size: 12px; }
.bspkfw-iew-error {
    color: #b91c1c;
    background: #fef2f2;
    border: 1px solid #fecaca;
    padding: 8px 10px;
    border-radius: 4px;
    margin-top: 8px;
}
.bspkfw-iew-preview-wrap {
    overflow-x: auto;
    border: 1px solid #e5e7eb;
    border-radius: 4px;
    margin-top: 8px;
}
.bspkfw-iew-preview { min-width: 100%; }
.bspkfw-iew-preview th, .bspkfw-iew-preview td {
    padding: 6px 8px;
    font-size: 12px;
    white-space: nowrap;
}
.bspkfw-iew-preview thead tr:first-child th {
    background: #f3f4f6;
    font-weight: 600;
}
.bspkfw-iew-preview thead tr:last-child th {
    background: #f9fafb;
    border-top: 1px solid #e5e7eb;
}
.bspkfw-iew-preview thead tr:last-child .form-control {
    width: 100%;
    font-size: 12px;
    padding: 4px 6px;
}
.bspkfw-iew-summary dl {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: 4px 16px;
    margin: 8px 0;
}
.bspkfw-iew-summary dt { font-weight: 600; color: #374151; }
.bspkfw-iew-summary dd { margin: 0; color: #1f2937; }
.bspkfw-iew-summary-final dt { color: #065f46; }
.bspkfw-iew-ok { color: #065f46; }
.bspkfw-iew-errors {
    background: #fef2f2;
    border: 1px solid #fecaca;
    border-radius: 4px;
    padding: 10px 12px;
    margin-top: 12px;
}
.bspkfw-iew-errors h3 { font-size: 14px; margin: 0 0 8px; color: #991b1b; }
.bspkfw-iew-errors ul { margin: 0; padding-left: 18px; color: #7f1d1d; font-size: 12px; }
/* Discard warnings (Validate step) — amber, never blocks Apply. */
.bspkfw-iew-warnings {
    background: #fffbeb;
    border: 1px solid #fde68a;
    border-radius: 4px;
    padding: 10px 12px;
    margin-top: 12px;
}
.bspkfw-iew-warnings h3 { font-size: 14px; margin: 0 0 8px; color: #92400e; }
.bspkfw-iew-warnings ul { margin: 0; padding-left: 18px; color: #78350f; font-size: 12px; }
.bspkfw-iew-fkpreview { margin-top: 12px; }
.bspkfw-iew-fkpreview h3 { font-size: 14px; margin: 0 0 8px; }
.bspkfw-iew-fkpreview .data-table th,
.bspkfw-iew-fkpreview .data-table td { font-size: 12px; padding: 4px 8px; }
.bspkfw-iew-fkpreview-bad { color: #991b1b; font-style: italic; }

/* -----------------------------------------------------------------
   Theme picker (theme-switcher.js)
   Skeleton classes on .theme-picker-* — themes can override colours
   without touching layout.
   ----------------------------------------------------------------- */
.theme-picker .modal-box { width: 480px; max-width: 95vw; }
.theme-picker-list {
    list-style: none;
    margin: 0;
    padding: 0;
}
.theme-picker-item {
    display: grid;
    grid-template-columns: 24px 1fr;
    align-items: baseline;
    gap: 4px 12px;
    padding: 12px 14px;
    border: 1px solid #e5e7eb;
    border-radius: 6px;
    margin-bottom: 8px;
    cursor: pointer;
    background: #fff;
    transition: background 0.12s, border-color 0.12s;
}
.theme-picker-item:hover { background: #f9fafb; border-color: #d1d5db; }
.theme-picker-item--active { border-color: #6366f1; background: #eef2ff; }
.theme-picker-item--active:hover { background: #e0e7ff; }
.theme-picker-item--applying { opacity: 0.5; pointer-events: none; }
.theme-picker-check {
    grid-column: 1 / 2;
    grid-row: 1 / 3;
    text-align: center;
    color: #6366f1;
    font-weight: 700;
    font-size: 16px;
}
.theme-picker-name  { grid-column: 2 / 3; font-weight: 600; color: #111827; }
.theme-picker-desc  { grid-column: 2 / 3; color: #6b7280; font-size: 12px; }
.theme-picker-empty,
.theme-picker-error,
.theme-picker-loading {
    padding: 16px;
    text-align: center;
    color: #6b7280;
    font-size: 13px;
}
.theme-picker-error { color: #b91c1c; }

/* -----------------------------------------------------------------
   File-upload parser hint (edit-modal.js setParsingHint)
   Inline "Analyzing…" indicator inside a parser-enabled .file-upload
   container while POST /api/{table}/_parse_file is in flight. Themes
   re-skin via the same selectors.
   ----------------------------------------------------------------- */
.file-upload .bspkfw-parsing-hint {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    margin-top: 6px;
    color: #6b7280;
    font-size: 12px;
    font-style: italic;
}
.bspkfw-parsing-spinner {
    width: 12px;
    height: 12px;
    border: 2px solid #d1d5db;
    border-top-color: #6366f1;
    border-radius: 50%;
    animation: bspkfw-parsing-spin 0.8s linear infinite;
}
@keyframes bspkfw-parsing-spin { to { transform: rotate(360deg); } }

/* --- Encrypted fields (FieldEncryption helper UI) --- */
.bspkfw-encrypted-field { position: relative; }
.bspkfw-enc-helper {
    margin-top: 0.4em;
    padding: 0.5em 0.7em;
    background: #f7f8fa;
    border: 1px solid #e3e6eb;
    border-radius: 4px;
    font-size: 0.9em;
    display: flex;
    flex-wrap: wrap;
    gap: 0.5em;
    align-items: center;
}
.bspkfw-enc-badge {
    display: inline-block;
    padding: 0.1em 0.5em;
    background: #e3e6eb;
    color: #555;
    border-radius: 3px;
    font-size: 0.85em;
}
.bspkfw-enc-warning {
    flex-basis: 100%;
    padding: 0.4em 0.6em;
    background: #fff7e6;
    border: 1px solid #ffd591;
    border-radius: 3px;
    color: #8a5a00;
    display: flex;
    gap: 0.4em;
    align-items: flex-start;
}
.bspkfw-enc-warning-icon { flex-shrink: 0; }
.bspkfw-enc-warning-text { flex: 1; }
.bspkfw-enc-info-icon {
    background: none;
    border: none;
    color: #1d4ed8;
    cursor: pointer;
    font-size: 1em;
    padding: 0 0.3em;
    flex-shrink: 0;
}
.bspkfw-enc-info-icon:hover { text-decoration: underline; }
.bspkfw-enc-password-row {
    flex-basis: 100%;
    display: flex;
    gap: 0.5em;
    align-items: center;
}
.bspkfw-enc-password-label { flex-shrink: 0; font-weight: 500; }
.bspkfw-enc-password { flex: 1; min-width: 8em; }
.bspkfw-enc-decrypt-btn { flex-shrink: 0; }
.bspkfw-enc-decrypted-hint {
    flex-basis: 100%;
    color: #047857;
    font-size: 0.85em;
}
.bspkfw-enc-reveal-denied {
    flex-basis: 100%;
    color: #888;
    font-size: 0.85em;
    font-style: italic;
}
.bspkfw-enc-locked .form-control { background: #f0f0f0; color: #888; font-style: italic; }

/* --- Async (server-callback) field validators --- */
/* Spinner glyph over the input's right edge while the validator
   request is in flight. Inputs can't host CSS pseudo-elements, so the
   spinner is a sibling <span> injected by the form JS into the
   .form-group (which carries position: relative) and positioned
   absolutely from the input's measured offset. CSS owns the visual +
   the animation; JS owns placement and lifecycle. */
.form-group.bspkfw-async-validating { position: relative; }
.form-group.bspkfw-async-validating > .form-control { padding-right: 2.4em; }
.bspkfw-async-spinner {
    position: absolute;
    width: 1em;
    height: 1em;
    box-sizing: border-box;
    border: 0.15em solid rgba(99, 102, 241, 0.25);
    border-top-color: #6366f1;
    border-radius: 50%;
    pointer-events: none;
    animation: bspkfw-async-spin 0.8s linear infinite;
}
@keyframes bspkfw-async-spin { to { transform: rotate(360deg); } }
/* Optional fading green ✓ on validated success (showValidatedTick). */
.form-group.bspkfw-async-ok > .form-control {
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><path d='M3 8 L7 12 L13 4' fill='none' stroke='%23047857' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'/></svg>");
    background-repeat: no-repeat;
    background-position: right 0.6em center;
    background-size: 1em 1em;
    padding-right: 2.4em;
    transition: background-image 0.2s ease-out;
}

/* ============================================================================
 * CSP Phase 2 — classes extracted from inline style= attributes so the policy
 * can run a strict `style-src 'self'` (no 'unsafe-inline'). Each utility maps
 * to exactly the property it replaced, so the sweep is visually a no-op.
 * Add new utilities here rather than reaching for an inline style.
 * ==========================================================================*/

/* Text */
.u-muted  { color: #6b7280; }
.u-warn   { color: #b45309; }
.u-fs-13  { font-size: 13px; }
.u-fs-14  { font-size: 14px; }

/* Margin — only the values actually used across framework pages. */
.u-m-0     { margin: 0; }
.u-my-16   { margin: 16px 0; }
.u-mt-8    { margin-top: 8px; }
.u-mt-12   { margin-top: 12px; }
.u-mt-16   { margin-top: 16px; }
.u-mt-24   { margin-top: 24px; }
.u-mb-0    { margin-bottom: 0; }
.u-mb-8    { margin-bottom: 8px; }
.u-mb-12   { margin-bottom: 12px; }
.u-mb-14   { margin-bottom: 14px; }
.u-mb-16   { margin-bottom: 16px; }
.u-mb-20   { margin-bottom: 20px; }
.u-mb-24   { margin-bottom: 24px; }
.u-mr-8    { margin-right: 8px; }
.u-mr-auto { margin-right: auto; }

/* Padding / layout */
.u-p-16        { padding: 16px; }
.u-flex        { display: flex; }
.u-flex-col    { display: flex; flex-direction: column; }
.u-flex-center { display: flex; justify-content: center; }
.u-gap-8       { gap: 8px; }
.u-hidden      { display: none; }
.u-grow        { flex: 1; }
.u-pointer     { cursor: pointer; }

/* Compound / semantic */
.bspkfw-divider     { margin-top: 32px; border-top: 1px solid #e5e7eb; padding-top: 24px; }
.bspkfw-divider-sm  { margin-top: 16px; border-top: 1px solid #e5e7eb; padding-top: 16px; }
.auth-lead        { font-size: 14px; color: #6b7280; margin-bottom: 20px; }

/* CSS-hidden honeypot (anti-spam). Was an inline style on .bspkfw-honeypot.
 * Form.php ALSO emits this rule inline with every anti-spam form
 * (Form::HONEYPOT_STYLE) so custom-chrome pages that skip base.css still
 * hide the field — keep the two copies in sync. This one stays so
 * framework-chrome pages don't depend on the inline emission. */
.bspkfw-honeypot { position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden; }

/* The raw multi-select behind the tag picker stays hidden; Kselect renders
 * its own widget over it. Was an inline `style="display:none"`. */
select[data-bspkfw-tag-picker] { display: none; }

/* MFA setup — QR frame + manual-entry secret block. */
.mfa-qr-frame { width: 240px; height: 240px; padding: 8px; background: #fff; border: 1px solid #e5e7eb; border-radius: 8px; }
.mfa-secret   { display: block; margin-top: 8px; padding: 10px; background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 6px; font-family: monospace; font-size: 15px; letter-spacing: 2px; word-break: break-all; }

/* MFA backup codes — the printable code list. Was an inline style on the
 * existing .bspkfw-backup-codes element. */
.bspkfw-backup-codes { font-family: ui-monospace, SFMono-Regular, monospace; font-size: 16px; line-height: 1.8; background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 8px; padding: 16px 16px 16px 36px; }

/* Schema modal — narrow dialog box. */
.modal-box--narrow { max-width: 380px; }

/* ============================================================
   BspkfwChart (bspkfw-chart.js) — SVG time-series charts.
   Every colour routes through a --chart-* custom property so
   themes restyle charts by overriding the variables on
   .bspkfw-chart — no JS changes. Series cycle through the 6-slot
   palette via .bspkfw-chart-series-N classes.
   ============================================================ */
.bspkfw-chart {
    --chart-series-1: #2563eb;
    --chart-series-2: #16a34a;
    --chart-series-3: #d97706;
    --chart-series-4: #dc2626;
    --chart-series-5: #7c3aed;
    --chart-series-6: #0d9488;
    --chart-grid: #e5e7eb;
    --chart-axis-text: #6b7280;
    --chart-tooltip-bg: rgba(17, 24, 39, 0.92);
    --chart-tooltip-color: #f9fafb;
    position: relative;
    display: flex;
    flex-direction: column;
    width: 100%;
    height: 260px;   /* default — consumers override on their own container */
}
.bspkfw-chart svg { display: block; flex: 1 1 auto; min-height: 0; width: 100%; }

/* Per-series colour binding — the JS assigns these classes cyclically. */
.bspkfw-chart-series-1 { --chart-series-color: var(--chart-series-1); }
.bspkfw-chart-series-2 { --chart-series-color: var(--chart-series-2); }
.bspkfw-chart-series-3 { --chart-series-color: var(--chart-series-3); }
.bspkfw-chart-series-4 { --chart-series-color: var(--chart-series-4); }
.bspkfw-chart-series-5 { --chart-series-color: var(--chart-series-5); }
.bspkfw-chart-series-6 { --chart-series-color: var(--chart-series-6); }

.bspkfw-chart .bspkfw-chart-line {
    fill: none;
    stroke: var(--chart-series-color);
    stroke-width: 2;
    stroke-linejoin: round;
    stroke-linecap: round;
}
.bspkfw-chart .bspkfw-chart-area { fill: var(--chart-series-color); opacity: 0.12; stroke: none; }
.bspkfw-chart .bspkfw-chart-bar  { fill: var(--chart-series-color); }
.bspkfw-chart .bspkfw-chart-dot  { fill: var(--chart-series-color); }
.bspkfw-chart .bspkfw-chart-grid-line { stroke: var(--chart-grid); stroke-width: 1; }
.bspkfw-chart text { fill: var(--chart-axis-text); font-size: 11px; }
.bspkfw-chart .bspkfw-chart-empty { font-size: 13px; }

.bspkfw-chart-tooltip {
    position: absolute;
    z-index: 5;
    pointer-events: none;
    background: var(--chart-tooltip-bg);
    color: var(--chart-tooltip-color);
    padding: 6px 9px;
    border-radius: 6px;
    font-size: 12px;
    line-height: 1.5;
    white-space: nowrap;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
}
.bspkfw-chart-tooltip-time { font-weight: 600; }
.bspkfw-chart-tooltip-row  { display: flex; align-items: center; gap: 6px; }

.bspkfw-chart-legend {
    display: flex;
    flex-wrap: wrap;
    gap: 4px 14px;
    padding: 2px 0 6px;
    font-size: 12px;
    color: var(--chart-axis-text);
}
.bspkfw-chart-legend-item { display: inline-flex; align-items: center; gap: 6px; }
.bspkfw-chart-swatch {
    flex: 0 0 auto;
    width: 10px;
    height: 10px;
    border-radius: 2px;
    background: var(--chart-series-color);
}

/* ── Development history page (/dev-history) ──────────────────────────
   Bespoke presentation for the super-admin git-history browser
   (pages/dev_history.php + js/pages/dev_history.js). Wider than the
   680px profile column — commit summaries are prose. */
.devh-page { max-width: 900px; margin: 0 auto; }

.devh-toolbar {
    display: flex;
    gap: 10px;
    align-items: center;
    margin-bottom: var(--bspkfw-space-row, 16px);
}
.devh-search { flex: 1 1 auto; }
.devh-filter { flex: 0 0 auto; width: auto; }

.devh-list { display: flex; flex-direction: column; gap: 12px; }

.devh-row {
    background: #fff;
    border: 1px solid #e5e7eb;
    border-radius: 8px;
    padding: var(--bspkfw-space-row, 16px);
}
.devh-meta {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 4px 12px;
    margin-bottom: 8px;
}
.devh-badge--framework { background: #dbeafe; color: #1e40af; }
.devh-badge--project   { background: #dcfce7; color: #166534; }
.devh-hash   { font-size: 12px; color: #6b7280; }
.devh-author { font-size: 13px; color: #374151; font-weight: 600; }
.devh-when   { font-size: 13px; color: #6b7280; margin-left: auto; }

.devh-text {
    margin: 0;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    font-size: 14px;
    line-height: 1.55;
}
.devh-text--summary { font-style: italic; color: #374151; }
.devh-toggle { margin-top: 8px; }
.devh-full {
    margin: 10px 0 0;
    padding: 12px;
    background: #f9fafb;
    border: 1px solid #e5e7eb;
    border-radius: 6px;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    font-size: 13px;
    line-height: 1.5;
    max-height: 70vh;
    overflow-y: auto;
}

.devh-pager {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 14px;
    margin-top: var(--bspkfw-space-section-gap, 20px);
}
.devh-pager-label { font-size: 13px; color: #6b7280; }

.devh-unavailable code { user-select: all; }

/* ============================================================
   BspkfwScanner (bspkfw-scanner.js) — camera barcode / QR
   reader modal. Standard .modal chrome; these rules style only
   the camera viewport inside it.
   ============================================================ */
.bspkfw-scanner-modal .modal-box { width: 440px; }

.bspkfw-scanner-viewport {
    position: relative;
    background: #000;
    border-radius: 8px;
    overflow: hidden;
    aspect-ratio: 4 / 3;
}
.bspkfw-scanner-viewport video {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* Aiming frame — flashes green (.is-hit on the viewport) when a scan
   is accepted, the "that one counted" cue. */
.bspkfw-scanner-reticle {
    position: absolute;
    inset: 14% 10%;
    border: 2px solid rgba(255, 255, 255, 0.8);
    border-radius: 10px;
    pointer-events: none;
    /* dark halo either side of the white line so the frame reads on
       bright scenes (a white shelf label) and dark ones alike */
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.4), inset 0 0 0 1px rgba(0, 0, 0, 0.4);
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.bspkfw-scanner-viewport.is-hit .bspkfw-scanner-reticle {
    border-color: #22c55e;
    box-shadow: 0 0 0 4px rgba(34, 197, 94, 0.35);
}

/* Torch toggle — capability-gated by the JS (hidden unless the active
   camera track reports torch support). */
.bspkfw-scanner-torch {
    position: absolute;
    right: 10px;
    bottom: 10px;
    background: rgba(17, 24, 39, 0.65);
    border-color: rgba(255, 255, 255, 0.5);
    color: #f9fafb;
}
.bspkfw-scanner-torch.is-active {
    background: #f9fafb;
    color: #111827;
}

.bspkfw-scanner-status {
    margin: 12px 0 0;
    min-height: 1.4em;
    text-align: center;
    font-size: 14px;
    color: #6b7280;
}
.bspkfw-scanner-error { margin: 12px 0 0; }
