/* ═══════════════════════════════════════════════════════════════════
   WerkWyse site CSS — small overrides on top of the compiled theme
   (css/werkwyse-bootstrap.css). Loaded after the theme and vendor CSS.
   ═══════════════════════════════════════════════════════════════════ */

/* Column-aligned digits everywhere (tables, totals, rates) */
body {
    font-variant-numeric: tabular-nums;
}

/* Tab hover: fill the full border box instead of Bootstrap's default
   transparent bottom, which looked broken against the tab content */
.nav-tabs {
    --bs-nav-tabs-link-hover-border-color: var(--bs-border-color) var(--bs-border-color) var(--bs-border-color);
}

/* #47 dashboard: Chart.js needs an explicit-height wrapper for
   responsive resizing (maintainAspectRatio: false in werkwyse-ui.js). */
.chart-wrap {
    position: relative;
    height: 280px;
}

/* #67 chart palette — VARIANT B (Firdaus's demo review): the stock
   Chart.js default palette (docs/latest/general/colors) everywhere —
   blue/red/teal/orange/purple/yellow for pie slices, blue + red for the
   two-series bars (chargeable/fee = blue, nonchargeable/cost = red),
   blue for single-series bars. SAME hexes in both themes (the stock
   experience — no dark tints); the palette's own grey stays RESERVED
   for the Unassigned (NULL FK) slice. Consumed by js/werkwyse-ui.js via
   getComputedStyle; the MutationObserver rebuild picks values up on
   theme flip. --bs-primary/--bs-secondary are deliberately NOT these
   (theme chrome stays untinted). */
:root {
    --ww-chart-1: #36a2eb; /* chart.js blue */
    --ww-chart-2: #ff6384; /* chart.js red */
    --ww-chart-3: #4bc0c0; /* chart.js teal */
    --ww-chart-4: #ff9f40; /* chart.js orange */
    --ww-chart-5: #9966ff; /* chart.js purple */
    --ww-chart-6: #ffcd56; /* chart.js yellow */
    --ww-chart-unassigned: #c9cbcf;
    /* folded sub-1% named slices — a darker grey than Unassigned: Other
       IS real data, Unassigned is a missing FK. */
    --ww-chart-other: #6b7280;
    --ww-chart-chargeable: #36a2eb;
    --ww-chart-nonchargeable: #ff6384;
}

[data-bs-theme="dark"] {
    --ww-chart-1: #36a2eb;
    --ww-chart-2: #ff6384;
    --ww-chart-3: #4bc0c0;
    --ww-chart-4: #ff9f40;
    --ww-chart-5: #9966ff;
    --ww-chart-6: #ffcd56;
    --ww-chart-unassigned: #c9cbcf;
    --ww-chart-other: #6b7280;
    --ww-chart-chargeable: #36a2eb;
    --ww-chart-nonchargeable: #ff6384;
}

/* #70 chart-color badges — a CATEGORICAL badge set on the same
   --ww-chart-* vars the charts read, so a category badge and its pie
   slice/bar share one hex by construction (can't drift apart). Dark
   text on all seven: every chart hex fails white-text WCAG, and the
   vars are theme-invariant so one ruleset covers both color modes
   (purple is the marginal one at 4.19:1 — avoid it for high-traffic
   categories). Grammar: categories/tags only, NEVER status — success/
   warning/danger stay semantic (text-bg-*); grey stays reserved for
   "unassigned/other/none", matching its chart meaning. */
