/* ==========================================================================
   Goody - 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;
}

/* 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 1.5rem;
    /* 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: 1.0625rem;
    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.625rem;
    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-divider {
    margin: 0.5rem 0.25rem;
    border: 0;
    border-top: var(--cz-border-width) solid var(--cz-border-color);
    opacity: 1;
}

.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 1.5rem;
        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. */
.shell-main {
    padding-bottom: 3rem;
}
/* --- 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 {
    padding-bottom: 2rem;
}

.profile-head {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1rem;
    padding: 1.5rem 0 1.25rem;
    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: 2rem;
        padding: 2rem 0 1.5rem;
        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: 1.5rem;
    margin-bottom: 1rem;
}

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

.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 {
    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: 2rem;
}

/* 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;
}
