/* ==========================================================================
   Oki ERP - Custom Styles & Bubble Chat Feature
   ========================================================================== */

/* --- Bubble Chat Trigger & Container --- */
.bubble-chat-root {
    position: fixed;
    right: 1.5rem;
    bottom: calc(4.5rem + 1.25rem);
    /* Clear persistent bottom toolbar on mobile */
    z-index: 9999 !important;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    touch-action: none;
}

.bubble-chat-root.dragging {
    user-select: none;
    cursor: grabbing;
}

@media (min-width: 768px) {
    .bubble-chat-root {
        right: 2rem;
        bottom: 2rem;
    }
}

@media (min-width: 1200px) {
    .bubble-chat-root {
        right: 2.75rem;
        bottom: 2.75rem;
    }
}

.bubble-chat-btn {
    width: 56px;
    height: 56px;
    border-radius: 50% !important;
    cursor: grab;
    touch-action: none;
    transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.2s ease;
}

.bubble-chat-btn:hover {
    transform: scale(1.08) translateY(-2px);
    box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.25) !important;
}

.bubble-chat-btn:active,
.bubble-chat-btn.is-dragging {
    cursor: grabbing;
    transform: scale(0.96);
}

.bubble-badge {
    animation: badge-pulse 2.2s infinite ease-in-out;
}

@keyframes badge-pulse {

    0%,
    100% {
        transform: translate(-50%, -50%) scale(1);
    }

    50% {
        transform: translate(-50%, -50%) scale(1.15);
    }
}

/* --- Floating Chat Card / Window --- */
.bubble-chat-card {
    width: 380px;
    height: 540px;
    max-height: calc(100vh - 9rem);
    margin-bottom: 0.75rem;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.22) !important;
    border: 1px solid var(--bs-border-color);
    transform-origin: bottom right;
}

.chat-drag-handle {
    cursor: grab;
    touch-action: none;
}

.chat-drag-handle:active {
    cursor: grabbing;
}

@media (max-width: 576px) {
    .bubble-chat-card {
        width: calc(100vw - 2.5rem);
        height: calc(100vh - 11rem);
        max-height: 520px;
    }
}

/* --- Message Hover Actions & Reaction Bar --- */
.message-bubble-wrapper {
    position: relative;
}

.message-actions-bar {
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.15s ease, transform 0.15s ease;
    transform: translateY(0.25rem);
}

.message-row:hover .message-actions-bar,
.message-row:focus-within .message-actions-bar {
    opacity: 1;
    pointer-events: auto;
    transform: translateY(0);
}

.message-reaction-popover {
    box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.2) !important;
}

.emoji-btn {
    transition: transform 0.15s ease;
}

.emoji-btn:hover {
    transform: scale(1.28);
}

.chat-reaction-badge {
    font-size: 0.72rem;
    font-weight: 500;
    line-height: 1.1;
    text-decoration: none;
    transition: transform 0.15s ease, background-color 0.15s ease;
}

.chat-reaction-badge:hover {
    transform: scale(1.08);
}

/* --- Message Replies / Threading --- */
.chat-reply-quote {
    max-width: 100%;
    opacity: 0.92;
    transition: opacity 0.15s ease;
}

.chat-reply-quote:hover {
    opacity: 1;
}

.chat-replying-banner {
    border-top: 1px solid var(--bs-border-color);
}

.message-highlight {
    animation: msg-pulse-highlight 1.2s ease-out;
}

@keyframes msg-pulse-highlight {
    0% {
        background-color: rgba(var(--bs-primary-rgb), 0.25);
        border-radius: 0.75rem;
    }

    100% {
        background-color: transparent;
    }
}

/* --- Animations --- */
.animate-rise {
    animation: bubble-rise 0.22s cubic-bezier(0.16, 1, 0.3, 1);
}

@keyframes bubble-rise {
    from {
        opacity: 0;
        transform: translateY(16px) scale(0.96);
    }

    to {
        opacity: 1;
        transform: translateY(0) scale(1);
    }
}

.animate-slide-in-right {
    animation: slide-in-right 0.2s ease-out;
}

@keyframes slide-in-right {
    from {
        opacity: 0;
        transform: translateX(10px);
    }

    to {
        opacity: 1;
        transform: translateX(0);
    }
}

/* --- Typing Indicator --- */
.typing-dot {
    display: inline-block;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background-color: var(--bs-secondary-color);
    animation: typing-wave 1.2s infinite ease-in-out;
}

.typing-dot:nth-child(1) {
    animation-delay: 0s;
}

.typing-dot:nth-child(2) {
    animation-delay: 0.2s;
}

.typing-dot:nth-child(3) {
    animation-delay: 0.4s;
}

@keyframes typing-wave {

    0%,
    60%,
    100% {
        transform: translateY(0);
        opacity: 0.4;
    }

    30% {
        transform: translateY(-4px);
        opacity: 1;
    }
}

/* --- Custom Utilities --- */
.fs-xxs {
    font-size: 0.72rem !important;
}

.btn-xxs {
    width: 22px;
    height: 22px;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

.cursor-pointer {
    cursor: pointer;
}

.no-scrollbar::-webkit-scrollbar {
    display: none;
}

.no-scrollbar {
    -ms-overflow-style: none;
    scrollbar-width: none;
}

.chat-messages-container::-webkit-scrollbar {
    width: 5px;
}

.chat-messages-container::-webkit-scrollbar-track {
    background: transparent;
}

.chat-messages-container::-webkit-scrollbar-thumb {
    background: var(--bs-border-color);
    border-radius: 4px;
}

.chat-messages-container::-webkit-scrollbar-thumb:hover {
    background: var(--bs-secondary-color);
}
/*
 * The control that removes a picture sits on the picture, and stays out of the
 * way until somebody reaches for it — a permanent cross over somebody's face
 * reads as damage. Focus brings it back for a keyboard, and a touch screen has
 * no hover to wait for, so there it is simply always there.
 */
.avatar-remove {
    opacity: 0.65;
    transition: opacity 0.15s ease-in-out;
}

.avatar-picker:hover .avatar-remove,
.avatar-picker:focus-within .avatar-remove,
.avatar-remove.is-marked {
    opacity: 1;
}

@media (hover: none) {
    .avatar-remove {
        opacity: 1;
    }
}

/*
 * `PhotoField`'s own picture is the control — the camera mark is what says
 * so. It stays faint until the picture is reached for, the same restraint
 * `.avatar-remove` uses, and a touch screen has no hover to wait for either.
 */
.avatar-picker-hint {
    background: rgba(0, 0, 0, 0.25);
    opacity: 0;
    transition: opacity 0.15s ease-in-out;
}

.avatar-picker:hover .avatar-picker-hint,
.avatar-picker:focus-within .avatar-picker-hint,
.avatar-picker.is-dragging .avatar-picker-hint {
    opacity: 1;
}

@media (hover: none) {
    .avatar-picker-hint {
        opacity: 0.55;
    }
}

/* A drop is imminent — the ring says the picture is a target, not just a picture. */
.avatar-picker.is-dragging img {
    outline: 2px dashed var(--bs-primary);
    outline-offset: 2px;
}

/* ==========================================================================
   Profile shell — two columns, and only one of them scrolls.

   The rail is fixed to the left and never moves with the page; inside it, the
   title and the search are the first row and stay put while the second row
   scrolls in place. Used by `app/(profile)` only.

   Colours are the theme's own: the shell paints nothing of its own beyond a
 surface tint for the search box and the hover states,
   so the switcher decides how it looks in either mode.
   ========================================================================== */

:root {
    --shell-rail: 240px;
    --shell-topbar: 5rem;
    --shell-bg: var(--cz-body-bg);
    --shell-surface: rgba(0, 0, 0, 0.06);
    --shell-surface-hover: rgba(0, 0, 0, 0.1);
}

[data-bs-theme="dark"] {
    --shell-surface: rgba(255, 255, 255, 0.08);
    --shell-surface-hover: rgba(255, 255, 255, 0.14);
}

@media (min-width: 1600px) {
    :root {
        --shell-rail: 288px;
    }
}

.shell {
    min-height: 100vh;
    background-color: var(--shell-bg);
}

/* --- The rail --- */

.shell-sidebar {
    background-color: var(--shell-bg);
}

/* The rail exists from lg up only — below it the handheld foot menu is the
   navigation, so there is no panel to slide in and nothing to toggle. */
@media (min-width: 992px) {
    .shell-sidebar {
        position: fixed;
        top: 0;
        bottom: 0;
        left: 0;
        z-index: 1030;
        flex-direction: column;
        width: var(--shell-rail);
        border-right: var(--cz-border-width) solid var(--cz-border-color);
    }

    .shell-page {
        margin-left: var(--shell-rail);
    }
}

/* Row one — the title and the search. Never scrolls. */
.shell-sidebar-head {
    flex: 0 0 auto;
    padding: 0.75rem 0.75rem 0.5rem;
}

.shell-brand {
    display: flex;
    align-items: center;
    justify-content: space-between;
    min-height: 2.75rem;
    margin-bottom: 0.75rem;
}

.shell-workspace-switcher > .btn {
    display: flex;
    align-items: center;
    font-size: 0.875rem;
}

/* Pushed to the end rather than spaced apart: narrowed, the name is lifted
   out of the flow and "space between" would have nothing left to space. */
.shell-workspace-caret {
    margin-inline-start: auto;
}

.shell-workspace-switcher .dropdown-item {
    font-size: 0.875rem;
}

/* Row two — everything else, scrolling inside the rail. */
.shell-sidebar-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 0 0.75rem 1rem;
    /* Out of the way until the reader is actually in the rail. */
    scrollbar-width: none;
}

