/*
 * Application wide variables.
 */
:root {
    --outter-bg-color-light: #93a9b4;
    --outter-bg-color-dark: #93a9b4;

    --outter-bg: linear-gradient(180deg, var(--outter-bg-color-light) 0%, var(--outter-bg-color-dark) 100%);

    --inner-bg-color-light: white;
    --inner-bg-color-dark: #009fff;

    --inner-bg: linear-gradient(180deg, var(--inner-bg-color-light) 0%, var(--inner-bg-color-dark) 100%);

    /* .btn-sidebar/.btn-link (old, pre-Phase-1-shell sidebar/link classes) are the
       last consumers of --menu-bg/--menu-shadow; out of this phase's scope. */
    --menu-bg-color-light: #828282;
    --menu-bg-color-dark: #666666;

    --menu-bg: linear-gradient(180deg, var(--menu-bg-color-light) 0%, var(--menu-bg-color-dark) 51%, var(--menu-bg-color-dark) 100%);
    --menu-shadow: 0 1px 5px 0 var(--menu-bg-color-dark);

    /* Two WayToGo custom screens (TimesheetEntry.html, TimesheetEntryByClock.html)
       reference --border-color-light directly; kept for them, repointed to the
       app's hairline-border token so they repaint along with everything else. */
    --border-color-light: var(--s360-border-strong);

    --chk-bg-color: linear-gradient(180deg, var(--menu-bg-color-light) 0%, var(--menu-bg-color-dark) 100%);

    --grid-alternating-color: #f7f7f7;

    --error-color: red;

    /*
     * Single source of truth for the app's font stack (see "Single Font-Family
     * Source of Truth" in Frontend/AI/kb/components.md). --s360-font-stack is
     * what every declaration site actually renders with; it defaults to the
     * Plex alternative but can be repointed at --s360-font-stack-inter, at
     * runtime, via the `font` URL parameter or Framework.setFont() -- see
     * index.html's startup() and framework.js.
     */
    --s360-font-stack-plex: "IBM Plex Sans Variable", "IBM Plex Sans", InterVariable, Inter, Arial;
    --s360-font-stack-inter: InterVariable, Inter, Arial;
    --s360-font-stack: var(--s360-font-stack-plex);

    /*
     * Font formatting.
     */
    font-family: var(--s360-font-stack);
    font-size: 11px;
    font-weight: lighter;

    /*
     * App-shell design tokens (Phase 1 visual modernization). Reused by later
     * phases when they restyle controls/popups/grids -- see "App Shell Layout"
     * in Frontend/AI/kb/architecture.md.
     */
    --s360-bg: #f3f4f6;             /* page background, behind the white shell panels */
    --s360-surface: #ffffff;        /* navbar / sidebar / content-card background */
    --s360-border: #e5e7eb;         /* light hairline borders */
    --s360-border-strong: #d1d5db;  /* slightly stronger border, e.g. breadcrumb chevrons */
    --s360-text: #1f2937;           /* primary text on a surface */
    --s360-text-muted: #6b7280;     /* secondary/muted text */
    --s360-brand: #39285f;          /* logo purple -- selected-state text */
    --s360-brand-soft: #efeaf7;     /* selected/hover tint */
    --s360-accent: #007bc1;         /* logo blue -- links */
    --s360-radius: 8px;             /* card corner radius */
    --s360-radius-sm: 6px;          /* small-control corner radius (menu items, link pills) */
    --s360-shadow-card: 0 1px 2px rgba(0,0,0,.04), 0 1px 3px rgba(0,0,0,.06);
    --s360-shadow-pop: 0 10px 30px rgba(0,0,0,.18);
}

.invalid {
    margin: 1px !important;
    border: solid 1px var(--error-color) !important;
}

hr {
    margin-top: 3px;
    border: none;
    height: 1px;
    color: var(--s360-border-strong);
    background-color: var(--s360-border-strong);
}

.grid-container {
    display: grid;
    /*grid-column-gap: 5px;*/
}

.play-icon-black {
    display: inline;
    height: 10px;
    width: 10px;
    padding: 0 10px;
}

.play-icon {
    display: inline;
    height: 10px;
    width: 10px;
    padding: 0 10px;
    filter: invert(100%);
}

/* ---------------------------------------------------------------------
   Icon sprite (Phase 4). See "Icon Sprite (Lucide, Phase 4)" in
   Frontend/AI/kb/components.md for the full contract; short version: an
   icon is <svg class="s360-icon"><use href="assets/icons/lucide.svg#lucide-
   NAME"></use></svg>, built by Utils.icon(name, extraClass) in
   scripts/Stack360Utils.js. Color comes from the ordinary CSS `color`
   property (the sprite's symbols carry no fill/stroke of their own, by
   design, so they inherit currentColor through the <use> reference) --
   set `color` on the <svg> itself or an ancestor, never here.
   --------------------------------------------------------------------- */