.text-bg-chart-blue    { background-color: var(--ww-chart-1); color: #212529; }
.text-bg-chart-red     { background-color: var(--ww-chart-2); color: #212529; }
.text-bg-chart-teal    { background-color: var(--ww-chart-3); color: #212529; }
.text-bg-chart-orange  { background-color: var(--ww-chart-4); color: #212529; }
.text-bg-chart-purple  { background-color: var(--ww-chart-5); color: #212529; }
.text-bg-chart-yellow  { background-color: var(--ww-chart-6); color: #212529; }
.text-bg-chart-grey    { background-color: var(--ww-chart-unassigned); color: #212529; }

/* #48: cap long flush lists (the unused-activities card) at chart height. */
.list-scroll {
    max-height: 280px;
    overflow-y: auto;
}

/* Dashboard grid rhythm (demo feedback 2026-08-01): the grid ROWS own
   the vertical spacing (g-4 mb-4 gutters) — cards inside row columns
   carry NO bottom margin. Mechanism ruling (2026-10-01): c-card declares
   `<c-vars class="mb-4" />` and the dashboard cards pass class="mb-0" —
   HTML's first-class-attribute-wins rule replaces the default (a
   declared c-var is consumed, not forwarded). Card mb-4 + row gutter
   doubled the gap (48px next to the rhythm's 24px).

   Equal-height rows (#66): every in-row card fills its stretched grid
   column, so the row's cards — and their footers, via the stock flex
   card's growing .card-body — level out. height:100% is only safe
   BECAUSE the card margin is zero: with mb-4 in play, a 100%-high card
   in a stretched column absorbs its own margin and overflows the row
   (which is why Bootstrap's $card-height:100% Sass default was
   rejected — house-wide it would hit every detail page's c-card). The
   margin used to be zeroed by an !important rule in this block; the
   ruling moved it to the markup, so the rule carries ONLY the height.
   h-100 can't simply be added either: with `class` now a declared c-var,
   a caller's class replaces the whole default (class="mb-0 h-100" would
   work, but every caller would have to repeat it) — this scoped rule is
   the seam. */
.dashboard-grid .row > [class*="col"] > .card {
    height: 100%;
}

/* Stretched-row fill (Firdaus 2026-08-21, post-#69 demo review): the
   #66 rule stretches the card, Bootstrap's flex card grows the
   .card-body — but a chart-wrap keeps its declared height and leaves a
   dead band below the canvas (most visible: Firm hours next to the tall
   Completion card). Let the wrap grow with the body: only bodies whose
   direct child is a chart-wrap become flex columns (stat cards and
   empty-state bodies untouched); the wrap's declared height (280px
   default, or the adaptive inline height) stays the floor via
   flex-basis — nothing shrinks. Hand-rolled cards (no .card-body:
   flush lists/tables) instead pin their footer with margin-top:auto —
   on c-cards that's a no-op because the grown body already consumes
   the free space. */
.dashboard-grid .card-body:has(> .chart-wrap) {
    display: flex;
    flex-direction: column;
}

.dashboard-grid .card-body:has(> .chart-wrap) > .chart-wrap {
    flex: 1 1 auto;
}

.dashboard-grid .card > .card-footer {
    margin-top: auto;
}

/* Completion banner progress track (demo feedback 2026-08-01): the
   theme's card bg == secondary-bg in BOTH modes, so the stock track
   (var(--bs-secondary-bg)) was invisible on the card. The palette's
   secondary stone at 35% alpha reads as a neutral rail in both modes —
   darker on the light card, lighter on the dark one — without a second
   "data" color fighting the teal fill (Firdaus-approved mock). */
.dashboard-grid .progress {
    background-color: rgba(120, 113, 108, 0.35);
}

/* Row headers (#90 Part 2, §2.4 — urus anatomy): icon (primary accent)
   + full-strength title directly above a logical card group (2+ cards
   that are one idea — personal: Work days, Annual leave); a row of
   unrelated topics gets none. The h2 keeps the document outline honest;
   the 8px margin-bottom (against the 24px row rhythm above it) binds
   the header to the group it names. The anatomy also carries urus's
   optional subtitle line and right-slot controls — unused today. */
.dashboard-grid .dashboard-row-header {
    font-size: 0.875rem;
    font-weight: 600;
    margin-bottom: 0.5rem;
}

.dashboard-grid .dashboard-row-header > i {
    color: var(--bs-primary);
}

/* ═══════════════════════════════════════════════════════════════════
   GLOBAL HTMX PROGRESS BAR (#htmx_progress)
   Thin top-of-page bar (YouTube/NProgress style) shown during HTMX
   requests. Created and driven by js/werkwyse-ui.js (#115: counted at
   htmx:beforeSend → trickle, completed on htmx:afterRequest → done +
   fade), with a ~120ms delay-to-show so fast swaps stay flicker-free.
   Port of urus-web's mechanism (urus.css / urus-ui.js).

   #115 defense-in-depth: visibility rides on htmx's own .htmx-request
   class — htmx strips that class from history snapshots, so a restored
   (dirty-snapshot) bar can never render even before JS removes it. The
   ungated rule must not come back. The done-fade rule below is
   untouched: done only applies once the tracker is empty, i.e. no
   .htmx-request remains in the body, so the fade wins exactly as before.

   Orange (not brand teal): contrasts with both the teal accents and
   the neutral chrome in either mode.
   ═══════════════════════════════════════════════════════════════════ */
#htmx_progress {
    position: fixed;
    top: 0;
    left: 0;
    width: 0;
    height: 3px;
    background: var(--bs-orange, #fd7e14);
    box-shadow: 0 0 8px var(--bs-orange, #fd7e14);
    z-index: 9999;
    opacity: 0;
    pointer-events: none;
    transition: width 0.2s ease;
}
body:has(.htmx-request) #htmx_progress.htmx-progress-active {
    opacity: 1;
}
#htmx_progress.htmx-progress-done {
    opacity: 0;
    transition: width 0.2s ease, opacity 0.3s ease 0.1s;
}

/* ═══════════════════════════════════════════════════════════════════
   SHELL (#26) — urus layout port: viewport-locked app shell with a
   200px sectioned sidebar (#149 density arc, 2026-10-07 — was 240px);
   #id_content is the only content scroll region (#176: it holds the
   banner + the feedback box + #id_main — the topbar stays pinned).
   Rule blocks ported from urus-web's urus.css; all colors via
   var(--bs-*) theme tokens (no new tokens). Drawer/backdrop/active-sync
   behavior lives in js/werkwyse-ui.js.
   ═══════════════════════════════════════════════════════════════════ */
:root {
    --sidebar-width: 200px;
}

.werkwyse-sidebar {
    width: var(--sidebar-width);
    flex-shrink: 0;
    display: flex;
    flex-direction: column;
    background: var(--bs-secondary-bg);
    border-right: 1px solid var(--bs-border-color);
    transition: margin-left 0.3s ease, transform 0.3s ease;
}

.werkwyse-content-wrapper {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

/* Content scroll region — the page itself never scrolls. #176: the
   region moved up off #id_main so the banner and the feedback box
   scroll with the content while the topbar stays pinned. */
#id_content {
    flex: 1;
    overflow-y: auto;
}

/* #37 item 5 (urus parity): wider horizontal padding around the content
   cards than Bootstrap's 0.75rem container default. #149: the content
   column rides container-fluid (the old capped container class is gone
   house-wide), the house padding follows the class rename. */
#id_main .container-fluid {
    padding-left: 1.5rem;
    padding-right: 1.5rem;
}

/* #31 toasts/flashes: the channel box lives outside #id_main, so the card
   inset rule doesn't reach it — align it explicitly. (The alert margins
   the old note described now live in the shared .ww-alert rule set
   below.) */
#alert_container {
    padding-left: 1.5rem;
    padding-right: 1.5rem;
}

/* #168: the banner renders from the shell (above the alert box), where
   the #id_main .container-fluid inset rule doesn't reach it — it carries
   the same house content inset so the announcement stays inside the
   content column's left/right lines. */
#whats_new_banner {
    padding-left: 1.5rem;
    padding-right: 1.5rem;
}

/* #176: the ONE alert anatomy — toasts, session flashes and the
   What's-new banner all ride html/cotton/alert.html, and this is the
   shared rule set: flex row, the icon slot, the text fill, the close
   placement (Bootstrap's .alert-dismissible would absolutely position
   the close; the shared anatomy keeps it in the row) and the COMPACT
   vertical padding (Firdaus's eyeball ruling 2026-10-14 — the stock
   1rem top/bottom read too airy, the banner's old py-2 bar is the
   house rhythm; source order beats .alert at equal specificity).
   #184: the row is TOP-aligned so the icon and close pin to line 1 on
   wrapped messages, and the horizontal padding is set here too — the
   longhand beats .alert-dismissible's hidden 3rem right reserve by
   source order (equal specificity, this file loads after the bootstrap
   build). First alert in a box keeps the old mt-4 gap; stacked alerts
   keep a 1rem gap between themselves. */
.ww-alert {
    display: flex;
    align-items: flex-start;
    gap: 0.5rem;
    margin-top: 1.5rem;
    margin-bottom: 0;
    padding-top: 0.5rem;
    padding-right: 1rem;
    padding-bottom: 0.5rem;
    padding-left: 1rem;
}
.ww-alert + .ww-alert {
    margin-top: 1rem;
}
.ww-alert-text {
    flex: 1 1 auto;
}
.ww-alert > i {
    /* FA sets line-height 1 on the host — without the override the glyph
       box (14px) reads misaligned against the 21px text line once the
       row stops centering. */
    line-height: 1.5;
}
.ww-alert .btn-close {
    position: static;
    /* The 0.25em padding gives a 21px box — exactly the text line, so a
       one-line alert stays one line tall. Neutralizes the .alert-dismissible
       leftover (1.25rem 1rem) #176's position reset left behind. */
    padding: 0.25em;
}

/* Mobile (< 992px): sidebar becomes a fixed drawer over a backdrop.
   100dvh accounts for the mobile browser URL bar so the pinned
   utilities bar doesn't clip; 100vh fallback for older engines. */
@media (max-width: 991.98px) {
    .werkwyse-sidebar {
        position: fixed;
        top: 0;
        left: 0;
        height: 100vh;
        height: 100dvh;
        z-index: 1040;
        transform: translateX(-100%);
    }
    .werkwyse-sidebar.show {
        transform: translateX(0);
    }
    #sidebar_backdrop {
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background: rgba(0, 0, 0, 0.5);
        z-index: 1035;
        display: none;
    }
    #sidebar_backdrop.show {
        display: block;
    }
}

/* ═══════════════════════════════════════════════════════════════════
   TOPBAR (#27) — two-row bar ported from urus.css.
   Row 1: hamburger + page title (61px, matches the sidebar brand band)
   Row 2: hoisted contextual tabs (nav-underline, 3px active)
   ═══════════════════════════════════════════════════════════════════ */
#topbar {
    background: var(--bs-secondary-bg);
    flex-shrink: 0;
}
#topbar_row1 {
    display: flex;
    align-items: center;
    min-height: 61px;
    border-bottom: 1px solid var(--bs-border-color);
    padding: 0 1.5rem; /* #37 item 5: match the wider content padding */
    gap: 0.5rem;
}
#topbar_row1 > #topbar_title {
    min-width: 0; /* allow flex truncation — text-truncate needs this */
}

