/* ParkOps — application shell on top of Bootstrap 5.
   Design language: soft neutral. Light warm-grey ground, white surfaces
   separated by hairlines rather than shadows, generous radii, one restrained
   accent. Bootstrap still supplies the components; this file supplies the
   frame, the table chrome and the attendance-specific pieces. */

:root {
    --sidebar-width: 258px;
    /* Wide enough for a centred 1.15rem glyph plus its hit area; anything less
       and the icons stop being comfortable targets. */
    --sidebar-rail: 4.25rem;

    /* Ground and surfaces */
    --surface: #ffffff;
    --surface-sunken: #fafaf9;
    --surface-raised: #ffffff;
    --app-bg: #f7f7f6;

    /* Hairlines — the only separator this design uses */
    --line: #e9e7e4;
    --line-strong: #dcd9d4;

    /* Ink */
    --ink: #1c1a17;
    --ink-soft: #57534e;
    --ink-muted: #8b857e;

    /* The sticky navbar's rendered height. Anything else that sticks to the top
       of the page parks below it rather than sliding underneath. */
    --navbar-height: 65px;

    /* Single accent, plus its wash for active/selected states */
    --accent: #4f46e5;
    --accent-ink: #4338ca;
    --accent-wash: #eef2ff;

    --radius-lg: 16px;
    --radius-md: 11px;
    --radius-sm: 8px;

    /* One inset for every band of the table card — title, toolbar, the gutter
       around the grid, the footer. A token rather than four copies of a number
       precisely so they cannot drift apart the next time one is nudged. */
    --table-inset: 1.05rem;

    /* Cells are tighter than the card's own inset: the grid is boxed and inset
       already, so paying the card's gutter again inside every one of nine
       columns costs three hundred pixels that the names would rather have. */
    --cell-pad-x: .78rem;

    /* Row height, as a token for the same reason as the inset: every body cell
       derives its vertical padding from it, so the grid's density retunes from
       one place instead of a dozen. */
    --row-pad-y: .78rem;

    /* Banding and hover, in that order of strength: the stripe only has to be
       felt, the hover has to be seen — and has to beat the stripe, or hovering
       an already-banded row would look like nothing happened. */
    --row-stripe: #f7f6f3;
    --row-hover: #eae7e1;

    /* The tint behind a table's header and footer. --surface-sunken is a
       whisker away from white — enough for a hover, not enough to make a band
       read as a band. */
    --chrome-bg: #f3f2ee;

    /* Status palette: a soft wash and a readable ink per semantic. Twenty rows
       of Bootstrap's saturated .text-bg-* fills turn a table into a traffic
       jam; these carry the same meaning at a fraction of the volume. */
    --ok-wash: #ecfdf5;      --ok-ink: #047857;      --ok-line: #a7f3d0;
    --warn-wash: #fffbeb;    --warn-ink: #b45309;    --warn-line: #fde68a;
    --danger-wash: #fef2f2;  --danger-ink: #b91c1c;  --danger-line: #fecaca;
    --info-wash: #eff6ff;    --info-ink: #1d4ed8;    --info-line: #bfdbfe;
    --neutral-wash: #f5f5f4; --neutral-ink: #57534e; --neutral-line: #e2e0dc;
    --strong-wash: #eceae7;  --strong-ink: #292524;  --strong-line: #d6d3d1;

    /* The only shadow in the design: just enough to lift a surface off the
       ground without becoming a drop shadow. */
    --shadow-sm: 0 1px 2px rgba(28, 26, 23, .04);
    --shadow-md: 0 8px 24px rgba(28, 26, 23, .07);
}

[data-bs-theme="dark"] {
    --surface: #1c1c1b;
    --surface-sunken: #191918;
    --surface-raised: #232322;
    --app-bg: #131312;

    --line: #2e2d2b;
    --line-strong: #3d3b38;

    --ink: #ece9e5;
    --ink-soft: #b5afa7;
    --ink-muted: #8a847c;

    --accent: #818cf8;
    --accent-ink: #a5b4fc;
    --accent-wash: #262a45;

    --row-stripe: #1f1f1e;
    --row-hover: #2b2b29;
    --chrome-bg: #201f1e;

    /* Washes are alpha rather than flat colours here: they have to sit on both
       the card and the hovered row without either one showing a seam. */
    --ok-wash: rgba(16, 185, 129, .15);   --ok-ink: #6ee7b7;      --ok-line: rgba(16, 185, 129, .3);
    --warn-wash: rgba(245, 158, 11, .16); --warn-ink: #fcd34d;    --warn-line: rgba(245, 158, 11, .32);
    --danger-wash: rgba(239, 68, 68, .16);--danger-ink: #fca5a5;  --danger-line: rgba(239, 68, 68, .32);
    --info-wash: rgba(59, 130, 246, .16); --info-ink: #93c5fd;    --info-line: rgba(59, 130, 246, .32);
    --neutral-wash: #2a2a28;              --neutral-ink: #b5afa7; --neutral-line: #3a3937;
    --strong-wash: #373633;               --strong-ink: #e7e5e4;  --strong-line: #484643;

    --shadow-sm: 0 1px 2px rgba(0, 0, 0, .3);
    --shadow-md: 0 8px 24px rgba(0, 0, 0, .38);
}

body {
    background-color: var(--app-bg);
    color: var(--ink);
    font-family: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    font-size: .925rem;
    -webkit-text-size-adjust: 100%;
    -webkit-font-smoothing: antialiased;
}

/* Bootstrap paints its own body colour; keep the two in step so components
   inherit the same ground in both themes. */
[data-bs-theme] body {
    --bs-body-bg: var(--app-bg);
    --bs-body-color: var(--ink);
    --bs-border-color: var(--line);
    --bs-secondary-color: var(--ink-soft);
    --bs-emphasis-color: var(--ink);
    --bs-link-color-rgb: 79, 70, 229;
    --bs-link-hover-color-rgb: 67, 56, 202;
}

[data-bs-theme="dark"] body {
    --bs-link-color-rgb: 129, 140, 248;
    --bs-link-hover-color-rgb: 165, 180, 252;
}

h1, h2, h3, h4, h5, h6 {
    letter-spacing: -.015em;
    font-weight: 600;
}

/* ── Shell ────────────────────────────────────────────────────────────── */

.app-shell {
    display: flex;
    min-height: 100vh;
}

.app-main {
    flex: 1 1 auto;
    min-width: 0; /* lets wide tables scroll instead of stretching the page */
    display: flex;
    flex-direction: column;
}

.app-content {
    flex: 1 1 auto;
}