.s360-icon {
    width: 16px;
    height: 16px;
    display: inline-block;
    vertical-align: middle;
    flex: 0 0 auto;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* Review round -- Blake: "the eye icon is slow to load or even fails for some reason." A grid
   cellRenderer used to build a fresh <img src="assets/icons/eye.svg"> per ROW (HrParent2.js and
   4 sibling screens' PersonInfo/BenefitsChangeData/StatusInfo/ProjectInfo/TimesheetDescription
   cellRenderers), which is one HTTP request per row under the dev server's no-freshness headers
   -- 100 rows, 100 requests, each competing with the actual data call for the connection pool
   (see "DB connection pool" for the same class of per-row-request problem elsewhere in this
   project). Fixed by rendering the SAME "eye" glyph from the already-self-hosted Lucide sprite
   instead (Utils.icon('eye', 's360-grid-eye') -- one sprite fetch per session, zero further
   requests no matter how many rows a grid renders). Sized/positioned to match the old
   <img style="transform: scale(.6); position: relative; top: -3px; left: -5px;"> rendering
   (measured live: the old icon's painted box was 19.2x19.2px, vertically centered on the row to
   within 1px with no manual offset needed at all -- .s360-icon's own `vertical-align: middle`
   already did that; the only additional offset needed was horizontal, to sit at the same
   left-inset within the cell's own padding). See "Grid Eye Icon: Sprite Instead of Per-Row <img>
   Requests" in kb/components.md. */
.s360-grid-eye {
    width: 19px;
    height: 19px;
    position: relative;
    top: -1px;
    left: 1px;
    color: #6b7280;
}

/* TreeView.js's default leaf/parent icons (Document Management, Document
   Viewing, and any future TreeView user) -- brand-colored, same ~16px
   footprint the document.png/openFolder.png PNGs they replace used. */
.s360-tree-icon {
    display: inline-flex;
    color: var(--s360-brand);
    vertical-align: middle;
}

/* TreeView.js (scripts/TreeView.js -- Document Management, Document Viewing, and any future
   TreeView user) builds a plain <ul>/<li> tree with no class of its own on either tag; every
   rule below is scoped to ".tj_container" (the id-less wrapper div TreeView.js itself always
   adds this class to) so no other list in the app is affected. Removes the browser's default
   disc markers and 40px UA-stylesheet indent; replaces it with a deliberate ~16px indent per
   nesting level (0 at the root) and puts each node's expand/collapse glyph + icon + caption on
   one line (TreeView.js emits the icon as a block-level <div>, which -- unstyled -- wraps the
   caption text onto its own line below the icon; flex fixes that without changing TreeView.js
   itself). */
.tj_container ul {
    list-style: none;
    margin: 0;
    padding-left: 16px;
}

.tj_container > ul {
    padding-left: 0;
}

.tj_container li {
    list-style: none;
}

.tj_description {
    display: flex;
    align-items: center;
    gap: 4px;
    padding: 2px 0;
    cursor: pointer;
}

.tj_icon {
    display: inline-flex;
    align-items: center;
    flex: 0 0 auto;
}

.tj_mod_icon {
    display: inline-flex;
    align-items: center;
    flex: 0 0 auto;
}

/* Borderless, background-less icon-only button: 26px box, centered icon, a
   hover tint, and the app's normal focus ring. Used by the breadcrumb back/
   forward buttons (framework.html) and available for any future icon-only
   button. See "Icon-Only Buttons Have No Border (Phase 4)" in
   Frontend/AI/kb/components.md. */
.s360-icon-btn {
    appearance: none;
    -webkit-appearance: none;
    border: none;
    background: transparent;
    border-radius: var(--s360-radius-sm);
    width: 26px;
    height: 26px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: var(--s360-text-muted);
    cursor: pointer;
    padding: 0;
    /* Anchors the busy-spinner ::after (see "Button busy spinner: .s360-icon-
       btn" further down this file) so it can be centered over the button's
       own <svg> child regardless of that svg's own size. */
    position: relative;
}

.s360-icon-btn:hover:not(:disabled) {
    background: var(--s360-brand-soft);
    color: var(--s360-brand);
}

.s360-icon-btn:focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px var(--s360-accent);
}

.s360-icon-btn:disabled {
    opacity: 0.4;
    cursor: default;
}

/* Borderless, background-less brand-colored search icon -- same 25x25 box
   the PNG-background version always used (kept for the 36+ screens that
   reference this class name; see kb/components.md).

   EVERY rule below (rest AND :disabled) uses the SAME ".kiss input[type=button]
   .btn-smart-chooser[:disabled]" prefix -- three class/attribute selectors plus
   one element, specificity (0,3,1) -- rather than the bare ".btn-smart-chooser"
   (0,1,0) the first two rules of this family used pre-review. That under-
   specification is what caused a real, shipped flash: SmartChooser.js's
   template renders the button DISABLED until its data actually loads
   (kiss/component/smartChooser/SmartChooser.js), and default-theme.css's
   generic "input[type=button]:disabled { background: var(--s360-bg); color:
   var(--s360-text-muted); border-color: var(--s360-border); }" is (0,2,1) --
   which beats a bare ".btn-smart-chooser:disabled" (0,2,0) or an un-gated
   ".btn-smart-chooser { background: transparent }" (0,1,0) for the
   background-color/color longhands specifically, even though this file loads
   before default-theme.css in <head> (specificity is checked before source
   order, and always wins the tie regardless of it). The result: every smart-
   chooser button painted a light gray box (--s360-bg) with muted text-color
   for as long as it stayed disabled -- typically under a second, until the
   screen's own async load enables it -- then snapped to its real transparent/
   brand-color icon look, reading as an unwanted "flash". See "Smart-Chooser
   Disabled-State Flash (Phase 4 Review)" in Frontend/AI/kb/components.md. */
.kiss input[type=button].btn-smart-chooser {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 25px;
    height: 25px;
    padding: 0;
    border: none;
    background-color: transparent;
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2339285f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m21 21-4.34-4.34'/%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3C/svg%3E");
    /* Two-layer composition -- see the value-keyed glyph family further up
       this file for the full explanation; same mechanism, just a 15px icon
       box instead of 14px. */
    background-repeat: no-repeat, no-repeat;
    background-position: center, center;
    background-size: 0 0, 15px 15px;
    background-image: var(--s360-spinner-icon), var(--s360-btn-icon);
    transition: var(--s360-btn-icon-transition);
    color: var(--s360-brand);
    border-radius: var(--s360-radius-sm);
}

.kiss input[type=button].btn-smart-chooser:hover:not(:disabled) {
    background-color: var(--s360-brand-soft);
}

.kiss input[type=button].btn-smart-chooser:disabled {
    background-color: transparent;
    border-color: transparent;
    color: var(--s360-text-muted);
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m21 21-4.34-4.34'/%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3C/svg%3E");
}

/* ---------------------------------------------------------------------
   Icon-only value-keyed buttons (Phase 4 review round). Blake: "in general,
   icon buttons (where it's only the icon) should not have a border" -- a
   push-button whose caption (PushButton.js renders <push-button>X</push-button>
   as <input type=button value="X">, so this keys on the FINAL value attribute,
   not the source tag) is EXACTLY one of the glyphs below becomes a borderless,
   background-less brand-colored icon button, same box size, hover tint, focus
   ring kept. "?" is handled separately below (ID-keyed, not value-keyed --
   see its own comment) since the same glyph means two different things
   depending on the button's own handler. Only "<"/">" are actually used
   anywhere in screens/ today (day/week steppers on Appointments, Time Off
   Request, Expense Edit by Project); the rest are shipped inert, ready for a
   future screen with a matching caption -- see "Icon-Only Value-Keyed Buttons
   (Phase 4 Review)" in Frontend/AI/kb/components.md.
   The glyph text itself is hidden via `color: transparent` (NOT font-size: 0,
   which would shrink the button's line-box and its overall height -- a real
   frozen-box-size risk) -- the character still occupies its normal layout
   width/height, just painted invisibly, so the box is unaffected either way.
   --------------------------------------------------------------------- */
.kiss input[type=button][value="<"],
.kiss input[type=button][value=">"],
.kiss input[type=button][value="<<"],
.kiss input[type=button][value=">>"],
.kiss input[type=button][value="^"],
.kiss input[type=button][value="v"],
.kiss input[type=button][value="▲"],
.kiss input[type=button][value="▼"],
.kiss input[type=button][value="+"],
.kiss input[type=button][value="-"] {
    border: none;
    background-color: transparent;
    color: transparent;
    /* Two-layer composition (see "Button busy spinner: shared assets" further
       down this file) -- --s360-btn-icon (set per glyph below) is the resting
       icon, fully sized/visible; --s360-spinner-icon starts at 0x0 on the same
       centered position and is swapped in by .s360-busy, which only touches
       background-size below (both layers already use the "center" keyword,
       so they stay centered on the same point as they shrink/grow -- no
       position to animate). */
    background-repeat: no-repeat, no-repeat;
    background-position: center, center;
    background-size: 0 0, 14px 14px;
    background-image: var(--s360-spinner-icon), var(--s360-btn-icon);
    transition: var(--s360-btn-icon-transition);
    border-radius: var(--s360-radius-sm);
}

.kiss input[type=button][value="<"]:hover:not(:disabled),
.kiss input[type=button][value=">"]:hover:not(:disabled),
.kiss input[type=button][value="<<"]:hover:not(:disabled),
.kiss input[type=button][value=">>"]:hover:not(:disabled),
.kiss input[type=button][value="^"]:hover:not(:disabled),
.kiss input[type=button][value="v"]:hover:not(:disabled),
.kiss input[type=button][value="▲"]:hover:not(:disabled),
.kiss input[type=button][value="▼"]:hover:not(:disabled),
.kiss input[type=button][value="+"]:hover:not(:disabled),
.kiss input[type=button][value="-"]:hover:not(:disabled) {
    background-color: var(--s360-brand-soft);
}

.kiss input[type=button][value="<"] {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2339285f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m15 18-6-6 6-6'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="<"]:disabled {
    background-color: transparent;
    border-color: transparent;
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m15 18-6-6 6-6'/%3E%3C/svg%3E");
}

.kiss input[type=button][value=">"] {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2339285f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m9 18 6-6-6-6'/%3E%3C/svg%3E");
}

.kiss input[type=button][value=">"]:disabled {
    background-color: transparent;
    border-color: transparent;
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m9 18 6-6-6-6'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="<<"] {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2339285f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m11 17-5-5 5-5'/%3E%3Cpath d='m18 17-5-5 5-5'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="<<"]:disabled {
    background-color: transparent;
    border-color: transparent;
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m11 17-5-5 5-5'/%3E%3Cpath d='m18 17-5-5 5-5'/%3E%3C/svg%3E");
}

.kiss input[type=button][value=">>"] {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2339285f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 17 5-5-5-5'/%3E%3Cpath d='m13 17 5-5-5-5'/%3E%3C/svg%3E");
}

.kiss input[type=button][value=">>"]:disabled {
    background-color: transparent;
    border-color: transparent;
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 17 5-5-5-5'/%3E%3Cpath d='m13 17 5-5-5-5'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="^"] {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2339285f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m18 15-6-6-6 6'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="^"]:disabled {
    background-color: transparent;
    border-color: transparent;
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m18 15-6-6-6 6'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="v"] {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2339285f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="v"]:disabled {
    background-color: transparent;
    border-color: transparent;
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="▲"] {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2339285f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m18 15-6-6-6 6'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="▲"]:disabled {
    background-color: transparent;
    border-color: transparent;
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m18 15-6-6-6 6'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="▼"] {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2339285f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="▼"]:disabled {
    background-color: transparent;
    border-color: transparent;
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="+"] {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2339285f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14'/%3E%3Cpath d='M12 5v14'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="+"]:disabled {
    background-color: transparent;
    border-color: transparent;
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14'/%3E%3Cpath d='M12 5v14'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="-"] {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2339285f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="-"]:disabled {
    background-color: transparent;
    border-color: transparent;
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14'/%3E%3C/svg%3E");
}

/* "?" push-buttons mean two different things depending on the handler --
   see "Inline Field-Level `?` Help Button, Distinct From the Same-Glyph
   Search-Chooser Button" in Frontend/AI/kb/popups.md -- so they can't be
   told apart by `value="?"` alone the way the chevron/plus/minus family
   above can. Classified by reading each button's own onclick handler (grep
   confirms these are the only "?"-captioned push-buttons in screens/, and
   all 10 were classifiable from markup+handler alone -- no residual/ambiguous
   ones): calls Utils.component(...) (opens a chooser/selection popup) -> the
   search icon; calls Utils.popup_open('...help-popup') (a plain read-only
   explanation) -> the circle-help icon. ID-keyed rather than a new class,
   per Blake's own suggestion, so no screen needed editing. See "?" Button
   Classification (Phase 4 Review)" in Frontend/AI/kb/components.md for the
   full per-button table.
   --------------------------------------------------------------------- */
.kiss input[type=button]#search-project,
.kiss input[type=button]#search-worker,
.kiss input[type=button]#search-client,
.kiss input[type=button]#choose-project,
.kiss input[type=button]#do-search-project,
.kiss input[type=button]#searchWorker,
.kiss input[type=button]#searchProject,
.kiss input[type=button]#rp-display-order-help {
    border: none;
    background-color: transparent;
    color: transparent;
    /* Two-layer composition -- see the value-keyed glyph family above for the
       full explanation (identical mechanism, same 14px centered icon box). */
    background-repeat: no-repeat, no-repeat;
    background-position: center, center;
    background-size: 0 0, 14px 14px;
    background-image: var(--s360-spinner-icon), var(--s360-btn-icon);
    transition: var(--s360-btn-icon-transition);
    border-radius: var(--s360-radius-sm);
}

.kiss input[type=button]#search-project,
.kiss input[type=button]#search-worker,
.kiss input[type=button]#search-client,
.kiss input[type=button]#choose-project,
.kiss input[type=button]#do-search-project,
.kiss input[type=button]#searchWorker,
.kiss input[type=button]#searchProject:hover:not(:disabled),
.kiss input[type=button]#rp-display-order-help:hover:not(:disabled) {
    background-color: var(--s360-brand-soft);
}

.kiss input[type=button]#search-project,
.kiss input[type=button]#search-worker,
.kiss input[type=button]#search-client,
.kiss input[type=button]#choose-project,
.kiss input[type=button]#do-search-project,
.kiss input[type=button]#searchWorker,
.kiss input[type=button]#searchProject {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2339285f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m21 21-4.34-4.34'/%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3C/svg%3E");
}

.kiss input[type=button]#search-project:disabled,
.kiss input[type=button]#search-worker:disabled,
.kiss input[type=button]#search-client:disabled,
.kiss input[type=button]#choose-project:disabled,
.kiss input[type=button]#do-search-project:disabled,
.kiss input[type=button]#searchWorker:disabled,
.kiss input[type=button]#searchProject:disabled {
    background-color: transparent;
    border-color: transparent;
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m21 21-4.34-4.34'/%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3C/svg%3E");
}

.kiss input[type=button]#rp-display-order-help {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2339285f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3'/%3E%3Cpath d='M12 17h.01'/%3E%3C/svg%3E");
}

.kiss input[type=button]#rp-display-order-help:disabled {
    background-color: transparent;
    border-color: transparent;
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3'/%3E%3Cpath d='M12 17h.01'/%3E%3C/svg%3E");
}

/* ---------------------------------------------------------------------
   Captioned buttons with an icon at the LEFT of the caption (Phase 4 review
   round). Border and background are KEPT -- these are ordinary bordered
   buttons, unlike the icon-ONLY family above -- Blake: "if the icon is going
   to the left of a word, those can have a border". Keyed on the FINAL `value`
   attribute (PushButton.js renders <push-button>Caption</push-button> as
   <input type=button value="Caption">), so no screen needed editing. Left
   padding widens from 12px to 30px (icon at 10px, 14px wide, ~6px gap to the
   caption text); right padding, top/bottom padding, border-width, and font-
   size are all UNCHANGED from `.kiss input[type=button]`'s geometry rule, so a
   button's HEIGHT never changes and its WIDTH only grows where the button was
   already auto-sized to its content (the overwhelming majority -- `.kiss
   input[type=button]` sets no width of its own). Shipped for every caption in
   the policy set -- see "Captioned Button Icons (Phase 4 Review)" in
   Frontend/AI/kb/components.md for the full per-caption survey table, the
   handful of genuinely fixed-pixel-width buttons that needed their OWN small
   per-screen width increase, and why every other width form (max-content,
   fit-content, 100%, or no width at all) can never clip from added padding by
   construction (they all resize to fit their content or container).
   --------------------------------------------------------------------- */