.shell-sidebar-body::-webkit-scrollbar {
    width: 0;
}

.shell-sidebar:hover .shell-sidebar-body {
    scrollbar-width: thin;
    scrollbar-color: var(--shell-surface-hover) transparent;
}

.shell-sidebar:hover .shell-sidebar-body::-webkit-scrollbar {
    width: 0.375rem;
}

.shell-sidebar-body::-webkit-scrollbar-thumb {
    background-color: var(--shell-surface-hover);
    border-radius: 0.1875rem;
}

/* --- Floating labels --- */

/*
 * Floating labels, at the height of an ordinary field.
 *
 * Bootstrap's own are 3.5rem tall, because the label lives *inside* the box and
 * the value has to move down to make room for it. Ours puts the lifted label on
 * the top border instead, so the box keeps only the value and can be the same
 * 40px as every other control on the page — which is also why a button beside
 * one lines up without being told to.
 *
 * The value never moves. In Bootstrap it shifts down the moment somebody types;
 * here the label leaves the box rather than sharing it, so the first character
 * lands exactly where the label was and nothing under the caret jumps.
 *
 * Selectors are the theme's own, repeated verbatim, because that is what it
 * takes to outweigh them — same specificity, later in the cascade. The
 * `:-moz-placeholder` pairs are Firefox's and are dropped by every other
 * browser, which is why they sit in rules of their own.
 */

.form-floating {
    /* What colour to erase the border in, behind the lifted label: whatever the
       field is sitting on. Body background unless a surface says otherwise. */
    --cz-floating-notch: var(--cz-body-bg);
    /* Half the field's height — how far the label travels to reach the top
       border. Written out of the same numbers the field is built from below, so
       the two cannot drift apart. */
    --cz-floating-lift: calc((0.875rem * 1.5 + 1.06rem + var(--cz-border-width, 1px) * 2) / 2);
}

.form-floating > .form-control,
.form-floating > .form-control-plaintext,
.form-floating > .form-select {
    height: auto;
    line-height: 1.5;
    min-height: 0;
    padding: 0.53rem 1rem;
}

.form-floating > .form-control-plaintext:focus,
.form-floating > .form-control-plaintext:not(:placeholder-shown),
.form-floating > .form-control:-webkit-autofill,
.form-floating > .form-control:focus,
.form-floating > .form-control:not(:placeholder-shown) {
    padding-bottom: 0.53rem;
    padding-top: 0.53rem;
}

.form-floating > .form-control:not(:-moz-placeholder) {
    padding-bottom: 0.53rem;
    padding-top: 0.53rem;
}

.form-floating > .form-select {
    padding-bottom: 0.53rem;
    padding-left: 1rem;
    padding-top: 0.53rem;
}

/*
 * At rest the label is the placeholder: the same size as the value that will
 * replace it, on the same line, so nothing about the field looks answered
 * before it is.
 */
.form-floating > label {
    font-size: 0.875rem;
    height: auto;
    line-height: 1.5;
    margin-left: 0.625rem;
    padding: 0 0.375rem;
    top: 50%;
    transform: translateY(-50%);
    transform-origin: 0 50%;
}

/*
 * Lifted: centred on the top border, which is the whole point — the question
 * stays readable and costs no height.
 *
 * `top` is set once and never touched again; the whole journey is a `transform`.
 * That is the only way the theme's `transition: transform .1s` can animate it —
 * `top` is a layout property, so moving the label with it would jump, and
 * reflow the field to do it.
 */
.form-floating > .form-control-plaintext ~ label,
.form-floating > .form-control:-webkit-autofill ~ label,
.form-floating > .form-control:focus ~ label,
.form-floating > .form-control:not(:placeholder-shown) ~ label,
.form-floating > .form-select ~ label {
    transform: translateY(-50%) translateY(calc(var(--cz-floating-lift) * -1)) scale(0.85);
}

.form-floating > .form-control:not(:-moz-placeholder) ~ label {
    transform: translateY(-50%) translateY(calc(var(--cz-floating-lift) * -1)) scale(0.85);
}

/*
 * The border, erased behind the label that is now sitting on it. Only once the
 * label has lifted — at rest there is nothing to erase, and a strip drawn across
 * the middle of the field would be a line nobody asked for.
 *
 * Behind the label's own text and in front of the field, which works because
 * the theme puts the label on `z-index: 2` and that makes it a stacking context
 * of its own.
 */
.form-floating > .form-control-plaintext ~ label::after,
.form-floating > .form-control:-webkit-autofill ~ label::after,
.form-floating > .form-control:focus ~ label::after,
.form-floating > .form-control:not(:placeholder-shown) ~ label::after,
.form-floating > .form-select ~ label::after {
    background-color: var(--cz-floating-notch) !important;
    content: "";
    height: 2px;
    inset: 50% 0 auto;
    margin-top: -1px;
    position: absolute;
    z-index: -1;
}

.form-floating > .form-control:not(:-moz-placeholder) ~ label::after {
    background-color: var(--cz-floating-notch) !important;
    content: "";
    height: 2px;
    inset: 50% 0 auto;
    margin-top: -1px;
    position: absolute;
    z-index: -1;
}

/* A field on a tinted surface — the "Search near you" card is one — has to erase
   the border in that tint rather than in the page's own colour. */
.bg-body-tertiary .form-floating {
    --cz-floating-notch: var(--cz-tertiary-bg);
}

.bg-body-secondary .form-floating {
    --cz-floating-notch: var(--cz-secondary-bg);
}

/* --- Search --- */

.shell-search {
    position: relative;
}

.shell-search-icon {
    position: absolute;
    top: 50%;
    left: 0.75rem;
    display: flex;
    transform: translateY(-50%);
    color: var(--cz-secondary-color);
    font-size: 0.9375rem;
    pointer-events: none;
}

.shell-search-input {
    width: 100%;
    height: 2.5rem;
    padding: 0 0.875rem 0 2.25rem;
    border: 0;
    border-radius: 1.25rem;
    background-color: var(--shell-surface);
    color: var(--cz-body-color);
    font-size: 0.9375rem;
}

.shell-search-input::placeholder {
    color: var(--cz-secondary-color);
}

.shell-search-input:focus {
    background-color: var(--shell-surface-hover);
    outline: 0;
}

/* --- Nav --- */

.shell-nav {
    display: flex;
    flex-direction: column;
    padding-top: 0.5rem;
}

.shell-nav-link {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    width: 100%;
    padding: 0.5625rem 0.75rem;
    border: 0;
    border-radius: 0.5rem;
    background-color: transparent;
    color: var(--cz-body-color);
    font-size: 0.9375rem;
    font-weight: 600;
    line-height: 1.5;
    text-align: start;
    text-decoration: none;
    white-space: nowrap;
    transition: background-color 0.15s ease-in-out, color 0.15s ease-in-out;
}

.shell-nav-link:hover,
.shell-nav-link:focus-visible {
    background-color: var(--shell-surface);
    color: var(--cz-emphasis-color);
}

.shell-nav-link.active {
    color: var(--cz-primary);
}

.shell-nav-link::after {
    display: none;
}

.shell-nav-icon {
    position: relative;
    display: flex;
    flex-shrink: 0;
    font-size: 1.375rem;
    line-height: 1;
}

.shell-nav-badge {
    position: absolute;
    top: -0.375rem;
    left: 0.875rem;
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 1.125rem;
    height: 1.125rem;
    padding: 0 0.3125rem;
    border: 0.125rem solid var(--shell-bg);
    border-radius: 0.625rem;
    background-color: var(--cz-danger);
    color: #fff;
    font-size: 0.6875rem;
    font-weight: 600;
    line-height: 1;
}

.shell-nav-label {
    overflow: hidden;
    text-overflow: ellipsis;
}

.shell-nav-section {
    display: flex;
    flex-direction: column;
}

/* The "there is more under this" mark. Absolute, inside the row's own right
   padding, so a section is exactly as wide as a plain link — a glyph in the
   flow pushed it wider than every other row. */
.shell-nav-section .shell-nav-link {
    position: relative;
}

