html, body {
    font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}

h1:focus {
    outline: none;
}

a, .btn-link {
    color: #0071c1;
}

.btn-primary {
    color: #fff;
    background-color: #1b6ec2;
    border-color: #1861ac;
}

.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
  box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}

.content {
    padding-top: 1.1rem;
}

.valid.modified:not([type=checkbox]) {
    outline: 1px solid #26b050;
}

.invalid {
    outline: 1px solid red;
}

.validation-message {
    color: red;
}

#blazor-error-ui {
    color-scheme: light only;
    background: lightyellow;
    bottom: 0;
    box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
    box-sizing: border-box;
    display: none;
    left: 0;
    padding: 0.6rem 1.25rem 0.7rem 1.25rem;
    position: fixed;
    width: 100%;
    z-index: 1000;
}

    #blazor-error-ui .dismiss {
        cursor: pointer;
        position: absolute;
        right: 0.75rem;
        top: 0.5rem;
    }

.blazor-error-boundary {
    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
    padding: 1rem 1rem 1rem 3.7rem;
    color: white;
}

    .blazor-error-boundary::after {
        content: "An error has occurred."
    }

code {
    color: #c02d76;
}

@keyframes fadeIn {
    from { opacity: 0; transform: translateY(-4px); }
    to   { opacity: 1; transform: translateY(0); }
}

.animate-fade-in {
    animation: fadeIn 0.2s ease-out forwards;
}

.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
    color: var(--bs-secondary-color);
    text-align: end;
}

.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
    text-align: start;
}

@keyframes ucb-particle-burst {
    0%   { transform: translate(0, 0) scale(1); opacity: 1; }
    100% { transform: translate(var(--dx), var(--dy)) scale(0); opacity: 0; }
}

/* --- Drag-to-reorder (SortableJS) visual feedback --- */
/* Nothing floats above the page while dragging. SortableJS moves the *real* row through
   the list on every swap, so the item is always sitting in the slot it would land in —
   dropping just leaves it there, and a cancelled drag puts it back (see ucb.initSortable). */
/* No `transform` in this transition: SortableJS animates the rows shuffling out of the
   dragged row's way by driving inline transforms, and a competing transition here fights
   it — the shuffle goes mushy and the rows slide again when SortableJS clears its inline
   styles. Everything else may still transition. */
.sortable-item {
    cursor: grab;
    transition: box-shadow 120ms ease, border-color 120ms ease, background-color 120ms ease;
}
.sortable-item:active {
    cursor: grabbing;
}

/* A checked row takes part in reordering as a destination only: an unchecked item can be
   dropped between things already ticked off — which a row excluded from `draggable` made
   impossible — but the checked row itself cannot be picked up (it is in SortableJS's `filter`,
   and in shopping mode it has no grip). It must therefore not inherit the grab cursor above,
   which would advertise a drag that is refused. The rows stay tappable for check/uncheck, so
   the pointer belongs to that action. */
.sortable-drop-target-only,
.sortable-drop-target-only:active {
    cursor: default;
}

/* Set on <body> for the length of a drag (see ucb.initSortable's onStart/onEnd). The
   SortableJS fallback path, unlike native HTML5 drag, does not suppress text selection,
   so dragging a row down the list otherwise paints a selection across every row it
   passes. Scoped to the drag so ordinary text selection still works the rest of the time. */
body.ucb-dragging {
    -webkit-user-select: none;
    user-select: none;
}

/* The row being moved, shown in its live position in the list. It is a normal row with a
   lift on it — not a placeholder and not a copy — because it IS the item, already sitting
   where releasing would leave it. No transform: SortableJS measures this element's rect
   for its swap maths, so restyle it, never resize it. */
.sortable-chosen {
    outline: 2px solid rgba(245, 158, 11, 0.55); /* amber-500 */
    outline-offset: 2px;
    box-shadow: 0 10px 25px rgba(15, 23, 42, 0.16),
                0 0 0 4px rgba(245, 158, 11, 0.14);
}

/* SortableJS's fallback drag always builds a floating clone that tracks the pointer. We
   never show it.
   Its position is a fixed-position transform derived from the pointer delta since drag
   start, and that drifts the moment the page or the in-store container auto-scrolls
   mid-drag: the clone ends up hundreds of pixels from the cursor, floating over unrelated
   rows. It also duplicates the row that is already visible in the list. The clone still
   has to exist — SortableJS measures and moves it, and toggles its inline `display` around
   its own elementFromPoint hit tests — so hide it with `visibility`, which those inline
   display toggles do not disturb. */