.kiss input[type=button][value="Add"],
.kiss input[type=button][value="Edit"],
.kiss input[type=button][value="Delete"],
.kiss input[type=button][value="Remove"],
.kiss input[type=button][value="Filter"],
.kiss input[type=button][value="Filters"],
.kiss input[type=button][value="Report"],
.kiss input[type=button][value="Export"],
.kiss input[type=button][value="Import"],
.kiss input[type=button][value="Print"],
.kiss input[type=button][value="Search"],
.kiss input[type=button][value="Refresh"],
.kiss input[type=button][value="Reset"],
.kiss input[type=button][value="Move Up"],
.kiss input[type=button][value="Move Down"],
.kiss input[type=button][value="View"] {
    padding-left: 30px;
    /* Two-layer composition -- see "Button busy spinner: shared assets"
       further down this file. Resting: spinner 0x0 centered on the icon's
       OWN center point (17px = 10px inset + half of 14px), icon full size
       at its normal 10px inset. .s360-busy (also further down) swaps which
       layer is 0x0 vs 14x14, animating position together with size so the
       collapse/grow is centered on that same 17px point rather than the
       box's left edge. */
    background-repeat: no-repeat, no-repeat;
    background-position: 17px center, 10px center;
    background-size: 0 0, 14px 14px;
    background-image: var(--s360-spinner-icon), var(--s360-btn-icon);
    transition: var(--s360-btn-icon-transition);
}

.kiss input[type=button][value="Add"] {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2339285f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14'/%3E%3Cpath d='M12 5v14'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="Add"]:disabled {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14'/%3E%3Cpath d='M12 5v14'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="Edit"] {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2339285f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z'/%3E%3Cpath d='m15 5 4 4'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="Edit"]:disabled {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z'/%3E%3Cpath d='m15 5 4 4'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="Delete"] {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2339285f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 11v6'/%3E%3Cpath d='M14 11v6'/%3E%3Cpath d='M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6'/%3E%3Cpath d='M3 6h18'/%3E%3Cpath d='M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="Delete"]:disabled {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 11v6'/%3E%3Cpath d='M14 11v6'/%3E%3Cpath d='M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6'/%3E%3Cpath d='M3 6h18'/%3E%3Cpath d='M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="Remove"] {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2339285f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 6 6 18'/%3E%3Cpath d='m6 6 12 12'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="Remove"]:disabled {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 6 6 18'/%3E%3Cpath d='m6 6 12 12'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="Filter"] {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2339285f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 20a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341L21.74 4.67A1 1 0 0 0 21 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14z'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="Filter"]:disabled {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 20a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341L21.74 4.67A1 1 0 0 0 21 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14z'/%3E%3C/svg%3E");
}

/* "Filters" (Phase 6 -- the drawer trigger button, Drawer.filterButton()) reuses the exact
   same funnel icon as the singular "Filter" caption above -- same policy, same icon, just the
   plural caption the drawer trigger button actually uses. */
.kiss input[type=button][value="Filters"] {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2339285f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 20a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341L21.74 4.67A1 1 0 0 0 21 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14z'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="Filters"]:disabled {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 20a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341L21.74 4.67A1 1 0 0 0 21 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14z'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="Report"] {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2339285f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z'/%3E%3Cpath d='M14 2v5a1 1 0 0 0 1 1h5'/%3E%3Cpath d='M10 9H8'/%3E%3Cpath d='M16 13H8'/%3E%3Cpath d='M16 17H8'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="Report"]:disabled {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z'/%3E%3Cpath d='M14 2v5a1 1 0 0 0 1 1h5'/%3E%3Cpath d='M10 9H8'/%3E%3Cpath d='M16 13H8'/%3E%3Cpath d='M16 17H8'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="Export"] {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2339285f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 15V3'/%3E%3Cpath d='M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4'/%3E%3Cpath d='m7 10 5 5 5-5'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="Export"]:disabled {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 15V3'/%3E%3Cpath d='M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4'/%3E%3Cpath d='m7 10 5 5 5-5'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="Import"] {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2339285f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3v12'/%3E%3Cpath d='m17 8-5-5-5 5'/%3E%3Cpath d='M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="Import"]:disabled {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3v12'/%3E%3Cpath d='m17 8-5-5-5 5'/%3E%3Cpath d='M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="Print"] {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2339285f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2'/%3E%3Cpath d='M6 9V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6'/%3E%3Crect x='6' y='14' width='12' height='8' rx='1'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="Print"]:disabled {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2'/%3E%3Cpath d='M6 9V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6'/%3E%3Crect x='6' y='14' width='12' height='8' rx='1'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="Search"] {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2339285f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m21 21-4.34-4.34'/%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="Search"]:disabled {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m21 21-4.34-4.34'/%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="Refresh"] {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2339285f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8'/%3E%3Cpath d='M21 3v5h-5'/%3E%3Cpath d='M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16'/%3E%3Cpath d='M8 16H3v5'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="Refresh"]:disabled {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8'/%3E%3Cpath d='M21 3v5h-5'/%3E%3Cpath d='M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16'/%3E%3Cpath d='M8 16H3v5'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="Reset"] {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2339285f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8'/%3E%3Cpath d='M3 3v5h5'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="Reset"]:disabled {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8'/%3E%3Cpath d='M3 3v5h5'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="Move Up"] {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2339285f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m5 12 7-7 7 7'/%3E%3Cpath d='M12 19V5'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="Move Up"]:disabled {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m5 12 7-7 7 7'/%3E%3Cpath d='M12 19V5'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="Move Down"] {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2339285f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 5v14'/%3E%3Cpath d='m19 12-7 7-7-7'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="Move Down"]:disabled {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 5v14'/%3E%3Cpath d='m19 12-7 7-7-7'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="View"] {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2339285f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0'/%3E%3Ccircle cx='12' cy='12' r='3'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="View"]:disabled {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0'/%3E%3Ccircle cx='12' cy='12' r='3'/%3E%3C/svg%3E");
}

/* Four more captioned-icon buttons (Phase 4 review round two). Blake: "maybe add
   icons to like Reply and New. Handled can be a checkmark to the left of the
   word." Same mechanism as the block above (value-keyed, border/background kept,
   left padding 30px, no screen edits) -- grepped screens/ for these 4 captions:
   36 instances total (New 13, Reply 8, Handled 5, Send 10), ZERO with an inline
   width style -- every one is auto-sized, so none can clip from added padding by
   construction, the same reasoning as the first 15-caption survey. The five
   Messages screens (at/hr/misc/project/messageSingle) carry all four captions at
   once, exactly as Blake described. */
.kiss input[type=button][value="New"],
.kiss input[type=button][value="Reply"],
.kiss input[type=button][value="Handled"],
.kiss input[type=button][value="Send"] {
    padding-left: 30px;
    /* Two-layer composition -- see "Button busy spinner: shared assets"
       further down this file. Resting: spinner 0x0 centered on the icon's
       OWN center point (17px = 10px inset + half of 14px), icon full size
       at its normal 10px inset. .s360-busy (also further down) swaps which
       layer is 0x0 vs 14x14, animating position together with size so the
       collapse/grow is centered on that same 17px point rather than the
       box's left edge. */
    background-repeat: no-repeat, no-repeat;
    background-position: 17px center, 10px center;
    background-size: 0 0, 14px 14px;
    background-image: var(--s360-spinner-icon), var(--s360-btn-icon);
    transition: var(--s360-btn-icon-transition);
}

.kiss input[type=button][value="New"] {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2339285f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14'/%3E%3Cpath d='M12 5v14'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="New"]:disabled {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14'/%3E%3Cpath d='M12 5v14'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="Reply"] {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2339285f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 18v-2a4 4 0 0 0-4-4H4'/%3E%3Cpath d='m9 17-5-5 5-5'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="Reply"]:disabled {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 18v-2a4 4 0 0 0-4-4H4'/%3E%3Cpath d='m9 17-5-5 5-5'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="Handled"] {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2339285f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="Handled"]:disabled {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="Send"] {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2339285f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z'/%3E%3Cpath d='m21.854 2.147-10.94 10.939'/%3E%3C/svg%3E");
}

.kiss input[type=button][value="Send"]:disabled {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z'/%3E%3Cpath d='m21.854 2.147-10.94 10.939'/%3E%3C/svg%3E");
}



.title {
    position: absolute;
    z-index: -1;
    right: 0;
    left: 10px;
    margin-top: 5px;

    display: block;
    background-color: transparent;
}

.subtitle {
    position: absolute;
    top: 60px;
    left: 120px;
    font-size: 11px;
    font-weight: bold;
    font-style: italic;
}

.hr-purple, .hr-green {
    position: absolute;
    right: 10px;
    height: 1px;
}

.hr-purple {
    background-color: #041c3b;
    left: 115px;
    top: 55px;
}

.hr-green {
    background-color: #466083;
    left: 115px;
    top: 56px;
}

.logo {
    width: 275px;
}

.tab-header:before {
    z-index: 1;
}

.tab-header {
    position: relative;
    display: flex;
    list-style: none;
    margin: 0;
    padding: 0;
}

/* Underline tabs: no gradient box, no per-tab border -- just muted text that
   turns brand-colored and semi-bold, with a 2px bottom border, when active. The
   header keeps a single hairline rule (.tab-header:after) instead of each li
   drawing its own border. Padding is tuned (6px 12px, border-box) so the li's
   OWN box height matches the old bordered-box height exactly -- see "Control
   Design System (Phase 2)" in kb/components.md for the frozen-box-size method. */
.tab-header li {
    position: relative;
    box-sizing: border-box;
    margin: 0;
    padding: 5px 12px;
    border: none;
    border-bottom: 2px solid transparent;
    background: transparent;
    color: var(--s360-text-muted);
    font-weight: 500;
    cursor: default;
    z-index: 0;
}

.tab-header li:hover {
    color: var(--s360-text);
}

.tab-header li.active {
    background: transparent;
    color: var(--s360-brand);
    font-weight: 600;
    border-bottom-color: var(--s360-brand);
    z-index: 2;
}

.tab-header li.active:hover {
    color: var(--s360-brand);
}

.tab-header:after {
    position: absolute;
    content: "";
    width: 100%;
    bottom: 0;
    left: 0;
    border-bottom: 1px solid var(--s360-border);
    z-index: 1;
}

.tab-content {
    height: calc(100% - 45px);
    padding: 10px;
    border-width: 1px;
    border-style: solid;
    border-color: var(--s360-border);
    border-top: none;
    border-bottom-left-radius: 6px;
    border-bottom-right-radius: 6px;
    background: white;
}

/* Tab content switch fade (Phase 4). TabContainer.js (scripts/TabContainer.js)
   toggles the `hidden` attribute AND `style.display` synchronously on click --
   this rule adds no delay to either: it is a pure CSS animation that restarts
   whenever a `.tab-content` re-enters the render tree (goes from display:none
   back to displayed), the same restart-on-display mechanism the popup open
   animation above relies on. A test that checks `.hidden`/visibility
   immediately after a tab click sees the correct state instantly; the fade is
   purely cosmetic on top of that already-correct state. */
.tab-content:not([hidden]) {
    animation: s360-tab-fade 120ms ease;
}

