/* ==========================================================================
   SnitchOS portal redesign — design-system foundation (Wave R1)
   Loaded AFTER app.min.css + brand.css so it layers over Bootstrap/Tabler.
   Implements the shared REDESIGN_SPEC.md tokens and restyles the app shell
   into the "operations console" look: dark grouped left rail + light canvas
   + card surfaces. Existing pages keep working; this only re-skins.
   ========================================================================== */

:root {
    /* Left rail (dark) */
    --sn-rail: #0F1B2D;
    --sn-rail-2: #17273C;
    --sn-rail-text: #93A2B5;
    --sn-rail-active: #ffffff;

    /* Canvas + surfaces */
    --sn-canvas: #F2F4F7;
    --sn-card: #ffffff;
    --sn-line: #DFE3E9;
    --sn-ink: #0F1B2D;
    --sn-muted: #5A616E;

    /* Interactive / brand — use sparingly.
       --sn-accent is a FILL/outline token: #0E9E60 on paper is 3.46:1, which
       clears WCAG 1.4.11 for a graphic but fails 1.4.3 for small text. Any
       accent-coloured LABEL uses --sn-accent-text instead (5.38:1 on white,
       4.88:1 on the canvas, 4.79:1 on --sn-accent-soft). Never swap them:
       accent-text as a fill reads muddy, accent as text fails AA. */
    --sn-accent: #0E9E60;
    --sn-accent-text: #0C7A4C;
    --sn-accent-soft: rgba(14, 158, 96, .10);
    /* Focus ring — the darker green so the 2px outline clears 3:1 against the
       navy .btn-primary (3.21:1) as well as the light canvas. */
    --sn-focus-ring: #0C7A4C;

    /* Productivity semantics — use EVERYWHERE for data viz (not brand).
       The base hues are tuned as FILLS (bars, stripes, dots). For small text
       on their soft backgrounds the fills are too light for WCAG AA, so each
       band also has a darker *-text variant (>=4.5:1) — use those for labels,
       pills, deltas. Never swap the fills for text; never use text vars as fills. */
    --sn-prod: #4c9a6b;
    --sn-neut: #cf9a3d;
    --sn-unprod: #c6493b;
    --sn-prod-text: #37744f;   /* on --sn-prod-soft, ~4.9:1 */
    --sn-neut-text: #8a6420;   /* on --sn-neut-soft, ~4.8:1 (fill is only ~2.5:1) */
    --sn-unprod-text: #b3392c; /* on --sn-unprod-soft, ~4.7:1 */
    --sn-idle: #9aa3b2;
    --sn-offline: #d7dbe3;
    --sn-prod-soft: #e7f0ea;
    --sn-neut-soft: #f7eeda;
    --sn-unprod-soft: #f6e2df;

    /* Informational cyan — the ONE band with no productivity meaning, so it has
       no fill twin. It exists because .alert-info / .alert-secondary need a
       readable label colour and the theme's own (#059bb4) is 2.88:1 on its
       subtle background. 5.37:1 on #daf4f9, 8.03:1 on white. Text only. */
    --sn-info-text: #056b7d;

    /* Shape */
    --sn-radius: 11px;
    --sn-shadow: 0 1px 2px rgba(18, 21, 27, .04), 0 6px 18px rgba(18, 21, 27, .05);

    /* Rail geometry — mirror the theme's --theme-sidenav-width (245px). */
    --sn-rail-width: 245px;
}

/* --------------------------------------------------------------------------
   Dark mode — remap the surface/ink/line tokens onto the theme's own dark
   palette (app.min.css [data-bs-theme=dark]: body-bg #17181e, secondary-bg
   #1e1f27, tertiary-bg #272832, border #293036, body-color #aab8c5,
   secondary-color #8391a2) so our console layer sits *with* Tabler's dark
   theme instead of punching light holes in it. The rail was already dark, so
   it barely shifts; canvas/card/line/ink/muted flip. Soft band backgrounds
   become low-alpha tints of their hue (readable dark chips), and each band
   text var brightens for legible small text on those tints. Everything else
   (accent, radius, geometry) is theme-neutral and inherited from :root.
   -------------------------------------------------------------------------- */
[data-bs-theme="dark"] {
    --sn-rail: #0B1524;
    --sn-rail-2: #17273C;
    --sn-rail-text: #8495ab;
    --sn-rail-active: #ffffff;

    --sn-canvas: #17181e;
    --sn-card: #1e1f27;
    --sn-line: #293036;
    --sn-ink: #d3dae2;
    --sn-muted: #8391a2;

    --sn-accent: #4CE38D;              /* Signal Green on dark surfaces */
    /* On the dark card (#1e1f27) Signal Green is 9.9:1 and 6.96:1 on its own
       soft tint, so the text twin does not need to darken here. */
    --sn-accent-text: #4CE38D;
    --sn-accent-soft: rgba(76, 227, 141, .16);
    --sn-focus-ring: #4CE38D;

    /* Fills brighten slightly so they read against the dark card surface. */
    --sn-prod: #5cb37f;
    --sn-neut: #dcab52;
    --sn-unprod: #e06456;
    --sn-prod-text: #7ecb9a;
    --sn-neut-text: #e6bd6e;
    --sn-unprod-text: #f0938a;
    --sn-idle: #59606e;
    --sn-offline: #2b3038;
    --sn-prod-soft: rgba(92, 179, 127, .16);
    --sn-neut-soft: rgba(220, 171, 82, .16);
    --sn-unprod-soft: rgba(224, 100, 86, .16);

    /* The theme's own dark cyan already clears AA on the dark alert tint
       (7.24:1 on the canvas, 6.66:1 on a card), so this keeps it rather than
       inventing a second value to maintain. */
    --sn-info-text: #6ad3e5;

    --sn-shadow: 0 1px 2px rgba(0, 0, 0, .3), 0 6px 18px rgba(0, 0, 0, .35);
}

/* --------------------------------------------------------------------------
   Canvas — drive the body off the tokens so it follows the active theme.
   (Previously a bare `body` rule hard-pinned the light canvas even in dark
   mode.) The tokens above already remap per data-bs-theme, so one rule works.
   -------------------------------------------------------------------------- */
body {
    background-color: var(--sn-canvas);
    color: var(--sn-ink);
}

.content-page .container-fluid {
    color: var(--sn-ink);
}

/* Tabular numerals for any figure that reads as data. */
.sn-num,
.kpi-card .kpi-value,
.score-pill {
    font-variant-numeric: tabular-nums;
}

/* Uppercase eyebrow used by kpi_card / page_head. */
.sn-eyebrow {
    font-size: 10.5px;
    letter-spacing: .10em;
    text-transform: uppercase;
    color: var(--sn-muted);
    font-weight: 600;
    line-height: 1.2;
}

/* --------------------------------------------------------------------------
   Left rail — restyle the theme's .sidenav-menu into the dark grouped rail.
   -------------------------------------------------------------------------- */
.sidenav-menu {
    background: var(--sn-rail) !important;
    border-right: 1px solid rgba(255, 255, 255, .04);
}

/* Only pin our rail width in the DEFAULT layout. The theme drives its own
   width for condensed / offcanvas / on-hover states (data-sidenav-size); a
   flat `width: 245px` here fought those and broke the collapsed rail. Scope
   to the default size and fall back to the theme var so we never override it. */
[data-sidenav-size="default"] .sidenav-menu {
    width: var(--theme-sidenav-width, var(--sn-rail-width));
}

.sidenav-menu .logo {
    border-bottom: 1px solid rgba(255, 255, 255, .05);
}

/* Group section header (label + icon) — a real <button> for a11y (Wave R8),
   reset to look like the original static label. */
.sn-nav-group-title {
    display: flex;
    align-items: center;
    width: 100%;
    gap: .5rem;
    padding: .55rem 1.25rem .3rem;
    margin-top: .35rem;
    /* Rail hierarchy: group headers read LARGER than their submenu links
       (14.5px semibold vs 13px items) — was a 10.5px uppercase eyebrow, which
       made the headers look subordinate to their own children. */
    font-size: 14.5px;
    letter-spacing: .01em;
    /* Lightened from #6b7382 (~3.6:1 on the #161a23 rail) toward the rail's
       body text so the group headers clear AA for non-large text. */
    color: var(--sn-rail-text);
    font-weight: 600;
    background: none;
    border: 0;
    cursor: pointer;
    text-align: left;
}

.sn-nav-group-title .ti {
    font-size: 17px;
    opacity: .8;
}

.sn-nav-group-title:hover {
    color: var(--sn-rail-active);
}

.sn-nav-group-title:focus-visible {
    outline: 2px solid var(--sn-accent);
    outline-offset: -2px;
    border-radius: 4px;
}

/* A group whose active page is inside it gets a subtly brighter label. */
.sn-nav-group.is-active > .sn-nav-group-title {
    color: var(--sn-rail-active);
}

/* Chevron — pushed to the end of the header, rotates on expand/collapse. */
.sn-nav-group-chevron {
    margin-left: auto;
    font-size: 13px !important;
    opacity: .65;
    transition: transform .15s ease;
}