@media (min-width: 992px) {
    .app-sidebar {
        width: var(--sidebar-width);
        flex: 0 0 var(--sidebar-width);
        position: sticky;
        top: 0;
        height: 100vh;
        overflow: hidden; /* clips the rail's contents while it animates shut */
        transition: width .18s ease, flex-basis .18s ease;
        display: flex;
        flex-direction: column;
    }

    /* Above the offcanvas breakpoint Bootstrap turns the body back into a plain
       block with `flex-grow: 0`, so a menu taller than the viewport simply ran
       past the rail's `overflow: hidden` with no way to reach the rest. Giving
       it the rail's full height puts the overflow back inside .sidebar-nav,
       which is what scrolls. */
    .app-sidebar > .offcanvas-body {
        flex: 1 1 auto;
        min-height: 0;
    }

    /* Collapsed to a rail, not to nothing. Zero width did animate shut nicely,
       but it also took the navigation with it — the only way back to another
       screen was to reopen the sidebar first. Keeping the icons leaves every
       destination one click away while still handing the width to the content.
       A width rather than display:none, so the close and reopen still animate
       and the offcanvas markup underneath is left untouched. */
    .sidebar-collapsed .app-sidebar {
        width: var(--sidebar-rail);
        flex-basis: var(--sidebar-rail);
    }

    /* Everything that is words goes; everything that is a glyph stays. */
    .sidebar-collapsed .sidebar-nav .nav-link span,
    .sidebar-collapsed .sidebar-heading,
    .sidebar-collapsed .brand-text,
    .sidebar-collapsed .sidebar-footer {
        display: none;
    }

    /* A group folded shut by the stored state would take its icons down with
       it, and in the rail there is no heading left to unfold it by — so the
       fold is suspended here rather than left to strand its own items. */
    .sidebar-collapsed .sidebar-nav .collapse {
        display: block;
    }

    .sidebar-collapsed .sidebar-nav {
        padding-left: .45rem;
        padding-right: .45rem;
    }

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

    .sidebar-collapsed .sidebar-nav .nav-link i {
        width: auto;
        font-size: 1.15rem;
    }

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

    .sidebar-collapsed .brand {
        gap: 0;
    }
}

@media (prefers-reduced-motion: reduce) {
    .app-sidebar,
    .sidebar-caret {
        transition: none;
    }
}

/* ── Sidebar ──────────────────────────────────────────────────────────── */

.app-sidebar {
    background: var(--surface-sunken);
    border-right: 1px solid var(--line);
    color: var(--ink-soft);
}

.app-sidebar .offcanvas-header {
    background: var(--surface-sunken);
    color: var(--ink);
    border-color: var(--line) !important;
}

.sidebar-brand {
    flex: 0 0 auto;
    align-items: center;
    padding: 1.25rem 1.35rem 1.1rem;
}

/* ── Brand lockup ─────────────────────────────────────────────────────── */

/* Rendered by <x-brand>. The mark is parkops-icon.svg as supplied; the
   wordmark is HTML text rather than parkops-logo.svg, because an SVG used as
   an <img> is an isolated document that cannot load the Poppins the artwork
   asks for — it would fall back to a system face and stop being the wordmark.
   Everything below reproduces the artwork's own values: Poppins 500, "Park" in
   #111827 with "Ops" in #1f4fd8, and the tagline in #6b7280 with the same
   2/15em tracking the SVG sets. */
.brand {
    --brand-mark: 2.1rem;
    --brand-name: 1.05rem;
    /* The artwork sets the tagline at 15/56 of the wordmark, which lands under
       6px once the lockup is chrome-sized. Stated per size instead, at the
       smallest that still reads. */
    --brand-tagline: .64rem;
    --brand-gap: .6rem;

    display: inline-flex;
    align-items: center;
    gap: var(--brand-gap);
    min-width: 0;
    text-decoration: none;
}

.brand-lg {
    --brand-mark: 3.4rem;
    --brand-name: 1.75rem;
    --brand-tagline: .72rem;
    --brand-gap: .8rem;
}

/* A fixed box, or a slow SVG reflows the row it sits in as it arrives. */
.brand-mark {
    flex: 0 0 auto;
    width: var(--brand-mark);
    height: var(--brand-mark);
    display: block;
}

.brand-text {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

.brand-name {
    font-family: "Poppins", "Inter", system-ui, sans-serif;
    font-weight: 500;
    font-size: var(--brand-name);
    line-height: 1.1;
    letter-spacing: -.005em;
    color: #111827;
    white-space: nowrap;
}

.brand-name > span {
    color: #1f4fd8;
}

.brand-tagline {
    font-family: "Poppins", "Inter", system-ui, sans-serif;
    font-size: var(--brand-tagline);
    line-height: 1.35;
    letter-spacing: .133em;
    color: #6b7280;
    margin-top: .18em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* The supplied dark artwork puts the same #1f4fd8 on a near-black plate, which
   works at logo size but drops to about 2.2:1 against our dark surface — below
   readable for a 17px wordmark. Same hue, lifted until it can be read. */
[data-bs-theme="dark"] .brand-name {
    color: #ffffff;
}

[data-bs-theme="dark"] .brand-name > span {
    color: #6b86ff;
}

[data-bs-theme="dark"] .brand-tagline {
    color: var(--ink-muted);
}

/* The markup asks for a column with `flex-column`, but that utility only sets
   the direction — Bootstrap never makes .offcanvas-body a flex container, so
   the body stayed a block and .sidebar-nav never received a height to scroll
   within. It still became a scroll container of its own (overflow-y: auto
   below), and `overscroll-behavior: contain` then refused to pass the gesture
   up to the body that could actually scroll. The result on a phone: a menu
   taller than the drawer that would not move at all.
   Declared for every width, because the desktop rail has the same shape — it
   only escaped notice there by usually being short enough to fit. */
.app-sidebar > .offcanvas-body {
    display: flex;
    min-height: 0;
}

.sidebar-nav {
    overflow-y: auto;
    /* A flex item will not shrink below its content unless told to, which is
       what kept the scrollbar from ever appearing. Brand and footer stay put;
       only the menu scrolls. */
    min-height: 0;
    overscroll-behavior: contain;
    padding: .35rem .7rem 1rem;
}

.sidebar-heading {
    padding: 1.15rem .65rem .4rem;
    font-size: .67rem;
    font-weight: 600;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--ink-muted);
}

/* The heading is a <button> now; strip the control chrome so it still reads as a
   quiet label rather than growing into a second tier of buttons in the rail. */
.sidebar-heading-toggle {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .5rem;
    width: 100%;
    background: none;
    border: 0;
    text-align: left;
    transition: color .12s ease;
}

.sidebar-heading-toggle:hover {
    color: var(--ink-soft);
}

.sidebar-caret {
    font-size: .7rem;
    transition: transform .18s ease;
}

.sidebar-heading-toggle[aria-expanded="false"] .sidebar-caret {
    transform: rotate(-90deg);
}

.sidebar-nav .nav-link {
    display: flex;
    align-items: center;
    gap: .7rem;
    padding: .5rem .65rem;
    margin-bottom: 1px;
    border-radius: var(--radius-sm);
    color: var(--ink-soft);
    font-size: .885rem;
    font-weight: 500;
    transition: background-color .12s ease, color .12s ease;
}

.sidebar-nav .nav-link i {
    width: 1.15rem;
    text-align: center;
    font-size: 1.02rem;
    color: var(--ink-muted);
}

.sidebar-nav .nav-link:hover {
    color: var(--ink);
    background: rgba(120, 113, 108, .09);
}

.sidebar-nav .nav-link:hover i {
    color: var(--ink-soft);
}

.sidebar-nav .nav-link.active {
    color: var(--accent-ink);
    background: var(--accent-wash);
    font-weight: 600;
}

.sidebar-nav .nav-link.active i {
    color: var(--accent);
}

.sidebar-footer {
    flex: 0 0 auto;
    padding: .9rem 1.35rem;
    border-top: 1px solid var(--line);
}

/* ── Navbar ───────────────────────────────────────────────────────────── */

.app-main > .navbar,
.navbar.app-navbar {
    background: var(--surface);
    border-bottom: 1px solid var(--line);
}

/* A rule between the menu toggle and the date, so the gap reads as a deliberate
   break between the control and the label instead of a button left floating. */
.navbar-date {
    padding-left: 1rem;
    border-left: 1px solid var(--line);
    line-height: 1.2;
}

.avatar-initial {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2rem;
    height: 2rem;
    border-radius: 50%;
    background: var(--accent-wash);
    color: var(--accent-ink);
    font-weight: 600;
    font-size: .82rem;
}

/* ── Cards ────────────────────────────────────────────────────────────── */

.card {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
}

.card-header,
.card-footer {
    background: transparent;
    border-color: var(--line);
}

.card-body.border-bottom,
.card-footer {
    border-color: var(--line) !important;
}

/* ── Stat tiles (spec §8) ─────────────────────────────────────────────── */

.stat-card {
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    box-shadow: none;
}

.stat-card .stat-value {
    font-size: 1.75rem;
    font-weight: 650;
    line-height: 1.15;
    letter-spacing: -.02em;
    font-variant-numeric: tabular-nums;
}

.stat-card .stat-label {
    font-size: .715rem;
    font-weight: 600;
    letter-spacing: .07em;
    text-transform: uppercase;
    color: var(--ink-muted);
}

.stat-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.5rem;
    height: 2.5rem;
    border-radius: var(--radius-md);
    font-size: 1.15rem;
}