.shell-nav-caret {
    position: absolute;
    inset-inline-end: 0.625rem;
    width: 0.3125rem;
    height: 0.3125rem;
    border-right: 0.09375rem solid currentcolor;
    border-bottom: 0.09375rem solid currentcolor;
    opacity: 0.5;
    transform: translateY(-0.0625rem) rotate(45deg);
    transition: transform 0.15s ease-in-out;
}

[aria-expanded='true'] > .shell-nav-caret {
    transform: translateY(0.0625rem) rotate(-135deg);
}

.shell-nav-subnav {
    display: flex;
    flex-direction: column;
    margin: 0.125rem 0 0.25rem 1.375rem;
    padding-left: 0.625rem;
    border-left: var(--cz-border-width) solid var(--cz-border-color);
}

.shell-nav-link-sub {
    padding-block: 0.4375rem;
    font-size: 0.8125rem;
}

.shell-nav-link-sub .shell-nav-icon {
    font-size: 1rem;
}

.shell-divider {
    margin: 0.5rem 0.25rem;
    border: 0;
    border-top: var(--cz-border-width) solid var(--cz-border-color);
    opacity: 1;
}

/* Which application the rail has been drilled into — its name, and the one
   way back out. Drawn only inside one: My work is the top level and says so
   by being what the rail opens on. */
.shell-space-head {
    display: flex;
    align-items: center;
    gap: 0.25rem;
    margin-bottom: 0.5rem;
    padding-bottom: 0.5rem;
    border-bottom: var(--cz-border-width) solid var(--cz-border-color);
}

.shell-space-back {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 1.75rem;
    height: 1.75rem;
    border-radius: var(--cz-border-radius-sm);
    color: var(--cz-secondary-color);
    text-decoration: none;
}

.shell-space-back:hover {
    color: var(--cz-emphasis-color);
    background-color: var(--cz-secondary-bg);
}

.shell-space-title {
    min-width: 0;
    color: var(--cz-emphasis-color);
    font-size: 0.875rem;
    font-weight: 600;
}



.shell-more-wrap .dropdown-menu {
    --cz-dropdown-min-width: 12rem;
}

/* --- Following accounts --- */

.shell-section {
    padding-top: 0.25rem;
}

.shell-heading {
    margin: 0 0.25rem 0.375rem;
    color: var(--cz-secondary-color);
    font-size: 0.875rem;
    font-weight: 600;
}

.shell-note {
    margin: 0 0.25rem;
    color: var(--cz-secondary-color);
    font-size: 0.875rem;
}

.shell-note-link {
    color: var(--cz-primary);
    text-decoration: none;
}

.shell-people {
    margin: 0;
    padding: 0;
    list-style: none;
}

.shell-person {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    padding: 0.375rem 0.25rem;
    border-radius: 0.5rem;
    color: var(--cz-body-color);
    text-decoration: none;
}

.shell-person:hover {
    background-color: var(--shell-surface);
}

.shell-person-avatar-wrap {
    flex-shrink: 0;
    display: block;
    line-height: 0;
}

.shell-person-avatar {
    flex-shrink: 0;
    width: 2.25rem;
    height: 2.25rem;
    border-radius: 50%;
    object-fit: cover;
}

.shell-person-text {
    display: block;
    min-width: 0;
}

.shell-person-name,
.shell-person-handle {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.shell-person-name {
    font-size: 0.875rem;
    font-weight: 600;
}

.shell-person-handle {
    color: var(--cz-secondary-color);
    font-size: 0.8125rem;
}

.shell-more {
    display: inline-block;
    margin: 0.375rem 0.25rem 0;
    color: var(--cz-secondary-color);
    font-size: 0.875rem;
    text-decoration: none;
}

.shell-more:hover {
    color: var(--cz-primary);
}

/* --- The page beside the rail --- */

.shell-topbar {
    position: sticky;
    top: 0;
    z-index: 1029;
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 1rem;
    background-color: var(--shell-bg);
    border-bottom: var(--cz-border-width) solid var(--cz-border-color);
}

/* One row at every width. These are icon buttons of a fixed size and they
   belong beside each other; `nowrap` is what keeps them there when the bar is
   narrow, rather than letting them break into a column. */
.shell-actions {
    display: flex;
    flex-direction: row;
    flex-wrap: nowrap;
    align-items: center;
    gap: 0.25rem;
    margin-left: auto;
}

.shell-actions>* {
    flex-shrink: 0;
}

@media (min-width: 992px) {
    /*
     * Fixed rather than sticky from lg up: the controls are chrome over the page
     * and they hold their corner while it scrolls under them. `absolute` would
     * be carried up the page with the scroll — only `fixed` is out of the
     * scroll as well as out of the flow. It starts where the rail ends, so the
     * two never overlap, and it is given an explicit height because the room it
     * no longer takes in the flow has to be handed back exactly.
     */
    .shell-topbar {
        position: fixed;
        top: 0;
        right: 0;
        left: var(--shell-rail);
        height: var(--shell-topbar);
        justify-content: flex-end;
        padding: 0.75rem 1rem;
        border-bottom: 0;
    }

    /* The room the fixed bar covers, given back — otherwise the page begins
       underneath it. */
    .shell-page {
        padding-top: var(--shell-topbar);
    }

    .shell-actions {
        padding: 0.25rem;
        border-radius: 2rem;
        background-color: var(--shell-surface);
    }
}

/* The page's own column. It fills whatever the rail leaves at every width — the
   wall of tiles is the body of a profile, and a cap here left the right of a
   wide screen empty while the tiles stayed small.

   The bottom padding is clearance rather than spacing: below lg the foot menu
   is fixed over the page, and the last row of anything would otherwise sit
   under it. Exempt from the 1–2 scale for that reason. */
.shell-main {
    --page-gutter: 1rem;

    padding: var(--page-gutter);
    padding-bottom: 3rem;
}

/*
 * A band that reaches the edges of the page through the room the page keeps
 * around itself — a tinted landing section, where the tint is the point and a
 * gutter either side of it reads as a mistake. A page's ordinary content
 * keeps the room.
 */
.bleed {
    margin-right: calc(var(--page-gutter) * -1);
    margin-left: calc(var(--page-gutter) * -1);
}

/* --- The handheld foot menu --- */

/*
 * Below lg this is the whole of the navigation. The rail is not rendered there
 * at all, so nothing a reader wants is behind a toggler: every destination is
 * one thumb-reach tap, and the middle one — posting — is the raised button,
 * because it is the only item that makes something rather than going somewhere.
 */
.shell-tabbar {
    position: fixed;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 1030;
    display: flex;
    align-items: stretch;
    padding-bottom: env(safe-area-inset-bottom);
    background-color: var(--shell-bg);
    border-top: var(--cz-border-width) solid var(--cz-border-color);
}

/* Reserves the height the fixed bar covers, so the last row of a page is never
   trapped underneath it. */
.shell-tabbar-spacer {
    height: calc(4rem + env(safe-area-inset-bottom));
}

.shell-tabbar-link {
    display: flex;
    flex: 1 1 0;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.125rem;
    min-width: 0;
    padding: 0.5rem 0.25rem;
    border: 0;
    background-color: transparent;
    color: var(--cz-secondary-color);
    text-decoration: none;
}

.shell-tabbar-link.active {
    color: var(--cz-primary);
}

.shell-tabbar-icon {
    position: relative;
    display: flex;
    font-size: 1.375rem;
    line-height: 1;
}

.shell-tabbar-label {
    overflow: hidden;
    max-width: 100%;
    font-size: 0.6875rem;
    font-weight: 600;
    line-height: 1.2;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.shell-tabbar-badge {
    position: absolute;
    top: -0.3125rem;
    left: 0.75rem;
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 1rem;
    height: 1rem;
    padding: 0 0.25rem;
    border: 0.125rem solid var(--shell-bg);
    border-radius: 0.5rem;
    background-color: var(--cz-danger);
    color: #fff;
    font-size: 0.625rem;
    font-weight: 600;
    line-height: 1;
}

/* The one item that makes something. Given the theme's own primary rather than
   an invented colour, so the switcher still decides how it looks. */
.shell-tabbar-link-primary .shell-tabbar-icon {
    width: 2.75rem;
    height: 1.75rem;
    align-items: center;
    justify-content: center;
    border-radius: 0.5rem;
    background-color: var(--cz-primary);
    color: var(--cz-primary-bg-subtle);
    font-size: 1.125rem;
}

/* The chat bubble floats over the same corner, so it clears the bar. */
@media (max-width: 991.98px) {
    .shell-actions .btn-icon {
        --cz-btn-size: 2.5rem;
    }

    .shell-topbar {
        justify-content: space-between;
    }
}

/* ==========================================================================
   A profile — the person, their numbers, and their organization.

   Read in one column on a phone and beside the picture from md up: the picture,
   their name, the handle, the three numbers, what you can do about it, then the
   small print. The stat row replaced a pair of coloured panels that pushed the
   organization below the fold on every phone.
   ========================================================================== */

.profile-page {
    /* Clearance over the foot menu, as on `.shell-main`. */
    padding-bottom: 2rem;
}

.profile-head {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.75rem;
    padding: 1rem 0;
    text-align: center;
}

.profile-avatar {
    flex-shrink: 0;
    width: 7rem;
    height: 7rem;
    font-size: 2rem;
}

.profile-identity {
    min-width: 0;
    width: 100%;
}

/* Their name carries the header: it is what a reader recognises, and it is the
   h1. It is allowed a second line — cutting somebody's name off with an ellipsis
   to keep it on one is worse than wrapping it. */
.profile-name {
    margin: 0 0 0.125rem;
    font-size: 1.75rem;
    font-weight: 700;
    line-height: 1.2;
    overflow-wrap: break-word;
}

/* The name and the pencil beside it. A flex row of its own, so it has to be
   told to centre: the head's `text-align` moves the text inside a box, not the
   box, and the row was sitting against the left edge of a full-width column
   while everything under it was centred.

   It wraps, but only as a last resort. The name shrinks first — down to its
   longest word, wrapping onto as many lines as it needs with the pencil still
   beside it — and the pencil drops to a line of its own only when even that
   word leaves no room for it. */
.profile-name-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
}

/* Never squashed: it is a circle with an icon in it, and there is nothing in it
   to give up. */
.profile-name-row > .btn {
    flex-shrink: 0;
}

/* The handle beneath it: the address, said quietly. Bounded at 16 characters by
   the API, so one line always holds it. */
.profile-handle {
    overflow: hidden;
    margin: 0 0 0.875rem;
    color: var(--cz-secondary-color);
    font-size: 0.9375rem;
    text-overflow: ellipsis;
    white-space: nowrap;
}

@media (min-width: 768px) {
    .profile-head {
        flex-direction: row;
        align-items: center;
        gap: 1rem;
        padding: 1rem 0;
        text-align: start;
    }

    .profile-avatar {
        width: 9rem;
        height: 9rem;
        font-size: 2.5rem;
    }

    .profile-name-row {
        justify-content: flex-start;
    }

    .profile-name {
        font-size: 2rem;
    }
}

/* --- The three numbers --- */

.profile-stats {
    display: flex;
    align-items: baseline;
    justify-content: center;
    gap: 1rem;
    margin-bottom: 0.75rem;
}

@media (min-width: 768px) {
    .profile-stats {
        justify-content: flex-start;
        gap: 1rem;
    }
}

.profile-stat {
    display: flex;
    align-items: baseline;
    gap: 0.3125rem;
    padding: 0;
    border: 0;
    background-color: transparent;
    color: var(--cz-body-color);
    text-decoration: none;
}

.profile-stat:not(.profile-stat-static):hover .profile-stat-label {
    color: var(--cz-primary);
}

.profile-stat-value {
    font-size: 1.0625rem;
    font-weight: 700;
    line-height: 1.2;
}

.profile-stat-label {
    color: var(--cz-secondary-color);
    font-size: 0.875rem;
}

/* --- What you can do about them --- */

/*
 * Full width on a phone, because there are at most two of them and a thumb
 * should not have to aim. They keep their natural width from sm up.
 */
.profile-actions>div {
    justify-content: center;
}

.profile-actions .btn {
    flex: 1 1 0;
    min-width: 8rem;
}

@media (min-width: 768px) {
    .profile-actions>div {
        justify-content: flex-start;
    }

    .profile-actions .btn {
        flex: 0 0 auto;
    }
}

.profile-bio {
    margin: 0.875rem 0 0;
    color: var(--cz-secondary-color);
    font-size: 0.8125rem;
}

.profile-bio-dot {
    margin: 0 0.375rem;
}

/* --- The organization: tabs, then a wall of tiles --- */

.profile-tabs {
    display: flex;
    border-top: var(--cz-border-width) solid var(--cz-border-color);
    border-bottom: var(--cz-border-width) solid var(--cz-border-color);
}

.profile-tab {
    display: flex;
    flex: 1 1 0;
    align-items: center;
    justify-content: center;
    gap: 0.375rem;
    padding: 0.75rem 0.5rem;
    border: 0;
    border-bottom: 2px solid transparent;
    margin-bottom: calc(var(--cz-border-width) * -1);
    background-color: transparent;
    color: var(--cz-secondary-color);
    font-size: 0.9375rem;
    font-weight: 600;
}

.profile-tab.active {
    border-bottom-color: var(--cz-emphasis-color);
    color: var(--cz-emphasis-color);
}

/* The same tabs in a column beside the page rather than across it — an organization's
   sections sit in the narrow half, where four full-size tabs would wrap. */
.profile-tabs-sm .profile-tab {
    gap: 0.25rem;
    padding: 0.5rem 0.25rem;
    font-size: 0.8125rem;
}

.profile-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 0.125rem;
    margin: 0.125rem 0 0;
    padding: 0;
    list-style: none;
}