.sortable-fallback {
    visibility: hidden !important;
}

/* ============================ SWIPE TO DELETE ============================
   Swiping a row to the right reveals a red delete panel and, past ~70% of the row width,
   deletes it through the same optimistic-delete-plus-Undo path as the row x and the item
   dialog. Matches the native Android gesture.

   The wrapper is the element SortableJS sees: it keeps `data-item-id` and `sortable-item`,
   so reordering measures and moves exactly what it always did. Only `.ucb-swipe-surface`
   translates, which is also what keeps the transform out of SortableJS's way — it drives
   its own inline transforms on the wrapper during a drag, and two writers on one element
   fight. */
.ucb-swipe-row {
    position: relative;
    /* Clips the red panel to the row's own rounded corners. The panel is a sibling behind
       the surface rather than a background on the wrapper, so it can hold an icon and a
       label that stay put while the row slides off them. */
    overflow: hidden;
}

.ucb-swipe-panel {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding-left: 1rem;
    z-index: 0;
    background-color: #ef4444; /* red-500 */
    color: #fff;
    /* Never a target: it sits under the row the whole time, and the gesture is driven from the
       container, not from this layer. */
    pointer-events: none;
    font-size: 0.75rem;
    font-weight: 800;
    /* Painted only while a swipe is live: an always-present red layer shows through the
       row's own rounded corners on some browsers, and shows up in a screenshot of a list
       nobody is touching. */
    opacity: 0;
}

.ucb-swipe-row.ucb-swiping .ucb-swipe-panel {
    opacity: 1;
}

.ucb-swipe-surface {
    position: relative;
    /* The panel is last in the DOM so the row's content reads first; this is what still
       paints the row on top of it. */
    z-index: 1;
    /* `pan-y` hands vertical scrolling to the browser and leaves horizontal movement to the
       gesture handler, so the list still scrolls normally under a finger that starts on a
       row. Without it the handler would have to preventDefault from a non-passive
       touchmove and take over scrolling itself. */
    touch-action: pan-y;
}

/* Only while settling: during the gesture the surface must track the finger exactly, and a
   transition there lags it behind the touch. */
.ucb-swipe-surface.ucb-swipe-settling {
    transition: transform 180ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

@media (prefers-reduced-motion: reduce) {
    .ucb-swipe-surface.ucb-swipe-settling {
        transition-duration: 1ms;
    }
}

/* The row's own delete control, shown only where the swipe is not available.
   `(hover: none) and (pointer: coarse)` is the phone/tablet test — a primary pointer that
   cannot hover and is a finger — and it is deliberately narrower than "does this device have
   a touchscreen at all". A touch laptop or a tablet with a mouse reports `hover: hover` and
   keeps the button, which is right: the swipe is driven by `pointerType === 'touch'`, so a
   mouse cannot perform it however touch-capable the hardware is. Testing
   `navigator.maxTouchPoints` instead would take the only mouse-reachable delete away from
   exactly those machines.

   Hiding it costs screen-reader users nothing: a swipe is not a gesture they can perform
   (assistive tech consumes horizontal swipes), so on a touch-only device the row control was
   never their route either — the item details dialog's "Delete item" is, and that is
   untouched. `display: none` also takes it out of the tab order and the accessibility tree,
   rather than leaving an invisible target behind. */
@media (hover: none) and (pointer: coarse) {
    .ucb-row-delete {
        display: none;
    }
}

/* ============================ CHAT SCREEN ============================
   Chat is a destination you walk back from, not a panel on the list, so it covers the
   page and docks its composer to the bottom edge.

   The height comes from --ucb-chat-h (window.visualViewport, published by
   ucb.chatViewportAttach) rather than from `inset: 0` or `100dvh`, because neither of
   those shrinks when the virtual keyboard opens: the composer would end up behind the
   keyboard. --ucb-chat-top follows the visual viewport's offset so the layer stays over
   the visible area on iOS, where a fixed element is positioned against the layout
   viewport. Both fall back to a full-height layer on browsers without visualViewport. */
.ucb-chat-screen {
    position: fixed;
    left: 0;
    right: 0;
    top: var(--ucb-chat-top, 0px);
    height: var(--ucb-chat-h, 100dvh);
}

/* Set on <body> while the Chat layer is up. The list underneath keeps its scroll
   position (it is inert, not removed), and this stops a drag on the layer scrolling it. */
body.ucb-scroll-locked {
    overflow: hidden;
}