/* ── Forms ────────────────────────────────────────────────────────────── */

.form-control,
.form-select {
    background-color: var(--surface);
    border-color: var(--line-strong);
    border-radius: var(--radius-sm);
    color: var(--ink);
    font-size: .9rem;
    padding: .45rem .7rem;
}

/* Bootstrap reserves 2.25rem on the right of a <select> for its chevron; the
   shorthand above overwrote that, leaving the arrow printed over the end of
   the longest option. Put the room back. */
.form-select {
    padding-right: 2.1rem;
}

.form-control:focus,
.form-select:focus {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px rgba(79, 70, 229, .13);
}

.form-label {
    font-size: .82rem;
    font-weight: 500;
    color: var(--ink-soft);
    margin-bottom: .3rem;
}

.input-group-text {
    background: var(--surface-sunken);
    border-color: var(--line-strong);
    color: var(--ink-muted);
}

.btn {
    border-radius: var(--radius-sm);
    font-size: .88rem;
    font-weight: 500;
    padding: .45rem .85rem;
}

.btn-primary {
    --bs-btn-bg: var(--accent);
    --bs-btn-border-color: var(--accent);
    --bs-btn-hover-bg: var(--accent-ink);
    --bs-btn-hover-border-color: var(--accent-ink);
    --bs-btn-active-bg: var(--accent-ink);
    --bs-btn-active-border-color: var(--accent-ink);
}

.btn-outline-secondary {
    --bs-btn-color: var(--ink-soft);
    --bs-btn-border-color: var(--line-strong);
    --bs-btn-hover-bg: var(--surface-sunken);
    --bs-btn-hover-color: var(--ink);
    --bs-btn-hover-border-color: var(--line-strong);
}

.modal-content {
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    background: var(--surface);
}

.modal-header,
.modal-footer {
    border-color: var(--line);
}

/* Every form modal in the app wraps its header, body and footer in a <form>,
   and that wrapper silently breaks .modal-dialog-scrollable: Bootstrap caps
   .modal-content and hides its overflow, expecting .modal-body to be a flex
   child it can shrink. A plain block in between takes its natural height
   instead, so the body never gets a height to scroll within — the content is
   simply clipped and the footer becomes unreachable.

   Giving the wrapper the column layout .modal-content would have had puts the
   scroll back where Bootstrap expects it. Scoped to scrollable dialogs so a
   plain modal still sizes to its content. */
.modal-dialog-scrollable .modal-content > form {
    display: flex;
    flex-direction: column;
    max-height: 100%;
    overflow: hidden;
}

.dropdown-menu {
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    box-shadow: 0 8px 24px rgba(28, 26, 23, .07);
    background: var(--surface);
}

/* ── Tables ───────────────────────────────────────────────────────────── */

.table-wrap {
    overflow-x: auto;
}

/* A width:100% table shrinks to its container instead of overflowing it, so
   the wrapper would never actually scroll — the floor is what makes narrow
   screens scroll rather than crush the columns. */
.table-wrap > .table {
    min-width: var(--table-wrap-min-width, 34rem);
}

.table {
    --bs-table-bg: transparent;
    --bs-table-color: var(--ink);
    --bs-table-border-color: var(--line);
    margin-bottom: 0;
}

.table > :not(caption) > * > * {
    padding: var(--row-pad-y) var(--cell-pad-x);
    background-color: transparent;
}

/* .table-sm asks for a compact grid, and the one place it is used sits in a
   modal body that already supplies its own gutter — so it keeps a tight inset
   instead of stacking a card-sized one on top. Same specificity as the rule
   above, so it has to stay below it to win. */
.table-sm > :not(caption) > * > * {
    padding: .45rem .55rem;
}

/* A plain, readable label in full ink — the header earns its hierarchy from
   weight, the tint behind it and a firmer rule underneath, not from shrinking
   the text until it needs deciphering. */
.table thead th {
    font-size: .86rem;
    font-weight: 600;
    color: var(--ink);
    white-space: nowrap;
    border-bottom: 1px solid var(--line-strong) !important;
    background: var(--chrome-bg);
    vertical-align: middle;
}

/* Kept out of the rule above because `.table thead th` outranks `.table-sm`'s
   padding shorthand on specificity, and would otherwise un-compact the tables
   that asked to be compact (the roster grid, the deduction list). */
.table:not(.table-sm) thead th {
    padding-top: .62rem;
    padding-bottom: .62rem;
}

/* The roster grid is one cell per day across a fortnight, so it earns a
   tighter column than the master-data grids and says so with .table-sm. */
.table-sm td,
.table-sm thead th {
    font-size: .8rem;
}

/* A ruled grid, not a list. Every cell carries a hairline on all four sides,
   which is what keeps a nine-column row readable across its width — the eye
   tracks a lane instead of drifting between columns. */
.table > :not(caption) > * > * {
    border: 1px solid var(--line);
}

.table tbody tr {
    border-color: var(--line);
    transition: background-color .12s ease;
}

/* Banding, at the lightest tint that still separates one row from the next. */
.table > tbody > tr:nth-of-type(odd) > * {
    background-color: var(--row-stripe);
}


.table td {
    vertical-align: middle;
    font-size: .9rem;
    line-height: 1.45;
    color: var(--ink);
}

/* Right-aligned cells are money, counts and dates by convention here, and all
   three only line up in a column if their digits are the same width. */
.table td.text-end,
.table td.text-center {
    font-variant-numeric: tabular-nums;
}

/* Secondary lines inside a cell (a code under a name, a note under a total)
   share one scale instead of each caller inventing a font-size. */
.table td .cell-sub {
    font-size: .765rem;
    color: var(--ink-muted);
    line-height: 1.35;
}

/* Same specificity as the striping rule above and declared after it, which is
   what lets a hover repaint an odd row as well as an even one. */
.table-hover > tbody > tr:hover > * {
    --bs-table-accent-bg: var(--row-hover);
    background-color: var(--row-hover);
}