@media (min-width: 992px) {
    .profile-grid {
        grid-template-columns: repeat(4, 1fr);
        gap: 0.5rem;
    }
}

@media (min-width: 1400px) {
    .profile-grid {
        grid-template-columns: repeat(5, 1fr);
    }
}

/* The whole tile is the link, so it carries none of a link's decoration: the
   caption is a caption, not a row of blue underlined text. */
.profile-tile {
    display: flex;
    flex-direction: column;
    height: 100%;
    overflow: hidden;
    background-color: var(--shell-surface);
    color: inherit;
    text-decoration: none;
}

.profile-tile:hover,
.profile-tile:focus-visible {
    background-color: var(--shell-surface-hover);
    color: inherit;
}

.profile-tile:hover .profile-tile-img {
    transform: scale(1.03);
}

.profile-tile:focus-visible {
    outline: 2px solid var(--cz-emphasis-color);
    outline-offset: -2px;
}

@media (min-width: 992px) {
    .profile-tile {
        border-radius: 0.5rem;
    }
}

/* Portrait, as a feed of things to open is: the picture is the control, and
   the caption sits under it rather than over somebody's produce. */
.profile-tile-media {
    position: relative;
    aspect-ratio: 3 / 4;
    overflow: hidden;
    background-color: var(--shell-surface-hover);
}

.profile-tile-img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform 0.25s ease-out;
}

@media (prefers-reduced-motion: reduce) {
    .profile-tile-img {
        transition: none;
    }

    .profile-tile:hover .profile-tile-img {
        transform: none;
    }
}

.profile-tile-pin,
.profile-tile-counts {
    position: absolute;
    display: flex;
    align-items: center;
    color: #fff;
    font-size: 0.75rem;
    font-weight: 600;
    line-height: 1;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
}

.profile-tile-views,
.profile-tile-likes {
    display: flex;
    align-items: center;
    gap: 0.1875rem;
}

.profile-tile-likes i {
    color: var(--cz-danger);
}

.profile-tile-pin {
    top: 0.375rem;
    left: 0.375rem;
    padding: 0.1875rem 0.375rem;
    border-radius: 0.25rem;
    background-color: var(--cz-danger);
    text-shadow: none;
}

/* More than one thing may be said in the corner — how many opened it, how many
   liked it — so the corner is a row and each mark is one item in it. */
.profile-tile-counts {
    bottom: 0.375rem;
    left: 0.375rem;
    gap: 0.5rem;
}

/* Sold out is said on the tile rather than by taking it down: what somebody
   sells is part of who they are, and an empty grid says the wrong thing. */
.profile-tile-veil {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: rgba(0, 0, 0, 0.55);
}

.profile-tile-veil-text {
    padding: 0.25rem 0.5rem;
    border: 1px solid rgba(255, 255, 255, 0.75);
    border-radius: 0.25rem;
    color: #fff;
    font-size: 0.75rem;
    font-weight: 600;
    text-transform: uppercase;
}

.profile-tile-body {
    display: flex;
    flex: 1 1 auto;
    flex-direction: column;
    gap: 0.125rem;
    padding: 0.375rem 0.5rem 0.5rem;
}

.profile-tile-price {
    font-size: 0.8125rem;
    font-weight: 700;
    line-height: 1.2;
}

.profile-tile-unit {
    color: var(--cz-secondary-color);
    font-size: 0.6875rem;
    font-weight: 500;
}

.profile-tile-move {
    display: flex;
    align-items: center;
    gap: 0.1875rem;
    font-size: 0.6875rem;
    font-weight: 600;
}

.profile-tile-was {
    color: var(--cz-secondary-color);
    font-weight: 500;
}

/* Two lines of caption and then an ellipsis: the tile is a fixed size, and a
   long name must not push the row it is in taller than its neighbours. */
.profile-tile-title {
    display: -webkit-box;
    overflow: hidden;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    color: var(--cz-secondary-color);
    font-size: 0.75rem;
    line-height: 1.3;
}