/* #40 (Bootstrap-breadcrumb amendment): the row-1 crumb rides the
   breadcrumb component. The divider is the docs' SVG chevron via the
   component's own CSS var — the #6c757d fill reads on both themes (an
   SVG fill can't follow --bs-breadcrumb-divider-color). Bootstrap
   mutes the ACTIVE item — the inverse of our muted-prefix grammar — so
   it is set back to body color; the h1 inside it must not inflate to
   UA heading styles. */
#topbar_title {
    --bs-breadcrumb-divider: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Cpath d='M2.5 0L1 1.5 3.5 4 1 6.5 2.5 8l4-4-4-4z' fill='%236c757d'/%3E%3C/svg%3E");
}
#topbar_title .breadcrumb-item.active {
    color: var(--bs-body-color);
}
.topbar-crumb-current {
    font: inherit;
    margin: 0;
}

/* Bootstrap ships no min-width utility — the crumb's truncation chain
   (nav → ol → active li) pins it explicitly (utilities convention:
   !important). */
.min-w-0 {
    min-width: 0 !important;
}
#topbar_row2 {
    padding: 0 1.5rem; /* #37 item 5: match the wider content padding */
}

/* nav-underline: primary = selected (matches the sidebar convention) */
.nav-underline {
    --bs-nav-underline-link-active-color: var(--bs-link-color);
}
.nav-underline .nav-link {
    color: var(--bs-body-color);
    padding: 0.75rem 1rem;
}
.nav-underline .nav-link:hover:not(.active) {
    background: var(--bs-tertiary-bg);
    border-bottom-color: var(--bs-border-color);
}
#subnav {
    --bs-nav-underline-border-width: 3px;
    scrollbar-width: none;
}
#subnav::-webkit-scrollbar {
    display: none;
}
#subnav .nav-link.active {
    font-weight: 600;
}
/* #37 (item 4): tab label + count badge never wrap onto two lines */
#subnav .nav-link {
    white-space: nowrap;
}

/* Row-2 right-edge fade on narrow screens: hints at horizontal scroll
   when tabs overflow (mask on the nav itself; invisible when no overflow) */
@media (max-width: 767.98px) {
    #subnav {
        mask-image: linear-gradient(to right, #000 calc(100% - 2rem), transparent 100%);
        -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 2rem), transparent 100%);
    }
}

/* Desktop: sidebar collapses via negative margin (topbar hamburger) */
@media (min-width: 992px) {
    .werkwyse-sidebar.collapsed {
        margin-left: calc(-1 * var(--sidebar-width));
    }
}

/* Sidebar section group headers (small caps) */
.sidebar-heading {
    font-size: 0.6875rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--bs-secondary-color);
    padding: 0.75rem 0.75rem 0.25rem;
    margin-top: 1rem;
    margin-bottom: 0;
}
.sidebar-heading:first-child {
    margin-top: 0;
}

/* ═══════════════════════════════════════════════════════════════════
   BRAND MARK — the wordmark is a generated inline SVG
   (html/cotton/brand_mark.html, scripts/build-brandmark.py from the
   vendored Dune Rise outlines): tight viewBox centers exactly, no font
   metrics; fill/stroke currentColor flips with data-bs-theme like FA.
   ═══════════════════════════════════════════════════════════════════ */
.brand-mark {
    display: block;
    margin: 0 auto;
    height: auto;
}
.brand-mark-sidebar {
    width: 10rem;
}
.brand-mark-auth {
    width: min(100%, 26rem);
}

/* Teal 61px brand band (matches urus; works in both color modes).
   #37 item 1: centered vertically AND horizontally. 2026-08-03: the
   font-based wordmark (letterspacing could never center exactly)
   became the SVG brand mark above. */
#sidebar_brand {
    min-height: 61px;
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: var(--bs-primary);
    border-bottom: 1px solid var(--bs-border-color);
}

/* Primary nav — teal text + 3px left indicator when active */
#sidebar_nav .nav-link {
    color: var(--bs-body-color);
    border-left: 3px solid transparent;
    border-radius: 0;
    padding-left: 0.75rem;
    padding-right: 0.75rem;
}
#sidebar_nav .nav-link.active {
    color: var(--bs-link-color);
    font-weight: 600;
    border-left-color: currentcolor;
}
#sidebar_nav .nav-link:hover:not(.active) {
    background: var(--bs-tertiary-bg);
}

/* Utilities bar (user, notifications placeholder, logout) — pinned bottom
   via mt-auto; the theme toggle moved to the topbar (#37 items 2+7) */
#sidebar_utilities {
    min-height: 57px;
}
#sidebar_utilities .flex-fill {
    min-width: 0; /* let text-truncate shrink the user name in the flex row */
}
#sidebar_utilities .nav-link {
    color: var(--bs-body-color);
}
#sidebar_utilities .nav-link:hover {
    background: var(--bs-tertiary-bg);
}

/* Sidebar nav scrolls internally; brand + utilities stay put */
.sidebar-nav-scroll {
    flex: 1;
    overflow-y: auto;
}

/* ═══════════════════════════════════════════════════════════════════
   GLOBAL SEARCH (#36) — topbar search input + results dropdown.
   Ported from urus-web (static/css/urus.css:919-1018); all colors via
   var(--bs-*) theme tokens, so dark mode holds by construction. The
   htmx-indicator display rules are the first in the project — the
   spinner is wired via the input's hx-indicator, so the request class
   lands on .global-search-spinner (not on the input itself).
   ═══════════════════════════════════════════════════════════════════ */

/* --- Wrapper: positions the icon, spinner, and close button over the input --- */
.global-search-wrapper {
    width: 260px;
}
#global_search_mobile.global-search-wrapper {
    width: auto; /* mobile variant grows to fill topbar row 1 (flex-grow-1) */
}

/* --- Input: padded left for icon, right for spinner --- */
.global-search-input {
    padding-left: 2.25rem !important;
    padding-right: 2rem !important;
    background-color: var(--bs-body-bg) !important;
}
/* Remove the browser-native clear (×) button on type="search" */
.global-search-input::-webkit-search-cancel-button {
    -webkit-appearance: none;
}

/* --- Magnifying glass icon (left) --- */
.global-search-icon {
    position: absolute;
    left: 0.75rem;
    top: 50%;
    transform: translateY(-50%);
    color: var(--bs-secondary-color);
    font-size: 0.875rem;
    pointer-events: none;
    z-index: 4;
}

/* --- HTMX spinner (right) — htmx adds .htmx-request to the hx-indicator
       target (this span) for the request's lifetime --- */
.global-search-spinner {
    position: absolute;
    right: 0.5rem;
    top: 50%;
    transform: translateY(-50%);
    z-index: 4;
}
.global-search-spinner .htmx-indicator {
    display: none;
}
.global-search-spinner.htmx-request .htmx-indicator {
    display: inline-block;
}
.global-search-spinner .spinner-border {
    width: 0.75rem;
    height: 0.75rem;
}