.table-empty td {
    padding: 3rem 1rem;
    text-align: center;
    color: var(--ink-muted);
}

/* ── Row actions ──────────────────────────────────────────────────────── */

/* Square, quiet icon buttons rather than a row of outlined pills: on a
   twenty-row table the outlines are the loudest thing on screen, and they are
   the one control the eye should reach for last. Colour arrives on hover. */
.row-actions {
    display: inline-flex;
    align-items: center;
    justify-content: flex-end;
    gap: .25rem;
    vertical-align: middle;
}

.btn.btn-icon {
    --bs-btn-color: var(--ink-soft);
    --bs-btn-border-color: var(--line-strong);
    --bs-btn-bg: var(--surface);
    --bs-btn-hover-color: var(--ink);
    --bs-btn-hover-bg: var(--surface);
    --bs-btn-hover-border-color: var(--line-strong);
    --bs-btn-active-color: var(--ink);
    --bs-btn-active-bg: var(--surface);
    --bs-btn-active-border-color: var(--line-strong);
    --bs-btn-disabled-color: var(--ink-muted);
    --bs-btn-disabled-bg: transparent;
    --bs-btn-disabled-border-color: transparent;

    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.95rem;
    height: 1.95rem;
    padding: 0;
    font-size: .92rem;
    line-height: 1;
    border-radius: var(--radius-sm);
    transition: background-color .12s ease, color .12s ease, border-color .12s ease;
}

.btn.btn-icon > i {
    line-height: 1;
}

/* Same treatment, but sized to hold a word. Used where the action is the row's
   primary one and an icon alone would be a guessing game. */
.btn.btn-icon.btn-icon-label {
    width: auto;
    gap: .35rem;
    padding: 0 .6rem;
    font-size: .8rem;
    font-weight: 500;
}

/* The destructive action stays neutral until it is aimed at, so a full table
   of delete buttons is not a wall of red. */
.btn.btn-icon.btn-icon-danger {
    --bs-btn-hover-color: var(--danger-ink);
    --bs-btn-hover-bg: var(--danger-wash);
    --bs-btn-hover-border-color: var(--danger-line);
    --bs-btn-active-color: var(--danger-ink);
    --bs-btn-active-bg: var(--danger-wash);
    --bs-btn-active-border-color: var(--danger-line);
}

.btn.btn-icon.btn-icon-accent {
    --bs-btn-hover-color: var(--accent-ink);
    --bs-btn-hover-bg: var(--accent-wash);
    --bs-btn-hover-border-color: transparent;
    --bs-btn-active-color: var(--accent-ink);
    --bs-btn-active-bg: var(--accent-wash);
    --bs-btn-active-border-color: transparent;
}

/* An action column carries no data, so it should claim no width: width:1% on
   an auto-layout table means "as narrow as the content allows". */
.table td:has(> .row-actions) {
    width: 1%;
    white-space: nowrap;
}

/* ── Table card ───────────────────────────────────────── */

/* The title band. This is the module's heading — the layout skips its own
   when a card claims it — so it holds the same three things the page header
   used to: name, one line of context, and the primary action. */
.dt-card-header {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: .75rem;
    padding: .85rem var(--table-inset);
    background: var(--chrome-bg);
    border-bottom: 1px solid var(--line);
}

.dt-card-title h2 {
    margin: 0;
    font-size: 1rem;
    font-weight: 600;
    color: var(--ink);
    letter-spacing: -.01em;
}

.dt-card-title p {
    margin: .15rem 0 0;
    font-size: .8rem;
    color: var(--ink-muted);
}

.dt-card-actions {
    display: flex;
    flex-wrap: wrap;
    gap: .5rem;
}

/* ── DataTables (server-side) chrome ──────────────────────────────────── */

/* The card is four bands: the module's filter toolbar, the page-size row,
   the grid, and a footer with the row count and the pager. The table sits
   flush — the bands carry the padding, and every one of them reads
   --table-inset, so the filters, the first column and the row count all share
   one left edge. Retune the token, not the individual rules. */
.dt-container {
    width: 100%;

    /* Below this width an 8-column table stops being readable and starts
       being a word puzzle, so the table row scrolls instead of squeezing.
       Tables with fewer columns can lower it on their own wrapper. */
    --dt-table-min-width: 44rem;
}

/* ── Filter toolbar ── */

/* DataTables moves this element into the toolbar row's end cell, so it owns
   no padding, border or ground of its own — the row it lands in supplies all
   three. It only has to lay its own controls out. */
.dt-filters > .row {
    --bs-gutter-x: .5rem;
    flex-wrap: wrap;
    justify-content: flex-end;
    margin: 0;
    width: 100%;
}

/* The cell is a flex container, so the filter bar has to claim the whole of
   it before its own justify-content can push the controls to the right edge —
   the position "Search:" occupies in the layout this is modelled on. */
.dt-layout-cell .dt-filters {
    width: 100%;
}

/* The slot's markup is column-sized for a full-width bar; in the toolbar the
   controls should hug their content and wrap instead. */
.dt-layout-cell .dt-filters > .row > * {
    flex: 0 1 auto;
    width: auto;
    max-width: 100%;
}

.dt-filters .form-control,
.dt-filters .form-select {
    min-width: 8.5rem;
}

.dt-filters input[type="search"] {
    min-width: 15rem;
}

/* Give the filters the room; the page-size menu next to them is a fixed,
   small thing that never needs to grow. */
.dt-container .dt-layout-row:first-child .dt-layout-cell.dt-layout-end {
    flex: 1 1 auto;
    min-width: 0;
}

/* One height for every control in the toolbar. Without it a <select> and an
   <input> differ by a pixel or two and the whole bar reads as misaligned. */
.dt-filters .form-control,
.dt-filters .form-select {
    height: 2.25rem;
    font-size: .865rem;
}

/* The magnifier is drawn rather than marked up, so every page's own filter
   slot gets it without touching that page's Blade. */
.dt-filters input[type="search"] {
    padding-left: 2.15rem;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%238b857e' stroke-width='1.6' stroke-linecap='round'%3E%3Ccircle cx='6.9' cy='6.9' r='4.5'/%3E%3Cpath d='M10.3 10.3 13.8 13.8'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: left .72rem center;
    background-size: 15px 15px;
}

/* Chrome's own clear affordance is all but invisible on a dark ground. */
[data-bs-theme="dark"] .dt-filters input[type="search"]::-webkit-search-cancel-button {
    filter: invert(1) opacity(.55);
}

/* ── Layout rows ── */

/* DataTables' Bootstrap 5 renderer stamps `row mt-2` onto every layout row,
   and Bootstrap spacing utilities are !important — a plain `margin: 0` loses
   to them and leaves the rows drifting below the card's hairlines. */
.dt-container .dt-layout-row {
    margin: 0 !important;
    align-items: center;
    row-gap: .5rem;
}

/* Scoped to the chrome rows: `:not(:last-child)` would also catch the table
   row, whose border-bottom then doubled up with the footer's border-top. */
/* Borders are anchored to the table row rather than :first-child/:last-child,
   because DataTables also drops its processing bar into the container and
   would otherwise take the first slot. */
.dt-container .dt-layout-row:not(.dt-layout-table) {
    padding: .7rem var(--table-inset);
    gap: .6rem;
    border-bottom: 0;
    background: var(--surface);
}