/* ==========================================================================
   One thing in somebody's organization — the page a tile opens.

   Written for a phone first: one column, in the order the thing is asked about,
   and the way back at the very top, because the shell above it carries no
   breadcrumb and never will. From lg up the picture takes the left half and
   stays there — sticky under the fixed bar — while the column beside it scrolls,
   which is what the template's own product page does with its gallery.
   ========================================================================== */

.organization-item {
    /* Clearance over the foot menu, as on `.shell-main`. */
    padding-bottom: 2rem;
}

/* The way back is a direction, not an act, so it is a row of text rather than a
   button competing with the two that do something. */
.organization-item-back {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    padding: 0.75rem 0;
    color: var(--cz-secondary-color);
    font-size: 0.875rem;
    font-weight: 500;
    text-decoration: none;
}

.organization-item-back:hover {
    color: var(--cz-emphasis-color);
}

@media (min-width: 992px) {
    .organization-item-frame {
        position: sticky;
        top: calc(var(--shell-topbar) + 1rem);
        align-self: flex-start;
    }
}

/* Square, not portrait: the tile crops to a thumbnail, the page shows the thing.
   1:1 is the template's product frame, and it suits both a bag photographed
   close up and a wide shot of a stall. */
.organization-item-media {
    position: relative;
    aspect-ratio: 1 / 1;
    overflow: hidden;
    border-radius: 0.75rem;
    background-color: var(--shell-surface);
}

.organization-item-img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* The price at the size a price is read at, with the unit beside it in the
   secondary colour so the number is what the eye lands on. */
.organization-item-price {
    font-size: 1.75rem;
    font-weight: 700;
    line-height: 1.1;
}

.organization-item-unit {
    color: var(--cz-secondary-color);
    font-size: 1rem;
    font-weight: 500;
}

/* Where, when, and how many have looked. A line each on a phone, wrapping into
   a row wherever there is width for one. */
.organization-item-facts {
    display: flex;
    flex-wrap: wrap;
    gap: 0.375rem 1rem;
    margin: 0 0 1rem;
    padding: 0;
    list-style: none;
    color: var(--cz-secondary-color);
    font-size: 0.875rem;
}

.organization-item-facts > li {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
}

/* Who is selling it, given a surface of its own: on this marketplace the seller
   is the answer to "is this real", not a line of small print under the picture.
   It stays a column at every width — the two controls are full-width targets on
   a phone, and the column it sits in is too narrow from lg up to put them beside
   a name without cramping both. */
.organization-item-seller {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    padding: 0.875rem;
    border-radius: 0.75rem;
    background-color: var(--shell-surface);
}

.organization-item-seller-who {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    color: inherit;
    text-decoration: none;
}

.organization-item-seller-avatar {
    display: block;
    flex-shrink: 0;
    width: 3rem;
    height: 3rem;
    font-size: 1rem;
    font-weight: 600;
}

.organization-item-seller-text {
    display: flex;
    min-width: 0;
    flex-direction: column;
}

/* The name leads here too, as it does on their profile — and here it does hold
   one line: it sits beside a fixed avatar in a column too narrow to give it two. */