/* --- Mobile close button (right, after spinner position) --- */
.global-search-close {
    position: absolute;
    right: 0.25rem;
    top: 50%;
    transform: translateY(-50%);
    z-index: 4;
    background: none;
    border: none;
    padding: 0.25rem;
    color: var(--bs-secondary-color);
    line-height: 1;
    cursor: pointer;
}
.global-search-close:hover {
    color: var(--bs-body-color);
}

/* --- Results dropdown (hidden until the JS sync shows it) --- */
.global-search-dropdown {
    display: none;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    margin-top: 0.25rem;
    background: var(--bs-body-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius);
    box-shadow: var(--bs-box-shadow-sm);
    max-height: 400px;
    overflow-y: auto;
    z-index: 1050;
    scrollbar-width: thin;
}

.global-search-group-header {
    font-size: 0.7rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    padding: 0.5rem 0.75rem 0.25rem;
    color: var(--bs-secondary-color);
    border-bottom: 1px solid var(--bs-border-color-translucent);
}

.global-search-item {
    display: flex;
    flex-direction: column;
    padding: 0.5rem 0.75rem;
    text-decoration: none;
    color: var(--bs-body-color);
    border-bottom: 1px solid var(--bs-border-color-translucent);
}
.global-search-item:last-child {
    border-bottom: none;
}
.global-search-item:hover {
    background: var(--bs-tertiary-bg);
    color: var(--bs-body-color);
}

/* ═══════════════════════════════════════════════════════════════════
   CONTENT SLICE 1 (#28) — urus table hierarchy + info tip.
   Rule blocks ported from urus-web's urus.css; all colors via
   var(--bs-*) theme tokens. Applies to tables inside the elevation
   cards (base_list.html, compliance_report.html) and globally to
   thead (e.g. the calendar grid).
   ═══════════════════════════════════════════════════════════════════ */

/* Table visual hierarchy (inside cards on secondary-bg):
   thead (tertiary-bg) > group header (subtle tint) > body rows (card bg)
   Column headers don't wrap — long headers scroll via .table-responsive. */
.table > thead {
    --bs-table-bg: var(--bs-tertiary-bg);
}
.table > thead th {
    white-space: nowrap;
}

/* Group header rows — subtle tint, less prominent than thead.
   Uses emphasis-color-rgb (black in light, white in dark) for auto-adaptation.
   Bootstrap's compile-time .table-secondary doesn't adapt to dark mode
   (Bootstrap issue #39272), so we override for both modes. */
.table-secondary,
.table-light {
    --bs-table-color: var(--bs-body-color);
    --bs-table-bg: rgba(var(--bs-emphasis-color-rgb), 0.06);
    --bs-table-border-color: var(--bs-border-color);
    --bs-table-hover-color: var(--bs-body-color);
    --bs-table-hover-bg: rgba(var(--bs-emphasis-color-rgb), 0.08);
}

/* Tables in cards: uniform cell padding (Firdaus's playground ruling
   2026-10-10, variant "d"): 8px vertical / 12px horizontal on EVERY cell —
   no first/last-column card-edge, no actions exemption. The old split
   (1rem first column, .5rem elsewhere) read as left-heavy. */
.card .table th,
.card .table td {
    padding: 0.5rem 0.75rem;
}

/* Tables in cards: prevent double bottom border when no card-footer */
.card.overflow-hidden .table-responsive:last-child .table > :last-child > tr:last-child > * {
    border-bottom-width: 0;
}

/* Select-all columns (#139 — daysheet copy): the checkbox column hugs
   its content; width:1% lets the remaining columns claim the free space. */
.ww-select-all-col {
    width: 1%;
}

/* Table header — sortable columns use body color, not primary. The
   :not(.btn) guard is the settlement-review black-icon fix (ruling
   note_3975409870): the unguarded rule bled body color onto
   a.btn.btn-primary in the thead — the + Add / picker Choose icons
   rendered black on teal (light theme). */
#id_table thead a:not(.btn) {
    color: var(--bs-body-color);
}
#id_table thead a:not(.btn):hover {
    color: var(--bs-body-color);
}

/* <c-info-tip> metric-definition hover icon (ⓘ) — slightly smaller than
   the label text so it reads as an adornment */
.urus-info-tip {
    cursor: help;
    font-size: 0.8em;
}

/* ═══════════════════════════════════════════════════════════════════
   MODAL DIALOGS (#32) — <dialog class="werkwyse-modal">
   Native <dialog> instead of Bootstrap Modal JS; the outer dialog is
   transparent while Bootstrap's .modal-dialog / .modal-content /
   .modal-header / etc. classes still style the inner content. The
   browser handles focus trap, Escape, and top-layer stacking.

   Bootstrap's .modal-header/.modal-body/.modal-footer read --bs-modal-*
   custom properties that are ONLY defined inside `.modal`. Since we use
   <dialog>, we redeclare the same vars here so the inner padding /
   border / radius render correctly. The header bottom / footer top
   borders ride gray-500, not the stock border-color (#dee2e6 was
   imperceptible on the white content — the eyeball round read the
   controls as floating outside the dialog).

   Ported wholesale from urus-web (static/css/urus.css:1063-1109), class
   renamed urus-modal → werkwyse-modal.
   ═══════════════════════════════════════════════════════════════════ */
dialog.werkwyse-modal {
    padding: 0;
    border: 0;
    background: transparent;
    color: inherit;
    max-width: 100vw;
    max-height: 100vh;
    margin: auto;
    overflow: visible;

    --bs-modal-zindex: 1055;
    --bs-modal-width: 500px;
    --bs-modal-padding: 1rem;
    --bs-modal-margin: 0.5rem;
    --bs-modal-color: var(--bs-body-color);
    --bs-modal-bg: var(--bs-body-bg);
    --bs-modal-border-color: var(--bs-border-color-translucent);
    --bs-modal-border-width: var(--bs-border-width);
    --bs-modal-border-radius: var(--bs-border-radius-lg);
    --bs-modal-box-shadow: var(--bs-box-shadow-sm);
    --bs-modal-inner-border-radius: calc(var(--bs-border-radius-lg) - (var(--bs-border-width)));
    --bs-modal-header-padding-x: 1rem;
    --bs-modal-header-padding-y: 1rem;
    --bs-modal-header-padding: 1rem 1rem;
    --bs-modal-header-border-color: var(--bs-gray-500);
    --bs-modal-header-border-width: var(--bs-border-width);
    --bs-modal-title-line-height: 1.5;
    --bs-modal-footer-gap: 0.5rem;
    --bs-modal-footer-bg: ;
    --bs-modal-footer-border-color: var(--bs-gray-500);
    --bs-modal-footer-border-width: var(--bs-border-width);
}
/* #177 playground find (Firdaus 2026-10-14): gray-500 is a fixed LIGHT
   grey — perfect on the light dialog, glaring against the dark zinc
   surface — so dark rides gray-600. Light is byte-unchanged. */
[data-bs-theme="dark"] dialog.werkwyse-modal {
    --bs-modal-header-border-color: var(--bs-gray-600);
    --bs-modal-footer-border-color: var(--bs-gray-600);
}
/* The backdrop rides Bootstrap's standard 0.5 dim with no blur — the
   page behind the dialog stays readable. */
dialog.werkwyse-modal::backdrop {
    background: rgba(0, 0, 0, 0.5);
}