/* The grid is boxed and inset now, so the bands around it need no rules of
   their own — the table's border is the only edge in play. */
.dt-container .dt-layout-table ~ .dt-layout-row {
    border-top: 0;
    padding-top: 0;
}

/* Belt and braces: DataTables builds a chrome row only for slots that are
   actually filled, so an empty one should never appear. If a release ever
   emits one anyway, it must not show as a stray band of padding. */
.dt-container .dt-layout-row:not(.dt-layout-table):not(:has(> .dt-layout-cell > *)) {
    display: none;
}

/* `.row > *` gives every grid child width:100% until the md breakpoint; the
   chrome cells should hug their contents at any width instead. */
.dt-container .dt-layout-cell {
    padding: 0;
    width: auto;
    max-width: 100%;
}

/* The footer's left cell holds two things now — the page size and the row
   count — so they sit together with a gap rather than being pushed to
   opposite ends by the integration's justify-content-between. */
.dt-container .dt-layout-cell.dt-layout-start {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-start !important;
    gap: .3rem .85rem;
}

/* Only the table itself scrolls sideways — the length menu, row count and
   pager must stay put when a wide table overflows. */
.dt-container .dt-layout-row.dt-layout-table {
    overflow-x: auto;
    display: block;
    padding: var(--table-inset);

    /* Edge shading that shows only while there is more table to reach: the
       local-attachment gradients scroll away with the content and uncover the
       scroll-attachment ones, which stay pinned as the shadow. Without it a
       wide table on a phone looks complete when half of it is off-screen. */
    background:
        linear-gradient(to right, var(--surface) 40%, rgba(255, 255, 255, 0)) left center,
        linear-gradient(to left, var(--surface) 40%, rgba(255, 255, 255, 0)) right center,
        radial-gradient(farthest-side at 0 50%, rgba(28, 26, 23, .11), rgba(28, 26, 23, 0)) left center,
        radial-gradient(farthest-side at 100% 50%, rgba(28, 26, 23, .11), rgba(28, 26, 23, 0)) right center;
    background-repeat: no-repeat;
    background-size: 34px 100%, 34px 100%, 13px 100%, 13px 100%;
    background-attachment: local, local, scroll, scroll;
}

[data-bs-theme="dark"] .dt-container .dt-layout-row.dt-layout-table {
    background:
        linear-gradient(to right, var(--surface) 40%, rgba(0, 0, 0, 0)) left center,
        linear-gradient(to left, var(--surface) 40%, rgba(0, 0, 0, 0)) right center,
        radial-gradient(farthest-side at 0 50%, rgba(0, 0, 0, .45), rgba(0, 0, 0, 0)) left center,
        radial-gradient(farthest-side at 100% 50%, rgba(0, 0, 0, .45), rgba(0, 0, 0, 0)) right center;
    background-repeat: no-repeat;
    background-size: 34px 100%, 34px 100%, 13px 100%, 13px 100%;
    background-attachment: local, local, scroll, scroll;
}

.dt-container .dt-layout-row.dt-layout-table > .dt-layout-cell {
    width: 100%;
}

/* Without a floor the table honours width:100% and crushes its columns to
   fit, so the scroll container it sits in never actually scrolls. */
.dt-container table.dataTable {
    min-width: var(--dt-table-min-width);

    /* The grid is a closed box inside the card rather than a block bleeding
       into its edges: its own border finishes the outer column and the last
       row, which the per-cell hairlines cannot do on their own. */
    border: 1px solid var(--line);
}

/* ── Page size and row count ── */

.dt-container .dt-length,
.dt-container .dt-search {
    display: flex;
    align-items: center;
    gap: .45rem;
    font-size: .82rem;
    color: var(--ink-soft);
    white-space: nowrap;
}

.dt-container .dt-length select {
    width: auto;
    min-width: 3.9rem;
    height: 1.95rem;
    padding: .1rem 1.8rem .1rem .55rem;
    font-size: .8rem;
    background-color: var(--surface);
}

.dt-container .dt-info {
    font-size: .82rem;
    color: var(--ink-soft);
    padding-top: 0;
    white-space: nowrap;
}

/* DataTables sniffs each column's type and right-aligns anything numeric,
   which is how an all-digit NIK or phone number ends up hugging the next
   column. Every column here that genuinely wants alignment says so with a
   .text-end / .text-center utility, and those are !important, so neutralising
   the automatic rule costs nothing and fixes the accidents. */
.dt-container table.dataTable th.dt-type-numeric,
.dt-container table.dataTable td.dt-type-numeric,
.dt-container table.dataTable th.dt-type-date,
.dt-container table.dataTable td.dt-type-date {
    text-align: left;
}

/* ── Sortable headers ── */

/* The arrow is a quiet affordance, not a decoration: absent until the column
   is hovered, solid once it is the one actually being sorted on. */
.table thead th.dt-orderable-asc,
.table thead th.dt-orderable-desc {
    cursor: pointer;
}

.table thead th.dt-orderable-asc:hover,
.table thead th.dt-orderable-desc:hover {
    color: var(--ink);
    background: var(--neutral-wash);
}

.table thead th.dt-ordering-asc,
.table thead th.dt-ordering-desc {
    color: var(--accent-ink);
}

.dt-column-order {
    opacity: .3;
    transition: opacity .12s ease;
}

th.dt-orderable-asc:hover .dt-column-order,
th.dt-orderable-desc:hover .dt-column-order {
    opacity: .6;
}

th.dt-ordering-asc .dt-column-order,
th.dt-ordering-desc .dt-column-order {
    opacity: 1;
}

/* ── Header alignment ── */

/* Every header label sits centred over its column, whatever the cells beneath
   it do: figures stay right-aligned, names stay left, but the head row reads as
   one band of labels instead of a zigzag. !important because the .text-end and
   .text-center utilities on those same cells carry it too. */
.table thead th {
    text-align: center !important;
}

/* DataTables pins the sort arrow to the column's right edge and reserves a 30px
   gutter for it. That reads fine under a left-aligned label and badly under a
   centred one — the label floats mid-column while its arrow sits far off at the
   rule, so the two stop looking like one control. Put the arrow back in the
   flow right after the label, and give the cell its symmetric padding back. */
.dt-container table.dataTable thead th.dt-orderable-asc,
.dt-container table.dataTable thead th.dt-orderable-desc {
    padding-right: var(--cell-pad-x);
}

/* The compact grid pads its cells with a literal, so the gutter it reclaims is
   a literal too — matching .table-sm's own .55rem inset above. */
.dt-container table.dataTable.table-sm thead th.dt-orderable-asc,
.dt-container table.dataTable.table-sm thead th.dt-orderable-desc {
    padding-right: .55rem;
}

/* position:relative rather than static: the ▲ and ▼ are absolutely positioned
   against this span, so it has to stay their containing block. It also loses
   the top/bottom stretch that gave it height, hence the explicit one. */
.dt-container table.dataTable thead th.dt-orderable-asc span.dt-column-order,
.dt-container table.dataTable thead th.dt-orderable-desc span.dt-column-order,
.dt-container table.dataTable.table-sm thead th.dt-orderable-asc span.dt-column-order,
.dt-container table.dataTable.table-sm thead th.dt-orderable-desc span.dt-column-order {
    position: relative;
    right: auto;
    top: auto;
    bottom: auto;
    display: inline-block;
    width: .7rem;
    height: 1em;
    margin-left: .2rem;
    vertical-align: middle;
}