.sn-nav-group-collapsed .sn-nav-group-chevron {
    transform: rotate(-90deg);
}

/* Group body (the items) — smooth height/opacity collapse. grid-template-rows
   1fr->0fr on the OUTER element + min-height:0/overflow:hidden on the single
   INNER wrapper (.sn-nav-group-body-inner, its only grid child) is the modern
   no-JS-measurement way to animate to/from "auto" height. */
.sn-nav-group-body {
    display: grid;
    grid-template-rows: 1fr;
    transition: grid-template-rows .18s ease;
}

.sn-nav-group-body-inner {
    min-height: 0;
    overflow: hidden;
    opacity: 1;
    transition: opacity .18s ease;
}

.sn-nav-group-collapsed .sn-nav-group-body {
    grid-template-rows: 0fr;
}

.sn-nav-group-collapsed .sn-nav-group-body-inner {
    opacity: 0;
}

@media (prefers-reduced-motion: reduce) {
    .sn-nav-group-chevron,
    .sn-nav-group-body {
        transition: none;
    }
}

/* Items */
.sidenav-menu .side-nav .side-nav-item .side-nav-link,
.sn-nav-link {
    display: flex;
    align-items: center;
    gap: .65rem;
    /* Indented under the (larger) group header so sub-items read as children. */
    padding: .45rem 1.25rem .45rem 1.9rem;
    color: var(--sn-rail-text);
    border-left: 3px solid transparent;
    border-radius: 0;
    transition: background-color .12s ease, color .12s ease, border-color .12s ease;
    text-decoration: none;
}

.sn-nav-link .menu-icon,
.sn-nav-link .ti {
    font-size: 18px;
    line-height: 1;
    width: 20px;
    text-align: center;
    flex: 0 0 auto;
}

.sn-nav-link .menu-text {
    /* Kept smaller than the 14.5px group headers (rail hierarchy). */
    font-size: 13px;
    font-weight: 500;
}

.sn-nav-link:hover {
    color: var(--sn-rail-active);
    background: var(--sn-rail-2);
}

/* Active item: colored left border + tinted background + white text. */
.sn-nav-link.is-active {
    color: var(--sn-rail-active);
    background: var(--sn-rail-2);
    border-left-color: var(--sn-accent);
    font-weight: 600;
}

.sn-nav-link.is-active .menu-text {
    font-weight: 600;
}

/* Beta pill in the rail. */
.sn-nav-badge {
    margin-left: auto;
    font-size: 9.5px;
    letter-spacing: .06em;
    text-transform: uppercase;
    padding: .1rem .35rem;
    border-radius: 5px;
    background: rgba(255, 255, 255, .08);
    color: var(--sn-rail-text);
}

/* Tenant picker inside the rail (top-level super-admin surface). */
.sn-rail-tenant {
    color: var(--sn-rail-text);
    padding: .35rem 1.25rem;
    display: block;
    font-size: 13px;
    text-decoration: none;
}

.sn-rail-tenant:hover,
.sn-rail-tenant.is-current {
    color: var(--sn-rail-active);
    background: var(--sn-rail-2);
}

/* --------------------------------------------------------------------------
   Topbar — keep the theme's chrome but flatten to the console look + host the
   breadcrumb.
   -------------------------------------------------------------------------- */
.app-topbar {
    background: var(--sn-card);
    border-bottom: 1px solid var(--sn-line);
}

.sn-breadcrumb {
    display: flex;
    align-items: center;
    gap: .4rem;
    font-size: 13px;
    color: var(--sn-muted);
    min-width: 0;
}

.sn-breadcrumb .sn-crumb-group {
    color: var(--sn-muted);
}

.sn-breadcrumb .sn-crumb-sep {
    color: var(--sn-line);
}