@keyframes s360-tab-fade {
    from { opacity: 0; }
    to   { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
    .tab-content:not([hidden]) {
        animation: none;
    }
}

.alternating-bg {
    background: var(--grid-alternating-color);
}

.flex-row {
    display: flex;
    flex-direction: row;
    flex-basis: 100%;
    align-items: center;
}

.btn-link {
    color: white;
}

.btn-sidebar, .btn-link {
    display: block;
    padding: 5px;
    font-size: 11px;
    font-weight: bold;
    border: none;
    border-radius: 4px;
    line-height: 1.3;
 /*   cursor: default !important; */
}

.btn-sidebar {
    margin-bottom: 7px;
    background: var(--menu-bg);
    box-shadow: var(--menu-shadow);
    color: white;
}

.btn-link:hover, .btn-sidebar:hover {
    background: black;
}

.btn-link:hover {
    cursor: pointer;
}

.btn-sidebar:hover {
    cursor: pointer;
}

.btn-bar, .btn-bar-comp {
    display: inline-flex;
    flex-direction: row;
}

.btn-bar > input[type="button"], .btn-bar-comp > input[type="button"] {
    margin: 0 5px;
}

.btn-bar-comp > input[type="button"]:first-child {
    margin-left: 0;
}

.btn-bar-comp > input[type="button"]:last-child {
    margin-right: 0;
}

/* .modal-content/.modal-title are styled in styles/default-theme.css, which
   loads AFTER this file and wins the (identical-specificity) cascade -- a
   duplicate definition here would just be dead code. See "Control Design
   System (Phase 2)" in kb/components.md. */

.bottom-toolbar {
    display: flex;
    flex-direction: row;
    justify-content: space-between;
    margin: 10px 0 0 0;
}

label {
    text-align: right;
}

.control-label {
    margin-right: 8px;
    text-align: right;
}

.error-label {
    color: var(--error-color);
}

.link-label {
    color: var(--s360-accent);
    font-weight: bold;
    cursor: pointer;
}

.link-label:hover {
    color: var(--s360-brand);
}

.link-label:active {
    color: var(--s360-brand);
}

input[type='date'] {
    width: 90px;
}

/**
 * agGrid styles.
 */
.ag-overlay {
    margin-top: 10px;
    font-size: 11px;
}

.ag-header-cell-text, .ag-cell {
    font-size: 11px;
}

.ag-header-cell-text {
    font-weight: 600;
}

/* Selected/hover row colors are set via the --ag-selected-row-background-color/
   --ag-row-hover-color variables in styles/default-theme.css's .ag-theme-balham
   rule (the vendor CSS's own .ag-row-selected/.ag-row-hover rules already consume
   those variables -- see "AG Grid Theme Overrides Must Live in default-theme.css,
   Not common.css" in kb/components.md). Hardcoded !important overrides here would
   fight that mechanism (an !important class rule beats a variable-driven one
   regardless of cascade order), so they are gone, not just repointed. */

.ag-cell-focus {
    border-color: transparent !important;
}

/* The following rules override the canonical kiss/Utils.css defaults for these
   selectors (see kiss/Utils.css for the base .kiss-popup-* and .kiss-msg-modal-*
   rules). Only the declarations that differ from the canonical values are
   listed here; everything else is inherited from kiss/Utils.css. Flat white
   surface, hairline border, app shadow/radius tokens, dimmer backdrop -- see
   "Control Design System (Phase 2)" in kb/components.md. */
.kiss-popup-background {
    background-color: rgba(17, 24, 39, .45);
}

.kiss-popup-content {
    background-color: var(--s360-surface);
    border: 1px solid var(--s360-border);
    border-radius: var(--s360-radius);
    box-shadow: var(--s360-shadow-pop);
    /* Open animation: 120ms fade + a slight scale up from 98%. Transform-only
       (never touches width/height), so it cannot affect Utils.popup_open()'s
       own measuring -- that function reads/sets width/height BEFORE
       DOMUtils.show(w) even runs, never re-measures afterward, and by the
       time any Playwright/measurement script looks at the popup the 120ms
       animation is long finished, settled at scale(1). Restarts correctly on
       every open because the whole subtree goes through display:none -> block
       each time (via the ancestor .kiss-popup-background's own show/hide),
       and a CSS animation on an element that re-enters the render tree always
       restarts from its 0% keyframe. See "Popup Open Animation (Phase 4)" in
       Frontend/AI/kb/popups.md. */
    animation: s360-popup-fade-scale 120ms ease;
}

@keyframes s360-popup-fade-scale {
    from { opacity: 0; transform: translate(-50%, -50%) scale(0.98); }
    to   { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}

/* .modal-content (the legacy popup family) gets the SAME open animation, but
   fade-only: its own "transform: translate(-50%, -50%) !important" (styles/
   default-theme.css) exists specifically to beat an INLINE transform that
   Utils.roundCssTransformMatrix()/dragModal() write at runtime, and per the
   CSS Animations spec a keyframe can never override an !important property
   even from within an @keyframes rule -- so a scale here would simply be
   ignored, or worse, silently fail once that inline matrix is present. Fade
   alone needs no such override (opacity isn't touched by that mechanism) and
   still gives every legacy dialog the same "arriving" feel as a kiss popup. */
.modal-content {
    animation: s360-popup-fade 120ms ease;
}

@keyframes s360-popup-fade {
    from { opacity: 0; }
    to   { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
    .kiss-popup-content, .modal-content {
        animation: none;
    }
}

/* Title bar is the SAME white surface as the body -- no colored background --
   just larger semi-bold text with a hairline rule underneath. The declared
   `height` on a kiss <popup> sizes the BODY only (Popup.js/Stack360Utils.js sets
   the height on the inner *--height body div, never the header), so this header
   is free to grow; it does, from ~24px to ~41px (padding 6px -> 10px 12px plus
   the larger 14px/600 title line) -- see kb/popups.md. */
.kiss-popup-header {
    color: var(--s360-text);
    background-color: var(--s360-surface);
    font-size: 14px;
    font-weight: 600;
    padding: 10px 12px;
    border-top-left-radius: var(--s360-radius);
    border-top-right-radius: var(--s360-radius);
    border-bottom: 1px solid var(--s360-border);
}

/* Add Animation (Frontend-only; not part of kiss/Utils.css) */
@-webkit-keyframes animatetop {
    from {top:-300px; opacity:0}
    to {top:0; opacity:1}
}

@keyframes animatetop {
    from {top:-300px; opacity:0}
    to {top:0; opacity:1}
}

.kiss-wmsg-modal-content {
    border-radius: var(--s360-radius);
    box-shadow: var(--s360-shadow-pop);
    /* Phase 4: no title bar -- a plain card, spinner + text laid out in a
       row. Phase 5 follow-up A: a small top-right toast, not a centered
       dialog -- hairline border, capped width, white surface (overriding
       the vendored kiss/Utils.css base rule's hardcoded "white" with the
       token, and its fixed 400px width with an auto/max-width pair). See
       "Wait Dialog (Utils.waitMessage, Phase 4)" in
       Frontend/AI/kb/components.md and "Non-Blocking Loading States
       (Phase 5)" in kb/architecture.md. */
    background-color: var(--s360-surface);
    border: 1px solid var(--s360-border);
    width: auto;
    max-width: 320px;
    box-sizing: border-box;
    padding: 14px 18px;
    display: flex;
    align-items: center;
    gap: 12px;
}

.kiss-wmsg-spinner {
    display: inline-flex;
    flex: 0 0 auto;
    color: var(--s360-brand);
}

.kiss-wmsg-spinner .s360-icon {
    width: 22px;
    height: 22px;
    animation: s360-spin 1s linear infinite;
}

@keyframes s360-spin {
    from { transform: rotate(0deg); }
    to   { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
    .kiss-wmsg-spinner .s360-icon {
        animation: none;
    }
}

.kiss-wmsg-text {
    font-size: 14px;
    margin: 0;
    color: var(--s360-text);
    /* min-width: 0 so a long message WRAPS within the 320px card instead of
       overflowing it -- a flex item's default min-width: auto lets its
       content's intrinsic (unwrapped) width win over the parent's
       max-width otherwise. */
    flex: 1 1 auto;
    min-width: 0;
}

.kiss-msg-modal {
    background-color: rgba(17, 24, 39, .45);
}

.kiss-msg-modal-content {
    border-radius: var(--s360-radius);
    box-shadow: var(--s360-shadow-pop);
}

/* Same white-surface treatment as the kiss-popup title bar -- always, for
   every title, including "Error"/"Warning" (Phase 2). Phase 4 adds a colored
   icon at the left of the title instead (kiss-msg-icon-*, set at runtime by
   Utils._setMsgDialogIcon() -- see kb/components.md, "Message/Confirm Dialog
   Structure") and lays the header out as icon + title + close-X in a row, so
   the header no longer needs the float/overflow trick the single-<p>-plus-
   floated-close layout used before. */
.kiss-msg-modal-header {
    display: flex;
    align-items: center;
    color: var(--s360-text);
    background-color: var(--s360-surface);
    font-size: 14px;
    font-weight: 600;
    height: auto;
    padding: 10px 12px;
    border-top-left-radius: var(--s360-radius);
    border-top-right-radius: var(--s360-radius);
    border-bottom: 1px solid var(--s360-border);
}

.kiss-msg-title {
    flex: 1 1 auto;
    min-width: 0;
    margin: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.kiss-msg-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 20px;
    height: 20px;
    margin-right: 8px;
    flex: 0 0 auto;
}

.kiss-msg-icon .s360-icon {
    width: 20px;
    height: 20px;
}

.kiss-msg-icon-error { color: #b42318; }
.kiss-msg-icon-warning { color: #b45309; }
.kiss-msg-icon-info { color: var(--s360-accent); }

/* The close X: a thin sprite icon (Utils.icon('x')), not the old bold
   "&times;" text glyph -- muted at rest, darker on hover. float/margin-top
   from the canonical kiss/Utils.css rule are harmless no-ops now that the
   header is a flex row (float doesn't apply to flex items), so only the
   properties that actually differ need overriding here. See "Message/
   Confirm Dialog Structure" in kb/components.md for the s360-no-drag class
   this element also carries (it is what makes the X actually close the
   dialog -- see Utils.makeDraggable's own doc comment). */
.kiss-msg-close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    margin: 0;
    font-size: inherit;
    font-weight: normal;
    color: var(--s360-text-muted);
    border-radius: var(--s360-radius-sm);
    cursor: pointer;
}

.kiss-msg-close:hover,
.kiss-msg-close:focus-visible {
    color: var(--s360-text);
    background: var(--s360-brand-soft);
}

.kiss-msg-close:focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px var(--s360-accent);
}

.kiss-msg-modal-body {
    font-size: 14px;
}

/* Ok / Yes / No sit bottom-right, matching every other popup's button-row
   convention -- overrides kiss/Utils.css's canonical "justify-content:
   center". */
.kiss-msg-modal-footer {
    margin-top: 10px;
    justify-content: flex-end;
    padding-right: 16px;
}

/* ---------------------------------------------------------------------
   Primary popup action button (Phase 5 review -- Blake: "In modals that have
   something like an 'ok' or 'cancel' option, make 'ok' (or similar) a primary
   button"). Scoped to the three popup-content container classes this app
   actually uses (.kiss-popup-content -- Utils.popup_open(); .modal-content --
   the deprecated MessageDialog family; .kiss-msg-modal-content --
   Utils.showMessage()/yesNo(), which covers both #message-ok and the yesNo
   "Yes" button explicitly called out below) so this can never repaint an
   ordinary screen button that happens to share one of these captions outside
   a popup. Keyed on the FINAL rendered `value`, both case variants actually
   used in this codebase ("OK" and "Ok") plus every other affirmative caption
   in the set Blake named; Cancel/No/Close are deliberately NOT in this list
   and stay the ordinary secondary button look. Checked the same 19-caption
   captioned-icon survey used elsewhere in this file (see "Captioned Button
   Icons" in kb/components.md): none of OK/Ok/Save/Yes/Submit/Apply/Update
   overlaps it, so no primary button in this set ever carries a left-aligned
   icon that would need repainting white -- if a future caption addition to
   either policy ever DOES overlap, that icon's own background-image data URI
   needs a white-stroke variant added here, scoped to this same selector
   family, or it must be excluded from one policy or the other.

   Every state below uses background-COLOR (never the `background` shorthand)
   for the identical reason documented on the button-family rules in
   styles/default-theme.css -- these buttons still need to survive that same
   shorthand/specificity trap now that they carry app-normal geometry (no
   background-image of their own, but no reason to reintroduce the risk).
   Hover color (#2c1a4d) matches the existing solid-brand-button precedent
   already shipped for #loginBtn (WhiteLabel/Stack360/login.html) rather than
   inventing a second "brand, one shade darker" value. Disabled follows that
   same precedent's own opacity-based dimming (#loginBtn:disabled) instead of
   hand-picking a separate muted-brand color -- box size (padding/border-
   width) is untouched in every state, per the frozen-box-size rule. --------
   --------------------------------------------------------------------- */
.kiss-popup-content input[type=button][value="OK"],
.kiss-popup-content input[type=button][value="Ok"],
.kiss-popup-content input[type=button][value="Save"],
.kiss-popup-content input[type=button][value="Yes"],
.kiss-popup-content input[type=button][value="Submit"],
.kiss-popup-content input[type=button][value="Apply"],
.kiss-popup-content input[type=button][value="Update"],
.modal-content input[type=button][value="OK"],
.modal-content input[type=button][value="Ok"],
.modal-content input[type=button][value="Save"],
.modal-content input[type=button][value="Yes"],
.modal-content input[type=button][value="Submit"],
.modal-content input[type=button][value="Apply"],
.modal-content input[type=button][value="Update"],
.kiss-msg-modal-content input[type=button][value="OK"],
.kiss-msg-modal-content input[type=button][value="Ok"],
.kiss-msg-modal-content input[type=button][value="Save"],
.kiss-msg-modal-content input[type=button][value="Yes"],
.kiss-msg-modal-content input[type=button][value="Submit"],
.kiss-msg-modal-content input[type=button][value="Apply"],
.kiss-msg-modal-content input[type=button][value="Update"] {
    background-color: var(--s360-brand);
    color: #fff;
    border-color: var(--s360-brand);
}

.kiss-popup-content input[type=button][value="OK"]:hover:not(:disabled),
.kiss-popup-content input[type=button][value="Ok"]:hover:not(:disabled),
.kiss-popup-content input[type=button][value="Save"]:hover:not(:disabled),
.kiss-popup-content input[type=button][value="Yes"]:hover:not(:disabled),
.kiss-popup-content input[type=button][value="Submit"]:hover:not(:disabled),
.kiss-popup-content input[type=button][value="Apply"]:hover:not(:disabled),
.kiss-popup-content input[type=button][value="Update"]:hover:not(:disabled),
.modal-content input[type=button][value="OK"]:hover:not(:disabled),
.modal-content input[type=button][value="Ok"]:hover:not(:disabled),
.modal-content input[type=button][value="Save"]:hover:not(:disabled),
.modal-content input[type=button][value="Yes"]:hover:not(:disabled),
.modal-content input[type=button][value="Submit"]:hover:not(:disabled),
.modal-content input[type=button][value="Apply"]:hover:not(:disabled),
.modal-content input[type=button][value="Update"]:hover:not(:disabled),
.kiss-msg-modal-content input[type=button][value="OK"]:hover:not(:disabled),
.kiss-msg-modal-content input[type=button][value="Ok"]:hover:not(:disabled),
.kiss-msg-modal-content input[type=button][value="Save"]:hover:not(:disabled),
.kiss-msg-modal-content input[type=button][value="Yes"]:hover:not(:disabled),
.kiss-msg-modal-content input[type=button][value="Submit"]:hover:not(:disabled),
.kiss-msg-modal-content input[type=button][value="Apply"]:hover:not(:disabled),
.kiss-msg-modal-content input[type=button][value="Update"]:hover:not(:disabled) {
    background-color: #2c1a4d;
    border-color: #2c1a4d;
}

.kiss-popup-content input[type=button][value="OK"]:focus-visible,
.kiss-popup-content input[type=button][value="Ok"]:focus-visible,
.kiss-popup-content input[type=button][value="Save"]:focus-visible,
.kiss-popup-content input[type=button][value="Yes"]:focus-visible,
.kiss-popup-content input[type=button][value="Submit"]:focus-visible,
.kiss-popup-content input[type=button][value="Apply"]:focus-visible,
.kiss-popup-content input[type=button][value="Update"]:focus-visible,
.modal-content input[type=button][value="OK"]:focus-visible,
.modal-content input[type=button][value="Ok"]:focus-visible,
.modal-content input[type=button][value="Save"]:focus-visible,
.modal-content input[type=button][value="Yes"]:focus-visible,
.modal-content input[type=button][value="Submit"]:focus-visible,
.modal-content input[type=button][value="Apply"]:focus-visible,
.modal-content input[type=button][value="Update"]:focus-visible,
.kiss-msg-modal-content input[type=button][value="OK"]:focus-visible,
.kiss-msg-modal-content input[type=button][value="Ok"]:focus-visible,
.kiss-msg-modal-content input[type=button][value="Save"]:focus-visible,
.kiss-msg-modal-content input[type=button][value="Yes"]:focus-visible,
.kiss-msg-modal-content input[type=button][value="Submit"]:focus-visible,
.kiss-msg-modal-content input[type=button][value="Apply"]:focus-visible,
.kiss-msg-modal-content input[type=button][value="Update"]:focus-visible {
    border-color: var(--s360-accent);
    box-shadow: 0 0 0 2px var(--s360-accent);
    outline: none;
}

.kiss-popup-content input[type=button][value="OK"]:disabled,
.kiss-popup-content input[type=button][value="Ok"]:disabled,
.kiss-popup-content input[type=button][value="Save"]:disabled,
.kiss-popup-content input[type=button][value="Yes"]:disabled,
.kiss-popup-content input[type=button][value="Submit"]:disabled,
.kiss-popup-content input[type=button][value="Apply"]:disabled,
.kiss-popup-content input[type=button][value="Update"]:disabled,
.modal-content input[type=button][value="OK"]:disabled,
.modal-content input[type=button][value="Ok"]:disabled,
.modal-content input[type=button][value="Save"]:disabled,
.modal-content input[type=button][value="Yes"]:disabled,
.modal-content input[type=button][value="Submit"]:disabled,
.modal-content input[type=button][value="Apply"]:disabled,
.modal-content input[type=button][value="Update"]:disabled,
.kiss-msg-modal-content input[type=button][value="OK"]:disabled,
.kiss-msg-modal-content input[type=button][value="Ok"]:disabled,
.kiss-msg-modal-content input[type=button][value="Save"]:disabled,
.kiss-msg-modal-content input[type=button][value="Yes"]:disabled,
.kiss-msg-modal-content input[type=button][value="Submit"]:disabled,
.kiss-msg-modal-content input[type=button][value="Apply"]:disabled,
.kiss-msg-modal-content input[type=button][value="Update"]:disabled {
    opacity: 0.6;
    cursor: default;
}

/* ---------------------------------------------------------------------
   Shared custom tooltip (Phase 4 review round). Blake: "create custom
   tooltips for hovers. Nothing crazy, just small white boxes." One
   `#s360-tooltip` element, positioned/shown via scripts/Stack360Utils.js's
   document-level delegated handlers -- see "Shared Tooltip Mechanism
   (Phase 4 Review)" in Frontend/AI/kb/components.md. z-index 8: above the
   breadcrumb/profile dropdown panel (5) but below every Kiss popup (10+,
   see the z-index note on .s360-dropdown-panel in framework.html).
   --------------------------------------------------------------------- */
.s360-tooltip {
    position: fixed;
    z-index: 8;
    display: none;
    background: var(--s360-surface);
    color: var(--s360-text);
    font-size: 11px;
    line-height: 1.4;
    padding: 4px 8px;
    border: 1px solid var(--s360-border);
    border-radius: 4px;
    box-shadow: var(--s360-shadow-card);
    /* Content-first sizing (tooltip-width review round -- Blake: "needs to be wide enough that
       each [filter] can fit on a single row"). `width: max-content` is load-bearing, not
       decorative: this box is `position: fixed` with `left` set via inline style and `right`
       left auto, and CSS2.1's shrink-to-fit algorithm for an auto-width fixed/absolute box with
       no `width` computes its "available width" as `containingBlockWidth - left` -- NOT as free
       space around the anchor. Every positionTooltip() call in Stack360Utils.js measures
       `offsetWidth` BEFORE recomputing `left` for the current show, so a tooltip anchored near
       the viewport's right edge (a large leftover `left` from a prior/near-edge position) got a
       shrink-to-fit width far narrower than its content or max-width actually allowed --
       confirmed live: a 3-line filter summary collapsed to ~92px content width and wrapped
       mid-filter. `width: max-content` sizes the box to its content FIRST, independent of
       `left`, before position is layered on top in JS. */
    width: max-content;
    max-width: min(560px, calc(100vw - 24px));
    text-align: left;
    /* Phase 6 review -- Blake: multi-line filter summaries, "one filter per line". A
       data-tooltip value containing literal "\n" characters (e.g. Drawer.filterButton()'s own
       summaryFn(), joined with "\n") renders as real line breaks via tip.textContent (preserves
       the characters) + this; ordinary single-line tooltips are unaffected (pre-line only
       changes how explicit newlines/existing whitespace behave, plain text still wraps
       normally at max-width). */
    white-space: pre-line;
    pointer-events: none;
    opacity: 0;
    /* Phase 5 review -- Blake: "dismiss them more quickly when we stop hovering" / "no
       fade-out longer than 60ms" (the show DELAY, ~700ms, is a separate JS timer in
       Stack360Utils.js before this transition even starts; this is only how long the box
       takes to actually fade once shown or hidden). 60ms both ways -- kept symmetric rather
       than split per-direction since a quick fade-IN is harmless and this keeps one rule. */
    transition: opacity 60ms ease;
}

.s360-tooltip.s360-tooltip-visible {
    opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
    .s360-tooltip {
        transition: none;
    }
}

/* =========================================================================
   Phase 5 -- Non-Blocking Loading States (UIModernizationPlan.md)
   Top progress bar, per-button busy spinner, AG Grid / screen-load loading
   skeletons, and the wait dialog's lighter, non-blocking backdrop. Markup
   for the progress bar and the two skeleton shapes is injected/removed by
   scripts/Loading.js and framework.js -- never by a screen. See
   "Non-Blocking Loading States (Phase 5)" in Frontend/AI/kb/architecture.md.
   ========================================================================= */

/* ---- Top progress bar -------------------------------------------------
   Fixed at the very top of the viewport, above everything else (including
   the navbar and every modal) so it reads as one continuous, page-wide
   indicator regardless of what else is on screen. Shown/hidden by adding/
   removing "s360-progress-visible" (scripts/Loading.js); the 150ms
   appear-delay and the fade are both implemented as this class's own
   opacity transition, so a call that finishes before the class is even
   added never renders anything. */
#s360-progress-bar {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: 2px;
    z-index: 20000;
    overflow: hidden;
    opacity: 0;
    pointer-events: none;
    transition: opacity 200ms ease;
}

#s360-progress-bar.s360-progress-visible {
    opacity: 1;
}

#s360-progress-bar::before {
    content: '';
    position: absolute;
    top: 0;
    left: -40%;
    height: 100%;
    width: 40%;
    background: var(--s360-brand);
    animation: s360-progress-slide 1.1s ease-in-out infinite;
}

@keyframes s360-progress-slide {
    0%   { left: -40%; width: 40%; }
    50%  { width: 60%; }
    100% { left: 100%; width: 40%; }
}

@media (prefers-reduced-motion: reduce) {
    #s360-progress-bar::before {
        animation: none;
        left: 0;
        width: 100%;
    }
}