/* ── Pagination ── */

/* One bordered group with shared edges, the way a pager is expected to look,
   rather than a scatter of loose chips. */
.dt-container .dt-paging .pagination {
    margin: 0;
    gap: 0;
    justify-content: flex-end;
}

.dt-container .dt-paging .page-link {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 1px solid var(--line-strong);
    border-radius: 0;
    margin-left: -1px;
    color: var(--accent);
    background: var(--surface);
    font-size: .84rem;
    font-weight: 500;
    font-variant-numeric: tabular-nums;
    padding: 0 .7rem;
    min-width: 2.25rem;
    height: 2.25rem;
    transition: background-color .12s ease, color .12s ease, border-color .12s ease;
}

.dt-container .dt-paging .page-item:first-child .page-link {
    margin-left: 0;
    border-top-left-radius: var(--radius-sm);
    border-bottom-left-radius: var(--radius-sm);
}

.dt-container .dt-paging .page-item:last-child .page-link {
    border-top-right-radius: var(--radius-sm);
    border-bottom-right-radius: var(--radius-sm);
}

.dt-container .dt-paging .page-link:focus {
    z-index: 2;
    box-shadow: 0 0 0 3px rgba(79, 70, 229, .13);
}

.dt-container .dt-paging .page-link:hover {
    background: var(--surface-sunken);
    color: var(--accent-ink);
    z-index: 1;
}

/* The current page is the one thing here worth a solid fill. */
.dt-container .dt-paging .page-item.active .page-link {
    background: var(--accent);
    border-color: var(--accent);
    color: #fff;
    font-weight: 600;
    z-index: 1;
}

.dt-container .dt-paging .page-item.disabled .page-link {
    color: var(--ink-muted);
    background: var(--surface);
    opacity: .55;
}

/* ── Loading ── */

/* A thin bar across the top beats a modal overlay that blocks the filters
   mid-typing. */
.dt-container .dt-processing {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    margin: 0;
    width: 100%;
    height: 2px;
    padding: 0;
    border: 0;
    background: transparent;
    box-shadow: none;
    text-indent: -9999px;
    overflow: hidden;
    z-index: 5;
}

.dt-container .dt-processing::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(90deg, transparent, var(--accent), transparent);
    animation: dt-progress 1s ease-in-out infinite;
}

@keyframes dt-progress {
    0% { transform: translateX(-100%); }
    100% { transform: translateX(100%); }
}

/* Rows fade while a fetch is in flight, so the grid reads as stale rather than
   current. The transition delay is what keeps a 40ms response from flickering:
   the dimming never starts before the answer has arrived. */
.dt-container table.dataTable > tbody {
    transition: opacity .15s ease;
}

.dt-container.dt-busy table.dataTable > tbody {
    opacity: .45;
    transition: opacity .15s ease .12s;
}

.card > .dt-container {
    position: relative;
}

/* ── Empty state ── */

.dt-empty {
    text-align: center;
    color: var(--ink-muted);
    padding: 0 !important;
}

.dt-empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: .6rem;
    padding: 2.5rem 1.25rem;
    text-align: center;
}

.dt-empty-state .dt-empty-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.75rem;
    height: 2.75rem;
    border-radius: 50%;
    background: var(--surface-sunken);
    border: 1px solid var(--line);
    color: var(--ink-muted);
    font-size: 1.2rem;
}

.dt-empty-state .dt-empty-title {
    margin: 0;
    font-size: .885rem;
    font-weight: 500;
    color: var(--ink-soft);
    max-width: 26rem;
}

/* ── Narrow screens ── */

@media (max-width: 575.98px) {
    .dt-container {
        --table-inset: .85rem;
    }

    /* The footer stacks: page size and count on one line, the pager below it
       and left-aligned, so neither is ever pushed off the card. */
    .dt-container .dt-layout-row:not(.dt-layout-table) > .dt-layout-cell {
        width: 100%;
        justify-content: flex-start !important;
    }

    .dt-container .dt-paging .pagination {
        flex-wrap: wrap;
        justify-content: flex-start;
    }

    /* Comfortable thumb targets once the pointer is a finger. */
    .dt-container .dt-paging .page-link {
        min-width: 2.25rem;
        height: 2.25rem;
    }

    .btn.btn-icon {
        width: 2.25rem;
        height: 2.25rem;
    }

    .dt-layout-cell .dt-filters > .row > * {
        width: 100%;
    }
}

/* ── Role → menu access ───────────────────────────────────────────────── */

/* One role at a time rather than a column per role: the list below stays the
   same width whether the system has three roles or fifty, which the matrix it
   replaced could not. */

/* ── The access dialog ── */

.access-modal-heading {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .25rem .5rem;
    min-width: 0;
}

.access-modal-heading .modal-title {
    font-size: 1rem;
    font-weight: 600;
}

/* The role the switches belong to. The picker is hidden in here, so this is
   the only thing naming it. */
.access-role-name {
    color: var(--accent-ink);
}

.access-modal-heading p {
    flex: 1 0 100%;
    margin: 0;
    font-size: .78rem;
    color: var(--ink-muted);
}

/* Copy, search and the bulk controls sit between the header and the scrolling
   body, so they stay put while the list moves under them. */
.menu-access .access-controls,
.menu-access .access-toolbar {
    flex: 0 0 auto;
}

/* Two stacked sticky bands inside one scroller: the column header owns the top
   edge, and a group heading parks directly beneath it rather than underneath
   it. The offset is the header's own height — retune both together. */
.menu-access .access-list {
    --access-head-h: 2rem;
}

.menu-access .access-head {
    position: sticky;
    top: 0;
    z-index: 3;
    min-height: var(--access-head-h);
}

.menu-access .access-group-head {
    top: var(--access-head-h);
}

/* The tally belongs with the button that acts on it. */
.menu-access .modal-footer .access-summary {
    margin-right: auto;
}

.menu-access.is-saving .access-list {
    opacity: .55;
    pointer-events: none;
    transition: opacity .15s ease .1s;
}

/* ── Role picker ── */

/* Its own band above the toolbar, because everything below it is meaningless
   until the role has been read. Three labelled fields rather than one inline
   row: "Role" and "Copy dari role" are the same control twice over and read as
   a pair of dropdowns unless each says which is which. */
.access-controls {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: .75rem 1rem;
    padding: .85rem var(--table-inset);
    border-bottom: 1px solid var(--line);
    background: var(--surface);
}

.access-field {
    display: flex;
    flex-direction: column;
    gap: .3rem;
    min-width: 0;
}

/* The search takes the slack, so the two dropdowns keep the width they need to
   show a full role name and nothing wider. */
.access-field-grow {
    flex: 1 1 15rem;
}

.access-field .form-label {
    margin: 0;
    font-size: .78rem;
    font-weight: 500;
    color: var(--ink-soft);
}

.access-field .form-select {
    width: auto;
    min-width: 16rem;
    max-width: 100%;
    height: 2.3rem;
    font-size: .9rem;
    font-weight: 500;
}

.access-role-badges {
    display: inline-flex;
    gap: .3rem;
}

/* Sits at the left of the toolbar; the buttons on that row claim the auto
   margin, so claiming one here too would push the two into each other. */
.access-summary {
    font-size: .82rem;
    color: var(--ink-soft);
    white-space: nowrap;
}