.sn-breadcrumb .sn-crumb-page {
    color: var(--sn-ink);
    font-weight: 600;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* --------------------------------------------------------------------------
   Cards — soften into the spec's surface look without ripping out Bootstrap.
   -------------------------------------------------------------------------- */
.content-page .card {
    background: var(--sn-card);
    border: 1px solid var(--sn-line);
    border-radius: var(--sn-radius);
    box-shadow: var(--sn-shadow);
}

.content-page .card .card-header {
    background: transparent;
    border-bottom: 1px solid var(--sn-line);
}

/* --------------------------------------------------------------------------
   R1 macro components (kpi_card / ratio_bar / score_pill / page_head).
   Kept here so later waves can use them without further shell edits.
   -------------------------------------------------------------------------- */

/* page_head */
.sn-page-head {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    justify-content: space-between;
    gap: .75rem;
    margin-bottom: 1rem;
}

.sn-page-head .sn-page-title {
    font-size: 1.35rem;
    font-weight: 700;
    color: var(--sn-ink);
    margin: 0;
    line-height: 1.15;
}

.sn-page-head .sn-page-subtitle {
    font-size: .85rem;
    color: var(--sn-muted);
    margin: .2rem 0 0;
}

/* kpi_card */
.kpi-card {
    position: relative;
    background: var(--sn-card);
    border: 1px solid var(--sn-line);
    border-radius: var(--sn-radius);
    box-shadow: var(--sn-shadow);
    padding: 1rem 1.1rem;
    overflow: hidden;
    height: 100%;
}

.kpi-card .kpi-stripe {
    position: absolute;
    inset: 0 auto 0 0;
    width: 4px;
    background: var(--sn-idle);
}

.kpi-card.band-prod .kpi-stripe {
    background: var(--sn-prod);
}

.kpi-card.band-neut .kpi-stripe {
    background: var(--sn-neut);
}

.kpi-card.band-unprod .kpi-stripe {
    background: var(--sn-unprod);
}

.kpi-card.band-accent .kpi-stripe {
    background: var(--sn-accent);
}

.kpi-card .kpi-value {
    font-size: 1.7rem;
    font-weight: 700;
    color: var(--sn-ink);
    line-height: 1.1;
}

.kpi-card .kpi-unit {
    font-size: .8rem;
    color: var(--sn-muted);
    font-weight: 500;
}

.kpi-card .kpi-delta {
    font-size: .78rem;
    font-weight: 600;
    display: inline-flex;
    align-items: center;
    gap: .2rem;
    margin-top: .3rem;
}

.kpi-card .kpi-delta.up {
    color: var(--sn-prod-text);
}

.kpi-card .kpi-delta.down {
    color: var(--sn-unprod-text);
}

.kpi-card .kpi-delta.flat {
    color: var(--sn-muted);
}

/* ratio_bar — stacked productivity ratio with an offline remainder track. */
.ratio-bar {
    display: flex;
    width: 100%;
    height: 10px;
    border-radius: 6px;
    overflow: hidden;
    background: var(--sn-offline);
}

.ratio-bar .ratio-seg {
    height: 100%;
}

.ratio-bar .ratio-prod {
    background: var(--sn-prod);
}

.ratio-bar .ratio-neut {
    background: var(--sn-neut);
}

.ratio-bar .ratio-unprod {
    background: var(--sn-unprod);
}

/* score_pill — 0..100 productivity score, band-colored. */
.score-pill {
    display: inline-flex;
    align-items: center;
    gap: .3rem;
    padding: .12rem .5rem;
    border-radius: 999px;
    font-weight: 700;
    font-size: .82rem;
    line-height: 1;
}

/* Pills are small text on soft backgrounds — use the AA text variants, not the
   fills (the amber fill is only ~2.5:1 on its soft chip). */
.score-pill.band-prod {
    background: var(--sn-prod-soft);
    color: var(--sn-prod-text);
}

.score-pill.band-neut {
    background: var(--sn-neut-soft);
    color: var(--sn-neut-text);
}

.score-pill.band-unprod {
    background: var(--sn-unprod-soft);
    color: var(--sn-unprod-text);
}

/* --------------------------------------------------------------------------
   R2 landing components (mini_toplist / trend_bars / deep-link chevron).
   -------------------------------------------------------------------------- */

/* Deep-link chevron on a card header — subtle until hover. */
.sn-card-link {
    font-size: .78rem;
    font-weight: 600;
    color: var(--sn-accent-text);
    text-decoration: none;
    display: inline-flex;
    align-items: center;
    gap: .15rem;
    white-space: nowrap;
}

.sn-card-link .ti {
    font-size: 15px;
    transition: transform .12s ease;
}

.sn-card-link:hover {
    color: var(--sn-accent-text);
}

.sn-card-link:hover .ti {
    transform: translateX(2px);
}

/* mini_toplist — ranked top-5 rows. */
.sn-toplist-rows {
    list-style: none;
    margin: 0;
    padding: 0;
}

.sn-toplist-row {
    display: flex;
    align-items: center;
    gap: .65rem;
    padding: .5rem .9rem;
    border-top: 1px solid var(--sn-line);
    font-size: .86rem;
    min-width: 0;
}

.sn-toplist-row:first-child {
    border-top: 0;
}

.sn-toplist-rank {
    flex: 0 0 auto;
    width: 20px;
    height: 20px;
    border-radius: 6px;
    background: var(--sn-accent-soft);
    color: var(--sn-accent-text);
    font-size: .72rem;
    font-weight: 700;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

.sn-toplist-name {
    flex: 1 1 auto;
    min-width: 0;
    color: var(--sn-ink);
}

.sn-toplist-value {
    flex: 0 0 auto;
    font-weight: 600;
    color: var(--sn-ink);
}

.sn-toplist-unit {
    font-size: .72rem;
    color: var(--sn-muted);
    font-weight: 500;
    margin-left: .12rem;
}

/* trend_bars — compact 7-day stacked sparkline. */
.sn-trend {
    display: flex;
    align-items: flex-end;
    gap: .4rem;
    height: 92px;
}

.sn-trend-col {
    flex: 1 1 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-end;
    height: 100%;
    min-width: 0;
}

.sn-trend-stack {
    width: 100%;
    max-width: 26px;
    display: flex;
    flex-direction: column;
    border-radius: 4px 4px 0 0;
    overflow: hidden;
    background: var(--sn-offline);
    min-height: 2px;
}

.sn-trend-seg {
    width: 100%;
    flex-basis: 0;
}

.sn-trend-seg.sn-seg-prod {
    background: var(--sn-prod);
}

.sn-trend-seg.sn-seg-neut {
    background: var(--sn-neut);
}

.sn-trend-seg.sn-seg-unprod {
    background: var(--sn-unprod);
}

.sn-trend-seg.sn-seg-idle {
    background: var(--sn-idle);
}

.sn-trend-label {
    font-size: 10px;
    color: var(--sn-muted);
    margin-top: .3rem;
    white-space: nowrap;
}

/* --------------------------------------------------------------------------
   Accessibility — keep focus visible over the darker rail + accent surfaces.
   -------------------------------------------------------------------------- */
.sn-nav-link:focus-visible,
.sn-rail-tenant:focus-visible {
    outline: 2px solid var(--sn-accent);
    outline-offset: -2px;
}

/* Ring colour is --sn-focus-ring, not --sn-accent: the primary CTA is Ink
   Navy now (brand.css --theme-btn-bg), and the paler accent green only made
   2.5:1 against it — under WCAG 1.4.11's 3:1 for a focus indicator. The
   darker green clears 3.21:1 on the navy button and 4.88:1 on the canvas. */
a:focus-visible,
.btn:focus-visible {
    outline: 2px solid var(--sn-focus-ring);
    outline-offset: 2px;
}

/* Topbar "Staff" chip — the way back to /staff from a customer page (D2-17).
   Tinted rather than plain so it reads as a mode switch, not a nav item. */
.sn-staff-chip {
    display: inline-flex;
    align-items: center;
    padding: .25rem .55rem;
    border-radius: 999px;
    background: var(--sn-accent-soft);
    color: var(--sn-accent-text);
    font-size: .8rem;
    font-weight: 600;
    text-decoration: none;
}
.sn-staff-chip:hover { color: var(--sn-accent-text); filter: brightness(.95); }

/* Skip-to-content link — off-screen until focused, then pinned top-left over
   everything so keyboard/AT users can jump past the rail + topbar. */
.sn-skip-link {
    position: fixed;
    top: -60px;
    left: .75rem;
    z-index: 1100;
    padding: .5rem .9rem;
    background: var(--sn-card);
    color: var(--sn-ink);
    border: 1px solid var(--sn-accent);
    border-radius: 8px;
    box-shadow: var(--sn-shadow);
    font-size: .9rem;
    font-weight: 600;
    text-decoration: none;
    transition: top .12s ease;
}
.sn-skip-link:focus {
    top: .75rem;
    outline: 2px solid var(--sn-accent);
    outline-offset: 2px;
}

/* Minimum tap target for icon-only / glyph-only link-style buttons (WCAG
   2.5.8 Target Size Minimum). `.btn-sm ... p-0` strips Bootstrap's built-in
   padding down to the glyph's own box, which renders well under 24px. Pad
   the hit area out with min-width/min-height instead of visible padding so
   dense table rows don't grow — text-bearing buttons using the same classes
   (e.g. a "+30d" link) just get extra invisible margin around their text. */
.btn.btn-sm.btn-link.p-0 {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 1.5rem;
    min-height: 1.5rem;
}

/* --------------------------------------------------------------------------
   usage_master_detail — shared Applications / Websites / Categories layout.
   Left ranked list + HTMX-swapped right detail panel (Users + Usage tabs).
   -------------------------------------------------------------------------- */

/* Shared productivity fills (bars) — reused by list rows, user bars, chart. */
.sn-fill-productive { background: var(--sn-prod); }
.sn-fill-neutral { background: var(--sn-neut); }
.sn-fill-unproductive { background: var(--sn-unprod); }
.sn-fill-ignore { background: var(--sn-idle); }

/* Classification badge (icon-only) in the master list + detail head. */
.sn-class-badge {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    border-radius: 999px;
    font-size: 14px;
}
.sn-class-prod { color: var(--sn-prod); background: var(--sn-prod-soft); }
.sn-class-neut { color: var(--sn-neut); background: var(--sn-neut-soft); }
.sn-class-unprod { color: var(--sn-unprod); background: var(--sn-unprod-soft); }
.sn-class-undef { color: var(--sn-muted); background: rgba(107, 114, 128, .12); }

.sn-md-list, .sn-md-detail-card { border: 1px solid var(--sn-line); }

/* Left toolbar: search + date/mode/productivity filters. */
.sn-md-toolbar {
    border-bottom: 1px solid var(--sn-line);
    display: flex;
    flex-direction: column;
    gap: .5rem;
}
.sn-md-search { position: relative; }
.sn-md-search .ti {
    position: absolute;
    left: 10px;
    top: 50%;
    transform: translateY(-50%);
    color: var(--sn-muted);
}
.sn-md-search input { padding-left: 30px; }
.sn-md-filters {
    display: flex;
    flex-wrap: wrap;
    gap: .5rem;
    align-items: center;
}
.sn-md-prod-filter { width: auto; }

/* Ranked list. */
.sn-md-rows {
    max-height: 620px;
    overflow-y: auto;
    /* Thin, themed scrollbar (Firefox + Chromium) so the long ranked list
       doesn't drop a chunky default track into the console look. */
    scrollbar-width: thin;
    scrollbar-color: var(--sn-line) transparent;
}
.sn-md-rows::-webkit-scrollbar { width: 8px; }
.sn-md-rows::-webkit-scrollbar-thumb {
    background: var(--sn-line);
    border-radius: 4px;
}
.sn-md-rows::-webkit-scrollbar-track { background: transparent; }
.sn-md-row {
    width: 100%;
    display: flex;
    align-items: center;
    gap: .55rem;
    padding: .5rem .85rem;
    border: 0;
    border-bottom: 1px solid var(--sn-line);
    background: var(--sn-card);
    text-align: left;
    cursor: pointer;
}
.sn-md-row:hover { background: var(--sn-canvas); }
/* Keyboard focus — the row is a <button>; give it a visible ring inset so it
   doesn't get clipped by the list's overflow. */
.sn-md-row:focus-visible {
    outline: 2px solid var(--sn-accent);
    outline-offset: -2px;
}
.sn-md-row.active {
    background: var(--sn-accent-soft);
    box-shadow: inset 3px 0 0 var(--sn-accent);
}
.sn-md-rank {
    flex: 0 0 1.4rem;
    color: var(--sn-muted);
    font-size: .8rem;
    text-align: right;
}
.sn-md-name {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
}
.sn-md-name-main { font-weight: 600; color: var(--sn-ink); }
.sn-md-name-sub { font-size: .75rem; color: var(--sn-muted); }
.sn-md-value {
    flex: 0 0 auto;
    font-weight: 600;
    color: var(--sn-ink);
    white-space: nowrap;
    text-align: right;
}
.sn-md-unit { font-size: .7rem; color: var(--sn-muted); margin-left: 1px; }
.sn-md-delta { font-size: .72rem; margin-left: .35rem; }
.sn-md-delta.up { color: var(--sn-prod-text); }
.sn-md-delta.down { color: var(--sn-unprod-text); }
.sn-md-delta.new { color: var(--sn-muted); }
.sn-md-bar {
    flex: 0 0 84px;
    height: 8px;
    border-radius: 4px;
    background: var(--sn-offline);
    overflow: hidden;
}
.sn-md-fill { display: block; height: 100%; border-radius: 4px; }
/* Summary hides the exe/host sub-label + distribution; Detailed shows all. */
.sn-md-list:not(.sn-md-detailed) .sn-md-bar { display: none; }

/* Right detail panel. */
.sn-md-detail-body { padding: 1rem 1.15rem; }
.sn-md-placeholder { padding: 3rem 1rem; }
.sn-md-detail-head { margin-bottom: .75rem; }
.sn-md-tabs { margin-bottom: .85rem; }
.sn-md-tabs .nav-link { background: none; border: 0; border-bottom: 2px solid transparent; }
.sn-md-tabs .nav-link.active {
    color: var(--sn-accent-text);
    border-bottom-color: var(--sn-accent);
}

/* Users tab. */
.sn-md-userlist { display: flex; flex-direction: column; gap: .45rem; }
.sn-md-user { display: flex; align-items: center; gap: .6rem; }
.sn-md-user-name { flex: 0 0 34%; min-width: 0; font-size: .85rem; color: var(--sn-ink); }
.sn-md-user-bar {
    flex: 1 1 auto;
    height: 10px;
    border-radius: 5px;
    background: var(--sn-offline);
    overflow: hidden;
}
.sn-md-user-val { flex: 0 0 auto; font-size: .8rem; color: var(--sn-muted); }

/* Usage tab — compact per-day bar chart. */
.sn-md-chart {
    display: flex;
    align-items: flex-end;
    gap: 4px;
    height: 180px;
    padding-top: .5rem;
    overflow-x: auto;
}
.sn-md-chart-col {
    flex: 1 0 22px;
    display: flex;
    flex-direction: column;
    align-items: center;
    height: 100%;
}
.sn-md-chart-track {
    flex: 1 1 auto;
    width: 60%;
    display: flex;
    align-items: flex-end;
    background: var(--sn-canvas);
    border-radius: 4px 4px 0 0;
    overflow: hidden;
}
.sn-md-chart-fill { width: 100%; border-radius: 4px 4px 0 0; }
.sn-md-chart-label {
    font-size: .65rem;
    color: var(--sn-muted);
    margin-top: 4px;
    white-space: nowrap;
}

/* Reclassify control. */
.sn-md-reclassify {
    margin-top: 1rem;
    padding-top: .85rem;
    border-top: 1px solid var(--sn-line);
}

/* --------------------------------------------------------------------------
   R6 — Classifications tabbed workspace, People roster, Roles matrix.
   -------------------------------------------------------------------------- */

/* Tab strip (Pending / Rules / Categories). Layers over Bootstrap .nav-tabs. */
.sn-tabs {
    border-bottom: 1px solid var(--sn-line);
    gap: .25rem;
}
.sn-tabs .nav-link {
    color: var(--sn-muted);
    border: 0;
    border-bottom: 2px solid transparent;
    border-radius: 0;
    padding: .55rem .9rem;
    font-weight: 600;
    font-size: .9rem;
    display: inline-flex;
    align-items: center;
}
.sn-tabs .nav-link:hover {
    color: var(--sn-ink);
    border-bottom-color: var(--sn-line);
}
.sn-tabs .nav-link.active {
    color: var(--sn-accent-text);
    background: transparent;
    border-bottom-color: var(--sn-accent);
}

/* Count pill on a tab. */
.sn-tab-badge {
    margin-left: .4rem;
    font-size: .7rem;
    font-weight: 700;
    min-width: 1.25rem;
    padding: .05rem .35rem;
    border-radius: 999px;
    background: var(--sn-accent-soft);
    color: var(--sn-accent-text);
    font-variant-numeric: tabular-nums;
}

/* Bulk-assign toolbar on the Pending tab. */
.sn-bulk-toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .5rem;
    border-bottom: 1px solid var(--sn-line);
    background: var(--sn-canvas);
}

.sn-pending-status { min-width: 3rem; display: inline-block; }

/* Category taxonomy — the Ignore/system bucket reads muted. */
.sn-cat-ignore { color: var(--sn-muted); }
.sn-cat-ignore code, .sn-cat-ignore .fw-medium { color: var(--sn-muted); }

/* Observed-categories member tables: dense so many sites fit per screen, with
   width-capped columns so a row never stretches name/summary edge-to-edge. */
.sn-cat-accordion .accordion-button { padding: .5rem .9rem; }
.sn-cat-accordion .accordion-body { background: var(--sn-canvas); }
.sn-cat-table { font-size: .85rem; table-layout: fixed; }
.sn-cat-table > :not(caption) > * > * { padding: .28rem .9rem; }
.sn-cat-table thead th {
    font-size: .72rem;
    text-transform: uppercase;
    letter-spacing: .02em;
    color: var(--sn-muted);
    font-weight: 600;
    border-bottom: 1px solid var(--sn-line);
    white-space: nowrap;
}
.sn-cat-table td { vertical-align: middle; }
.sn-cat-table .badge { font-size: .72rem; }
/* Name + summary truncate rather than wrap so density stays predictable. */
.sn-cat-name, .sn-cat-summary {
    max-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.sn-cat-name code { font-size: .76rem; }

/* Click-to-sort headers: pointer + a caret that flips with direction. Driven
   by the delegated handler in app.js (any table.sn-sortable), so these apply
   on every page that opts in, not just the classifications tables they were
   written for. */
th.sn-sort { cursor: pointer; user-select: none; white-space: nowrap; }
th.sn-sort:hover { color: var(--sn-ink); }
th.sn-sort:focus-visible {
    outline: 2px solid var(--sn-accent);
    outline-offset: -2px;
}
th.sn-sort::after {
    content: "\2195";           /* up-down arrows: sortable but unsorted */
    opacity: .35;
    margin-left: .3rem;
    font-size: .8em;
}
th.sn-sort-asc::after { content: "\2191"; opacity: 1; }
th.sn-sort-desc::after { content: "\2193"; opacity: 1; }
th.sn-sort-asc, th.sn-sort-desc { color: var(--sn-ink); }

/* --------------------------------------------------------------------------
   Scrollable table + sticky header. Wraps the LONG tables (rosters, logs) so
   the column names stay on screen while the reader scrolls the rows.

   This replaces `.table-responsive` alone rather than nesting inside it: that
   class is already `overflow-x: auto`, which per spec computes overflow-y to
   auto as well, so the box is a scrollport either way — adding a height just
   makes the vertical half of it real. Horizontal scrolling is unchanged.

   The header cells must paint an OPAQUE background of their own: the theme
   leaves `--theme-table-bg` transparent on a plain <thead>, and a transparent
   sticky header shows the rows sliding underneath it.
   -------------------------------------------------------------------------- */
.sn-table-scroll {
    max-height: min(65vh, 720px);
    overflow: auto;
}
.sn-table-scroll thead th {
    position: sticky;
    top: 0;
    z-index: 2;
    background: var(--sn-card);
    /* Explicit ink: the wrapped tables use `.table-light`, whose text color
       (#313a46) has no dark-theme variant — on the dark --sn-card ground it
       was ~1.4:1, i.e. invisible column names (review round-1 #1). */
    color: var(--sn-ink);
    /* border-bottom on a collapsed-border table scrolls away with the cell;
       an inset shadow is painted by the cell itself and stays put. */
    box-shadow: inset 0 -1px 0 var(--sn-line);
}

/* RBAC matrix — keep cells compact + centered. */
.sn-rbac-matrix td, .sn-rbac-matrix th { vertical-align: middle; }
.sn-rbac-matrix .ti { font-size: 17px; }

/* Enriched user roster — tighten the identifier column. */
.sn-roster code { font-size: .78rem; }

/* Compact roster: high-density rows so a big fleet fits on one screen. */
.sn-roster-dense { font-size: .85rem; }
.sn-roster-dense > :not(caption) > * > * { padding: .3rem .6rem; }
.sn-roster-dense td { vertical-align: middle; }
.sn-roster-dense .badge { font-size: .72rem; }
.sn-roster-btn {
    --bs-btn-padding-y: .05rem;
    --bs-btn-padding-x: .4rem;
    --bs-btn-font-size: .72rem;
    margin-left: .35rem;
}
/* "Uploaded late" — this sample was buffered on the endpoint while it was
   offline. Deliberately quiet: working offline is not a fault, and the row
   next to it is ordinary recorded work. The tooltip carries the two times. */
.sn-late-mark {
    margin-left: .25rem;
    font-size: .82em;
    cursor: help;
}
.sn-row-off td { opacity: .6; }
.sn-row-off td:nth-child(2) { opacity: 1; }  /* keep the status cell readable */
.sn-roster-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .75rem;
    padding-top: .5rem;
    padding-bottom: .5rem;
}
.sn-roster-search { max-width: 280px; }

/* Monitoring Active/Disabled dropdowns in the roster tables. */
.sn-mon-select {
    width: auto;
    display: inline-block;
    padding-top: .1rem;
    padding-bottom: .1rem;
    font-size: .78rem;
}
.sn-mon-on { color: var(--sn-prod); border-color: var(--sn-prod); }
.sn-mon-off { color: var(--sn-muted); }

/* Fleet-health chips in the Devices card header. */
.sn-fleet-chips { display: flex; flex-wrap: wrap; gap: .35rem; }

/* Per-user shift cell: custom shifts read solid, inherited read muted. */
.sn-shift-chip { font-size: .8rem; }
.sn-shift-custom { color: var(--sn-accent-text); font-weight: 500; }
.sn-shift-form .form-control { font-size: .75rem; }

/* ==========================================================================
   R7 — page-head actions (Print + Favorite star), Favorites strip,
   the Screenshots day viewer (pickers + player), and the @media print block.
   ========================================================================== */

/* page_head action buttons (Print / Favorite). Sit at the right of the head,
   next to any report filters. Unobtrusive, icon-first, accessible focus. */
.sn-page-head-actions {
    display: flex;
    align-items: center;
    gap: .4rem;
}
.sn-head-btn {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    border: 1px solid var(--sn-line);
    background: var(--sn-card);
    color: var(--sn-muted);
    border-radius: 8px;
    padding: .3rem .55rem;
    font-size: .82rem;
    line-height: 1;
    cursor: pointer;
}
.sn-head-btn:hover { color: var(--sn-ink); border-color: var(--sn-muted); }
.sn-head-btn:focus-visible {
    outline: 2px solid var(--sn-accent);
    outline-offset: 1px;
}
.sn-head-btn .ti { font-size: 16px; }
/* Active (favourited) star reads amber/gold, filled. The button LABEL text uses
   the darker amber text var for AA; only the icon keeps the bright fill hue. */
.sn-fav-btn.is-fav { color: var(--sn-neut-text); border-color: var(--sn-neut); }
.sn-fav-btn.is-fav .ti { color: var(--sn-neut); }

/* Favorites strip on the landing dashboard. */
.sn-fav-strip {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .5rem;
    margin-bottom: 1rem;
}
.sn-fav-strip-label {
    font-size: 10.5px;
    letter-spacing: .10em;
    text-transform: uppercase;
    color: var(--sn-muted);
    display: inline-flex;
    align-items: center;
    gap: .3rem;
}
.sn-fav-chip {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    background: var(--sn-card);
    border: 1px solid var(--sn-line);
    border-radius: 999px;
    padding: .28rem .7rem;
    font-size: .82rem;
    color: var(--sn-ink);
    text-decoration: none;
    box-shadow: var(--sn-shadow);
}
.sn-fav-chip:hover { border-color: var(--sn-accent); color: var(--sn-accent-text); }
.sn-fav-chip .ti { font-size: 14px; color: var(--sn-neut); }
.sn-fav-empty { font-size: .82rem; color: var(--sn-muted); }

/* --------------------------------------------------------------------------
   Screenshots page — the day viewer's pickers.

   "user can select date and user then it'll show the screenshots with the
   slider below it" — so the two pickers are ONE compact row directly under the
   page head, and everything below them is the picture. They are the only
   controls on the page, so they are prominent, but they get one row: every
   pixel they take comes straight off the screenshot.
   -------------------------------------------------------------------------- */
.sn-shot-pickers {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: .6rem 1.25rem;
    margin-bottom: .7rem;
}
.sn-shot-picker {
    display: flex;
    flex-direction: column;
    gap: .22rem;
    min-width: 0;
}
.sn-shot-picker-label {
    margin: 0;
    font-size: .7rem;
    font-weight: 600;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--sn-muted);
}
.sn-shot-day-nav {
    display: flex;
    align-items: center;
    gap: .3rem;
}
.sn-shot-day-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 32px;
}
.sn-shot-day-btn:focus-visible {
    outline: 2px solid var(--sn-focus-ring);
    outline-offset: 2px;
}
/* Next/Today past the tenant's today. Kept in the layout (greying out is the
   answer to "why can't I go forward?"; removing them is not) but inert to the
   pointer as well as to Tab, which JS drops from the tab order. */