/* ---- Button busy spinner: shared assets ---------------------------------
   Two spinner rings (brand-purple, and white for the two brand-filled icon
   buttons below whose resting icon is already white) plus the transitions
   every icon-turns-into-a-spinner consumer below shares, all as custom
   properties so this is the ONE place prefers-reduced-motion has to touch --
   every consumer just references these, never its own copy. See "Non-
   Blocking Loading States (Phase 5)" / the button-busy-spinner geometry
   review in Frontend/AI/kb/architecture.md for the full design. */
:root {
    --s360-spinner-purple: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='9' fill='none' stroke='%2339285f' stroke-width='3' stroke-linecap='round' stroke-dasharray='42 14'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='0.8s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/svg%3E");
    --s360-spinner-white: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='9' fill='none' stroke='%23ffffff' stroke-width='3' stroke-linecap='round' stroke-dasharray='42 14'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='0.8s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/svg%3E");
    /* Which ring an icon-bearing button uses -- purple everywhere except the
       two brand-filled scopes below, which override this to the white ring. */
    --s360-spinner-icon: var(--s360-spinner-purple);
    --s360-btn-icon-transition: background-size 180ms ease, background-position 180ms ease;
    --s360-icon-btn-transition: opacity 180ms ease, transform 180ms ease;
}