.access-summary strong {
    font-weight: 600;
    color: var(--ink);
}

/* Says out loud that what is on screen is not yet what is stored. */
.access-unsaved {
    display: inline-block;
    margin-left: .35rem;
    padding: .1rem .45rem;
    border-radius: 999px;
    background: var(--warn-wash);
    color: var(--warn-ink);
    font-size: .72rem;
    font-weight: 600;
}

/* ── Toolbar ── */

.access-toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .6rem;
    padding: .6rem var(--table-inset);
    border-bottom: 1px solid var(--line);
    background: var(--chrome-bg);
}

.access-controls .form-control {
    width: 100%;
    height: 2.3rem;
    font-size: .875rem;

    /* Same drawn magnifier as the table toolbars, so the two search boxes on
       this page are recognisably the same control. */
    padding-left: 2.15rem;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%238b857e' stroke-width='1.6' stroke-linecap='round'%3E%3Ccircle cx='6.9' cy='6.9' r='4.5'/%3E%3Cpath d='M10.3 10.3 13.8 13.8'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: left .72rem center;
    background-size: 15px 15px;
}

.access-toolbar-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .5rem;
    margin-left: auto;
}

.access-check-all {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    font-size: .82rem;
    color: var(--ink-soft);
    cursor: pointer;
    white-space: nowrap;
}

.access-check-all .form-check-input {
    margin: 0;
}

/* ── The list ── */

/* Capped rather than endless: on a 1366×768 laptop an unbounded list pushes
   the role picker and the save button off the top of the screen, which are the
   two controls this panel is for. */
/* The list flows with the page rather than scrolling inside the card.
   Capping it at 30rem hid more than half of it — 465px of a 963px list — behind
   a second scrollbar, on a page with room to spare below the card, and clipped
   whichever row happened to land on the fold so the card read as broken. */
.access-list {
    overflow: visible;
}

/* Names the two columns. The right-hand one holds controls now, and an
   unannounced bank of switches makes the reader deduce what they switch. */
.access-head {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 25rem;
    align-items: center;
    gap: .5rem;
    padding: .45rem var(--table-inset);
    border-bottom: 1px solid var(--line);
    background: var(--surface);
    font-size: .7rem;
    font-weight: 600;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--ink-muted);
}


.access-group + .access-group {
    border-top: 1px solid var(--line);
}

/* Sticky against the page now that the list no longer scrolls on its own, so
   the heading you are ticking under stays on screen — parked below the navbar
   rather than sliding under it. */
.access-group-head {
    position: sticky;
    top: var(--navbar-height);
    z-index: 2;
    display: flex;
    align-items: center;
    gap: .5rem;
    padding: .5rem var(--table-inset);
    background: var(--chrome-bg);
    border-bottom: 1px solid var(--line);
}

.access-group-label {
    display: inline-flex;
    align-items: center;
    gap: .5rem;
    font-size: .78rem;
    font-weight: 600;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--ink);
    cursor: pointer;
}

.access-group-label .form-check-input {
    margin: 0;
}

.access-group-count {
    margin-left: auto;
    font-size: .75rem;
    font-variant-numeric: tabular-nums;
    color: var(--ink-muted);
}

/* The caret is the fold control for a group and the disclosure for a row; one
   shape, one rotation, both places. */
.access-caret {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.5rem;
    height: 1.5rem;
    padding: 0;
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--ink-muted);
    font-size: .8rem;
    transition: background-color .12s ease, color .12s ease;
}

.access-caret:hover {
    background: var(--surface);
    color: var(--ink);
}

.access-caret > i {
    transition: transform .15s ease;
}

.access-caret[aria-expanded="false"] > i {
    transform: rotate(-90deg);
}

.access-group.is-collapsed .access-group-body {
    display: none;
}

/* ── A menu row ── */

/* Two columns, and they must be the same two .access-head declares — the
   header sits above every row and any difference reads as the whole table
   being crooked. A third `auto` track survived here from the design that had a
   disclosure caret; with only two children left it collected nothing but its
   own gap, which pulled the name column 8px narrower than the header's and
   knocked all nineteen rows out of line with it. */
.access-item {
    display: grid;
    /* A fixed lane for the switches, not an auto one. Sized to the widest bank
       (four verbs), so every row starts its switches at the same x instead of
       hugging the right edge and staggering by however many verbs it happens
       to have. The bank still packs left, so a row missing a verb closes the
       gap rather than leaving a hole — read and create line up everywhere,
       and only the rows with a gap shift the verbs after it. */
    grid-template-columns: minmax(0, 1fr) 25rem;
    align-items: center;
    gap: .5rem;
    padding: .4rem var(--table-inset);
    border-bottom: 1px solid var(--line);
    transition: background-color .12s ease;
}

.access-item:last-child {
    border-bottom: 0;
}

.access-item:hover {
    background: var(--row-hover);
}

.access-item-main {
    display: flex;
    align-items: center;
    gap: .65rem;
    min-width: 0;
    cursor: pointer;
}

.access-item-main .form-check-input {
    flex: 0 0 auto;
    margin: 0;
}

.access-item.is-locked .access-item-main {
    cursor: default;
}

.access-item.is-locked .form-check-input {
    opacity: .55;
}

.access-item-text {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: .2rem .6rem;
    min-width: 0;
}

.access-item-name {
    font-size: .885rem;
    color: var(--ink);
}

.access-item-name .bi-lock-fill {
    font-size: .72rem;
    color: var(--ink-muted);
    margin-left: .25rem;
}

.access-item-code {
    font-size: .74rem;
    color: var(--ink-muted);
}

/* ── What the row grants, one switch per verb ── */

/* Controls now, not a report. Each switch is a stored grant the middleware
   reads, so the bank is left-aligned in its column and evenly spaced: an
   administrator scans down a verb to compare rows, which ragged right edges
   made impossible. */
.access-item-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-start;
    gap: .15rem .35rem;
}

.access-switch {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    min-width: 5.9rem;
    padding: .1rem 0;
    margin: 0;
    font-size: .74rem;
    color: var(--ink-soft);
    cursor: pointer;
}

/* Bootstrap's .form-switch expects a .form-check wrapper and pulls the input
   left by its own padding. There is no wrapper here, so the geometry is set
   outright rather than fought with. */
.access-switch .form-check-input {
    flex: 0 0 auto;
    width: 1.85rem;
    height: 1rem;
    margin: 0;
    background-position: left center;
}

.access-switch .form-check-input:checked {
    background-position: right center;
}

/* A switch whose menu is untricked still shows its position — the row is off,
   not blank — but reads as inert, which is what it is until the menu is back. */
.access-item.is-off .access-switch {
    color: var(--ink-muted);
    cursor: not-allowed;
}

.access-item.is-off .access-switch .form-check-input {
    opacity: .4;
}

/* The one grant that cannot be undone by re-ticking a box, so it is the one
   the row says out loud once it is on. */
.access-switch .form-check-input[data-action="delete"]:checked {
    background-color: var(--danger-ink);
    border-color: var(--danger-ink);
}

.access-switch .form-check-input[data-action="delete"]:checked ~ span {
    color: var(--danger-ink);
    font-weight: 600;
}

.access-noroute {
    font-size: .72rem;
    font-style: italic;
    color: var(--ink-muted);
}

/* ── Empty state ── */