.organization-item-seller-name {
    overflow: hidden;
    font-size: 1.0625rem;
    font-weight: 600;
    line-height: 1.3;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.organization-item-seller-handle {
    color: var(--cz-secondary-color);
    font-size: 0.8125rem;
}

/* A table that is skimmed rather than read: the label column is fixed and quiet,
   and the value column carries the weight. */
.organization-item-table th {
    width: 40%;
    color: var(--cz-secondary-color);
    font-weight: 500;
}

.organization-item-more {
    padding-top: 1rem;
}

/* The heart once it has been pressed. `LikeButton` sets the class wherever it is
   drawn, and the filled shape on its own is too quiet to read as a state. */
.liked i {
    color: var(--cz-danger);
}

/* Following, said the same way and in the colour that means "held": the tick
   and the outstretched hand are near-identical shapes at icon size, and on a
   card there is no room for the word beside the tally to tell them apart.
   `FollowButton` sets the class wherever it is drawn — a card's mark and a
   page's labelled button alike, so the two never say it differently. */
.following i {
    color: var(--cz-success);
}

/* --- Card marks --- */

/* What a card says it amounts to: a row of icons, each with its tally beside it.

   Some of them press and some do not — Follow and the heart are controls, the
   comment and shelf counts are statements — and they are drawn to one rule on
   purpose. A reader takes the foot of a card in as one line, and a row that
   mixed bordered buttons with plain numbers would read as two unrelated things
   that happened to sit together. */
.card-stats {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.125rem;
}

/* No chrome at all. A border around each mark makes a row of five look like a
   toolbar; without one the icons themselves carry it, which is why they are
   larger and darker here than a caption would be. */
.card-stat {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    padding: 0.25rem 0.375rem;
    border: 0;
    border-radius: 0.375rem;
    background: none;
    color: var(--cz-body-color);
    font-size: 0.8125rem;
    font-weight: 600;
    line-height: 1;
    text-decoration: none;
    transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out;
}

.card-stat i {
    font-size: 1.125rem;
    line-height: 1;
}

/* The same row on a page somebody opened on purpose rather than on a card they
   are skimming: larger, because here it is the control they came for and not a
   summary of one. */
.card-stats-lg {
    gap: 0.25rem;
}

.card-stats-lg .card-stat {
    padding: 0.375rem 0.5rem;
    font-size: 0.875rem;
}

.card-stats-lg .card-stat i {
    font-size: 1.375rem;
}

/* Only the ones that press react to a pointer. A statement that lit up on hover
   would be promising something to press. */
a.card-stat:hover,
button.card-stat:hover {
    color: var(--cz-primary);
    background-color: var(--shell-surface-hover);
}

button.card-stat:disabled {
    opacity: 0.65;
    background: none;
}

/* --- Loading --- */

/*
 * A loading icon that actually turns. The theme's own `.animate-spin` is a
 * 10-second rotation meant for decoration; at that speed a loader reads as a
 * frozen page. Same `spin` keyframes, a speed that says "working".
 */
.icon-spin {
    animation: spin 0.9s linear infinite;
    display: inline-block;
}

@media (prefers-reduced-motion: reduce) {
    .icon-spin {
        animation-duration: 3s;
    }
}

/* --- Presence --- */

/*
 * The dot that says somebody is here.
 *
 * The ring is the page's own background rather than white, so the dot stays
 * separated from the picture under it in either theme. The glow is a second
 * shadow that breathes outward — slow, and never all the way to nothing, so it
 * reads as alive rather than as a notification demanding something.
 */
.presence-dot {
    border-radius: 50%;
    background-color: var(--cz-success);
    box-shadow:
        0 0 0 2px var(--cz-body-bg),
        0 0 0 0 rgba(var(--cz-success-rgb), 0.55);
    animation: presence-glow 2.2s ease-out infinite;
}

@keyframes presence-glow {
    0% {
        box-shadow:
            0 0 0 2px var(--cz-body-bg),
            0 0 0 0 rgba(var(--cz-success-rgb), 0.55);
    }

    70% {
        box-shadow:
            0 0 0 2px var(--cz-body-bg),
            0 0 0 6px rgba(var(--cz-success-rgb), 0);
    }

    100% {
        box-shadow:
            0 0 0 2px var(--cz-body-bg),
            0 0 0 0 rgba(var(--cz-success-rgb), 0);
    }
}

/*
 * Somebody who has asked for less movement still gets the dot, and still gets
 * the ring that separates it — it simply stops pulsing. The fact is the colour;
 * the animation was only ever emphasis.
 */
@media (prefers-reduced-motion: reduce) {
    .presence-dot {
        animation: none;
    }
}

/*
 * The theme's own `.animate-scale`, `.animate-shake`, `.animate-pulse`,
 * `.animate-rotate` and the `.animate-slide-*` family play a keyframe on
 * hover/focus for `.animate-target` — vendored in `theme.min.css`, which is
 * not hand-edited. Somebody who has asked for less movement still gets the
 * emphasis; it simply stops moving.
 */
@media (prefers-reduced-motion: reduce) {

    .animate-scale:focus-visible .animate-target,
    .animate-scale:hover .animate-target,
    .animate-shake:hover .animate-target,
    .animate-pulse:focus-visible .animate-target,
    .animate-pulse:hover .animate-target,
    .animate-rotate:focus-visible .animate-target,
    .animate-rotate:hover .animate-target,
    .animate-slide-end:focus-visible .animate-target,
    .animate-slide-end:hover .animate-target,
    .animate-slide-start:focus-visible .animate-target,
    .animate-slide-start:hover .animate-target,
    .animate-slide-up:focus-visible .animate-target,
    .animate-slide-up:hover .animate-target,
    .animate-slide-down:focus-visible .animate-target,
    .animate-slide-down:hover .animate-target {
        animation: none;
    }
}

/* --- Search --- */

/*
 * The browser's own clear button, off.
 *
 * WebKit draws a cross inside every `type="search"` field once there is text in
 * it, in exactly the place ours sits — so the reader is offered two, one of
 * which nobody styled and Firefox never shows. Ours is the one that stays; see
 * `app/components/SearchField.tsx`.
 */
.search-field::-webkit-search-cancel-button,
.search-field::-webkit-search-decoration {
    -webkit-appearance: none;
    appearance: none;
}

/* ==========================================================================
   Density
   ========================================================================== */

/*
 * The theme's spacing is written for a desktop and repeated unchanged on a
 * phone, where a dialog then spends most of its width on padding.
 *
 * `-3` — 1rem — is both the baseline and the ceiling here. Every padded
 * container sits at it, at every width, through the variables the theme
 * already reads: its own `--cz-` prefix, not Bootstrap's `--bs-`. One number
 * rather than a scale that grows with the screen, so a card reads the same on
 * a phone as on a desk. Nothing is restyled, only spaced.
 *
 * The same ceiling holds for this file's own rules. Rows inside a list are not
 * containers and keep a tighter step; a bottom padding that clears the fixed
 * foot menu is clearance rather than spacing, and says so where it stands.
 */
.modal {
    --cz-modal-padding: 1rem;
    --cz-modal-header-padding: 1rem;
    --cz-modal-margin: 0.5rem;
}

/* The body and, below it, the card's own header. */
.card {
    --cz-card-spacer-y: 1rem;
    --cz-card-spacer-x: 1rem;
    --cz-card-cap-padding-x: 1rem;
    --cz-card-cap-padding-y: 1rem;
}

.offcanvas {
    --cz-offcanvas-padding-y: 1rem;
    --cz-offcanvas-padding-x: 1rem;
}

.alert {
    --cz-alert-padding-x: 1rem;
    --cz-alert-padding-y: 1rem;
}

.accordion {
    --cz-accordion-btn-padding-y: 1rem;
}

/* Rows, not containers: a menu item padded like a card would be a menu you
   scroll. */
.dropdown-menu {
    --cz-dropdown-padding-x: 0.375rem;
    --cz-dropdown-padding-y: 0.375rem;
    --cz-dropdown-item-padding-y: 0.375rem;
}

.list-group {
    --cz-list-group-item-padding-y: 0.5rem;
}

/* ==========================================================================
   Overlays hold the page
   ========================================================================== */

/*
 * A dialog or a panel is the whole of what you are doing while it is open, so
 * the page behind it does not move.
 *
 * Bootstrap locks `body`, which is enough on an ordinary page and not enough
 * here: `body`'s overflow only reaches the viewport while `html`'s is
 * `visible`, and the sideways guard in `app/globals.css` is one declaration
 * away from breaking that. Locking `html` for as long as a backdrop is on
 * screen says it once, whatever else the two carry.
 */
html:has(body.modal-open),
html:has(.modal-backdrop),
html:has(.offcanvas-backdrop) {
    overflow: hidden;
}

/* And a scroll that reaches the end of a dialog stops there rather than
   carrying on down the page under it. */
.modal,
.offcanvas {
    overscroll-behavior: contain;
}

/* --- Handheld foot menu: a group's sheet --- */

/* HR is a heading and More is a leftover pile — neither has a page, so the tap
   opens this above the bar rather than following a link into nothing. */
.shell-tabsheet {
    position: fixed;
    right: 0;
    bottom: calc(4rem + env(safe-area-inset-bottom));
    left: 0;
    z-index: 1031;
    display: flex;
    flex-direction: column;
    max-height: 60vh;
    padding: 0.5rem;
    overflow-y: auto;
    background-color: var(--shell-bg);
    border-top: var(--cz-border-width) solid var(--cz-border-color);
    box-shadow: 0 -0.5rem 1.5rem rgba(0, 0, 0, 0.12);
}

.shell-tabsheet-backdrop {
    position: fixed;
    inset: 0;
    z-index: 1029;
    padding: 0;
    border: 0;
    background-color: rgba(0, 0, 0, 0.25);
}

.shell-tabsheet-heading {
    margin: 0.5rem 0.75rem 0.25rem;
    color: var(--cz-secondary-color);
    font-size: 0.75rem;
    font-weight: 600;
    text-transform: uppercase;
}

.shell-tabsheet-link {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    padding: 0.625rem 0.75rem;
    border-radius: 0.5rem;
    color: var(--cz-body-color);
    font-size: 0.9375rem;
    font-weight: 600;
    text-decoration: none;
}

.shell-tabsheet-link:hover,
.shell-tabsheet-link:focus-visible {
    background-color: var(--shell-surface);
    color: var(--cz-emphasis-color);
}

.shell-tabsheet-link.active {
    color: var(--cz-primary);
}

/* A workspace's own mark, held to the rail's brand height whatever it uploaded. */
.shell-brand-logo {
    max-width: 142px;
    max-height: 44px;
    width: auto;
    height: auto;
}

/* --- Dropping a file --- */

/* Bootstrap has no dashed border utility, and the dash is what says "put
   something here" — a solid box is a panel. Only the style: the colour and the
   width stay Bootstrap's, so the zone follows the theme like everything else. */
.border-dashed {
  border-style: dashed !important;
}

/* The whole zone is one control, so it takes the focus ring as one — the input
   inside it is visually hidden and cannot show one where the reader is looking. */
.dropzone:focus-visible {
  outline: 0;
  box-shadow: var(--bs-focus-ring-x, 0) var(--bs-focus-ring-y, 0) var(--bs-focus-ring-blur, 0)
    var(--bs-focus-ring-width) var(--bs-focus-ring-color);
}

/* --- A narrowed rail --- */

/*
 * The rail at icon width, with the word on hover instead of beside the icon.
 *
 * Everything keys off one attribute on `<html>`, written before paint by the
 * layout's own script and toggled by `RailToggle`, so the rail, the page beside
 * it and every flyout cannot disagree about which state they are in.
 *
 * Only from lg up, like the rail itself: below that the foot menu is the
 * navigation and there is nothing to narrow.
 */
@media (min-width: 992px) {
    :root {
        --shell-rail-narrow: 4.25rem;
    }

    [data-rail='collapsed'] .shell-sidebar {
        width: var(--shell-rail-narrow);
    }

    [data-rail='collapsed'] .shell-page {
        margin-left: var(--shell-rail-narrow);
    }

    /* Clipped rather than removed: the icon is not a name, and a reader on a
       screen reader should still hear "People" where a sighted one sees a
       person. `display: none` would take it out of the accessibility tree. */
    [data-rail='collapsed'] .shell-space-title,
    [data-rail='collapsed'] .shell-nav-caret,
    [data-rail='collapsed'] .shell-nav-label,
    [data-rail='collapsed'] .shell-workspace-name {
        position: absolute;
        width: 1px;
        height: 1px;
        margin: -1px;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
    }

    [data-rail='collapsed'] .shell-nav-link,
    [data-rail='collapsed'] .shell-space-head {
        justify-content: center;
        padding-inline: 0.5rem;
    }

    /* The inline sub-nav is the wide rail's way of showing sub-pages. Narrowed,
       the flyout is, and two of them at once would be two answers to one
       question. */
    [data-rail='collapsed'] .shell-nav-subnav {
        display: none;
    }

    [data-rail='collapsed'] .shell-sidebar-head,
    [data-rail='collapsed'] .shell-sidebar-body {
        padding-inline: 0.5rem;
    }

    /* The mark is a picture, not a word, so it stays — only the toggle moves
       under it, because the two of them side by side do not fit 4.25rem. */
    [data-rail='collapsed'] .shell-brand {
        flex-direction: column;
        gap: 0.375rem;
        min-height: auto;
    }

    /* The switcher loses its name and its chevron, and shows a building
       instead: an arrow on its own names nothing, and the list beside it
       already says it opens. */
    [data-rail='collapsed'] .shell-workspace-switcher > .btn {
        justify-content: center;
        padding-inline: 0.5rem;
    }

    [data-rail='collapsed'] .shell-workspace-caret {
        display: none;
    }

    [data-rail='collapsed'] .shell-workspace-icon {
        margin-inline-end: 0;
        font-size: 1.125rem;
    }

    [data-rail='collapsed'] .shell-workspace-switcher svg {
        margin-left: 0 !important;
    }

    /* The dropdown is as wide as the rail, which narrowed is too narrow to
       read a workspace's name in. It also stops being a dropdown: narrowed,
       it is a submenu like every other rail entry — it opens to the side, on
       hover, and shares their transparent-border bridge so the pointer can
       cross into it. A click still works, and closes it the same way. */
    [data-rail='collapsed'] .shell-workspace-switcher .dropdown-menu {
        inset: 0 auto auto 100%;
        width: auto;
        min-width: 12rem;
        margin: 0;
        border-left: 0.5rem solid transparent;
        background-clip: padding-box;
    }

    [data-rail='collapsed'] .shell-workspace-switcher:hover > .dropdown-menu,
    [data-rail='collapsed'] .shell-workspace-switcher:focus-within > .dropdown-menu {
        display: block;
    }

    /* A flyout is positioned against its own row and would be cut off by the
       body's scrollbox, so the narrowed rail does not scroll. It holds icons
       only — a dozen rows at most — and that fits any screen wide enough for a
       rail to be drawn at all. */
    [data-rail='collapsed'] .shell-sidebar-body {
        overflow: visible;
    }
}

.shell-nav-item {
    position: relative;
}

.shell-nav-flyout {
    display: none;
    position: absolute;
    top: 0;
    left: 100%;
    z-index: 1040;
    min-width: 11rem;
    padding: 0.5rem;
    border: var(--cz-border-width) solid var(--cz-border-color);
    /* The gap to the rail is the card's own transparent border, not a
       margin: empty space between them loses the pointer mid-journey and
       the card closes before the sub-pages inside can be reached. Set
       after the shorthand, or the shorthand would wipe it. */
    border-left: 0.5rem solid transparent;
    background-clip: padding-box;
    border-radius: 0.5rem;
    background-color: var(--cz-body-bg);
    box-shadow: var(--cz-box-shadow);
}

/* Shown on hover for a pointer and on focus for a keyboard — the sub-pages
   inside are the only way into them while the rail is narrowed, so they have to
   be reachable without one. */
@media (min-width: 992px) {
    [data-rail='collapsed'] .shell-nav-item:hover > .shell-nav-flyout,
    [data-rail='collapsed'] .shell-nav-item:focus-within > .shell-nav-flyout,
    [data-rail='collapsed'] .shell-nav-flyout:hover {
        display: block;
    }
}

.shell-nav-flyout-title {
    display: block;
    padding: 0.25rem 0.5rem;
    color: var(--cz-emphasis-color);
    font-size: 0.8125rem;
    font-weight: 600;
    white-space: nowrap;
}

.shell-nav-flyout-items {
    display: flex;
    flex-direction: column;
    margin-top: 0.25rem;
    padding-top: 0.25rem;
    border-top: var(--cz-border-width) solid var(--cz-border-color);
}

.shell-nav-flyout .shell-nav-link {
    justify-content: flex-start;
    white-space: nowrap;
}

.shell-rail-toggle {
    flex-shrink: 0;
}

/* Drawn either way — the mark is what says "workspace" whether or not the
   name is beside it. Only the gap goes when there is nothing to gap from. */
.shell-workspace-icon {
    margin-inline-end: 0.5rem;
    font-size: 1rem;
}

/* The switcher wears the same mark as a nav section, and needs the same
   anchor for it; the padding keeps a long name off it. */
.shell-workspace-switcher > .btn {
    position: relative;
    padding-inline-end: 1.5rem;
}

/* The plus that opens the create-workspace form, when the rail is narrowed. */
.shell-create-icon {
    margin-inline-end: 0.5rem;
}

@media (min-width: 992px) {
    [data-rail='collapsed'] .shell-create-label {
        position: absolute;
        width: 1px;
        height: 1px;
        margin: -1px;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
    }

    /* The gap was there to separate the icon from a word that is no longer
       beside it. */
    [data-rail='collapsed'] .shell-create-icon {
        margin-inline-end: 0;
    }

    [data-rail='collapsed'] .shell-create-link {
        padding-inline: 0.5rem;
    }
}

/* --- Imports --- */

/* A count and its name, small enough to sit on the header line. The mark is
   part of the label rather than a tile of its own — four tiles is most of a
   phone's first screen, above a page that is mostly a table. */
.import-stat {
    display: flex;
    align-items: baseline;
    gap: 0.375rem;
}

.import-stat-label {
    color: var(--cz-secondary-color);
    font-size: 0.8125rem;
    white-space: nowrap;
}

.import-stat-value {
    font-size: 1rem;
    font-weight: 600;
    line-height: 1;
}

@media (min-width: 576px) {
    .import-stat {
        flex-direction: column-reverse;
        gap: 0.125rem;
    }

    .import-stat-value {
        font-size: 1.25rem;
    }
}

/* Tabs rather than a button group: four of them, each with a count, and a
   group's shared border makes the counts read as one long number. */
.import-tabs {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem;
    padding: 0.25rem;
    border-radius: 0.625rem;
    background-color: var(--shell-surface, var(--cz-tertiary-bg));
}

.import-tab {
    display: flex;
    align-items: center;
    gap: 0.375rem;
    padding: 0.3125rem 0.75rem;
    border-radius: 0.5rem;
    color: var(--cz-secondary-color);
    font-size: 0.8125rem;
    font-weight: 600;
    text-decoration: none;
    transition: background-color 0.15s ease-in-out, color 0.15s ease-in-out;
}

.import-tab:hover {
    color: var(--cz-emphasis-color);
}

.import-tab.active {
    background-color: var(--cz-body-bg);
    color: var(--cz-emphasis-color);
    box-shadow: var(--cz-box-shadow-sm);
}

.import-tab-count {
    min-width: 1.25rem;
    padding: 0 0.3125rem;
    border-radius: 0.625rem;
    background-color: var(--cz-tertiary-bg);
    color: var(--cz-secondary-color);
    font-size: 0.6875rem;
    line-height: 1.25rem;
    text-align: center;
}

.import-tab.active .import-tab-count {
    background-color: var(--cz-primary);
    color: #fff;
}

/* An empty tab is an answer, not a gap — it gets room and a mark of its own. */
.import-empty-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 3.5rem;
    height: 3.5rem;
    border-radius: 50%;
    background-color: var(--cz-tertiary-bg);
    color: var(--cz-secondary-color);
    font-size: 1.375rem;
}