.sn-shot-day-btn.disabled,
.sn-shot-day-btn[aria-disabled="true"] {
    opacity: .45;
    pointer-events: none;
}
.sn-shot-day-input {
    width: auto;
    min-width: 9.5rem;
}
.sn-shot-user-select {
    min-width: 16rem;
    max-width: 28rem;
}

/* --------------------------------------------------------------------------
   Screenshot player — one panel, two homes.

   .sn-player is the full-screen overlay (built by snPlayerOverlay() in app.js,
   so no template carries markup for it); .sn-player-inline is the same panel
   mounted into the Screenshots page's card. Both paint their own dark ground
   in BOTH themes, like the old lightbox did: the subject is a photograph, and
   light chrome around a screenshot fights it. Only the controls use design
   tokens, on a raised dark surface.
   -------------------------------------------------------------------------- */
.sn-player {
    position: fixed;
    inset: 0;
    z-index: 1085;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1.25rem;
    background: rgba(9, 11, 16, .9);
}
.sn-player[hidden] { display: none; }
/* Stop the page behind the overlay scrolling under a trackpad swipe. */
body.sn-player-lock { overflow: hidden; }

.sn-player-panel {
    display: flex;
    flex-direction: column;
    gap: .55rem;
    width: 100%;
    max-width: 94vw;
    color: #e8ecf3;
}
.sn-player-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1rem;
}
.sn-player-caption {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: .55rem;
    min-width: 0;
}
.sn-player-name { font-weight: 600; font-size: 1.02rem; }
.sn-player-when { color: #b7c0cd; font-size: .9rem; }
.sn-player-count { color: #8d97a6; font-size: .82rem; }
/* Only the inline viewer fills this (the overlay's .sn-player-when already
   carries the date), so it collapses rather than leaving a gap in the row. */
.sn-player-day { color: #b7c0cd; font-size: .9rem; }
.sn-player-day:empty { display: none; }
/* "◀ Last capture" / "Next capture ▶" in the caption row (Sol review 23,
   improvement 2): the payload always knew the nearest capture days, but only
   an EMPTY day offered the jump. Styled as caption-weight text, not a pill,
   so the row's height — and with it the stage budget the viewer is tuned to —
   doesn't move. */
.sn-player-cap {
    background: transparent;
    border: 0;
    padding: 0;
    margin: 0;
    font: inherit;
    font-size: .78rem;
    color: #8d97a6;
    cursor: pointer;
    text-decoration: underline;
    text-underline-offset: 2px;
}
.sn-player-cap:hover { color: #e8ecf3; }
.sn-player-cap:focus-visible {
    outline: 2px solid var(--sn-accent);
    outline-offset: 2px;
    border-radius: 3px;
}
.sn-player-cap[hidden] { display: none; }
.sn-player-note {
    margin: 0;
    color: #e6bd6e;
    font-size: .8rem;
}
.sn-player-note[hidden] { display: none; }
.sn-player-close {
    flex: 0 0 auto;
    background: transparent;
    border: 0;
    color: #fff;
    font-size: 1.9rem;
    line-height: 1;
    cursor: pointer;
}
.sn-player-close:focus-visible {
    outline: 2px solid #fff;
    outline-offset: 3px;
    border-radius: 4px;
}

.sn-player-stage {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 220px;
}
.sn-player-img {
    /* contain, not cover: a screenshot cropped to fit is a screenshot with the
       taskbar or the thing you were looking for cut off. */
    max-width: 92vw;
    max-height: 78vh;
    object-fit: contain;
    border-radius: 8px;
    background: #0b0d12;
    box-shadow: 0 12px 40px rgba(0, 0, 0, .55);
}
.sn-player-img[hidden] { display: none; }
/* Two monitors of one capture, side by side: they were one moment on one desk
   and read as one picture. Three or more use the tab bar instead. */
.sn-player-stage.is-split { gap: .5rem; }
.sn-player-stage.is-split .sn-player-img { max-width: 46vw; }
.sn-player-inline .sn-player-stage.is-split .sn-player-img { width: 50%; }
/* Monitor tabs. Sit between the caption and the stage so switching monitor
   never moves the picture. */
.sn-player-monitors {
    display: flex;
    flex-wrap: wrap;
    gap: .35rem;
}
.sn-player-monitors[hidden] { display: none; }
.sn-player-mon {
    background: rgba(255, 255, 255, .06);
    border: 1px solid rgba(255, 255, 255, .14);
    border-radius: 999px;
    color: #c4ccd8;
    cursor: pointer;
    font-size: .76rem;
    line-height: 1;
    padding: .3rem .65rem;
}
.sn-player-mon:hover { color: #e8ecf3; }
.sn-player-mon.is-current {
    background: rgba(255, 255, 255, .16);
    border-color: rgba(255, 255, 255, .32);
    color: #fff;
}
.sn-player-mon:focus-visible {
    outline: 2px solid var(--sn-accent);
    outline-offset: 2px;
}
.sn-player-blur[hidden] { display: none; }
/* "×3" on a filmstrip tile: one tile per CAPTURE, this says how many monitors
   that capture holds. */
.sn-player-strip-item { position: relative; }
.sn-player-strip-n {
    position: absolute;
    right: 3px;
    bottom: 3px;
    background: rgba(0, 0, 0, .68);
    border-radius: 3px;
    color: #fff;
    font-size: .62rem;
    line-height: 1;
    padding: .12rem .22rem;
}
/* Messages sit OVER the stage rather than in the column, so a day switch can
   dim the picture and say "Loading…" without the layout collapsing and
   springing back on every pick. */
.sn-player-stage-msg {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: .7rem;
    padding: 1rem;
    text-align: center;
    pointer-events: none;
}
.sn-player-msg {
    margin: 0;
    color: #c4ccd8;
    font-size: .92rem;
}
.sn-player-msg[hidden] { display: none; }
.is-loading .sn-player-img { opacity: .5; }
/* "Jump to the last capture" out of an empty day — the one place the reader is
   otherwise stuck with nothing but a date picker to guess with. */
.sn-player-empty-nav {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: .5rem;
    pointer-events: auto;
}
.sn-player-empty-nav[hidden] { display: none; }
.sn-player-empty-btn {
    min-height: 32px;
    padding: .3rem .75rem;
    border: 1px solid rgba(255, 255, 255, .22);
    border-radius: 7px;
    background: rgba(255, 255, 255, .08);
    color: #e8ecf3;
    font-size: .82rem;
    cursor: pointer;
}
.sn-player-empty-btn:hover { background: rgba(255, 255, 255, .18); }
.sn-player-empty-btn:focus-visible {
    outline: 2px solid #fff;
    outline-offset: 2px;
}

/* Timeline: one tick per frame across the axis window (the working day, not a
   flat 24h), so gaps in the day — lunch, a machine asleep — are visible as
   gaps rather than compressed away by an index-based scrubber. */
.sn-player-track {
    position: relative;
    height: 26px;
    border-radius: 5px;
    background: rgba(255, 255, 255, .09);
    cursor: pointer;
    touch-action: none;
}
.sn-player-track:focus-visible {
    outline: 2px solid var(--sn-accent);
    outline-offset: 2px;
}
.sn-player-ticks { position: absolute; inset: 0; }
.sn-player-tick {
    position: absolute;
    top: 6px;
    width: 1px;
    height: 14px;
    margin-left: -0.5px;
    background: rgba(255, 255, 255, .38);
}
.sn-player-marker {
    position: absolute;
    top: -3px;
    width: 3px;
    height: 32px;
    margin-left: -1.5px;
    border-radius: 2px;
    background: var(--sn-accent);
    box-shadow: 0 0 0 1px rgba(0, 0, 0, .45);
    pointer-events: none;
}
/* Time under the pointer. A bar of identical ticks is otherwise pure guesswork
   to scrub. */
.sn-player-bubble {
    position: absolute;
    bottom: calc(100% + 6px);
    transform: translateX(-50%);
    padding: .15rem .4rem;
    border-radius: 5px;
    background: #10141c;
    border: 1px solid rgba(255, 255, 255, .2);
    color: #e8ecf3;
    font-size: .72rem;
    line-height: 1.4;
    white-space: nowrap;
    pointer-events: none;
}
.sn-player-bubble[hidden] { display: none; }
.sn-player-axis {
    position: relative;
    height: 1.1rem;
    font-size: .7rem;
    color: #98a2b1;
}
.sn-player-axis-label {
    position: absolute;
    transform: translateX(-50%);
    white-space: nowrap;
}
/* Hour labels rarely land on 0% and 100%, so the edge cases are tagged by
   app.js rather than inferred from :first-child/:last-child (which pulled the
   wrong labels off-centre once the axis became hourly). */
.sn-player-axis-label.is-start { transform: none; }
.sn-player-axis-label.is-end { transform: translateX(-100%); }

.sn-player-controls {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .4rem;
}
.sn-player-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 36px;
    height: 32px;
    padding: 0 .5rem;
    border: 1px solid rgba(255, 255, 255, .18);
    border-radius: 7px;
    background: rgba(255, 255, 255, .07);
    color: #e8ecf3;
    font-size: 15px;
    line-height: 1;
    cursor: pointer;
}
.sn-player-btn:hover { background: rgba(255, 255, 255, .16); }
.sn-player-btn:focus-visible {
    outline: 2px solid var(--sn-accent);
    outline-offset: 2px;
}
.sn-player-speed-label {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    margin: 0 0 0 .35rem;
    font-size: .78rem;
    color: #b7c0cd;
}
/* Bootstrap's .form-select paints a light control, which on this deliberately
   dark panel reads as a hole. Re-skin it to match the transport buttons in
   BOTH themes — the panel is dark either way. */
.sn-player-speed.form-select {
    width: auto;
    height: 32px;
    padding: 0 1.9rem 0 .55rem;
    border: 1px solid rgba(255, 255, 255, .18);
    background-color: rgba(255, 255, 255, .07);
    color: #e8ecf3;
    font-size: .78rem;
}
.sn-player-speed.form-select option { color: #0F1B2D; background: #fff; }

/* Save / Open full size / Fullscreen. Right-aligned on the controls row and
   styled as buttons, not links: "a way to save/download that screenshot" is a
   primary action on this page, not a footnote. */
.sn-player-links {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    margin-left: auto;
    font-size: .82rem;
}
.sn-player-links > a,
.sn-player-links > button {
    display: inline-flex;
    align-items: center;
    min-height: 32px;
    padding: 0 .7rem;
    border: 1px solid rgba(255, 255, 255, .18);
    border-radius: 7px;
    background: rgba(255, 255, 255, .07);
    /* #cfd6e0 on this panel is ~9:1 — comfortably past AA for small text. */
    color: #cfd6e0;
    font-size: .82rem;
    line-height: 1;
    text-decoration: none;
    cursor: pointer;
}
.sn-player-links > a:hover,
.sn-player-links > button:hover {
    background: rgba(255, 255, 255, .16);
    color: #fff;
    text-decoration: none;
}
.sn-player-links > a:focus-visible,
.sn-player-links > button:focus-visible {
    outline: 2px solid #fff;
    outline-offset: 2px;
}
/* Fullscreen only means anything to the inline viewer — the overlay IS it. */
.sn-player-fs { display: none; }

/* Filmstrip — inline only. One thumb per frame, so a reader can jump by
   looking rather than by scrubbing a bar of identical ticks. */
.sn-player-strip {
    position: relative;
    display: flex;
    gap: .3rem;
    height: 84px;
    padding: .25rem 0;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: thin;
    scrollbar-color: rgba(255, 255, 255, .3) transparent;
}
/* Not built for the overlay, and not built past the frame cap. */
.sn-player-strip:empty { display: none; }
.sn-player-strip::-webkit-scrollbar { height: 7px; }
.sn-player-strip::-webkit-scrollbar-thumb {
    background: rgba(255, 255, 255, .3);
    border-radius: 4px;
}
.sn-player-strip-item {
    flex: 0 0 auto;
    width: 112px;
    height: 70px;
    padding: 0;
    border: 1px solid rgba(255, 255, 255, .16);
    border-radius: 5px;
    background: #0b0d12;
    overflow: hidden;
    cursor: pointer;
}
.sn-player-strip-item img {
    display: block;
    width: 100%;
    height: 100%;
    /* cover, not contain: at 112px the frame is a colour-and-shape cue for
       "which part of the day is this", and letterboxing wastes half of it. */
    object-fit: cover;
}
.sn-player-strip-item.is-current {
    border-color: var(--sn-accent);
    box-shadow: 0 0 0 2px var(--sn-accent);
}
.sn-player-strip-item:focus-visible {
    outline: 2px solid #fff;
    outline-offset: 2px;
}

/* --------------------------------------------------------------------------
   The inline day viewer. Same panel, mounted in the page instead of over it.

   "show the screenshots with the slider below it on the bottom half of the
   page" — the whole thing has to fit a laptop viewport without scrolling, so
   the stage takes whatever is left and --sn-stage-reserve is everything else.
   That number is MEASURED, not guessed (the page rendered against these
   stylesheets, then scrollHeight minus the stage): topbar 65 + page head 48.4
   + its 16 margin + pickers 52.3 + their 11.2 margin + card border 2 + panel
   padding 24 + caption 24.5 + track 26 + axis 17.6 + controls 32 + strip 84 +
   the five .55rem gaps 44 + card margin 20 + footer 38.9 = 506. 508 leaves a
   hair of slack, so a 768px-tall viewport gets a 260px stage and no scrollbar.
   Below ~748px tall the 240px floor wins and the page scrolls a little: a
   stage thinner than that shows nothing worth looking at.
   -------------------------------------------------------------------------- */
.sn-viewer.card {
    background: #0d1117;
    border-color: #1e2531;
    overflow: hidden;
    --sn-stage-reserve: 508px;
}
.sn-player-panel.sn-player-inline {
    max-width: none;
    padding: .75rem;
}
/* The page IS the frame; there is nothing to close. */
.sn-player-inline .sn-player-close { display: none; }
.sn-player-inline .sn-player-fs { display: inline-flex; }
.sn-player-inline .sn-player-stage {
    height: clamp(240px, calc(100vh - var(--sn-stage-reserve)), 900px);
    min-height: 0;
    border-radius: 8px;
    background: #0b0d12;
}
.sn-player-inline .sn-player-img {
    width: 100%;
    height: 100%;
    max-width: 100%;
    max-height: 100%;
    border-radius: 8px;
    box-shadow: none;
}
/* Clicking the picture asks for it full-screen. */
.sn-player-inline .sn-player-img { cursor: zoom-in; }

@media (max-width: 768px) {
    .sn-player { padding: .6rem; }
    .sn-player-img { max-height: 60vh; }
    .sn-player-links { margin-left: 0; width: 100%; }
    /* Two rows: the day group keeps its own line, the employee select takes
       the next one full-width. */
    .sn-shot-picker { flex: 1 1 100%; }
    .sn-shot-user-select { min-width: 0; max-width: none; width: 100%; }
    .sn-viewer.card { --sn-stage-reserve: 380px; }
    .sn-player-inline .sn-player-stage {
        height: clamp(200px, calc(100vh - var(--sn-stage-reserve)), 900px);
    }
    .sn-player-strip { height: 64px; }
    .sn-player-strip-item { width: 84px; height: 52px; }
    .sn-player-axis { font-size: .62rem; }
}

/* --------------------------------------------------------------------------
   Mobile — the master/detail layout stacks (templates put the list above the
   detail on narrow screens). Shorten the ranked list so it doesn't eat the
   whole viewport before the detail panel; template agents add scroll-into-view
   on row tap. Chart/toolbar wrap naturally via their existing flex rules.
   -------------------------------------------------------------------------- */
@media (max-width: 768px) {
    .sn-md-rows {
        max-height: 320px;
    }
    .sn-md-toolbar {
        gap: .4rem;
    }
    /* Let the per-day chart keep its horizontal scroll rather than crushing
       bars below tap size. */
    .sn-md-chart {
        height: 150px;
    }
}

/* --------------------------------------------------------------------------
   htmx integration — indicators + spinner.
   `hx-indicator` targets get .htmx-request added while a request is in flight;
   .htmx-indicator elements are hidden until then. htmx ships baseline rules,
   but we redeclare them here so they survive our cascade and can theme. The
   .sn-spin utility spins any icon (e.g. ti-loader-2) used as a busy indicator.
   -------------------------------------------------------------------------- */
.htmx-indicator {
    opacity: 0;
    transition: opacity .15s ease;
}
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
    opacity: 1;
}

@media (prefers-reduced-motion: no-preference) {
    .sn-spin {
        animation: sn-spin 0.8s linear infinite;
    }
}
@keyframes sn-spin {
    to { transform: rotate(360deg); }
}

/* Global htmx error toast (wired in base.html). Fixed, dismissible, above the
   lightbox. Hidden until JS reveals it on htmx:responseError/sendError/timeout. */
.sn-htmx-toast {
    position: fixed;
    top: 1rem;
    right: 1rem;
    z-index: 1090;
    max-width: 360px;
    display: none;
    align-items: flex-start;
    gap: .5rem;
    padding: .7rem .85rem;
    border-radius: var(--sn-radius);
    background: var(--sn-unprod-soft);
    border: 1px solid var(--sn-unprod);
    color: var(--sn-unprod-text);
    box-shadow: var(--sn-shadow);
    font-size: .85rem;
}
.sn-htmx-toast.is-open { display: flex; }
.sn-htmx-toast .sn-htmx-toast-msg { flex: 1 1 auto; min-width: 0; }
.sn-htmx-toast .sn-htmx-toast-close {
    flex: 0 0 auto;
    background: transparent;
    border: 0;
    color: inherit;
    font-size: 1.1rem;
    line-height: 1;
    cursor: pointer;
    padding: 0;
}
.sn-htmx-toast .sn-htmx-toast-close:focus-visible {
    outline: 2px solid var(--sn-unprod-text);
    outline-offset: 2px;
    border-radius: 3px;
}

/* --------------------------------------------------------------------------
   Jump palette — Ctrl/Cmd+K. Built entirely by app.js (snPaletteBuild), so no
   template carries markup for it; these are the only styles it has. Unlike the
   screenshot player it is chrome, not a photograph, so it takes design tokens
   in both themes rather than painting its own dark ground.
   -------------------------------------------------------------------------- */
.sn-jump {
    position: fixed;
    inset: 0;
    z-index: 1090;              /* over the player's 1085 backdrop */
    display: flex;
    justify-content: center;
    align-items: flex-start;
    padding: 12vh 1rem 1rem;
    background: rgba(9, 11, 16, .45);
}
.sn-jump[hidden] { display: none; }
body.sn-jump-lock { overflow: hidden; }

.sn-jump-panel {
    width: 100%;
    max-width: 560px;
    max-height: 70vh;
    display: flex;
    flex-direction: column;
    background: var(--sn-card);
    border: 1px solid var(--sn-line);
    border-radius: var(--sn-radius);
    box-shadow: 0 12px 40px rgba(9, 11, 16, .28);
    overflow: hidden;
}
.sn-jump-input {
    flex: 0 0 auto;
    width: 100%;
    border: 0;
    border-bottom: 1px solid var(--sn-line);
    background: transparent;
    color: var(--sn-ink);
    font-size: 1rem;
    padding: .8rem 1rem;
    min-height: 44px;
}
.sn-jump-input::placeholder { color: var(--sn-muted); }
.sn-jump-input:focus {
    outline: none;
    border-bottom-color: var(--sn-accent);
}
.sn-jump-list {
    flex: 1 1 auto;
    overflow-y: auto;
    padding: .3rem;
}
.sn-jump-row {
    display: flex;
    align-items: center;
    gap: .6rem;
    padding: .45rem .6rem;
    border-radius: 7px;
    color: var(--sn-ink);
    text-decoration: none;
    font-size: .9rem;
}
.sn-jump-row .ti {
    flex: 0 0 auto;
    font-size: 16px;
    color: var(--sn-muted);
}
.sn-jump-row-label {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.sn-jump-row-group {
    flex: 0 0 auto;
    font-size: .74rem;
    color: var(--sn-muted);
}
.sn-jump-row:hover,
.sn-jump-row.is-active {
    background: var(--sn-accent-soft);
    color: var(--sn-ink);
}
.sn-jump-row.is-active .ti { color: var(--sn-accent-text); }
.sn-jump-empty,
.sn-jump-hint {
    margin: 0;
    padding: .55rem 1rem;
    font-size: .78rem;
    color: var(--sn-muted);
}
.sn-jump-hint { border-top: 1px solid var(--sn-line); }
.sn-jump-empty[hidden] { display: none; }

/* Topbar hint chip (partials/topbar.html) — deliberately quiet; it is a
   reminder that the shortcut exists, not a call to action. */
.sn-jump-chip {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    border: 1px solid var(--sn-line);
    border-radius: 999px;
    background: transparent;
    color: var(--sn-muted);
    padding: .25rem .6rem;
    font-size: .78rem;
    line-height: 1;
    cursor: pointer;
}
.sn-jump-chip:hover { color: var(--sn-ink); border-color: var(--sn-muted); }
.sn-jump-chip:focus-visible {
    outline: 2px solid var(--sn-accent);
    outline-offset: 1px;
}
.sn-jump-chip[hidden] { display: none; }
.sn-jump-chip .ti { font-size: 15px; }
.sn-jump-chip kbd {
    background: transparent;
    color: inherit;
    padding: 0;
    font-size: .78rem;
}

/* Copy buttons: the check that replaces the icon for a beat reads as success
   rather than as "the icon changed". */
.sn-copied { color: var(--sn-accent-text); }
.sn-copy-btn:focus-visible {
    outline: 2px solid var(--sn-accent);
    outline-offset: 1px;
    border-radius: 5px;
}

/* Type-to-filter boxes (app.js snFilterList). Comfortable hit target, capped
   width so a filter never reads as the page's main input. */
.sn-list-filter {
    min-height: 32px;
    max-width: 320px;
}
.sn-list-filter:focus {
    border-color: var(--sn-accent);
    box-shadow: 0 0 0 .2rem var(--sn-accent-soft);
}

/* --------------------------------------------------------------------------
   Alert text contrast (WCAG 1.4.3, small text, 4.5:1).

   The theme paints every .alert-* with `--theme-<v>-text-emphasis` on
   `--theme-<v>-bg-subtle`. Measured, every variant in the LIGHT theme fails,
   several of them badly — and these are the banners carrying "workspace
   locked", "pilot expires in 3 days" and "account suspended", which is exactly
   the copy nobody may squint at:

       variant     theme colour on subtle bg   was      now
       success     #0e9d6e on #dbf5ec          3.01:1   4.85:1  --sn-prod-text
       info        #059bb4 on #daf4f9          2.88:1   5.37:1  --sn-info-text
       secondary   #059bb4 on #daf4f9          2.88:1   5.37:1  --sn-info-text
       warning     #d08609 on #fef0da          2.63:1   4.76:1  --sn-neut-text
       danger      #cb3a3a on #fde3e3          4.11:1   4.88:1  --sn-unprod-text
       light       #8a969c on #fbfbfd          2.93:1   6.03:1  --sn-muted

   The DARK theme already passes everywhere (5.78:1 – 14.59:1 measured on the
   20%-tint background over both the canvas #17181e and a card #1e1f27), so
   nothing there needed rescuing. It gets the same rule anyway, because the band
   tokens are already defined per theme and their dark values also clear AA on
   that tint — success 6.61/6.08, warning 6.87/6.32, danger 6.26/5.80,
   info 7.24/6.66, light (--sn-muted on #22232c) 4.86 — so one rule serves both
   themes and there is no light-only branch to forget about later.

   Only the TEXT moves. The fills and borders are the theme's, they are what
   makes a warning look like a warning, and non-text contrast (1.4.11) is not
   in question for a band that also carries an icon and a heading.
   -------------------------------------------------------------------------- */
.alert-success { --theme-alert-color: var(--sn-prod-text);   --theme-alert-link-color: var(--sn-prod-text); }
.alert-warning { --theme-alert-color: var(--sn-neut-text);   --theme-alert-link-color: var(--sn-neut-text); }
.alert-danger  { --theme-alert-color: var(--sn-unprod-text); --theme-alert-link-color: var(--sn-unprod-text); }
.alert-info,
.alert-secondary { --theme-alert-color: var(--sn-info-text); --theme-alert-link-color: var(--sn-info-text); }
.alert-light   { --theme-alert-color: var(--sn-muted);       --theme-alert-link-color: var(--sn-muted); }

/* --------------------------------------------------------------------------
   Row density — html[data-density="compact"], set before the first paint by
   base.html's head snippet and toggled from the topbar (app.js snDensity*).

   Comfortable is the default and is simply the absence of the attribute, so
   this is the whole feature: tables, card padding and the table font size. It
   deliberately does NOT touch the rail, the topbar, buttons, form controls or
   the body font — "compact" here means "fit more rows on the screen", not
   "shrink the application", and hit targets stay where they are.

   @media screen because print has its own density (paper), and a printed
   report should look the same whichever way the reader happens to be browsing.
   The tokens are theme-neutral, so both themes get it for free.
   -------------------------------------------------------------------------- */
@media screen {
    [data-density="compact"] .table > :not(caption) > * > * {
        padding: .4rem .5rem;       /* theme default: .75rem .6rem */
    }
    [data-density="compact"] .table-sm > :not(caption) > * > * {
        padding: .3rem .4rem;       /* theme default: .5rem */
    }
    [data-density="compact"] .table {
        font-size: .8125rem;
    }
    [data-density="compact"] .card {
        --theme-card-spacer-y: .85rem;       /* theme default: 1.25rem */
        --theme-card-spacer-x: 1rem;         /* theme default: 1.25rem */
        --theme-card-cap-padding-y: .6rem;   /* theme default: .9375rem */
        --theme-card-cap-padding-x: 1rem;    /* theme default: 1.25rem */
    }
    /* A capped scrolling table is about screenfuls, and a compact screenful
       holds more rows — let it use them. */
    [data-density="compact"] .sn-table-scroll {
        max-height: min(72vh, 820px);
    }
}

/* --------------------------------------------------------------------------
   Print — hide the shell chrome (rail, topbar, filters, footer, actions) and
   expand the report content to the full page. Keeps tables/timelines legible.
   -------------------------------------------------------------------------- */
@media print {
    /* Re-pin the LIGHT token set even if the user prints from dark mode —
       otherwise dark canvas/ink tokens carry onto white paper (grey text on
       grey fills). Print is always light-on-white. */
    :root,
    [data-bs-theme="dark"] {
        --sn-canvas: #ffffff;
        --sn-card: #ffffff;
        --sn-line: #DFE3E9;
        --sn-ink: #0F1B2D;
        --sn-muted: #4a5160;
        --sn-prod: #4c9a6b;
        --sn-neut: #cf9a3d;
        --sn-unprod: #c6493b;
        --sn-prod-text: #37744f;
        --sn-neut-text: #8a6420;
        --sn-unprod-text: #b3392c;
        --sn-idle: #9aa3b2;
        --sn-offline: #d7dbe3;
        --sn-prod-soft: #e7f0ea;
        --sn-neut-soft: #f7eeda;
        --sn-unprod-soft: #f6e2df;
    }

    /* Shell chrome — remove entirely from the printed page. */
    .app-topbar,
    .sidenav-menu,
    .footer,
    .sn-page-head-filters,
    .sn-page-head-actions,
    .sn-fav-strip,
    .sn-htmx-toast,
    /* The jump palette and its topbar hint are navigation, and navigation is
       not part of a printed report. */
    .sn-jump,
    .sn-jump-chip,
    /* Interactive-only chrome from the see/use round: a dead search box on a
       printed roster and camera icons on printed log rows are just ink. */
    .sn-list-filter,
    .sn-copy-btn,
    .sn-rn-cam,
    .sn-row-cam-link,
    /* Dead ink on paper: the marker is an icon whose whole meaning lives in a
       tooltip, and paper has no hover. */
    .sn-late-mark,
    /* Pager buttons are dead ink; the "Showing 1–25 of 40" line beside them is
       NOT, so it stays. A paginated table prints the page on screen, the same
       way a filtered one prints what the filter left — both hide rows with an
       inline display, so paper cannot tell them apart, and "print what you are
       looking at" is the answer that never surprises anyone. The retained info
       line is what tells the reader the sheet is one page of several. */
    [data-pager-controls],
    /* The overlay player is dead ink on paper, and so is every transport
       control in the inline one: printing the Screenshots page means printing
       the frame on screen and who/when it is, not a paused video player. */
    .sn-player,
    .sn-player-track,
    .sn-player-axis,
    .sn-player-controls,
    .sn-player-strip,
    .sn-shot-day-nav .sn-shot-day-btn {
        display: none !important;
    }

    /* The day viewer paints itself dark on screen because its subject is a
       photograph. On white paper that is a wasteful black rectangle with
       invisible pale-grey caption text, so flatten it back to ink-on-white and
       let the image size itself to the page instead of the viewport. */
    .sn-viewer.card,
    .sn-player-inline .sn-player-stage {
        background: #fff !important;
    }
    .sn-player-inline,
    .sn-player-inline .sn-player-name,
    .sn-player-inline .sn-player-day,
    .sn-player-inline .sn-player-when,
    .sn-player-inline .sn-player-count {
        color: #0F1B2D !important;
    }
    .sn-player-inline .sn-player-stage {
        height: auto !important;
        min-height: 0 !important;
    }
    .sn-player-inline .sn-player-img {
        height: auto !important;
        max-height: 78vh !important;
        box-shadow: none !important;
        -webkit-print-color-adjust: exact !important;
        print-color-adjust: exact !important;
    }

    /* Reclaim the full page width the rail used to occupy. */
    .content-page,
    .wrapper,
    .container-fluid {
        margin: 0 !important;
        padding: 0 !important;
        width: 100% !important;
        max-width: 100% !important;
    }

    body { background: #fff !important; }

    /* Flatten card surfaces so ink isn't wasted on shadows/borders. */
    .card {
        box-shadow: none !important;
        border: 1px solid #ccc !important;
        break-inside: avoid;
    }

    .sn-page-title { font-size: 1.2rem !important; }

    /* Force browsers to actually ink the data-viz fills. Without exact color
       adjust, background-colored bars/stripes/pills print BLANK (browsers drop
       backgrounds to save toner) — which is exactly the data we're printing. */
    .ratio-bar,
    .ratio-bar .ratio-seg,
    .sn-trend-stack,
    .sn-trend-seg,
    .kpi-stripe,
    .score-pill,
    .sn-class-badge,
    .sn-md-bar,
    .sn-md-fill,
    .sn-md-user-bar,
    .sn-md-chart-track,
    .sn-md-chart-fill,
    .badge-productive,
    .badge-neutral,
    .badge-unproductive,
    /* Machines-page fleet badges (status pills + outdated chip). Subtle-bg
       Bootstrap badges otherwise print blank, dropping the status colour that
       is the point of the column. */
    .sn-fleet-table .badge {
        -webkit-print-color-adjust: exact !important;
        print-color-adjust: exact !important;
    }

    /* Let wide tables/timelines print in full instead of scrolling. */
    .table-responsive,
    .sn-md-rows,
    .sn-table-scroll,
    [style*="overflow-x"] {
        overflow: visible !important;
    }

    /* A capped, internally-scrolling table would otherwise print one screenful
       and drop the rest; the sticky header would repeat itself mid-page. */
    .sn-table-scroll { max-height: none !important; }
    .sn-table-scroll thead th {
        position: static !important;
        box-shadow: none !important;
    }

    /* The ranked list caps at 620px on screen; on paper that clips/overlaps
       the following content. Let it flow to its natural height. */
    .sn-md-rows {
        max-height: none !important;
    }

    a[href]::after { content: ""; }  /* don't append raw URLs after links */
}


/* ---------------------------------------------------------------------------
   Condensed rail — icon-only compact mode (hamburger toggles default <->
   condensed). The theme narrows .sidenav-menu to --theme-sidenav-width-sm;
   these rules hide every text bit of the custom rail and center the icons.
--------------------------------------------------------------------------- */
[data-sidenav-size="condensed"] .sidenav-menu .menu-text,
[data-sidenav-size="condensed"] .sidenav-menu .sn-nav-badge,
[data-sidenav-size="condensed"] .sidenav-menu .sn-nav-group-chevron,
[data-sidenav-size="condensed"] .sidenav-menu .sn-nav-group-title > span,
[data-sidenav-size="condensed"] .sidenav-menu .sn-rail-tenant,
[data-sidenav-size="condensed"] .sidenav-menu .sn-rail-tenant-filter {
    display: none;
}

[data-sidenav-size="condensed"] .sn-nav-group-title,
[data-sidenav-size="condensed"] .sn-nav-link.side-nav-link {
    justify-content: center;
    padding-inline: 0;
}

[data-sidenav-size="condensed"] .sn-nav-link .menu-icon {
    margin: 0;
}

/* Collapse state carries over into icon mode unchanged — a collapsed group
   shows just its group icon. Clicking a group icon while condensed reopens
   the full rail with that group expanded (nav.js). */