@media (prefers-reduced-motion: reduce) {
    :root {
        /* Same rings, minus each SVG's own <animateTransform> -- a CSS
           `animation` could be disabled with `animation: none`, but the
           motion here is baked into the background image itself, so the
           reduced-motion variant is a second, static copy of each ring.
           Transitions are turned off outright (a static ring, no icon-into-
           spinner morph) rather than merely shortened. */
        --s360-spinner-purple: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='9' fill='none' stroke='%2339285f' stroke-width='3' stroke-linecap='round' stroke-dasharray='42 14'/%3E%3C/svg%3E");
        --s360-spinner-white: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='9' fill='none' stroke='%23ffffff' stroke-width='3' stroke-linecap='round' stroke-dasharray='42 14'/%3E%3C/svg%3E");
        --s360-btn-icon-transition: none;
        --s360-icon-btn-transition: none;
    }
}

/* ---- Button busy spinner: generic (plain, no-icon buttons) --------------
   Added/removed by scripts/Loading.js (refcounted per button -- see
   Loading.buttonCallStart()/buttonCallEnd()), never by a screen. The
   button's own box size is frozen project-wide (Phase 2's rule), so this
   deliberately does NOT touch padding/width/height -- the spinner is drawn
   as a background-image INSIDE the button's existing 12px left padding
   (framework.html's ".kiss input[type=button]" geometry rule) rather than
   widening it. This is the UNCHANGED look for a caption with no icon at all
   (Save/Cancel/Ok/...); every icon-bearing family below (captioned-icon,
   icon-only value/id-keyed, .btn-smart-chooser) overrides it below with a
   spinner sized/positioned to exactly replace ITS OWN icon instead -- see
   "Button Busy Spinner: Icon Families (Phase 5 Review)" in
   Frontend/AI/kb/components.md. cursor:progress (deliberately NOT pointer-
   events:none) -- Utils.suspendDepth already blocks a push-button's own
   click handler while a call is in flight (see kiss/component/pushButton/
   PushButton.js), so a busy button stays a valid Playwright click target
   instead of silently redirecting the click to its parent. */
.kiss input[type=button].s360-busy,
input[type=button].s360-busy {
    cursor: progress;
    color: var(--s360-text-muted);
    background-repeat: no-repeat;
    background-position: 1px center;
    background-size: 10px 10px;
    background-image: var(--s360-spinner-purple);
}

/* ---- Button busy spinner: captioned-icon family (Add/Edit/.../Send) -----
   Same 20 captions as "Captioned Button Icons" above. The icon shrinks to
   0x0 about ITS OWN center (17px = 10px inset + half of 14px) while the
   spinner grows from that same center point -- background-position and
   background-size are animated TOGETHER (see --s360-btn-icon-transition
   above), which is what keeps the visual center fixed at 17px throughout
   the transition instead of the box collapsing toward its left edge. Text
   stays muted, exactly like the generic rule above. No background-color
   override here (unlike the borderless families below) -- these are
   ordinary bordered/backgrounded buttons and keep whatever hover tint a
   plain button of the same caption would already show; only the icon/
   spinner geometry is new. */
.kiss input[type=button][value="Add"].s360-busy,
.kiss input[type=button][value="Edit"].s360-busy,
.kiss input[type=button][value="Delete"].s360-busy,
.kiss input[type=button][value="Remove"].s360-busy,
.kiss input[type=button][value="Filter"].s360-busy,
.kiss input[type=button][value="Filters"].s360-busy,
.kiss input[type=button][value="Report"].s360-busy,
.kiss input[type=button][value="Export"].s360-busy,
.kiss input[type=button][value="Import"].s360-busy,
.kiss input[type=button][value="Print"].s360-busy,
.kiss input[type=button][value="Search"].s360-busy,
.kiss input[type=button][value="Refresh"].s360-busy,
.kiss input[type=button][value="Reset"].s360-busy,
.kiss input[type=button][value="Move Up"].s360-busy,
.kiss input[type=button][value="Move Down"].s360-busy,
.kiss input[type=button][value="View"].s360-busy,
.kiss input[type=button][value="New"].s360-busy,
.kiss input[type=button][value="Reply"].s360-busy,
.kiss input[type=button][value="Handled"].s360-busy,
.kiss input[type=button][value="Send"].s360-busy {
    cursor: progress;
    color: var(--s360-text-muted);
    background-image: var(--s360-spinner-icon), var(--s360-btn-icon);
    background-position: 10px center, 17px center;
    background-size: 14px 14px, 0 0;
}

/* Drawer-footer Search and the active Filter/Filters trigger are brand-
   purple-filled with a WHITE icon (see their own rules further down this
   file) -- their busy spinner must be white to match, and their hover-
   darken (#2c1a4d) must not flicker through while busy (Blake: "a busy
   button must not show the hover tint change flicker"), so background/
   border-color are pinned with !important -- every OTHER captioned-icon
   button above is left to the generic input[type=button]:hover tint, which
   is the same one a plain (no-icon) busy button already keeps unchanged.
   The caption text ALSO can't take the generic captioned-family busy rule's
   var(--s360-text-muted) (a dark gray) here -- on this button's own brand-
   purple fill that reads as nearly unreadable (Blake, review round). A
   translucent white keeps it legible while still reading as "dimmed",
   matching the same white-on-purple the caption already uses at rest. */
.s360-drawer-footer input[type=button][value="Search"].s360-busy,
.s360-filter-btn-wrap input[type=button].s360-filter-btn-active[value="Filter"].s360-busy,
.s360-filter-btn-wrap input[type=button].s360-filter-btn-active[value="Filters"].s360-busy {
    background-color: var(--s360-brand) !important;
    border-color: var(--s360-brand) !important;
    color: rgba(255, 255, 255, 0.7) !important;
}

/* ---- Button busy spinner: icon-ONLY families (borderless) ---------------
   Icon-only value-keyed buttons (the day/week steppers etc.), the ID-keyed
   "?" search/help buttons, and .btn-smart-chooser all center their icon
   with `background-position: center`, so -- unlike the captioned family
   above, which has to animate an explicit 10px/17px offset to keep its
   collapse centered -- centering it here is automatic: a box positioned
   "center" stays centered on the same point as it shrinks/grows, so only
   background-size needs to transition. color/background-color are forced
   transparent with !important: color because the generic rule above would
   otherwise reveal the (normally color:transparent, so invisible) glyph
   character in muted gray for the life of the call; background-color
   because these buttons' own `:hover:not(:disabled)` rule (higher
   specificity than a plain .s360-busy match) would otherwise still paint
   its brand-soft hover tint underneath the spinner while the mouse sits
   over a busy button -- the flicker Blake flagged. */
.kiss input[type=button][value="<"].s360-busy,
.kiss input[type=button][value=">"].s360-busy,
.kiss input[type=button][value="<<"].s360-busy,
.kiss input[type=button][value=">>"].s360-busy,
.kiss input[type=button][value="^"].s360-busy,
.kiss input[type=button][value="v"].s360-busy,
.kiss input[type=button][value="▲"].s360-busy,
.kiss input[type=button][value="▼"].s360-busy,
.kiss input[type=button][value="+"].s360-busy,
.kiss input[type=button][value="-"].s360-busy,
.kiss input[type=button]#search-project.s360-busy,
.kiss input[type=button]#search-worker.s360-busy,
.kiss input[type=button]#search-client.s360-busy,
.kiss input[type=button]#choose-project.s360-busy,
.kiss input[type=button]#do-search-project.s360-busy,
.kiss input[type=button]#searchWorker.s360-busy,
.kiss input[type=button]#searchProject.s360-busy,
.kiss input[type=button]#rp-display-order-help.s360-busy {
    cursor: progress;
    color: transparent !important;
    background-color: transparent !important;
    background-image: var(--s360-spinner-icon), var(--s360-btn-icon);
    background-position: center, center;
    background-size: 14px 14px, 0 0;
}

.kiss input[type=button].btn-smart-chooser.s360-busy {
    cursor: progress;
    background-color: transparent !important;
    background-image: var(--s360-spinner-icon), var(--s360-btn-icon);
    background-position: center, center;
    background-size: 15px 15px, 0 0;
}