.import-rows > thead th {
    color: var(--cz-secondary-color);
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}

/* ==========================================================================
   Chart colours.

   Every chart on the overview does one of two jobs, and each has one rule.

   **Magnitude** — a headcount per department, moves per month — is a single
   hue, light to dark. The bars are not coloured by their value: the length
   already says how much, and spending colour on it again would leave nothing
   to say identity with. Validated against this app's own surfaces (#ffffff
   light, #212529 dark): inside the lightness band, over the chroma floor, and
   past 3:1 on both.

   **State** — how the register stands — takes the reserved status scale, and
   those four never mean anything else. `--viz-warning` is deliberately below
   3:1 on the light surface; the label and the dot beside every segment are
   what carry it, which is why no status is ever drawn as colour alone.

   Dark is selected, not flipped: the same hues stepped for the dark surface.
   ========================================================================== */

:root {
    --viz-series: #2a78d6;
    --viz-series-soft: #9ec5f4;
    --viz-good: #0ca30c;
    --viz-warning: #fab219;
    --viz-critical: #d03b3b;
    --viz-muted: var(--cz-secondary-bg, #e9ecef);
    --viz-grid: rgba(0, 0, 0, 0.08);
}

[data-bs-theme="dark"] {
    --viz-series: #3987e5;
    --viz-series-soft: #184f95;
    --viz-good: #0ca30c;
    --viz-warning: #fab219;
    --viz-critical: #d03b3b;
    --viz-grid: rgba(255, 255, 255, 0.12);
}

/* A ranked bar is a box, so it is drawn as one. The rounded end sits at the
   value; the end anchored to the baseline stays square, so the bar reads as
   growing from the axis rather than floating off it. */
.viz-track-bar {
    height: 0.5rem;
    border-radius: 0.25rem;
    background-color: var(--viz-muted);
    overflow: hidden;
}

.viz-fill-bar {
    height: 100%;
    border-radius: 0 0.25rem 0.25rem 0;
    background-color: var(--viz-series);
}

.viz-area {
    fill: var(--viz-series);
    opacity: 0.14;
}

.viz-line {
    fill: none;
    stroke: var(--viz-series);
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
}

.viz-dot {
    fill: var(--viz-series);
    /* A ring in the surface colour, so a marker crossing the line stays
       readable where the two overlap. */
    stroke: var(--cz-body-bg, #fff);
    stroke-width: 2;
}

.viz-grid-line {
    stroke: var(--viz-grid);
    stroke-width: 1;
}

/* Touching segments of a stacked bar are separated by a gap in the surface
   colour, never by a stroke around each one: a border adds ink that is not
   data, and reads as a mark of its own at small sizes. */
.progress-stacked > .progress + .progress {
    border-left: 2px solid var(--cz-body-bg, #fff);
}

/* The hit target is wider than the mark it belongs to, so a column of a
   twelve-month chart can be hovered on a phone. */
.viz-hit {
    fill: transparent;
}

.viz-hit:hover + .viz-reveal,
.viz-hit:focus-visible + .viz-reveal {
    opacity: 1;
}

.viz-reveal {
    opacity: 0;
    transition: opacity 0.12s ease-in-out;
    pointer-events: none;
}

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

/* A stat tile is a link; it should say so by lifting, not by turning blue. */
.stretched-link-card {
    transition: transform 0.12s ease-in-out, box-shadow 0.12s ease-in-out;
}

.stretched-link-card:hover,
.stretched-link-card:focus-visible {
    transform: translateY(-2px);
    box-shadow: var(--cz-box-shadow) !important;
}

@media (prefers-reduced-motion: reduce) {
    .stretched-link-card {
        transition: none;
    }

    .stretched-link-card:hover,
    .stretched-link-card:focus-visible {
        transform: none;
    }
}

/* --- The people page --- */

/*
 * Nothing on this page may scroll the window sideways.
 *
 * `clip`, not `hidden`: `hidden` would make this a scroll container, and
 * everything held in place below — the controls, the column names, a group's
 * heading — would then stick to *it* rather than to the screen, which is to
 * say they would scroll away. `clip` cuts the overflow off without becoming
 * something to scroll.
 */
.people-page {
    overflow-x: clip;
}

/* --- A table heading that stays put --- */

/*
 * One heading for the whole table, held at the top of the screen while the
 * rows run under it. Grouped lists put their band headings in the body as
 * rows, so the column names are written once rather than again above every
 * group.
 *
 * Every table in the app has this — `.table-responsive-lg .table-sticky` on
 * the box, which `List` already writes, so a page that uses the composite
 * gets it without asking.
 *
 * Only from lg up, and that is not a preference: below lg the table sits in a
 * horizontal scroller, and a scroll container is what `sticky` sticks to — so
 * a heading there would hold still inside a box that is already the height of
 * its own content, which is to say it would not hold still at all. Narrow
 * screens scroll the page instead, where it was never in the way.
 *
 * Which is also why the roster is the one table without it: a day-by-person
 * grid is wider than any screen at every width, so it keeps a plain
 * `.table-responsive` and scrolls sideways in its own box.
 */
/* A table is as wide as its columns need; the box around it is not. */
.table-sticky {
    max-width: 100%;
}

@media (min-width: 992px) {
    /*
     * Everything that holds still stacks under the thing above it: the
     * topbar, then the controls, then the column names, then a group's
     * heading. `--list-controls` is the controls row's own height, written
     * there by `StickyControls` — the row wraps, so it is not one height and
     * a number in a stylesheet would be right at exactly one window width. A
     * page whose controls do not hold still never sets it, and the column
     * names then stop under the topbar alone.
     */
    .list-controls {
        position: sticky;
        /* Under the fixed topbar, not behind it. */
        top: var(--shell-topbar);
        z-index: 3;
        background-color: var(--cz-body-bg);
    }

    .table-sticky thead th {
        position: sticky;
        top: calc(var(--shell-topbar) + var(--list-controls, 0px));
        z-index: 2;
        background-color: var(--cz-body-bg);
    }

    /* A band's name, in the body where the rows it names are. It stops just
       below the column heading so the two are never on top of each other.
       The table's own background, not a fill of its own: a sticky row needs
       something opaque behind it or the rows show through as it passes, and
       that is all this is for. */
    .table-sticky .table-band th {
        position: sticky;
        top: calc(var(--shell-topbar) + var(--list-controls, 0px) + 3rem);
        z-index: 1;
        background-color: var(--cz-body-bg);
    }

    /* The same heading over a grid of cards, which has no column names above
       it to clear — so it stops where the table's would have started. */
    .people-band {
        position: sticky;
        top: calc(var(--shell-topbar) + var(--list-controls, 0px));
        z-index: 1;
        background-color: var(--cz-body-bg);
    }
}

/* --- The type scale --- */

/*
 * Three sizes, and a list page is the case they were drawn from.
 *
 *   16px  the thing a row is about — somebody's name
 *   14px  the table itself: its column names and its cells
 *   12px  what hangs off the name — a title, a department, a way to reach them
 *
 * Body text stays 16px everywhere else. A table is the one dense surface in
 * the app, so it is the one place that steps down.
 */
.table > :not(caption) > * > * {
    font-size: 0.875rem;
}

/* The row's subject, back up to body size. A reader scanning a register is
   looking for one thing on the row, and it is this one. */
.table .cell-name {
    font-size: 1rem;
}

/* What hangs off a name, wherever it is drawn — a table cell or a card. */
.cell-tag {
    font-size: 0.75rem;
}

/* An employee ID, wherever a seat is drawn. Tabular figures so a column of
   them lines up, and it never wraps or truncates: half an ID identifies
   nobody. */
.seat-id {
    flex-shrink: 0;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* A badge is sized in `em` by the theme, so inside a 14px cell it would come
   out at 10.5px and inside a card at 12px — the same tag, two sizes. Pinned
   to the scale instead. */
.badge {
    --cz-badge-font-size: 0.75rem;
}

/*
 * A name is not a link that happens to be a name — it is the row's subject,
 * and it reads as text. So it takes the body colour and keeps it through
 * every state, rather than turning blue on the way past.
 *
 * The same holds for every link in a list row — a department, a place, an
 * email address. A register where half the cells are blue reads as a page of
 * links rather than a page of people, and the row is already the thing being
 * scanned.
 *
 * Bootstrap colours a link from `--cz-link-color-rgb`, not `--cz-link-color`,
 * and swaps in `--cz-link-hover-color-rgb` on hover; both are set, or hover
 * falls through to the theme's own blue. `:where()` keeps the specificity at
 * zero for the state selectors, so `:visited` cannot outrank `:hover` by
 * being written later. `:not(:where(…))` leaves a button, a menu item or a
 * pager alone — those are controls that happen to be anchors, and they keep
 * the theme's own colours.
 */
.cell-name a,
.cell-tag a,
.table a:not(:where(.btn, .dropdown-item, .page-link)) {
    --cz-link-color-rgb: var(--cz-body-color-rgb);
    --cz-link-hover-color-rgb: var(--cz-body-color-rgb);
    color: var(--cz-body-color);
    text-decoration: none;
}

.cell-name a:where(:hover, :focus, :focus-visible, :active, :visited),
.cell-tag a:where(:hover, :focus, :focus-visible, :active, :visited),
.table a:not(:where(.btn, .dropdown-item, .page-link)):where(:hover, :focus, :focus-visible, :active, :visited) {
    color: var(--cz-body-color);
    text-decoration: none;
}

/* --- The cross in a search box --- */

/*
 * The browser's own, not one drawn here. The theme hides it on every
 * `.form-control`; a search box is the one field where it earns its keep, so
 * it is put back — plain and grey, the way a reader already knows it.
 *
 * WebKit and Blink draw it. Firefox draws none, and that is the ordinary
 * behaviour of a search input there rather than something to work around.
 */
.form-control[type="search"]::-webkit-search-cancel-button {
    -webkit-appearance: searchfield-cancel-button;
    display: block;
    cursor: pointer;
    filter: grayscale(1);
}

/* --- The totals in a payroll table --- */

/*
 * Gross and Net are the two columns anybody actually reads a payroll down,
 * and they sit among a dozen pay lines that look exactly like them. So they
 * are given a quiet backing and a rule either side: the eye finds the column
 * without the table having to shout, and the lines between them read as what
 * makes up the total rather than as more of the same.
 *
 * A tint of the body colour rather than a colour of its own — it has to work
 * on a striped row, a hovered row and a dark theme alike, and any one hue
 * would be wrong in at least one of them. It is mixed into the body
 * background, not left transparent, so the stripe underneath cannot show
 * through and break the column into bands.
 */
.table .cell-total {
    background-color: color-mix(in srgb, var(--cz-body-color) 4%, var(--cz-body-bg));
    border-inline: 1px solid var(--cz-border-color);
}

.table thead .cell-total {
    background-color: color-mix(in srgb, var(--cz-body-color) 7%, var(--cz-body-bg));
}

/**
 * A rule where a table turns a corner — what is earned on one side, what is
 * paid on the other. Where a column is already next to what it answers, the
 * line alone says so and a backing would only be noise.
 */
.table .cell-split {
    border-inline-start: 1px solid var(--cz-border-color);
}

/**
 * A figure card that answers to the pointer.
 *
 * Only enough to say "this is a thing, and you are on it": a hair's lift, a
 * firmer border and a shadow. A card is not a button — it does nothing when
 * pressed — so the movement stays small enough to read as feedback rather
 * than as an invitation.
 */
.stat-card {
    transition:
        transform 0.15s ease-out,
        box-shadow 0.15s ease-out,
        border-color 0.15s ease-out;
}

.stat-card:hover {
    transform: translateY(-2px);
    border-color: color-mix(in srgb, var(--cz-body-color) 25%, transparent) !important;
    box-shadow: var(--cz-box-shadow-sm);
}

@media (prefers-reduced-motion: reduce) {
    .stat-card,
    .stat-card:hover {
        transform: none;
        transition: none;
    }
}