/* #86 polish: every house dialog scrolls its BODY, viewport-bounded —
   header/footer stay pinned. The app shell's body is viewport-locked, so
   without this a long dialog's content clips with nothing to scroll
   (there is no page behind to scroll either). Stock
   .modal-dialog-scrollable's height math assumes .modal's fixed
   positioning, which this <dialog> port lacks — scope the rule to the
   dialog instead of adopting the stock class. */
dialog.werkwyse-modal .modal-content {
    display: flex;
    flex-direction: column;
    max-height: calc(100vh - 2 * var(--bs-modal-margin));
}
dialog.werkwyse-modal .modal-body {
    overflow-y: auto;
}

/* The modal footer is ONE row (Firdaus's eyeball ruling 2026-10-13 —
   with the quick-capture's More in the left slot + the dual saves, stock
   Bootstrap's flex-wrap broke the footer into two rows at desktop
   width). Below 576px it still wraps — three controls don't fit the
   narrowest phones. */
dialog.werkwyse-modal .modal-footer {
    flex-wrap: nowrap;
}
@media (max-width: 575.98px) {
    dialog.werkwyse-modal .modal-footer {
        flex-wrap: wrap;
    }
}

/* #86 (eyeball round 3): the picker table sits ON the modal surface. In
   light the modal is the top elevation step (tertiary-bg), so a fullpage
   card bg (secondary-bg) inside a dialog is a step DOWN — a darker
   "hole"; in dark the dialog rides the card color (#185), so the card
   merges instead. Either way the card keeps only its frame (border,
   radius, cell padding, last-row rule); the thead gets a tint band for
   separation, and header labels render body color like the fullpage
   #id_table rule. */
#modal .modal-body .card {
    background: transparent;
}
#modal .modal-body .table > thead {
    --bs-table-bg: rgba(var(--bs-emphasis-color-rgb), 0.07);
}
#modal .modal-body thead a {
    color: var(--bs-body-color);
}

/* #174: the picker's in-box Clear ✕ rides the box's right end, BEFORE
   the form-select caret — Bootstrap paints the caret at right .75rem
   (16px wide, padding-right 2.25rem), so 2rem clears it with a small
   gap. The wrapper is the positioning context; the ✕ floats over the
   anchor's padding area, so it stays clickable where the box text
   never reaches. */
.picker-field {
    position: relative;
}
.picker-field .picker-clear {
    position: absolute;
    top: 50%;
    right: 2rem;
    transform: translateY(-50%);
    padding: 0.25rem;
    z-index: 2;
}

/* Arc A (ruled 2026-10-02, wireframe variant iii): the PICKER dialog loses
   BOTH separator lines as a pair — header border-bottom + footer
   border-top are near-redundant with the dialog's elevation and the thead
   tint separation above; one line alone read accidental. Scoped to the
   picker content class: the #32 form dialogs keep their separator
   lines. */
#modal .picker-dialog .modal-header {
    border-bottom: 0;
}
#modal .picker-dialog .modal-footer {
    border-top: 0;
}

/* Arc A follow-up (2026-10-02 demo eyeball): the filter row's grid keys
   off the DIALOG, not the viewport — the picker is modal-xl (capped at
   1140px) and narrower than wide screens, so viewport breakpoints
   misplace the cells (the 992-1200px band dropped to 2-up inside a
   ~1100px dialog). The modal content is the query container. The
   container needs a DEFINITE width: the house <dialog> port sizes by
   content (UA width: fit-content) and inline-size containment cuts that
   feedback — modal-xl is picker-exclusive, so its width pins here. */
.picker-dialog.modal-content {
    container: picker / inline-size;
}
#modal .modal-dialog.modal-xl {
    width: min(1140px, calc(100vw - 2 * var(--bs-modal-margin)));
}
.picker-filter-grid {
    display: grid;
    gap: 0.5rem;
    grid-template-columns: 1fr;
    margin-bottom: 1rem;
}
@container picker (min-width: 576px) {
    .picker-filter-grid {
        grid-template-columns: repeat(2, 1fr);
    }
}
@container picker (min-width: 992px) {
    .picker-filter-grid {
        grid-template-columns: repeat(4, 1fr);
    }
}

/* #94: the hybrid choice input — closed, the box IS a .form-select (the
   widget template carries that class verbatim), so the only additions
   are the wrapper's positioning context (the options panel is a
   .dropdown-menu, absolutely positioned against the nearest positioned
   ancestor — without this it anchors to the card or the page) and the
   pointer cursor a native <select> shows. */
.hybrid-choice {
    position: relative;
}
.hybrid-choice [data-hybrid-box] {
    cursor: pointer;
}
/* The options list scrolls inside the panel at seven rows tall. */
.hybrid-choice-options {
    max-height: 224px;
    overflow-y: auto;
}

/* #108: the manage-icon anatomy — a configured wrapper flexes (the box
   keeps the width; the icons ride right of it, the options panel is
   position:fixed while open so the flex row doesn't disturb it). The
   icons are bare links; the disabled ✎ dims AND drops pointer events
   (the JS disable path relies on both). */
.hybrid-choice[data-manage-create-url] {
    display: flex;
    gap: 0.25rem;
    align-items: center;
}
.hybrid-choice[data-manage-create-url] [data-hybrid-box] {
    flex: 1 1 auto;
    min-width: 0;
}
.hybrid-choice .hybrid-manage-icon {
    flex: 0 0 auto;
    padding: 0.25rem;
    color: var(--bs-secondary-color);
    text-decoration: none;
    white-space: nowrap;
}
.hybrid-choice .hybrid-manage-icon:hover:not(.disabled) {
    color: var(--bs-emphasis-color);
}
.hybrid-choice .hybrid-manage-icon.disabled {
    opacity: 0.4;
    pointer-events: none;
}

/* #144 3b: the field-level-disabled hybrid box — the picker's inert-grey
   anatomy applied to the div box (a div never matches the :disabled
   pseudo-class, so the class carries it): pointer events drop (the JS
   open path is click/focus on [data-hybrid-box], both now unreachable)
   and the grey matches the Bootstrap disabled form-select look. */
.hybrid-choice [data-hybrid-box].disabled {
    pointer-events: none;
    cursor: default;
    background-color: var(--bs-secondary-bg);
    color: var(--bs-secondary-color);
}
/* #97: the day-ledger Allocation/Remark cells (ClampColumn) — long text
   clamps to 2 lines with an ellipsis below lg; at lg+ it truncates to a
   single line (the per-column max-widths make text-overflow bite inside
   the flexible table cell). Full text always rides the title tooltip. */
.werkwyse-cell-clamp {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}
@media (min-width: 992px) {
    .werkwyse-cell-clamp {
        display: block;
        white-space: nowrap;
        text-overflow: ellipsis;
    }
    .werkwyse-cell-alloc {
        max-width: 24rem;
    }
    .werkwyse-cell-remark {
        max-width: 18rem;
    }
}

/* #111 prose editor (django-prose-editor): map the package's theming
   custom properties to the house Bootstrap vars. Both themes ride
   [data-bs-theme], so the editor surface, menubar and dialogs follow
   the flip for free — the package ships light-only fallbacks (#fff/#333)
   and would otherwise glare in dark mode. */