.access-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: .6rem;
    padding: 2.5rem 1.25rem;
    text-align: center;
}

/* ── Row action menu ──────────────────────────────────────────────────── */

.row-menu .dropdown-menu {
    --bs-dropdown-min-width: 12rem;
    --bs-dropdown-font-size: .85rem;
    --bs-dropdown-padding-y: .3rem;
    --bs-dropdown-item-padding-y: .4rem;
    --bs-dropdown-item-padding-x: .75rem;
    --bs-dropdown-link-color: var(--ink-soft);
    --bs-dropdown-link-hover-color: var(--ink);
    --bs-dropdown-link-hover-bg: var(--surface-sunken);
    --bs-dropdown-link-active-color: var(--ink);
    --bs-dropdown-link-active-bg: var(--surface-sunken);
    --bs-dropdown-link-disabled-color: var(--ink-muted);
}

.row-menu .dropdown-item {
    display: flex;
    align-items: center;
    gap: .55rem;
    border-radius: var(--radius-sm);
    white-space: nowrap;
}

.row-menu .dropdown-item > i {
    font-size: .9rem;
    color: var(--ink-muted);
    width: 1rem;
    text-align: center;
}

.row-menu .dropdown-item-danger {
    --bs-dropdown-link-color: var(--danger-ink);
    --bs-dropdown-link-hover-color: var(--danger-ink);
    --bs-dropdown-link-hover-bg: var(--danger-wash);
}

.row-menu .dropdown-item-danger > i {
    color: var(--danger-ink);
}

/* A disabled entry stays in place rather than vanishing on some rows: an
   action that is missing on one row and present on the next reads as a bug. */
.row-menu .dropdown-item:disabled > i {
    color: var(--ink-muted);
}

/* ── Soft badge ───────────────────────────────────────────────────────── */

/* For labels that classify rather than alert — "System Role", "aksi". Quieter
   than any of the .text-bg-* pairs, which all carry a semantic this does not. */
.badge.badge-soft {
    background: var(--neutral-wash);
    color: var(--neutral-ink);
    box-shadow: inset 0 0 0 1px var(--neutral-line);
    font-weight: 500;
}

/* ── Narrow screens ───────────────────────────────────────────────────── */

@media (max-width: 767.98px) {
    .access-field,
    .access-field .form-select,
    .access-controls .form-control {
        width: 100%;
        min-width: 0;
    }

    .access-summary,
    .access-toolbar-actions {
        margin-left: 0;
    }

    /* The switches drop below the name rather than out of the row. They are the
       point of the screen now — hiding them the way the old chips were hidden
       would leave a phone able to see the mapping and not edit it. */
    .access-item {
        grid-template-columns: minmax(0, 1fr);
    }

    .access-item-actions {
        grid-column: 1 / -1;
        padding-left: 1.9rem;
    }

    /* The switch lane moves onto its own line down here, so the header has
       nothing to label on the right. Dropping the track as well as the label
       matters: a 25rem column reserved on a 390px screen leaves 21px for the
       word it still has to print on the left. */
    .access-head {
        grid-template-columns: minmax(0, 1fr);
    }

    .access-head > :nth-child(2) {
        display: none;
    }
}

/* ── Attendance / map ─────────────────────────────────────────────────── */

#attendanceMap,
#locationMap {
    height: 320px;
    width: 100%;
    border-radius: var(--radius-md);
    z-index: 0; /* keep Leaflet panes under Bootstrap modals and dropdowns */
}

@media (max-width: 575.98px) {
    #attendanceMap {
        height: 250px;
    }
}

.gps-readout {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: .5rem;
    text-align: center;
}

.gps-readout .readout-label {
    font-size: .67rem;
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--ink-muted);
}

.gps-readout .readout-value {
    font-size: 1.05rem;
    font-weight: 650;
    font-variant-numeric: tabular-nums;
}

/* Full width rather than a fourth readout cell: a street address is long and
   would squeeze the three numeric columns beside it. */
.gps-address {
    text-align: center;
}

.gps-address .readout-label {
    font-size: .67rem;
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--ink-muted);
}

.gps-address-value {
    font-size: .9rem;
    font-weight: 550;
    line-height: 1.35;
}

.camera-stage {
    position: relative;
    background: #14130f;
    border-radius: var(--radius-md);
    overflow: hidden;
    aspect-ratio: 4 / 3;
}

.camera-stage video,
.camera-stage img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.camera-placeholder {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: .5rem;
    color: #8b857e;
    font-size: .85rem;
}

.photo-thumb {
    width: 40px;
    height: 40px;
    object-fit: cover;
    border-radius: var(--radius-sm);
    border: 1px solid var(--line);
    cursor: pointer;
}

/* ── Misc ─────────────────────────────────────────────────────────────── */

.badge {
    border-radius: 999px;
    font-weight: 500;
    font-size: .735rem;
    padding: .28em .7em;
    letter-spacing: .01em;
    line-height: 1.35;
}

/* Bootstrap's .text-bg-* pairs a saturated fill with white text. That reads
   as an alert, and a table column of them reads as twenty alerts — so every
   badge is remapped onto the soft wash/ink pairs from :root. The declarations
   are !important because the utilities they override are, and the selector
   carries .badge so only badges are affected: .text-bg-* is also what paints
   the dashboard's stat icons, which should stay solid. */
.badge.text-bg-success { background: var(--ok-wash) !important; color: var(--ok-ink) !important; box-shadow: inset 0 0 0 1px var(--ok-line); }
.badge.text-bg-warning { background: var(--warn-wash) !important; color: var(--warn-ink) !important; box-shadow: inset 0 0 0 1px var(--warn-line); }
.badge.text-bg-danger { background: var(--danger-wash) !important; color: var(--danger-ink) !important; box-shadow: inset 0 0 0 1px var(--danger-line); }
.badge.text-bg-info { background: var(--info-wash) !important; color: var(--info-ink) !important; box-shadow: inset 0 0 0 1px var(--info-line); }
.badge.text-bg-primary { background: var(--accent-wash) !important; color: var(--accent-ink) !important; box-shadow: inset 0 0 0 1px var(--accent-wash); }
.badge.text-bg-secondary,
.badge.text-bg-light { background: var(--neutral-wash) !important; color: var(--neutral-ink) !important; box-shadow: inset 0 0 0 1px var(--neutral-line); }
.badge.text-bg-dark { background: var(--strong-wash) !important; color: var(--strong-ink) !important; box-shadow: inset 0 0 0 1px var(--strong-line); }

/* .badge-light already asks for a hairline in the markup; the wash supplies
   its own, so the Bootstrap border would double it. */
.badge.text-bg-light.border {
    border-color: transparent !important;
}

/* A status is scanned down a column rather than read, and at this size the
   dot lands before the word does. */
.badge-status {
    display: inline-flex;
    align-items: center;
    gap: .38em;
    font-weight: 600;
    padding-left: .6em;
}

.badge-status::before {
    content: "";
    width: .42em;
    height: .42em;
    border-radius: 50%;
    background: currentColor;
    flex: 0 0 auto;
}

.roster-cell {
    min-width: 2.4rem;
    text-align: center;
    font-size: .73rem;
    font-weight: 600;
}

.cursor-pointer {
    cursor: pointer;
}

.text-tabular {
    font-variant-numeric: tabular-nums;
}

.toast {
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    background: var(--surface);
}