/* ---- Button busy spinner: .s360-icon-btn (<button>, not <input>) --------
   Loading.js's document click listener now also captures button.s360-icon-
   btn (see scripts/Loading.js), so this real <button> element -- unlike a
   native input[type=button], which can't host ::before/::after -- gets a
   genuine morph: its own <svg><use> child fades/shrinks away and a ::after
   pseudo-element (the spinner ring, same 16x16 box, absolutely centered)
   fades/grows in over it. */
.s360-icon-btn svg {
    transition: var(--s360-icon-btn-transition);
}

.s360-icon-btn::after {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    width: 16px;
    height: 16px;
    background: var(--s360-spinner-icon) center / 16px 16px no-repeat;
    opacity: 0;
    transform: translate(-50%, -50%) scale(0.4);
    transition: var(--s360-icon-btn-transition);
    pointer-events: none;
}

.s360-icon-btn.s360-busy {
    cursor: progress;
    background: transparent !important;
}

.s360-icon-btn.s360-busy svg {
    opacity: 0;
    transform: scale(0.4);
}

.s360-icon-btn.s360-busy::after {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1);
}

/* ---- Shared shimmer, used by both loading skeletons below -------------- */
.s360-grid-skeleton-bar,
.s360-skel-bar {
    border-radius: 4px;
    background: linear-gradient(90deg, var(--s360-border) 25%, #eef0f3 37%, var(--s360-border) 63%);
    background-size: 400% 100%;
    animation: s360-skel-shimmer 1.2s ease-in-out infinite;
}

@keyframes s360-skel-shimmer {
    0%   { background-position: 100% 50%; }
    100% { background-position: 0 50%; }
}

@media (prefers-reduced-motion: reduce) {
    .s360-grid-skeleton-bar,
    .s360-skel-bar {
        animation: none;
        background: var(--s360-border);
    }
}

/* ---- AG Grid / Grid loading skeleton ------------------------------------
   Overlays ONLY the grid's own body viewport (.ag-body-viewport -- the
   scrollable row area; the header is a separate ancestor sibling and is
   never touched), so it can never change the grid's own size, row
   virtualization, selection, or sorting. Injected/removed entirely by
   scripts/Loading.js's AGGrid.prototype/Grid.prototype extensions -- see
   "Non-Blocking Loading States (Phase 5)" in kb/architecture.md. */
.s360-grid-skeleton {
    position: absolute;
    inset: 0;
    z-index: 1;
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 10px 12px;
    background: var(--s360-surface);
}

.s360-grid-skeleton-row {
    display: flex;
    align-items: center;
    gap: 16px;
    height: 14px;
    flex: 0 0 auto;
}

/* AG Grid's own "No Rows To Show" overlay (.ag-overlay, a sibling of
   .ag-body-viewport inside .ag-root -- always present in the DOM, not
   inserted/removed on demand) would otherwise render underneath the
   skeleton bars above, since the grid genuinely has zero rows for the
   skeleton's own duration (clear() already ran). "s360-skel-active" is
   added to the grid's own container div (never .ag-overlay itself) only
   while the skeleton is shown, by scripts/Loading.js -- AG Grid's
   unmodified logic decides on its own whether to show the overlay again
   the instant this class is removed (visible for a genuine zero-row
   result, hidden the instant real rows exist), so this rule only ever
   suppresses it for the skeleton's own window. */
.s360-skel-active .ag-overlay {
    visibility: hidden;
}

.s360-grid-skeleton-bar {
    height: 10px;
    flex: 0 1 auto;
}

/* ---- Screen-load skeleton ------------------------------------------------
   Shown in #data-pane by framework.js (Loading.showScreenSkeleton()) only
   once a screen navigation has taken longer than 200ms -- a rough outline
   (title bar, filter row, a block of grid-row bars) standing in for the old
   plain "Loading..." text. Always removed by the real screen's own content
   replacing #data-pane's innerHTML wholesale -- no separate hide path. */
.s360-screen-skeleton {
    height: 100%;
    box-sizing: border-box;
    padding: 16px;
    display: flex;
    flex-direction: column;
    gap: 16px;
}

.s360-skel-title {
    height: 22px;
    width: 220px;
}

.s360-skel-filter-row {
    display: flex;
    gap: 10px;
}

.s360-skel-filter {
    height: 24px;
    width: 160px;
}

.s360-skel-filter-btn {
    height: 24px;
    width: 90px;
}

.s360-skel-grid {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    gap: 10px;
    min-height: 0;
}

.s360-skel-row {
    height: 14px;
    flex: 0 0 auto;
}

/* ---- Wait dialog (Utils.waitMessage): non-blocking corner toast (Phase 5
   follow-up A) ----
   Explicit screen calls (file uploads, report generation -- long
   operations, not grid loads) still show this, but it is a small toast
   card docked top-right below the navbar, not a modal: NO backdrop at all
   (fully transparent), no focus steal, the rest of the page stays fully
   usable underneath -- suspendDepth + the button busy state (Phase 5) are
   what actually prevent a double-submit, not this dialog. Two classes
   (".kiss-msg-modal.s360-wait-modal") so none of this ever affects the
   Error/Confirm dialogs (plain ".kiss-msg-modal"), which correctly stay
   fully modal, backdrop included. See "Non-Blocking Loading States (Phase
   5)" in Frontend/AI/kb/architecture.md.

   The outer #wmsg-modal is kept permanently `display: flex` (overriding
   the vendored kiss/Utils.css base rule's `display: none` by specificity --
   two classes beat one) rather than toggled between "none"/something on
   every show/hide: an invisible (opacity: 0), non-interactive
   (pointer-events: none), full-viewport flex container costs nothing, and
   it's what lets Utils.waitMessage()/waitMessageEnd() animate the 120ms
   fade purely by toggling one class ("s360-wmsg-visible") -- the same
   opacity-transition convention already used by #s360-progress-bar and
   .s360-tooltip elsewhere in this file, chosen specifically because a
   `display: none` <-> block/flex toggle cannot be transitioned. flex
   align-items/justify-content on the OUTER container is what docks the
   card horizontally centered below the navbar; the vendored
   .kiss-wmsg-modal-content rule's own `margin: auto` is overridden back to
   0 here because flexbox's own auto-margin behavior on a flex ITEM would
   otherwise center it independently, overriding the container's
   justify-content/align-items entirely (redundant once the container
   itself centers, but still needed to cancel the vendored value).

   Review round (Blake, 2026-09-09): "leaves behind some draggable area" --
   after the toast faded out, its cursor still wanted to drag over that
   screen region, and Preferences/Logout underneath it could not be
   clicked. Root cause: `pointer-events: auto` on .kiss-wmsg-modal-content
   below was UNCONDITIONAL, not scoped to .s360-wmsg-visible -- a
   descendant's own explicit `pointer-events` value overrides an ancestor's
   `pointer-events: none` (this is real CSS behavior, not a bug in the
   ancestor rule), so the card stayed fully interactive-but-invisible at
   its docked position the entire time the page was open, regardless of
   opacity. Fixed two ways at once, per Blake's own "not opacity alone"
   instruction: the WRAPPER also gets `visibility: hidden` at rest (a
   browser never dispatches pointer events to a visibility:hidden element
   OR its descendants, unless a descendant explicitly sets its own
   `visibility: visible` -- this one doesn't), and the content's own
   pointer-events is now explicitly `none` at rest / `auto` only under
   `.s360-wmsg-visible`, so either mechanism alone would already fix this.
   See also Utils.makeDraggable()'s own new visibility guard
   (scripts/Stack360Utils.js) -- belt and suspenders, since a draggable
   header hidden by ANY future means (not just this toast) should never
   start a drag either. */
.kiss-msg-modal.s360-wait-modal {
    display: flex;
    align-items: flex-start;
    justify-content: center;
    padding: calc(var(--s360-navbar-h) + 12px) 12px 0 12px;
    overflow: visible;
    background: transparent;
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition: opacity 120ms ease, visibility 0s linear 120ms;
}

.kiss-msg-modal.s360-wait-modal.s360-wmsg-visible {
    opacity: 1;
    visibility: visible;
    transition: opacity 120ms ease;
}

@media (prefers-reduced-motion: reduce) {
    .kiss-msg-modal.s360-wait-modal {
        transition: none;
    }
}

.kiss-msg-modal.s360-wait-modal .kiss-wmsg-modal-content {
    margin: 0;
    pointer-events: none;
}

.kiss-msg-modal.s360-wait-modal.s360-wmsg-visible .kiss-wmsg-modal-content {
    pointer-events: auto;
}

/* =========================================================================
   Phase 6 -- Filters into a right-side Drawer (UIModernizationPlan.md)
   scripts/Drawer.js builds/maintains this markup entirely; a screen only
   ever writes the declarative `.s360-drawer` source div (see that file's
   own header comment for the full contract). z-index 9: above the page,
   the breadcrumb/profile dropdown (5), and the shared tooltip (8), but
   BELOW every Kiss popup (10+ -- see the z-index note on
   .s360-dropdown-panel in framework.html) so a Smart Chooser or any other
   popup opened from a field inside the drawer still renders on top of it.
   ========================================================================= */

.s360-drawer-backdrop {
    display: none;
    position: fixed;
    inset: 0;
    z-index: 9;
    background: rgba(17, 24, 39, .25);
    opacity: 0;
    transition: opacity 150ms ease;
}

.s360-drawer-backdrop.s360-drawer-open {
    display: block;
}

.s360-drawer-backdrop.s360-drawer-visible {
    opacity: 1;
}

.s360-drawer-panel {
    display: none;
    flex-direction: column;
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    width: 380px;
    max-width: 90vw;
    z-index: 9;
    background: var(--s360-surface);
    border-left: 1px solid var(--s360-border);
    box-shadow: var(--s360-shadow-pop);
    transform: translateX(100%);
    transition: transform 150ms ease;
}

.s360-drawer-panel.s360-drawer-open {
    display: flex;
}

.s360-drawer-panel.s360-drawer-visible {
    transform: translateX(0);
}

@media (prefers-reduced-motion: reduce) {
    .s360-drawer-backdrop, .s360-drawer-panel {
        transition: none;
    }
}

.s360-drawer-header {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    padding: 10px 12px 10px 16px;
    border-bottom: 1px solid var(--s360-border);
}

.s360-drawer-title {
    font-size: 14px;
    font-weight: 600;
    color: var(--s360-text);
}

.s360-drawer-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    padding: 16px;
}

/* scripts/Drawer.js gives the body/footer the "kiss" class too, so moved screen content keeps
   matching every ".kiss input[type=button]"/".kiss select" rule (framework.html) exactly as it
   did in its original position -- see that file's own comment for why. ".kiss"'s base rule
   (framework.html) sets height/padding/position meant for a whole screen's root, none of which
   apply here; this same, more specific selector (two classes beats ".kiss"'s one) neutralizes
   them and restores this file's own padding/flex sizing. */
.s360-drawer-body.kiss,
.s360-drawer-footer.kiss {
    height: auto;
    position: static;
}

.s360-drawer-body.kiss {
    padding: 16px;
}

.s360-drawer-footer.kiss {
    padding: 10px 16px;
}