.prose-editor,
.prose-editor-dialog {
    --prose-editor-background: var(--bs-body-bg);
    --prose-editor-foreground: var(--bs-body-color);
    --prose-editor-border-color: var(--bs-border-color);
    --prose-editor-active-color: var(--bs-primary-bg-subtle);
    --prose-editor-disabled-color: var(--bs-secondary-bg);
    --prose-editor-typographic: var(--bs-border-color);
}

/* #111 UI/UX round (A2): the notes tab list clamps each note card's body
   at 5 lines with an automatic ellipsis — the full text is one click
   away on the note read verb (the clamp's target). */
.note-clamp {
    display: -webkit-box;
    -webkit-line-clamp: 5;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

/* #111 UI/UX round (A2): inside the note form card the editor sits
   DIRECT in the card body (no card-in-card) — strip the ProseMirror
   surface's own border/background so it merges with the card body, and
   give the menubar one light separator under it instead of its boxed
   chrome. The card-body class is dropped (the table-in-card grammar,
   base_list): the surface rides flush to the card border and the
   toolbar/content carry their own inset, like the table cells' first/
   last padding. Scoped to the note form; other editor surfaces keep
   the package look. Both themes ride the house vars. */
.note-form-editor .prose-editor .ProseMirror {
    background: transparent;
    border: 0;
    border-radius: 0;
    padding: 0.5rem 1rem 1rem;
    min-height: 12rem;
}
/* #128: the flush/borderless surface above has no border to recolor —
   the browser's default contenteditable focus ring reads as a stray
   blue box around the whole work area. The caret + active menubar
   carry the focus signal (the bare-textarea convention; WCAG 2.4.7
   satisfied), so the outline retires on focus. NO :focus-visible
   fallback: text-entry elements match :focus-visible on EVERY focus
   including mouse (the spec assumes typing intent), so a fallback
   would reintroduce the reported box for everyone (Firdaus ruling,
   #128 note_3917076621). */
.note-form-editor .prose-editor .ProseMirror:focus {
    outline: none;
}
.note-form-editor .prose-menubar:not(:empty) {
    background: transparent;
    box-shadow: none;
    border-bottom: 1px solid var(--bs-border-color);
    padding: 4px 1rem;
}
.note-form-editor .errorlist {
    margin: 0;
    padding: 0.5rem 1rem 0;
}

/* #129: the controller's readonly display inputs (the day-page date
   middles, the proxy calendar middle, the Set-day-type value). The
   focus ring on a form control is Bootstrap's .form-control:focus
   chrome — a teal border tint + box-shadow halo, NOT the UA outline
   (outline: 0 lives in the same Bootstrap rule) — so both levers are
   needed. The substitute focus signal is the caret (readonly inputs
   place a caret + allow selection on click); readonly, never disabled,
   tab order kept — the #128 Q5 posture (note_3917710331). */
.ww-display-input:focus {
    border-color: #847c74; /* #117 light control boundary (#191: literal — the --ww- token is gone) */
    box-shadow: none;
}

[data-bs-theme="dark"] .ww-display-input:focus {
    border-color: #8A8A93; /* #117 dark control boundary */
}

/* #139 eyeball (Firdaus): the toggle-password input-group is ALWAYS
   .has-validation (the #117 error-display fix — the .invalid-feedback
   renders inside the group, as the input's sibling), which defeats
   Bootstrap's seam radius reset on the pristine page: the reset only
   matches :nth-last-child(n+3), counting a feedback sibling that exists
   only when errors render. Both children keep their outer rounded corners
   and the seam shows a notch. Flatten the seam sides explicitly — the
   control's right, the button's left — matching what Bootstrap's own
   reset produces with feedback present. */
.input-group.has-validation > .form-control:not(:last-child) {
    border-top-right-radius: 0;
    border-bottom-right-radius: 0;
}
.input-group.has-validation > .btn:not(:first-child) {
    border-top-left-radius: 0;
    border-bottom-left-radius: 0;
}

/* #147 (Firdaus's alignment idea): the boolean column's ✓/✗ icon rides
   inline padding matching a standard button's content offset (border +
   padding-x), so the data-row icon lines up under the tri-state
   segmented control's first button in the filter row above it. */
.ww-bool {
    padding-inline: calc(0.75rem + 1px);
}

/* #147 (Firdaus's color ruling, 2026-10-05): ONE boolean color idiom
   on all three surfaces — the column icons (base.tables.BooleanColumn:
   fa-check text-success / fa-xmark text-warning), the segmented
   pressed segment (.tristate-seg below), and the cycling state (this
   block). True = success, False = WARNING (Firdaus's pick — amber
   carries the attention; the wireframe doc's danger red stays declined
   as a filter state is not an error), All = the idle outline. The
   variables adapt to data-bs-theme automatically. */
.tristate {
    min-width: 2.75rem;
}
.tristate[data-state="true"] {
    --bs-btn-color: var(--bs-success-text-emphasis);
    --bs-btn-bg: var(--bs-success-bg-subtle);
    --bs-btn-border-color: var(--bs-success-border-subtle);
}
.tristate[data-state="false"] {
    --bs-btn-color: var(--bs-warning-text-emphasis);
    --bs-btn-bg: var(--bs-warning-bg-subtle);
    --bs-btn-border-color: var(--bs-warning-border-subtle);
}
.tristate-seg[data-state="true"] > .btn-check:checked + .btn {
    /* the CHECKED rule reads the --bs-btn-active-* trio — the base
       --bs-btn-* vars leave the pressed segment grey (the 2026-10-05
       demo-eyeball catch). Settlement ruling note_3975409870: the
       pressed segment is SOLID success/warning + a white glyph (the
       b-solid mock), matching the All segment's solid depth — the
       earlier pale emphasis vars read too faint from across the
       table. */
    --bs-btn-active-color: #fff;
    --bs-btn-active-bg: var(--bs-success);
    --bs-btn-active-border-color: var(--bs-success);
}
.tristate-seg[data-state="false"] > .btn-check:checked + .btn {
    --bs-btn-active-color: #fff;
    --bs-btn-active-bg: var(--bs-warning);
    --bs-btn-active-border-color: var(--bs-warning);
}

/* The number stepper: Bootstrap owns the input-group chrome;
   this selector owns ONLY the native spinner suppression (no utility
   covers it across the supported browsers) — no layout, color, width,
   height, spacing, focus, or disabled styling. */
.ww-number-stepper > .form-control[type="number"] {
    appearance: textfield;
}
.ww-number-stepper > .form-control[type="number"]::-webkit-outer-spin-button,
.ww-number-stepper > .form-control[type="number"]::-webkit-inner-spin-button {
    appearance: none;
    margin: 0;
}

/* The unselected preset rides the page surface: Bootstrap's outline
   button defaults to a transparent rest background, and a bg-body
   utility would suppress its hover/active backgrounds (!important).
   The variable changes only the ordinary rest background — Bootstrap
   still owns hover, active, focus, border, and disabled presentation. */
.ww-number-presets .btn {
    --bs-btn-bg: var(--bs-body-bg);
}

/* #135 round 2 (spec note_3988144925): the multi-select checklist — the
   select-looking toggle + its checkbox menu. The toggle rides
   form-select for the control boundary + caret (the theme variants
   switch automatically); this block trims the btn/form-select overlap
   to read as one grammar of filter controls. The menu sizes to its
   labels and never wraps a checkbox row. The muted "All" / "N of M"
   label rides the #151 empty-state rule (rendered server-side). */
.ww-checklist-toggle {
    font-weight: 400;
    min-width: 6.5rem;
    padding-top: 0.25rem;
    padding-bottom: 0.25rem;
    text-align: start;
}
.ww-checklist-menu {
    min-width: 100%;
    padding: 0.5rem 0.75rem;
}
.ww-checklist-menu .form-check {
    white-space: nowrap;
}
.ww-checklist-menu .form-check + .form-check {
    margin-top: 0.25rem;
}

/* #135 round 2.1 (amendment note_3988451452): the Role-column badge
   group — a deliberate exception to the #151 cell-filling badge idiom
   (that rule keeps the STATUS badges as full-width bands; a cell
   holding up to four role badges would otherwise stack four bands).
   The badges ride as compact pills in a wrapping flex row: a SINGLE
   badge centers in the cell (the amendment's ruling), MULTIPLE
   left-align and wrap onto as many lines as the cell needs. The
   single/multi state renders server-side on the group (the tri-state
   .active idiom), and the display override must out-specify the
   .table td .badge block rule above (0,1,2 — the child selector
   0,2,2 wins). */
.ww-role-badges {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem;
    justify-content: flex-start;
}
.ww-role-badges.single {
    justify-content: center;
}
.table td .ww-role-badges .badge {
    display: inline-block;
}

/* ═══ #147 item (4): the numeric operator filter — the joined
   = / ≤ / ≥ select + number box (base/forms.py NumericOperatorWidget;
   TimeOperator* → NumericOperator* with the allocations build, spec
   note_3960508930 — the same machinery serves the Tot hours and the
   Cost RM boxes, so the names are numeric-neutral). The settled box
   (note_3959430704 + note_3959442969): the pair rides flush right in
   the right-aligned cell; the operator part is FIXED width (Firdaus's
   build guard, sign-off note_3956042967: the symbol must never crop);
   the number part is 4rem min / 8rem max with right-aligned digits.
   The operator width was 4rem until the #147 follow-on verdict
   (note_3981537091) shrank it to 3.5rem (the mocks' winning
   assignment), then 3.75rem the same day (Firdaus's eyeball of the
   demo: 3.5rem clipped the ≤/≥ glyph slightly — note_3981633332's
   follow-up — 3.75rem restores half the guard's breathing room; back
   to 4rem if it still crops). The round-1 bare-class pin (.ww-time-op, 0,1,0) LOST
   the cascade to Bootstrap's .input-group>.form-select (0,2,0) — the
   fix-rider ruling (note_3959381095) re-pins on the child selectors
   (0,2,0, later than Bootstrap in the source order). Ratcheted by
   base/tests/test_numeric_operator_filter.py — the pin asserts the
   winning assignment, not token presence (the #147 pin-the-contract
   trap). */
.ww-num-filter {
    justify-content: flex-end;
}
.ww-num-filter > .ww-num-op {
    flex: none;
    width: 3.75rem;
}
.ww-num-filter > .ww-num-value {
    flex: 1 1 auto;
    min-width: 4rem;
    max-width: 8rem;
    text-align: right;
}


/* ═══════════════════════════════════════════════════════════════════
   DENSITY ARC (#149, 2026-10-07 — spec + approval note_3968398092; the
   wireframe/density-probe branch was the reference implementation,
   localhost-eyeball-approved, never merged). The 14px base and the
   sidebar token live in the theme/werkwyse sources above; this block
   carries the list-grammar posture: nowrap rows, never-wrapping filter
   input-groups, 10rem text truncation, the horizontal-scroll wrapper,
   and the sticky first/eye columns with Bootstrap's two-layer paint
   recomposed (the probe's ghost-through fix). Per-table adoption
   (which tables ride sticky, which columns truncate) is the census in
   app/common/tests/test_density_arc_149.py. */

/* (1) Table rows never wrap — house-wide on the list grammar (every
   list table renders inside #id_table). The table takes its natural
   width and grows right instead of squeezing cell content. */
#id_table .table th,
#id_table .table td {
    white-space: nowrap;
}

/* (2) Text-column truncation at 10rem with ellipsis — the td carries
   the ww-truncate class (column attrs in the apps' tables.py, census in
   test_density_arc_149.py) and the full text on its title. Truncate-
   Column's template wraps its text in responsive breakpoint divs, so
   the cap rides the inner divs too. Money/date/number columns carry no
   class and are untouched. */
#id_table td.ww-truncate {
    max-width: 10rem;
    overflow: hidden;
    text-overflow: ellipsis;
}
#id_table td.ww-truncate > div {
    max-width: 10rem;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* (3) Filter-row input-groups never wrap — the operator select + value
   box stay on one line and the picker Choose button never stacks. The
   settled .ww-num-* width rules above stay as-is; this only pins
   flex-wrap inside the table header (the ID-scoped selector beats
   Bootstrap's .input-group flex-wrap:wrap). */
#id_table thead .input-group {
    flex-wrap: nowrap;
}

/* (3b) #147 follow-on verdict (note_3981537091): filter-row plain
   selects get a floor so they can't collapse (the mocks' winning
   assignment — 6.5rem; the numeric operator select keeps its own
   pinned 3.75rem width and rides outside this rule). */
#id_table thead td select.form-select:not(.ww-num-op) {
    min-width: 6.5rem;
}

/* (3c) #151 (item 7 amendment, ruled 2026-10-09): an EMPTY select — the
   filter row's "All" or a form's "---------" — renders muted like
   placeholder ghost text; a chosen value renders at full prominence.
   (The hybrid box and the picker label input get the same treatment
   from their own machinery: the hybrid adds text-body-secondary when
   unselected, the picker rides the native placeholder.) */
.form-select:has(> option[value=""]:checked) {
    color: var(--bs-secondary-color);
}

/* (3d) #151 (item 4, ruled 2026-10-09) sized the table-cell badges;
   #199 (ruled 2026-10-22) promotes the sized idiom to a context-free
   .badge baseline — every badge home inherits it by default and
   Bootstrap's stock 0.75em is unreachable house-wide. Badge text rides
   its context's own font size (Firdaus's playground ruling
   2026-10-10: 1em + the body's 1.5 line-height, not Bootstrap's
   shrunken .75em/1 — a badge reads like its neighboring plain text,
   and the taller pill still fits the button-driven row height).
   Tables ADDITIONALLY make badges bands: expand to fill the cell's
   width (minus the cell's padding) with centered text — a solid band
   across the cell, not a content-sized pill. */
.badge {
    font-size: 1em;
    line-height: 1.5;
    padding: 4px 8px;
}
.table td .badge {
    display: block;
    text-align: center;
}

/* #202 (ruled 2026-10-22): dark-mode badge pill contrast — the Option A
   border rescue. The quiet bg-subtle fill cannot reach 3:1 vs the card
   without breaking the emphasis text's AA, so the pill boundary rides a
   1px border-subtle border, joining the alert anatomy (fill + border) in
   dark mode only. The selector keys on the emphasis classes and never the
   bare class: chart badges (text-bg-chart-*) also carry .badge and must
   stay borderless, and the #199 count pills are a separate class.
   Alerts need no rule — they already pair bg-subtle + border-subtle, so
   the re-tuned dark border-subtle token fixes them automatically. */