/* A screen groups its drawer's fields into titled sections with this
   convention -- see "Converting a Screen's Secondary Filters Into a
   Drawer" in Frontend/AI/kb/screens.md. Plain markup, no JS involved. */
.s360-drawer-section + .s360-drawer-section {
    margin-top: 18px;
}

.s360-drawer-section-title {
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .03em;
    color: var(--s360-text-muted);
    margin-bottom: 8px;
}

/* Review round (Blake, 2026-09-09): "we have more vertical space we can fill up. Put labels on
   their own line and put the input fields below the labels." One field per row -- label, 4px
   gap, then the control(s). Text inputs/selects/smart-choosers stretch to the field's own width
   (box-sizing: border-box so the stated width is the full outer box, matching how every other
   drawer/popup control in this app is measured); a radio group stays inline under its label (it
   doesn't benefit from stretching); a from/to date pair is two stacked `.s360-drawer-field`s,
   one per date, each full width; the two label list boxes (Show/Hide Labels) are the one
   deliberate side-by-side exception, in their own `.s360-drawer-field-pair`. */
.s360-drawer-field {
    display: flex;
    flex-direction: column;
    gap: 4px;
    margin-bottom: 12px;
}

.s360-drawer-field:last-child {
    margin-bottom: 0;
}

.s360-drawer-field-label {
    font-size: 12px;
    font-weight: 500;
    color: var(--s360-text-muted);
    /* common.css's own global "label { text-align: right; }" rule (screen-wide default, e.g.
       every "Caption:" label beside a control elsewhere in the app) otherwise wins here too for
       any real <label> element carrying this class -- explicit left override, since a label
       sitting ABOVE its own field (not beside it) reads naturally left-aligned. Confirmed live
       as a real regression (every drawer section title/field label rendered flush-right)
       while building this review round. */
    text-align: left;
    display: block;
}

.s360-drawer-field > select,
.s360-drawer-field > input[type=text],
.s360-drawer-field > input[type=date],
.s360-drawer-field > smart-chooser,
.s360-drawer-field-control {
    width: 100%;
    box-sizing: border-box;
}

.s360-drawer-field-pair {
    display: flex;
    gap: 12px;
    margin-bottom: 12px;
}

.s360-drawer-field-pair:last-child {
    margin-bottom: 0;
}

.s360-drawer-field-pair > .s360-drawer-field {
    flex: 1 1 0;
    margin-bottom: 0;
}

.s360-drawer-footer {
    flex: 0 0 auto;
    display: flex;
    justify-content: flex-end;
    gap: 8px;
    padding: 10px 16px;
    border-top: 1px solid var(--s360-border);
}

.s360-drawer-footer:empty {
    display: none;
}

/* Primary action inside a drawer footer (Apply/Search primary, Reset/Cancel
   stay the ordinary secondary look) -- same convention, same hover/focus/
   disabled treatment and hex values as the Kiss-popup/legacy-modal primary
   button rule above; "OK" reuses that rule verbatim (the caption isn't
   moved into a drawer footer with an icon of its own, so no white-stroke
   variant is needed for it). "Search" IS also in the universal captioned-
   icon family (a brand-purple magnifying glass, unconditionally, on every
   `value="Search"` button app-wide) -- on a primary (brand-purple
   background) button that icon would be invisible, so it gets its own
   white-stroke override here, same specificity as the generic rule
   (.s360-drawer-footer + input[type=button][value=Search] = one class +
   one element + two attributes, matching `.kiss input[type=button]
   [value="Search"]`'s own count) and therefore wins by simply being later
   in the cascade -- see "Icon-caption overlap check" in kb/popups.md for
   why this is the documented way to resolve a caption that's in both
   policies at once. */
.s360-drawer-footer input[type=button][value="OK"],
.s360-drawer-footer input[type=button][value="Ok"],
.s360-drawer-footer input[type=button][value="Save"],
.s360-drawer-footer input[type=button][value="Search"],
.s360-drawer-footer input[type=button][value="Apply"] {
    background-color: var(--s360-brand);
    color: #fff;
    border-color: var(--s360-brand);
}

.s360-drawer-footer input[type=button][value="OK"]:hover:not(:disabled),
.s360-drawer-footer input[type=button][value="Ok"]:hover:not(:disabled),
.s360-drawer-footer input[type=button][value="Save"]:hover:not(:disabled),
.s360-drawer-footer input[type=button][value="Search"]:hover:not(:disabled),
.s360-drawer-footer input[type=button][value="Apply"]:hover:not(:disabled) {
    background-color: #2c1a4d;
    border-color: #2c1a4d;
}

.s360-drawer-footer input[type=button][value="OK"]:focus-visible,
.s360-drawer-footer input[type=button][value="Ok"]:focus-visible,
.s360-drawer-footer input[type=button][value="Save"]:focus-visible,
.s360-drawer-footer input[type=button][value="Search"]:focus-visible,
.s360-drawer-footer input[type=button][value="Apply"]:focus-visible {
    border-color: var(--s360-accent);
    box-shadow: 0 0 0 2px var(--s360-accent);
    outline: none;
}

.s360-drawer-footer input[type=button][value="OK"]:disabled,
.s360-drawer-footer input[type=button][value="Ok"]:disabled,
.s360-drawer-footer input[type=button][value="Save"]:disabled,
.s360-drawer-footer input[type=button][value="Search"]:disabled,
.s360-drawer-footer input[type=button][value="Apply"]:disabled {
    opacity: 0.6;
    cursor: default;
}

.s360-drawer-footer input[type=button][value="Search"] {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m21 21-4.34-4.34'/%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3C/svg%3E");
    /* White icon on a brand-purple background needs a white spinner too --
       see "Button busy spinner: shared assets" further down this file. */
    --s360-spinner-icon: var(--s360-spinner-white);
}

.s360-drawer-footer input[type=button][value="Search"]:disabled {
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' opacity='0.7'%3E%3Cpath d='m21 21-4.34-4.34'/%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3C/svg%3E");
}

/* Filters trigger button + active-filter-count badge (Drawer.filterButton()
   in scripts/Drawer.js). The wrapper is an inline-block span the button's
   own layout flows through unchanged; the badge is absolutely positioned
   at its top-right corner and never affects box size (frozen-box-size
   rule) -- purely decorative, `pointer-events: none` so it can never
   intercept a click meant for the button beneath it. */
.s360-filter-btn-wrap {
    position: relative;
    display: inline-block;
}

.s360-filter-btn-wrap .s360-filter-badge {
    position: absolute;
    top: -8px;
    right: -8px;
    min-height: 18px;
    padding: 0 5px;
    font-size: 10px;
    /* Review round -- Blake: "The tooltip for active filters should work when hovering on the
       badge as well." Was pointer-events: none (purely decorative, clicks fall through to
       whatever's behind); re-enabled so the badge's own data-tooltip (Drawer.filterButton(),
       kept in sync with the button's) can actually be hovered -- safe to do since the badge is
       positioned OUTSIDE the button's own box (this same top/right corner offset) and so never
       overlaps its clickable area regardless. */
    box-shadow: 0 0 0 2px var(--s360-surface), 0 0 0 3px var(--kiss-badge-ring, var(--kiss-ui-ring));
}

/* Review round -- Blake: "If the filter is active, show the button as active/primary like it's
   pressed." Drawer.filterButton() toggles this class (on both the button and its wrap) once
   countFn() > 0. Reuses the exact primary-button colors/states already established for popup/
   drawer-footer OK-type buttons (see "Primary Popup Action Button" in kb/popups.md) -- box size
   untouched, per the frozen-box-size rule. Both captions this app actually uses for a Filters
   trigger ("Filter" on Messages, "Filters" on the other two pilots) are covered so this class
   works on any of them. */
.s360-filter-btn-wrap input[type=button].s360-filter-btn-active[value="Filter"],
.s360-filter-btn-wrap input[type=button].s360-filter-btn-active[value="Filters"] {
    background-color: var(--s360-brand);
    border-color: var(--s360-brand);
    color: #fff;
    --s360-btn-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 20a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341L21.74 4.67A1 1 0 0 0 21 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14z'/%3E%3C/svg%3E");
    /* White icon on a brand-purple background needs a white spinner too --
       see "Button busy spinner: shared assets" further down this file. */
    --s360-spinner-icon: var(--s360-spinner-white);
}

.s360-filter-btn-wrap input[type=button].s360-filter-btn-active[value="Filter"]:hover:not(:disabled),
.s360-filter-btn-wrap input[type=button].s360-filter-btn-active[value="Filters"]:hover:not(:disabled) {
    background-color: #2c1a4d;
    border-color: #2c1a4d;
}

/* Badge, active state: "the badge stays and reads in the brand color on white" -- white pill
   with brand-purple text, instead of the default accent (light blue) look, so it reads clearly
   against the now brand-purple button behind it. */
.s360-filter-btn-wrap.s360-filter-btn-active .s360-filter-badge {
    background: #fff;
    color: var(--s360-brand);
    box-shadow: 0 0 0 2px var(--s360-surface), 0 0 0 3px var(--s360-brand);
}

/* kiss/Utils.css's ".kiss-badge" rule sets its own `display: inline-flex` -- an AUTHOR-origin
   declaration, which beats the user-agent stylesheet's default `[hidden] { display: none }`
   regardless of specificity (author normal always outranks user-agent normal in the cascade).
   Drawer.filterButton() hides the badge via the `hidden` IDL property/attribute (the same
   convention DOMUtils.show()/hide() use everywhere else in this codebase), so this rule -- a
   higher-specificity AUTHOR rule of its own -- is what actually makes `hidden` win back. */
.s360-filter-badge[hidden] {
    display: none;
}

/* scripts/Loading.js's Loading.installSoftGridColors() -- the soft chip a colored grid CELL's
   text gets wrapped in (see "Soft Grid Cell/Row Colors" in kb/components.md for the full
   mechanism). Small, inline-block, padding tuned to add only ~4px total vertical space (2px top
   + 2px bottom) well inside a normal ~27-28px AG Grid row's existing headroom, so wrapping never
   grows the row -- centered on the word by ordinary inline layout regardless of the column's own
   text-align, since the chip is the ONLY content left in the cell once Loading.js replaces
   whatever produced the original color. A colored ROW keeps the simpler flat-background
   treatment (no chip) -- see the JS. */
/* Phase 7 (Optimistic UI, scripts/Optimistic.js): a row an Optimistic.run()
   add/update has applied to the grid while its background REST/SOAP call is
   still in flight (row.data._pending === true) renders muted -- the ONLY
   visual cue the pattern uses; no skeleton, no spinner on the row itself.
   Applied via AGGrid.prototype.show()'s patched gridOptions.getRowClass
   (Optimistic.installPendingRowClass()), so it tracks ag-grid's own
   virtualized row recycling correctly, unlike a one-time DOM class stamp.
   See "Optimistic UI Helper (Phase 7)" in kb/components.md. */
.s360-row-pending {
    opacity: .6;
}

.s360-cell-chip {
    display: inline-block;
    padding: 2px 8px;
    border-radius: 6px;
    line-height: 1.3;
}