[data-bs-theme="dark"] .badge.text-primary-emphasis {
    border: var(--bs-border-width) solid var(--bs-primary-border-subtle);
}
[data-bs-theme="dark"] .badge.text-secondary-emphasis {
    border: var(--bs-border-width) solid var(--bs-secondary-border-subtle);
}
[data-bs-theme="dark"] .badge.text-success-emphasis {
    border: var(--bs-border-width) solid var(--bs-success-border-subtle);
}
[data-bs-theme="dark"] .badge.text-warning-emphasis {
    border: var(--bs-border-width) solid var(--bs-warning-border-subtle);
}
[data-bs-theme="dark"] .badge.text-danger-emphasis {
    border: var(--bs-border-width) solid var(--bs-danger-border-subtle);
}
[data-bs-theme="dark"] .badge.text-info-emphasis {
    border: var(--bs-border-width) solid var(--bs-info-border-subtle);
}

/* (3e) #186 (pilot) restyled by #198 (ruled 2026-10-21): c-card's
   optional header_right slot. The header splits into the plain
   icon + title left and the badges zone right — the pilot's
   name-in-header truncation machinery is deleted; identity/names
   live in the card body. */
.ww-card-header-split {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 0.75rem;
}
.ww-header-badges {
    display: flex;
    gap: 0.5rem;
    flex-shrink: 0;
}
/* The #199 ruling deleted this zone's sizing rule — header badges
   inherit the context-free .badge baseline above; below ~lg they
   additionally truncate (the #198 RESTYLE ruling): a long badge label
   ellipsizes instead of pushing the header to two lines; desktop
   header badges never truncate. */
@media (max-width: 991.98px) {
    .ww-header-badges .badge {
        max-width: 10rem;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
}

/* (3f) #199 sub-ruling (a): the tab count pills (html/_nav_item.html,
   html/_nav_status.html) are numeric counters in compact tab chrome,
   NOT status badges — they drop .badge (the baseline above would size
   them at the 1em idiom) and ride this class replicating Bootstrap's
   stock badge values exactly (bold 0.75em / line-height 1 / 0.35em
   0.65em), so the tab render stays byte-similar and the badge grammar
   stays pure. The pill's color rides the server-side text-bg-*
   utility, exactly like the stock badge it replaces. */
.ww-tab-count {
    display: inline-block;
    padding: 0.35em 0.65em;
    font-size: 0.75em;
    font-weight: 700;
    line-height: 1;
    text-align: center;
    white-space: nowrap;
    vertical-align: baseline;
    border-radius: var(--bs-border-radius);
}

/* (4) Horizontal scroll posture — the wrapper around the table scrolls
   in x instead of squeezing, so a nowrap-grown table overflows into
   scroll inside the content region. The scroll wrapper is
   .table-responsive (Bootstrap already answers overflow-x: auto — the
   house rule re-pins the posture house-side). The card's own
   .overflow-hidden is left alone on purpose: beating its !important
   would make the CARD the scrollport and unstick the frozen columns. */
#id_table .table-responsive {
    overflow-x: auto;
}

/* (5) Sticky frozen columns — the first column (the record's name) and
   the trailing eye/actions column, for list tables whose Meta opts in
   via class="ww-sticky" (the census; ledgers/dashboard cards/pickers
   excluded with reasons). Sticky positions against the scroll region.

   Ghost-through fix (Firdaus's eyeball, probe round 2): Bootstrap keeps
   every cell OPAQUE on hover — background-color stays var(--bs-table-bg)
   (house tables compute it to the opaque body bg in both themes) and
   the hover tint rides as `inset 0 0 0 9999px var(--bs-table-hover-bg)`
   BOX-SHADOW, a tint over the opaque base. A hover rule that instead set
   background-color to the translucent hover token turns the frozen cell
   see-through exactly on hover (and the scrolled-under content reads as
   peeking past the eye button). The hover rules below therefore REBUILD
   Bootstrap's own composition — opaque base + inset tint + the frozen-
   edge separator — never a translucent background. The separator
   replaces Bootstrap's (transparent) accent inset, so the hover rules
   must re-list it. Verified by computed-style probe in the scrolled +
   hovered state, light AND dark (AC 6). */
#id_table .table.ww-sticky th:first-child,
#id_table .table.ww-sticky td:first-child {
    position: sticky;
    left: 0;
    z-index: 3;
    background-color: var(--bs-table-bg);
    box-shadow: 2px 0 0 var(--bs-border-color);
}
#id_table .table.ww-sticky th:last-child,
#id_table .table.ww-sticky td:last-child {
    position: sticky;
    right: 0;
    z-index: 3;
    background-color: var(--bs-table-bg);
    box-shadow: -2px 0 0 var(--bs-border-color);
}
#id_table .table.ww-sticky thead th:first-child,
#id_table .table.ww-sticky thead th:last-child {
    z-index: 4;
}

/* #196 follow-on demo bug: the open hybrid panel is position:fixed but
   stacks inside its td's context — a frozen cell (sticky, z-index 3)
   traps the dropdown under the LATER rows' frozen cells. The JS marks
   the row .hybrid-panel-open while its panel is open; these rules lift
   the open row's cells above the frozen layer so the dropdown paints
   over the rows below. The .ww-sticky-qualified selectors carry one
   more element than the frozen rules at the same id/class/pseudo count,
   so they win the cascade. */
#id_table tr.hybrid-panel-open > td {
    z-index: 6;
}
#id_table .table.ww-sticky tr.hybrid-panel-open > td:first-child {
    z-index: 6;
}
#id_table .table.ww-sticky tr.hybrid-panel-open > td:last-child {
    z-index: 6;
}
#id_table .table.ww-sticky.table-hover > tbody > tr:hover > td:first-child {
    background-color: var(--bs-table-bg);
    box-shadow: inset 0 0 0 9999px var(--bs-table-hover-bg),
        2px 0 0 var(--bs-border-color);
}
#id_table .table.ww-sticky.table-hover > tbody > tr:hover > td:last-child {
    background-color: var(--bs-table-bg);
    box-shadow: inset 0 0 0 9999px var(--bs-table-hover-bg),
        -2px 0 0 var(--bs-border-color);
}

/* ═══════════════════════════════════════════════════════════════════
   #147 MERGE FOLLOW-ON (verdict note_3981537091) — THE COLLECTION
   METER + FILL (the wireframe scenes B+C union): the Collection cell
   carries BOTH treatments — the 14% band tint fills the CELL to the
   collection % (the c-fill mock) AND a thin 4px meter strip runs along
   the cell's bottom edge growing to the same % (the b-meter mock,
   fill only — no track). The tint rides an INLINE background-image
   the renderer emits per row (a background-IMAGE beats Bootstrap's
   cell background-COLOR in both themes and the hover paint can't
   swallow it); the meter rides the per-row --w/--fill CSS vars the
   same inline style sets, resolving to the --bs-* band vars — which
   flip with data-bs-theme, so both treatments are dual-theme by
   construction. Winning assignments pinned in
   app/pm/tests/test_staging_all_merge_147.py.
   ═══════════════════════════════════════════════════════════════════ */
#id_table td.ww-meter-cell {
    position: relative;
    padding-bottom: 0.65rem;
}
#id_table td.ww-meter-cell .ww-meter {
    position: absolute;
    left: 0;
    bottom: 0;
    height: 4px;
    width: var(--w, 0%);
    background: var(--fill, transparent);
}
