/*
 * Jambo Cafe POS — design system.
 * Hand-rolled rather than Bootstrap: a POS needs big, unambiguous touch targets
 * and a layout that survives being used one-handed on a phone behind a counter.
 */

/* ---------- tokens ---------- */
:root {
    color-scheme: light;

    --bg: #faf7f2;
    --surface: #ffffff;
    --surface-2: #f4efe7;
    --surface-3: #ebe4d9;
    --ink: #1c1917;
    --ink-2: #57534e;
    --ink-3: #8b8378;
    --line: #e4dcd0;
    --line-strong: #d3c8b8;

    --brand: #c2571b;
    --brand-ink: #ffffff;
    --brand-soft: #fdf0e5;
    --brand-strong: #9a4315;

    --ok: #1f7a4d;
    --ok-soft: #e6f4ec;
    --warn: #a86a00;
    --warn-soft: #fdf3e0;
    --danger: #b3261e;
    --danger-soft: #fceceb;
    --info: #2563a8;
    --info-soft: #e8f0fa;

    --radius: 14px;
    --radius-sm: 10px;
    --radius-lg: 20px;

    --shadow-sm: 0 1px 2px rgb(28 25 23 / .06), 0 1px 3px rgb(28 25 23 / .04);
    --shadow: 0 2px 4px rgb(28 25 23 / .05), 0 8px 20px -6px rgb(28 25 23 / .12);
    --shadow-lg: 0 10px 40px -8px rgb(28 25 23 / .28);

    --tap: 48px;
    --font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

    /* Used as a mask, so the shape is all that matters here and the colour is
       decided where it is drawn — see the date field rules. Kept as a token
       because it is one shape used in more than one state. */
    --calendar-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8 2.75v3.5M16 2.75v3.5M3.75 9.75h16.5M5.5 4.75h13a1.75 1.75 0 0 1 1.75 1.75v12.5A1.75 1.75 0 0 1 18.5 20.75h-13a1.75 1.75 0 0 1-1.75-1.75V6.5A1.75 1.75 0 0 1 5.5 4.75Z'/%3E%3C/svg%3E");
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        color-scheme: dark;

        --bg: #14110e;
        --surface: #1e1a16;
        --surface-2: #272119;
        --surface-3: #332b21;
        --ink: #f7f2ea;
        --ink-2: #c4bbae;
        --ink-3: #94897b;
        --line: #332c23;
        --line-strong: #453b2f;

        --brand: #e2762f;
        --brand-ink: #1a0f07;
        --brand-soft: #33210f;
        --brand-strong: #f08d47;

        --ok: #4ade80;
        --ok-soft: #12251a;
        --warn: #e0a13a;
        --warn-soft: #2b2110;
        --danger: #f87171;
        --danger-soft: #2c1615;
        --info: #7dabe4;
        --info-soft: #14202e;

        --shadow-sm: 0 1px 2px rgb(0 0 0 / .4);
        --shadow: 0 2px 4px rgb(0 0 0 / .3), 0 8px 20px -6px rgb(0 0 0 / .5);
        --shadow-lg: 0 10px 40px -8px rgb(0 0 0 / .7);
    }
}

:root[data-theme="dark"] {
    color-scheme: dark;

    --bg: #14110e;
    --surface: #1e1a16;
    --surface-2: #272119;
    --surface-3: #332b21;
    --ink: #f7f2ea;
    --ink-2: #c4bbae;
    --ink-3: #94897b;
    --line: #332c23;
    --line-strong: #453b2f;

    --brand: #e2762f;
    --brand-ink: #1a0f07;
    --brand-soft: #33210f;
    --brand-strong: #f08d47;

    --ok: #4ade80;
    --ok-soft: #12251a;
    --warn: #e0a13a;
    --warn-soft: #2b2110;
    --danger: #f87171;
    --danger-soft: #2c1615;
    --info: #7dabe4;
    --info-soft: #14202e;

    --shadow-sm: 0 1px 2px rgb(0 0 0 / .4);
    --shadow: 0 2px 4px rgb(0 0 0 / .3), 0 8px 20px -6px rgb(0 0 0 / .5);
    --shadow-lg: 0 10px 40px -8px rgb(0 0 0 / .7);
}

/* ---------- base ---------- */
*, *::before, *::after { box-sizing: border-box; }

html, body { height: 100%; }

body {
    margin: 0;
    font-family: var(--font);
    font-size: 16px;
    line-height: 1.5;
    color: var(--ink);
    background: var(--bg);
    -webkit-font-smoothing: antialiased;
    -webkit-tap-highlight-color: transparent;
}

h1, h2, h3, h4 { margin: 0; font-weight: 650; letter-spacing: -.015em; line-height: 1.25; }
h1 { font-size: 1.5rem; }
h2 { font-size: 1.175rem; }
h3 { font-size: 1rem; }
p { margin: 0 0 .75rem; }
a { color: var(--brand); text-decoration: none; }

/* Underlining on hover is right for a link sitting in a sentence and wrong for
   everything else — a button that sprouts an underline reads as text, not as a
   control. So: bare links in prose underline, and anything wearing a class of
   its own says how it responds, below. `.link` opts back in where a prose link
   needed a class for its size or weight. */
a:not([class]):hover, .link:hover { text-decoration: underline; text-underline-offset: .15em; }
.link:hover { color: var(--brand-strong); }

:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; border-radius: 6px; }

.num { font-variant-numeric: tabular-nums; }
.muted { color: var(--ink-2); }
.dim { color: var(--ink-3); }
.danger { color: var(--danger); }
.tiny { font-size: .75rem; }
.small { font-size: .875rem; }
.strong { font-weight: 650; }
.right { text-align: right; }
.center { text-align: center; }
.nowrap { white-space: nowrap; }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.stack { display: flex; flex-direction: column; gap: .75rem; }
.row { display: flex; align-items: center; gap: .625rem; }
.row-between { display: flex; align-items: center; justify-content: space-between; gap: .75rem; }
.wrap { flex-wrap: wrap; }
.grow { flex: 1 1 auto; min-width: 0; }
.hide { display: none !important; }

/* Gone from the screen, still read out. */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/* ---------- app shell ---------- */
.app {
    display: grid;
    /* Wide enough for the longest item and its badge on one line — "Cash
       drawer" with a count beside it was wrapping at 232px. */
    grid-template-columns: 264px 1fr;
    min-height: 100dvh;

    /* How much of the viewport the fixed chrome above the content takes: the
       topbar, plus the impersonation strip when there is one. Pages that size
       themselves to the screen rather than to their content — the till — read
       it instead of hard-coding the bar's height, which is how the cart used to
       end up a strip's worth off the bottom of an owner's screen. */
    --chrome: 65px;
}

.app.is-impersonating { --chrome: 97px; }

.sidebar {
    display: flex;
    flex-direction: column;
    gap: .25rem;
    padding: 1rem .75rem;
    background: var(--surface);
    border-right: 1px solid var(--line);
    position: sticky;
    top: 0;
    height: 100dvh;
    overflow-y: auto;
}

.brand {
    display: flex;
    align-items: center;
    gap: .625rem;
    padding: .375rem .5rem 1.125rem;
}

.brand-mark {
    display: grid;
    place-items: center;
    width: 38px;
    height: 38px;
    border-radius: 11px;
    background: linear-gradient(145deg, var(--brand), var(--brand-strong));
    color: var(--brand-ink);
    font-size: 1.25rem;
    flex: none;
}

.brand-name { font-weight: 700; letter-spacing: -.02em; line-height: 1.1; }

.nav-link {
    display: flex;
    align-items: center;
    gap: .75rem;
    min-height: var(--tap);
    padding: 0 .75rem;
    border-radius: var(--radius-sm);
    color: var(--ink-2);
    font-weight: 550;
    white-space: nowrap;
    text-decoration: none;
}

.nav-link:hover { background: var(--surface-2); color: var(--ink); text-decoration: none; }
.nav-link[aria-current="page"] { background: var(--brand-soft); color: var(--brand-strong); }
.nav-icon { font-size: 1.15rem; width: 1.5rem; text-align: center; flex: none; }

.sidebar-foot { margin-top: auto; padding-top: .75rem; border-top: 1px solid var(--line); }

/* ---------- the account, and what folds out of it ---------- */
.avatar {
    display: grid;
    place-items: center;
    width: 34px;
    height: 34px;
    border-radius: 50%;
    background: var(--brand-soft);
    color: var(--brand-strong);
    font-size: .8125rem;
    font-weight: 700;
    letter-spacing: .01em;
    flex: none;
}

/* Same circle, with a photo in it instead of initials. */
img.avatar { object-fit: cover; }

.avatar-lg { width: 58px; height: 58px; font-size: 1.25rem; }

.user-menu { position: relative; }

.user-trigger {
    display: flex;
    align-items: center;
    gap: .625rem;
    width: 100%;
    min-height: var(--tap);
    padding: .25rem .5rem;
    border: 0;
    border-radius: var(--radius-sm);
    background: none;
    color: var(--ink-2);
    font: inherit;
    text-align: left;
    cursor: pointer;
}

.user-trigger:hover, .user-trigger[aria-expanded="true"] { background: var(--surface-2); color: var(--ink); }
.user-trigger.is-current { background: var(--brand-soft); color: var(--brand-strong); }

.user-caret { flex: none; color: var(--ink-3); transition: transform .15s ease; }
.user-trigger[aria-expanded="true"] .user-caret { transform: rotate(180deg); }

/* Upward: there is nothing below the foot of the sidebar but the edge of the
   screen. Sized to the trigger so it never widens the drawer on a phone. */
.menu-pop {
    position: absolute;
    left: 0;
    right: 0;
    bottom: calc(100% + .375rem);
    z-index: 30;
    display: flex;
    flex-direction: column;
    gap: .125rem;
    padding: .3125rem;
    border: 1px solid var(--line-strong);
    border-radius: var(--radius);
    background: var(--surface);
    box-shadow: var(--shadow-lg);
}

.menu-pop[hidden] { display: none; }

.menu-item {
    display: flex;
    align-items: center;
    gap: .625rem;
    width: 100%;
    min-height: 42px;
    padding: 0 .5rem;
    border: 0;
    border-radius: var(--radius-sm);
    background: none;
    color: var(--ink-2);
    font: inherit;
    font-size: .9375rem;
    font-weight: 550;
    text-align: left;
    text-decoration: none;
    cursor: pointer;
}

.menu-item:hover, .menu-item:focus-visible { background: var(--surface-2); color: var(--ink); text-decoration: none; }
.menu-item[aria-current="page"] { color: var(--brand-strong); }
.menu-item-danger { color: var(--danger); }
.menu-item-danger:hover, .menu-item-danger:focus-visible { background: var(--danger-soft); color: var(--danger); }

.menu-sep { height: 1px; margin: .25rem; background: var(--line); }

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

/* ---------- profile page ---------- */
/* Two columns: who you are stays put on the left while the sections scroll past
   it on the right. One column as soon as that stops being comfortable — on a
   phone the card simply leads. */
.profile-grid {
    display: grid;
    grid-template-columns: minmax(240px, 300px) minmax(0, 1fr);
    gap: 1.5rem;
    align-items: start;
    max-width: 78rem;
    margin-inline: auto;
}

.profile-aside { position: sticky; top: 5.25rem; min-width: 0; }

@media (max-width: 900px) {
    .profile-grid { grid-template-columns: 1fr; gap: 1rem; }
    .profile-aside { position: static; }
}

/* Cleared past the sticky topbar, or the heading of whatever was jumped to
   lands underneath it. */
.profile-section { scroll-margin-top: 5.5rem; }

.profile-nav { display: flex; flex-direction: column; gap: .125rem; padding: .5rem; border-top: 1px solid var(--line); }

.profile-nav-link {
    display: flex;
    align-items: center;
    gap: .625rem;
    min-height: 42px;
    padding: 0 .625rem;
    border-radius: var(--radius-sm);
    color: var(--ink-2);
    font-size: .9375rem;
    font-weight: 550;
    text-decoration: none;
}

.profile-nav-link:hover { background: var(--surface-2); color: var(--ink); text-decoration: none; }
.profile-nav-link.is-here { background: var(--brand-soft); color: var(--brand-strong); }

/* ---------- the photo ---------- */
.avatar-edit { position: relative; display: inline-block; }

.avatar-shell {
    position: relative;
    display: block;
    width: 96px;
    height: 96px;
    border-radius: 50%;
    overflow: hidden;
    cursor: pointer;
    box-shadow: 0 0 0 4px var(--surface), 0 0 0 5px var(--line);
}

.avatar-img { width: 100%; height: 100%; object-fit: cover; display: block; }
.avatar-xl { width: 96px; height: 96px; font-size: 2rem; }

.avatar-overlay {
    position: absolute;
    inset: 0;
    display: grid;
    place-content: center;
    justify-items: center;
    gap: .0625rem;
    background: rgb(0 0 0 / .5);
    color: #fff;
    opacity: 0;
    transition: opacity .15s ease;
}

.avatar-shell:hover .avatar-overlay, .avatar-shell:focus-within .avatar-overlay { opacity: 1; }

.avatar-remove-form { position: absolute; right: -2px; bottom: -2px; }

.avatar-remove {
    display: grid;
    place-items: center;
    width: 28px;
    height: 28px;
    border: 2px solid var(--surface);
    border-radius: 50%;
    background: var(--danger);
    color: #fff;
    font-size: .75rem;
    line-height: 1;
    cursor: pointer;
    transition: transform .15s ease;
}

.avatar-remove:hover { transform: scale(1.08); }

/* The cropper. Square, because the result is. */
.crop-stage {
    position: relative;
    width: min(320px, 100%);
    margin: 0 auto;
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--surface-3);
}

.crop-canvas { display: block; width: 100%; height: auto; touch-action: none; cursor: grab; }
.crop-canvas:active { cursor: grabbing; }

/* What will be kept, drawn over the top: the corners stay visible while the
   photo is being positioned, dimmed rather than hidden, because aiming at a
   circle you cannot see past is guesswork. */
.crop-mask {
    position: absolute;
    inset: 0;
    pointer-events: none;
    background: rgb(0 0 0 / .45);
    /* Transparent in the middle, so the dimming is only painted around it. */
    -webkit-mask: radial-gradient(circle at 50% 50%, transparent 0 49.5%, #000 50%);
    mask: radial-gradient(circle at 50% 50%, transparent 0 49.5%, #000 50%);
    box-shadow: inset 0 0 0 1px rgb(255 255 255 / .35);
    border-radius: inherit;
}

.crop-slider { max-width: 14rem; }

/* ---------- a field you click to change ---------- */
/* At rest it is the value and nothing else — no box, no border, no hint that a
   form is hiding here. The pencil turns up under the pointer, and the input
   only exists once you have asked for it. The height is held steady so the
   swap does not shift the page, and stays a comfortable target on a tablet. */
.editable-value {
    display: flex;
    align-items: center;
    gap: .5rem;
    width: 100%;
    min-height: 2.5rem;
    padding: 0;
    border: 0;
    background: none;
    color: var(--ink);
    font: inherit;
    font-size: 1.0625rem;
    text-align: left;
    cursor: text;
    transition: color .15s ease;
}

.editable-value:hover { color: var(--brand-strong); }
.editable-value.is-empty { color: var(--ink-3); font-style: italic; }
.editable-pen { flex: none; color: var(--ink-3); opacity: 0; transition: opacity .15s ease; }
.editable-value:hover .editable-pen, .editable-value:focus-visible .editable-pen { opacity: 1; }

/* The input that takes its place, at the same size so nothing jumps. */
.editable .input { min-height: 2.5rem; padding: .3125rem .625rem; font-size: 1.0625rem; }

/* Its read-only neighbours, so a field nobody may edit still lines up with one
   that can be. */
.readonly {
    display: flex;
    align-items: center;
    min-height: var(--tap);
    padding: 0 .75rem;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--surface-2);
    color: var(--ink-2);
}

/* ---------- label/value rows ---------- */
.rows { display: flex; flex-direction: column; }

.row-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .75rem;
    padding: .75rem 1rem;
    border-bottom: 1px solid var(--line);
    min-width: 0;
}

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

.modal[hidden] { display: none; }

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

/* The bar, plus anything glued underneath it. The stack is what sticks: give
   the bar its own `position: sticky` as well and the strip below would scroll
   away behind it, which is the one thing a warning strip must not do. */
.topbar-stack {
    position: sticky;
    top: 0;
    z-index: 20;
}

.topbar {
    display: flex;
    align-items: center;
    gap: .75rem;
    padding: .75rem 1.25rem;
    background: var(--surface);
    border-bottom: 1px solid var(--line);
}

.titlebar { display: flex; flex-direction: column; justify-content: center; gap: .0625rem; min-width: 0; }

/* One line, always: this sits in a bar that is stuck to the top of a till, and
   a trail that wraps would push the page down every time a name got long.
   Where you are gives up its tail first; how you got here stays readable. */
.crumbs { font-size: .75rem; line-height: 1.3; }

.crumbs ol {
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    min-width: 0;
    margin: 0;
    padding: 0;
    list-style: none;
}

.crumbs li { flex: none; white-space: nowrap; }
.crumbs li:last-child { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.crumbs li + li::before { content: "›"; margin: 0 .375rem; color: var(--ink-3); }
.crumbs a { color: var(--ink-3); text-decoration: none; }
.crumbs a:hover { color: var(--brand-strong); text-decoration: underline; }
.crumbs [aria-current="page"] { color: var(--ink-2); font-weight: 600; }

.content { padding: 1.25rem; flex: 1 1 auto; min-width: 0; }
.content-flush { padding: 0; }

/* ---------- buttons ---------- */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: .5rem;
    min-height: var(--tap);
    padding: 0 1.125rem;
    border: 1px solid var(--line-strong);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--ink);
    font: inherit;
    font-weight: 600;
    cursor: pointer;
    user-select: none;
    /* A label is one line or it is not a label. Without this a two-word button
       squeezed by its neighbours breaks in half — "Put back" sitting two lines
       tall beside a one-line Recipe and Edit — while single-word buttons in the
       same row have nothing to break and stay put. Every button in this app is
       two or three short words; if one ever needs more room than it has, the
       row it is in should give it up. */
    white-space: nowrap;
    transition: transform .12s ease, background .15s ease, border-color .15s ease,
                box-shadow .15s ease, color .15s ease;
}

.btn:hover { background: var(--surface-2); border-color: var(--line-strong); text-decoration: none; }
.btn:active { transform: translateY(1px); box-shadow: none; }
.btn:disabled { opacity: .45; cursor: not-allowed; }
.btn:disabled:hover { transform: none; box-shadow: none; background: var(--surface); }

.btn-primary { background: var(--brand); border-color: var(--brand); color: var(--brand-ink); }
.btn-primary:hover { background: var(--brand-strong); border-color: var(--brand-strong); }
.btn-ghost { background: transparent; border-color: transparent; color: var(--ink-2); }
.btn-ghost:hover { background: var(--surface-2); color: var(--ink); }
.btn-danger { background: var(--danger-soft); border-color: transparent; color: var(--danger); }
.btn-danger:hover { background: var(--danger-soft); border-color: var(--danger); color: var(--danger); }
.btn-sm { min-height: 36px; padding: 0 .75rem; font-size: .875rem; }
.btn-lg { min-height: 56px; padding: 0 1.5rem; font-size: 1.0625rem; }
.btn-block { width: 100%; }
.btn-icon { width: var(--tap); padding: 0; flex: none; }

/* ---------- how things answer a pointer ---------- */
/*
   One vocabulary across the whole app, so that "this is a control" never has to
   be said twice: a surface tint for anything sitting in a list, a one-pixel
   lift with a soft shadow for anything standing on its own, and the brand
   colour for the thing about to happen. Nothing underlines — see the note by
   the link rules at the top.

   The lifts are behind `hover: hover` on purpose. On the counter tablet there
   is no pointer to leave, and a lifted, shadowed button that stays that way
   until something else is tapped looks like a bug.
*/
@media (hover: hover) {
    .btn:hover { box-shadow: var(--shadow-sm); transform: translateY(-1px); }
    .btn-primary:hover { box-shadow: 0 6px 16px -8px var(--brand-strong); }
    .btn-ghost:hover { box-shadow: none; transform: none; }
    .card-link:hover { border-color: var(--brand); box-shadow: var(--shadow); transform: translateY(-2px); }
    .card-link:hover .card-link-go { transform: translateX(3px); color: var(--brand); }
}

.cat, .chip, .lock-choice, .card-link, .card-link-go, .crumbs a, .profile-nav-link {
    transition: background .15s ease, border-color .15s ease, color .15s ease,
                box-shadow .15s ease, transform .15s ease;
}

.cat:hover { background: var(--surface-2); border-color: var(--brand); color: var(--ink); text-decoration: none; }
.cat[aria-pressed="true"]:hover { background: var(--brand-strong); border-color: var(--brand-strong); color: var(--brand-ink); }
.crumbs a:hover { color: var(--brand-strong); text-decoration: none; }

/* A whole card that is one link: a settings category, and anything else that
   is best pointed at rather than read. */
.card-link { display: block; color: inherit; text-decoration: none; }
.card-link:hover { text-decoration: none; }
.card-link-go { flex: none; color: var(--ink-3); }

.card-mark {
    display: grid;
    place-items: center;
    width: 44px;
    height: 44px;
    border-radius: 12px;
    background: var(--brand-soft);
    color: var(--brand-strong);
    font-size: 1.375rem;
    flex: none;
}

/* Each category gets its own so the settings index is scannable by colour
   rather than by reading every heading. */
.card-mark-info { background: var(--info-soft); color: var(--info); }
.card-mark-ok { background: var(--ok-soft); color: var(--ok); }
.card-mark-warn { background: var(--warn-soft); color: var(--warn); }
.card-mark-plain { background: var(--surface-3); color: var(--ink-2); }

/* One card per row, and the column sits in the middle of the page. Content with
   a natural width pinned to the left edge leaves a metre of nothing beside it,
   which reads as a mistake rather than as breathing room. */
.settings-cards {
    display: flex;
    flex-direction: column;
    gap: .875rem;
    max-width: 46rem;
    margin-inline: auto;
}

/* The same trick, for anything else that caps its own width. */
.middle { margin-inline: auto; }

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

.card-pad { padding: 1rem; }
.card-head { padding: .875rem 1rem; border-bottom: 1px solid var(--line); font-weight: 650; }
.card-foot { display: flex; gap: .625rem; padding: .875rem 1rem; border-top: 1px solid var(--line); }

.badge {
    display: inline-flex;
    align-items: center;
    gap: .3rem;
    padding: .2rem .5rem;
    border-radius: 999px;
    font-size: .75rem;
    font-weight: 650;
    background: var(--surface-3);
    color: var(--ink-2);
    white-space: nowrap;
}

.badge-ok { background: var(--ok-soft); color: var(--ok); }
.badge-warn { background: var(--warn-soft); color: var(--warn); }
.badge-danger { background: var(--danger-soft); color: var(--danger); }
.badge-info { background: var(--info-soft); color: var(--info); }
.badge-brand { background: var(--brand-soft); color: var(--brand-strong); }

/* ---------- forms ---------- */
.field { display: flex; flex-direction: column; gap: .375rem; }
.label { font-size: .8125rem; font-weight: 650; color: var(--ink-2); }

.input, .select, .textarea {
    width: 100%;
    min-height: var(--tap);
    padding: .625rem .875rem;
    border: 1px solid var(--line-strong);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--ink);
    font: inherit;
}

.textarea { min-height: 84px; resize: vertical; }
.input::placeholder { color: var(--ink-3); }

/*
 * The calendar button inside a date field.
 *
 * Left to itself every browser draws something different there, and on the one
 * these tills actually run on — Chrome on Android — it is a dropdown arrow,
 * which reads as "there is a list behind this" rather than "there is a
 * calendar". That arrow is not the picker button in a shadow tree that a
 * -webkit- rule can repaint: Android renders the whole control as a menulist
 * and draws the arrow as part of it, which is why styling the picker indicator
 * changed nothing there. So the appearance goes first, and the icon is drawn by
 * something of ours.
 *
 * That something is a wrapper, the way .search-box draws its magnifier: an
 * input cannot carry a pseudo-element of its own. The browser's own button is
 * kept rather than hidden — stretched over the icon and made invisible — so
 * where it exists (Chrome and Safari on a desktop) clicking the calendar opens
 * the real picker, and where it does not (Android) the icon sits inside the
 * field's own padding, so the tap lands on the field, which opens the picker
 * anyway. Nothing here is a replacement picker; the native one always runs.
 *
 * A mask rather than a background image, so the icon takes its colour from the
 * theme instead of needing a second copy of the SVG for dark mode.
 */
.input[type="date"],
.input[type="datetime-local"] {
    -webkit-appearance: none;
    appearance: none;
}

/* Safari puts a stepper in the field, which is the same wrong promise. */
.input[type="date"]::-webkit-inner-spin-button,
.input[type="datetime-local"]::-webkit-inner-spin-button {
    -webkit-appearance: none;
    display: none;
}

.date-box { position: relative; display: inline-flex; }
.date-box .input { padding-right: 2.5rem; }

.date-box::after {
    content: "";
    position: absolute;
    right: .875rem;
    top: 50%;
    width: 1.125rem;
    height: 1.125rem;
    transform: translateY(-50%);
    background: var(--ink-3);
    -webkit-mask: var(--calendar-icon) center / contain no-repeat;
    mask: var(--calendar-icon) center / contain no-repeat;
    pointer-events: none;
}

.date-box:focus-within::after { background: var(--brand); }

/* Kept for the click and nothing else. */
.date-box .input::-webkit-calendar-picker-indicator {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: auto;
    width: 2.5rem;
    height: auto;
    margin: 0;
    padding: 0;
    opacity: 0;
    cursor: pointer;
}

/* Firefox ignores every -webkit- rule above and draws its own calendar inside
   the field, so ours would be the second one on the line. Let it keep its own:
   a different icon, not two. */
@supports (-moz-appearance: none) {
    .date-box::after { display: none; }
    .date-box .input { padding-right: .875rem; }
}

/* Chrome pads the editable part of a date field, which leaves it a couple of
   pixels taller than a text input with the same padding beside it. */
.input[type="date"]::-webkit-datetime-edit-fields-wrapper { padding: 0; }
.input:focus, .select:focus, .textarea:focus { outline: none; border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-soft); }

/* ---------- dropdown ---------- */
/* The select is still there, still in the form, still the thing that gets
   posted — see assets/controllers/dropdown_controller.js. It is taken out of
   the layout rather than hidden outright so that the browser can still put its
   own "choose one of these" against a required field left empty. */
.select.is-enhanced {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: 0;
    border: 0;
    opacity: 0;
    pointer-events: none;
}

.dd { position: relative; }

.dd-trigger {
    display: flex;
    align-items: center;
    gap: .5rem;
    width: 100%;
    min-height: var(--tap);
    padding: .625rem .875rem;
    border: 1px solid var(--line-strong);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--ink);
    font: inherit;
    text-align: left;
    cursor: pointer;
    transition: border-color .15s ease, box-shadow .15s ease, background .15s ease;
}

.dd-trigger:hover { border-color: var(--brand); }
.dd-trigger[aria-expanded="true"] { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-soft); }
.dd-text { flex: 1 1 auto; min-width: 0; }
.dd-text.is-placeholder { color: var(--ink-3); }
.dd-caret { display: flex; flex: none; color: var(--ink-3); transition: transform .15s ease; }
.dd-trigger[aria-expanded="true"] .dd-caret { transform: rotate(180deg); }

/* Pinned to the viewport by the controller, so nothing it sits inside can clip
   it — the till's discount box is a modal that scrolls. */
.dd-panel {
    position: fixed;
    z-index: 90;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: .3125rem;
    border: 1px solid var(--line-strong);
    border-radius: var(--radius);
    background: var(--surface);
    box-shadow: var(--shadow-lg);
    animation: dd-in .12s ease-out;
}

.dd-panel[hidden] { display: none; }
.dd-panel:focus-visible { outline: none; }

/* Sticky, so narrowing a long list never scrolls the box you are typing in out
   of sight. */
.dd-search {
    position: sticky;
    top: 0;
    z-index: 1;
    padding: .1875rem .1875rem .4375rem;
    margin: -.3125rem -.3125rem 0;
    background: var(--surface);
    border-bottom: 1px solid var(--line);
}

.dd-search .input { min-height: 38px; padding: .375rem .625rem; font-size: .9375rem; }
.dd-empty { padding: .75rem .625rem; color: var(--ink-3); font-size: .875rem; }
.dd-option[hidden] { display: none; }

.dd-option {
    display: flex;
    align-items: center;
    gap: .5rem;
    min-height: 44px;
    padding: .5rem .625rem;
    border-radius: var(--radius-sm);
    color: var(--ink-2);
    cursor: pointer;
}

.dd-option:hover, .dd-option.is-active { background: var(--surface-2); color: var(--ink); }
.dd-option[aria-selected="true"] { color: var(--brand-strong); font-weight: 650; }

/* The tick is drawn rather than reserved: a column of empty space in front of
   every unselected row makes a short list look like a form. */
.dd-option[aria-selected="true"]::after {
    content: "✓";
    margin-left: auto;
    font-size: .8125rem;
}

.dd-option[aria-disabled="true"] { opacity: .45; cursor: not-allowed; }

@keyframes dd-in {
    from { opacity: 0; transform: translateY(-4px); }
    to { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
    .dd-panel { animation: none; }
    .dd-caret { transition: none; }
}

.alert { padding: .75rem 1rem; border-radius: var(--radius-sm); font-weight: 550; }
.alert-danger { background: var(--danger-soft); color: var(--danger); }
.alert-ok { background: var(--ok-soft); color: var(--ok); }

/* ---------- tables ---------- */
.table { width: 100%; border-collapse: collapse; }
.table th, .table td { padding: .75rem 1rem; text-align: left; border-bottom: 1px solid var(--line); }
.table th { font-size: .75rem; text-transform: uppercase; letter-spacing: .04em; color: var(--ink-3); font-weight: 700; }
.table tbody tr:last-child td { border-bottom: 0; }
.table-hover tbody tr:hover { background: var(--surface-2); }
.table-wrap { overflow-x: auto; }

/* A person in a table cell: their face, then their name over their email. The
   whole cell is the link, so the target is a row's worth of tap rather than
   eleven characters of underlined name. */
.user-cell { display: flex; align-items: center; gap: .625rem; min-width: 0; color: inherit; text-decoration: none; }
.user-cell:hover .strong { color: var(--brand-strong); }

/*
 * On a phone a six-column table is a sideways scroll inside a page that already
 * scrolls down, which is a poor thing to hand someone holding a phone behind a
 * counter. `.table-stack` turns each row into a card at that width: the cell
 * left without a data-label heads the card, and every other cell prints its
 * column name beside its value. Nothing is hidden — the same figures are all
 * still there, just down the screen instead of across it.
 */
@media (max-width: 720px) {
    .table-stack thead { display: none; }
    .table-stack, .table-stack tbody { display: block; width: 100%; }

    /* A flex column rather than a plain block, so the heading cell can be
       pulled to the top of the card even when its column sits mid-table. */
    .table-stack tbody tr {
        display: flex;
        flex-direction: column;
        width: 100%;
        padding: .875rem 1rem;
        border-bottom: 1px solid var(--line);
    }
    .table-stack tbody tr:last-child { border-bottom: 0; }

    .table-stack td {
        display: flex;
        align-items: baseline;
        justify-content: space-between;
        gap: 1rem;
        padding: .1875rem 0;
        border: 0;
        text-align: right;
    }

    .table-stack td[data-label]::before {
        content: attr(data-label);
        flex: none;
        color: var(--ink-3);
        font-size: .75rem;
        font-weight: 700;
        letter-spacing: .04em;
        text-transform: uppercase;
        text-align: left;
    }

    /* Unlabelled cells carry the row on their own: the name, or its actions. */
    .table-stack td:not([data-label]) { text-align: left; }

    .table-stack .cell-title {
        order: -1;
        padding-bottom: .375rem;
        font-size: 1rem;
        font-weight: 650;
    }

    .table-stack .cell-actions { order: 1; padding-top: .5rem; }
    .table-stack .cell-actions .btn { width: 100%; }

    /* A cell holding more than one control — a form and a link, say — shares the
       card's width between them rather than letting the pair keep their
       desktop widths and push the card past the screen. */
    .table-stack .cell-actions > .row { width: 100%; }
    .table-stack .cell-actions > .row > * { flex: 1 1 0; min-width: 0; }

    /* Nothing overflows once the rows are stacked. */
    .table-stack { min-width: 0; }
}

.empty { padding: 3rem 1.25rem; text-align: center; color: var(--ink-3); }
.empty-mark { font-size: 2.5rem; display: block; margin-bottom: .5rem; }

/* ---------- responsive shell ---------- */
.sidebar-toggle { display: none; }

@media (max-width: 900px) {
    .app { grid-template-columns: 1fr; }

    .sidebar {
        position: fixed;
        inset: 0 auto 0 0;
        width: 264px;
        z-index: 60;
        transform: translateX(-100%);
        transition: transform .2s ease;
        box-shadow: var(--shadow-lg);
    }

    .app[data-nav="open"] .sidebar { transform: none; }

    .app[data-nav="open"]::after {
        content: "";
        position: fixed;
        inset: 0;
        background: rgb(0 0 0 / .45);
        z-index: 50;
    }

    .sidebar-toggle { display: inline-flex; }
    .content { padding: 1rem; }
}

/* ---------- POS ---------- */
.pos {
    display: grid;
    grid-template-columns: 1fr minmax(340px, 400px);
    height: calc(100dvh - var(--chrome));
    min-height: 0;
}

.pos-menu {
    display: flex;
    flex-direction: column;
    min-width: 0;
    min-height: 0;
}

.pos-search {
    display: flex;
    gap: .625rem;
    padding: .875rem 1rem .625rem;
}

.search-box { position: relative; flex: 1 1 auto; }
.search-box .input { padding-left: 2.5rem; }
.search-box::before {
    content: "⌕";
    position: absolute;
    left: .875rem;
    top: 50%;
    transform: translateY(-50%);
    font-size: 1.25rem;
    color: var(--ink-3);
    pointer-events: none;
}

.cats {
    display: flex;
    gap: .5rem;
    padding: 0 1rem .75rem;
    overflow-x: auto;
    scrollbar-width: none;
}

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

.cat {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    min-height: 42px;
    padding: 0 .9rem;
    border: 1px solid var(--line-strong);
    border-radius: 999px;
    background: var(--surface);
    color: var(--ink-2);
    font: inherit;
    font-weight: 600;
    font-size: .9375rem;
    white-space: nowrap;
    cursor: pointer;
    flex: none;
}

.cat[aria-pressed="true"] {
    background: var(--brand);
    border-color: var(--brand);
    color: var(--brand-ink);
}

/* The same box and pills over an ordinary page rather than the till's flush
   scrolling column: that one carries the padding itself because it runs edge to
   edge, and .content has already paid for it everywhere else. The pills still
   scroll sideways on a phone, so a long list of categories does not wrap into
   three rows above a table. */
.filters {
    display: flex;
    flex-direction: column;
    gap: .625rem;
    margin-bottom: 1rem;
}

.filters .cats { padding: 0; }

.products {
    flex: 1 1 auto;
    overflow-y: auto;
    padding: 0 1rem 1.25rem;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(148px, 1fr));
    gap: .75rem;
    align-content: start;
}

.tile {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    gap: .5rem;
    min-height: 104px;
    padding: .75rem;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--surface);
    color: var(--ink);
    font: inherit;
    text-align: left;
    cursor: pointer;
    box-shadow: var(--shadow-sm);
    transition: transform .06s ease, border-color .12s ease, box-shadow .12s ease;
}

.tile:hover { border-color: var(--brand); box-shadow: var(--shadow); }
.tile:active { transform: scale(.975); }
.tile-name { font-weight: 620; line-height: 1.25; }
.tile-foot { display: flex; align-items: center; justify-content: space-between; gap: .5rem; }
.tile-price { font-weight: 700; color: var(--brand-strong); }
.tile-star { color: var(--warn); font-size: .8rem; }

/* How many of this are on the ticket. It lives in the foot, beside the price,
   because anything added to the tile's own flow grows the row it sits in — and
   a grid that shifts under a finger tapping the same tile four times in a row
   is worse than no count at all. margin-right keeps it next to the price when
   the star or the out badge is holding the other end. */
.tile-added {
    margin-right: auto;
    font-size: .6875rem;
    font-weight: 650;
    color: var(--ok);
    white-space: nowrap;
}

.tile-added.is-bumped { animation: tile-added-in .28s ease; }

@keyframes tile-added-in {
    from { opacity: 0; transform: translateY(2px); }
}

@media (prefers-reduced-motion: reduce) {
    .tile-added.is-bumped { animation: none; }
}

/* Out of an ingredient. Dimmed and flagged, but deliberately still tappable —
   the till does not refuse a sale over a stock figure nobody has corrected. */
.tile-out { border-style: dashed; border-color: var(--line-strong); }
.tile-out .tile-name, .tile-out .tile-price { opacity: .55; }
.tile-out-badge {
    font-size: .625rem;
    font-weight: 700;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--danger);
    background: var(--danger-soft);
    padding: .0625rem .3125rem;
    border-radius: 999px;
}

/* ---------- cart ---------- */
.cart {
    display: flex;
    flex-direction: column;
    min-height: 0;
    background: var(--surface);
    border-left: 1px solid var(--line);
}

.cart-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .5rem;
    padding: .875rem 1rem;
    border-bottom: 1px solid var(--line);
}

.types { display: flex; gap: .3rem; padding: .625rem 1rem; border-bottom: 1px solid var(--line); }

.type {
    flex: 1 1 0;
    min-height: 40px;
    border: 1px solid var(--line-strong);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--ink-2);
    font: inherit;
    font-size: .875rem;
    font-weight: 600;
    cursor: pointer;
}

.type[aria-pressed="true"] { background: var(--surface-3); color: var(--ink); border-color: var(--line-strong); }

.lines { flex: 1 1 auto; overflow-y: auto; padding: .5rem; min-height: 0; }

.line {
    display: grid;
    grid-template-columns: 1fr auto;
    gap: .375rem .75rem;
    padding: .625rem;
    border-radius: var(--radius-sm);
}

.line + .line { border-top: 1px solid var(--line); }
.line-name { font-weight: 600; }
.line-total { font-weight: 700; font-variant-numeric: tabular-nums; }
.line-note { grid-column: 1 / -1; font-size: .8125rem; color: var(--brand-strong); }
.line-controls { grid-column: 1 / -1; display: flex; align-items: center; gap: .375rem; }

.qty { display: inline-flex; align-items: center; border: 1px solid var(--line-strong); border-radius: 999px; overflow: hidden; }
.qty button {
    width: 34px;
    height: 34px;
    border: 0;
    background: var(--surface-2);
    color: var(--ink);
    font: inherit;
    font-size: 1.125rem;
    font-weight: 700;
    cursor: pointer;
}
.qty button:hover { background: var(--surface-3); }
.qty span { min-width: 2.25rem; text-align: center; font-weight: 700; font-variant-numeric: tabular-nums; }

.cart-foot { border-top: 1px solid var(--line); padding: .875rem 1rem 1rem; }
.totals { display: flex; flex-direction: column; gap: .3rem; margin-bottom: .75rem; }
.total-row { display: flex; justify-content: space-between; font-variant-numeric: tabular-nums; }
.total-row.grand { font-size: 1.375rem; font-weight: 750; padding-top: .4rem; border-top: 1px dashed var(--line-strong); }

/* The toggle bar, its grabber, and the running total in the topbar all belong
   to the phone layout, where the cart is a sheet instead of a column. The
   handle disappears as a box rather than as a rule of its own, so on a wide
   screen the cart is laid out exactly as it was before it had one. */
.cart-toggle { display: none; }
.cart-handle { display: contents; }
.cart-grip { display: none; }

/* The order, in the topbar: the same two figures the sheet's own bar carries,
   for the half of the screen where the sheet is shut and out of sight. */
.cart-badge {
    display: none;
    align-items: center;
    gap: .5rem;
    min-height: 40px;
    padding: 0 .75rem 0 .5rem;
    border: 0;
    border-radius: 999px;
    background: var(--brand);
    color: var(--brand-ink);
    font: inherit;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    cursor: pointer;
}

.cart-badge-count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 24px;
    height: 24px;
    padding: 0 .375rem;
    border-radius: 999px;
    background: rgb(255 255 255 / .24);
    font-size: .8125rem;
}

/* What stands in the badge's place while the ticket is empty: the way back to
   the orders list, on the half of the screen where the sidebar is behind a
   hamburger and there is otherwise nowhere to go from here. Hidden at the same
   width the badge is, because above it the cart is a column in plain sight and
   the sidebar is already on screen. */
.cart-exit { display: none; }

/* ---------- modal / sheet ---------- */
.modal {
    position: fixed;
    inset: 0;
    z-index: 80;
    display: grid;
    place-items: center;
    padding: 1rem;
    background: rgb(0 0 0 / .5);
}

.modal-card {
    width: min(520px, 100%);
    max-height: 90dvh;
    overflow-y: auto;
    background: var(--surface);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);
}

.modal-head { display: flex; align-items: center; justify-content: space-between; padding: 1rem 1.125rem; border-bottom: 1px solid var(--line); }
.modal-body { padding: 1.125rem; }
.modal-foot { display: flex; gap: .625rem; padding: 1rem 1.125rem; border-top: 1px solid var(--line); }

.chips { display: flex; flex-wrap: wrap; gap: .5rem; }
.chip {
    /* Flex rather than a line-height trick, because a chip is not always a
       button: the shopping list draws them as links, and an inline <a> ignores
       min-height and drops its text onto the baseline — which read as
       top-heavy pills wherever a low ingredient was listed. */
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: .375rem;
    min-height: 40px;
    line-height: 1.2;
    padding: 0 .875rem;
    border: 1px solid var(--line-strong);
    border-radius: 999px;
    background: var(--surface);
    color: var(--ink-2);
    font: inherit;
    font-weight: 600;
    font-size: .9rem;
    cursor: pointer;
}
.chip:hover { background: var(--surface-2); }
.chip[aria-pressed="true"] { background: var(--brand-soft); border-color: var(--brand); color: var(--brand-strong); }

.pay-methods { display: grid; grid-template-columns: repeat(3, 1fr); gap: .5rem; }
.keypad { display: grid; grid-template-columns: repeat(3, 1fr); gap: .5rem; }
.keypad button { min-height: 52px; font-size: 1.125rem; }

@media (max-width: 900px) {
    .pos { grid-template-columns: 1fr; height: auto; }

    .cart {
        /* How much of the sheet stays on screen when it is shut. The closed
           transform and the header's own height are both built from it, so the
           bar you can see and the bar you can grab are the same 68px, and the
           drag has one number to measure against. */
        --peek: 68px;
        --grip-band: 14px;

        position: fixed;
        inset: auto 0 0 0;
        z-index: 40;
        max-height: 82dvh;
        border-left: 0;
        border-top: 1px solid var(--line);
        border-radius: var(--radius-lg) var(--radius-lg) 0 0;
        box-shadow: var(--shadow-lg);
        transform: translateY(calc(100% - var(--peek)));
        transition: transform .22s cubic-bezier(.32, .72, 0, 1);
    }

    .pos[data-cart="open"] .cart { transform: none; }

    .cart-handle {
        display: block;
        position: relative;
        padding-top: var(--grip-band);
        background: var(--brand);
        border-radius: var(--radius-lg) var(--radius-lg) 0 0;
        /* The sheet follows the finger, so the page must not: without this the
           browser claims the gesture as a scroll and the drag never starts. */
        touch-action: none;
        user-select: none;
        -webkit-user-select: none;
        cursor: grab;
    }

    .cart-handle.is-dragging { cursor: grabbing; filter: brightness(1.06); }

    .cart-grip {
        display: block;
        width: 40px;
        height: 5px;
        margin: 0 auto;
        border-radius: 999px;
        background: rgb(255 255 255 / .5);
        transition: transform .18s cubic-bezier(.32, .72, 0, 1), background-color .18s ease;
    }

    /* Held. The grabber swells and brightens under the finger the moment it
       lands, before the sheet has moved at all — which is the difference
       between a bar that has been touched and one that is being dragged, and
       the only way a thumb resting on the header knows it has hold of it. */
    .cart-handle.is-dragging .cart-grip {
        transform: scale(1.45, 1.4);
        background: rgb(255 255 255 / .95);
    }

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

    .cart-toggle {
        display: flex;
        align-items: center;
        justify-content: space-between;
        width: 100%;
        min-height: calc(var(--peek) - var(--grip-band));
        padding: 0 1rem;
        border: 0;
        background: transparent;
        color: var(--brand-ink);
        font: inherit;
        font-weight: 700;
        font-size: 1.0625rem;
        cursor: inherit;
    }

    .cart-badge { display: inline-flex; }
    .cart-exit { display: inline-flex; }

    /* The head and the order-type row used to be display:none until the sheet
       was open. They are only ever off the bottom of the screen while it is
       shut, and hiding them made the sheet two different heights — which a drag
       measured against one of them would have snapped to the wrong place, and
       which popped at the end of every open. */
    .lines { max-height: 40dvh; }
    .products { padding-bottom: 90px; grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); }
}

/* ---------- receipt ---------- */
.receipt {
    width: min(360px, 100%);
    margin: 0 auto;
    padding: 1.25rem;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    font-family: var(--mono);
    font-size: .8125rem;
}

.receipt hr { border: 0; border-top: 1px dashed var(--line-strong); margin: .75rem 0; }
.receipt-row { display: flex; justify-content: space-between; gap: .75rem; }
.receipt-head { text-align: center; margin-bottom: .75rem; }

@media print {
    .sidebar, .topbar, .no-print { display: none !important; }
    .app { display: block; }
    .content { padding: 0; }
    .receipt { border: 0; box-shadow: none; width: 100%; }
    body { background: #fff; }
}

/* ---------- stat tiles ---------- */
.kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: .75rem; }

/* On a phone the row settles at two across, which leaves an odd third tile
   sitting half-width beside a hole. Whichever tile is last and lands on an odd
   count takes the whole row instead — three tiles read as two and a banner,
   which is a shape, where two and a gap only reads as something missing. */
@media (max-width: 640px) {
    .kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .kpis > :last-child:nth-child(odd) { grid-column: 1 / -1; }
}

.kpi {
    padding: 1rem 1.125rem;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    box-shadow: var(--shadow-sm);
}

/* Values and labels wear ink tokens — the number carries the meaning, not a colour. */
.kpi-label { font-size: .75rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--ink-3); }
.kpi-value { font-size: 1.75rem; font-weight: 720; letter-spacing: -.02em; font-variant-numeric: tabular-nums; margin-top: .2rem; }
.kpi-note { font-size: .8125rem; color: var(--ink-2); }

/* ---------- kitchen ---------- */
.tickets { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: .875rem; align-items: start; }

.ticket { display: flex; flex-direction: column; overflow: hidden; }
.ticket-head { display: flex; align-items: center; justify-content: space-between; gap: .5rem; padding: .75rem 1rem; border-bottom: 1px solid var(--line); }
.ticket-num { font-weight: 750; font-size: 1.125rem; font-variant-numeric: tabular-nums; }
.ticket-items { list-style: none; margin: 0; padding: .625rem 1rem; display: flex; flex-direction: column; gap: .5rem; }
.ticket-items li { display: flex; gap: .625rem; }
.ticket-qty { min-width: 1.75rem; font-weight: 750; font-variant-numeric: tabular-nums; color: var(--brand-strong); }
.ticket-foot { padding: .75rem 1rem 1rem; margin-top: auto; }

/* ---------- charts ----------
 * Series colours live in their own tokens, kept apart from the UI palette: a bar
 * is data, not decoration, and the three tender hues were validated for
 * colourblind separation and contrast against both surfaces. Magnitude charts use
 * a single hue (the brand accent) — identity only earns extra colours where the
 * categories themselves are the subject, which here is the tender split alone.
 */
:root {
    --viz-accent: var(--brand);
    --viz-mute: #cbbfad;
    --viz-grid: var(--line);

    --viz-series-1: #2a78d6;
    --viz-series-2: #eb6834;
    --viz-series-3: #1baf7a;
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --viz-mute: #4a4034;

        --viz-series-1: #3987e5;
        --viz-series-2: #d95926;
        --viz-series-3: #199e70;
    }
}

:root[data-theme="dark"] {
    --viz-mute: #4a4034;

    --viz-series-1: #3987e5;
    --viz-series-2: #d95926;
    --viz-series-3: #199e70;
}

.chart { display: grid; grid-template-columns: auto 1fr; gap: .5rem; }

.chart-yaxis { position: relative; height: 160px; min-width: 3rem; }
.chart-ytick {
    position: absolute;
    right: 0;
    transform: translateY(-50%);
    font-size: .6875rem;
    font-variant-numeric: tabular-nums;
    color: var(--ink-3);
}

.chart-plot { position: relative; height: 160px; border-bottom: 1px solid var(--line-strong); }
.chart-rule { position: absolute; left: 0; right: 0; height: 0; border-top: 1px solid var(--viz-grid); }

.chart-cols { position: absolute; inset: 0; display: flex; align-items: flex-end; gap: 2px; }
.chart-col { position: relative; display: flex; flex: 1 1 0; align-items: flex-end; justify-content: center; height: 100%; }

.chart-bar {
    position: relative;
    width: 100%;
    max-width: 24px;
    border-radius: 4px 4px 0 0;
    background: var(--viz-accent);
}

/* Context bars recede so the one that matters — today — reads first. */
.chart-bar-mute { background: var(--viz-mute); }

.chart-xaxis { display: flex; gap: 2px; margin-top: .375rem; }
.chart-xlabel { flex: 1 1 0; text-align: center; font-size: .6875rem; color: var(--ink-3); }

.chart-value {
    position: absolute;
    bottom: 100%;
    left: 50%;
    transform: translateX(-50%);
    margin-bottom: .25rem;
    font-size: .6875rem;
    font-weight: 650;
    white-space: nowrap;
    color: var(--ink-2);
}

/* Hover and keyboard focus show the same readout; values are also in the table below. */
.chart-tip {
    position: absolute;
    bottom: 100%;
    left: 50%;
    transform: translateX(-50%);
    z-index: 5;
    margin-bottom: .375rem;
    padding: .3rem .5rem;
    border-radius: 8px;
    background: var(--ink);
    color: var(--surface);
    font-size: .75rem;
    white-space: nowrap;
    opacity: 0;
    pointer-events: none;
    transition: opacity .12s ease;
}

.chart-col:hover .chart-tip, .chart-col:focus .chart-tip, .chart-col:focus-visible .chart-tip { opacity: 1; }
.chart-col:hover .chart-value, .chart-col:focus .chart-value { opacity: 0; }
.chart-col:first-child .chart-tip { left: 0; transform: none; }
.chart-col:last-child .chart-tip { left: auto; right: 0; transform: none; }
.chart-col:focus { outline: none; }
.chart-col:focus-visible .chart-bar { outline: 3px solid var(--brand); outline-offset: 2px; }

/* Horizontal magnitude bars — one hue, value at the tip. */
.hbars { display: grid; gap: .625rem; }
.hbar { display: grid; grid-template-columns: minmax(5rem, 8rem) 1fr auto; align-items: center; gap: .75rem; }
.hbar-name { font-size: .875rem; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hbar-fill { display: block; height: 12px; min-width: 2px; border-radius: 0 4px 4px 0; background: var(--viz-accent); }
.hbar-value { font-size: .8125rem; font-variant-numeric: tabular-nums; color: var(--ink-2); }

/* Part-to-whole: 2px of surface separates the segments, never a border. */
.tender-bar { display: flex; gap: 2px; height: 14px; }
.tender-seg { display: block; height: 100%; }
.tender-seg:first-child { border-radius: 4px 0 0 4px; }
.tender-seg:last-child { border-radius: 0 4px 4px 0; }
.tender-seg:only-child { border-radius: 4px; }

.legend { display: flex; flex-wrap: wrap; gap: .25rem 1rem; margin-top: .75rem; }
.legend-key { display: inline-flex; align-items: center; gap: .4rem; font-size: .8125rem; color: var(--ink-2); }
.legend-swatch { width: 10px; height: 10px; border-radius: 3px; flex: none; }

.viz-table { margin-top: .75rem; }
.viz-table summary { font-size: .8125rem; color: var(--ink-2); cursor: pointer; }
.viz-table summary:hover { color: var(--ink); }
.viz-table table { margin-top: .5rem; }

.dash-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 1.25rem; align-items: start; }

/* Three across at most. auto-fit fits as many columns as the window allows, and
   on a wide screen that is four — which leaves the six cards as a row of four
   and an orphaned pair. Capping it keeps them two even rows of three. */
@media (min-width: 1500px) {
    .dash-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* And where there are four cards rather than six — the cash drawer — two rows
   of two beats three and a straggler. */
@media (min-width: 1100px) {
    .dash-grid.grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.hero-value { font-size: 2.75rem; font-weight: 750; letter-spacing: -.03em; line-height: 1.1; margin-top: .25rem; }

.delta { display: inline-flex; align-items: center; gap: .25rem; font-size: .8125rem; font-weight: 650; }
.delta-up { color: var(--ok); }
.delta-down { color: var(--danger); }

/* Headline row: one hero figure, then the supporting tiles filling the rest. */
.dash-head { display: grid; grid-template-columns: minmax(240px, 1fr) 3fr; gap: 1.25rem; align-items: stretch; }

@media (max-width: 900px) {
    .dash-head { grid-template-columns: 1fr; }
}

/* ---------- streamed alerts ---------- */
/* Bottom-right, clear of the sidebar and of the till's total, which sits top-right. */
/* ---------- off-canvas panel ---------- */
.offcanvas-backdrop {
    position: fixed;
    inset: 0;
    z-index: 70;
    background: rgb(0 0 0 / .38);
    opacity: 0;
    visibility: hidden;
    transition: opacity .2s ease, visibility .2s ease;
}

.offcanvas-backdrop.is-open { opacity: 1; visibility: visible; }

.offcanvas {
    position: fixed;
    inset: 0 0 0 auto;
    z-index: 71;
    display: flex;
    flex-direction: column;
    width: min(34rem, 100%);
    background: var(--surface);
    border-left: 1px solid var(--line);
    box-shadow: var(--shadow-lg);
    visibility: hidden;
    /* Only while it is shut. An element with a transform becomes the containing
       block for its position:fixed children, and the enhanced dropdowns place
       their panels against the viewport — leave a transform on the open state
       and every select inside opens in the wrong place. */
    transform: translateX(100%);
    transition: transform .24s ease, visibility .24s ease;
}

.offcanvas.is-open { visibility: visible; transform: none; }

.offcanvas-head {
    display: flex;
    align-items: center;
    gap: .75rem;
    padding: .875rem 1.125rem;
    border-bottom: 1px solid var(--line);
}

.offcanvas-head h2 { margin: 0; font-size: 1.0625rem; }

.offcanvas-form { display: flex; flex-direction: column; min-height: 0; flex: 1 1 auto; }

/* No padding of its own: the fields bring their own, the same ones the
   full-page form uses. */
.offcanvas-body { flex: 1 1 auto; overflow-y: auto; min-height: 0; }

.offcanvas-foot {
    display: flex;
    gap: .625rem;
    justify-content: flex-end;
    padding: .875rem 1.125rem;
    border-top: 1px solid var(--line);
}

/* The page behind stays put while the panel is over it. */
body.has-offcanvas { overflow: hidden; }

/* The shape of what is on its way, held for as long as the network takes. Bars
   rather than a spinner: a spinner says "something is happening", where this
   says "something this shape is happening", and the panel does not appear to
   change its mind when the answer lands. */
.panel-skeleton { gap: .75rem; }

.skeleton-bar {
    display: block;
    height: 12px;
    border-radius: 999px;
    background: linear-gradient(90deg, var(--surface-2) 25%, var(--surface-3) 50%, var(--surface-2) 75%);
    background-size: 240% 100%;
    animation: skeleton-sweep 1.3s ease-in-out infinite;
}

@keyframes skeleton-sweep {
    from { background-position: 120% 0; }
    to { background-position: -120% 0; }
}

@media (prefers-reduced-motion: reduce) {
    .skeleton-bar { animation: none; }
}

@media (prefers-reduced-motion: reduce) {
    .offcanvas, .offcanvas-backdrop { transition: none; }
}

.toasts {
    position: fixed;
    right: 1rem;
    bottom: 1rem;
    /* Above everything but the lock: a refusal is often about the panel or the
       dialog that is covering the page, and it has to be readable over it. */
    z-index: 100;
    display: flex;
    flex-direction: column;
    gap: .5rem;
    width: min(360px, calc(100vw - 2rem));
    pointer-events: none;
}

.toast {
    display: flex;
    gap: .625rem;
    padding: .75rem .875rem;
    border-radius: var(--radius-sm);
    border: 1px solid var(--line-strong);
    border-left: 3px solid var(--ink-3);
    background: var(--surface);
    box-shadow: var(--shadow);
    font-size: .875rem;
    pointer-events: auto;
    animation: toast-in .2s ease-out;
}

.toast-warn { border-left-color: var(--warn); }
.toast-danger { border-left-color: var(--danger); }
.toast-ok { border-left-color: var(--ok); }

.toast-mark { flex: none; }
.toast-body { min-width: 0; }
.toast-title { font-weight: 650; }
.toast-note { color: var(--ink-2); font-size: .8125rem; }

.toast-leaving { opacity: 0; transition: opacity .2s ease-in; }

@keyframes toast-in {
    from { opacity: 0; transform: translateY(6px); }
    to { opacity: 1; transform: none; }
}

/* A notice that slides in beside someone pulling shots is a distraction, not a
   feature — respect the setting and just appear. */
@media (prefers-reduced-motion: reduce) {
    .toast { animation: none; }
    .toast-leaving { transition: none; }
}

/* ---------- connection banner ---------- */
/* Top-centre, above everything: losing the network mid-order is the one thing
   that has to interrupt whatever else is on screen. */
.net-banner {
    position: fixed;
    top: 1rem;
    left: 50%;
    z-index: 90;
    display: flex;
    align-items: center;
    gap: .5rem;
    padding: .5rem .875rem;
    border-radius: 999px;
    background: var(--warn);
    color: #fff;
    font-size: .8125rem;
    font-weight: 600;
    box-shadow: var(--shadow-lg);
    transform: translate(-50%, -1.75rem);
    opacity: 0;
    transition: opacity .25s ease, transform .25s ease;
}

.net-banner-visible { opacity: 1; transform: translate(-50%, 0); }

.net-banner-spinner {
    width: 14px;
    height: 14px;
    border: 2px solid rgb(255 255 255 / .35);
    border-top-color: #fff;
    border-radius: 50%;
    flex: none;
    animation: net-spin .8s linear infinite;
}

@keyframes net-spin { to { transform: rotate(360deg); } }

@media (prefers-reduced-motion: reduce) {
    .net-banner { transition: none; }
    .net-banner-spinner { animation-duration: 2.4s; }
}

/* ---------- impersonation ---------- */
/* An owner standing in for someone else. On every page for as long as it lasts,
   because the danger of impersonation is entirely in forgetting you are in it —
   an order rung up from here belongs to the account being stood in for, and no
   amount of explaining afterwards changes that.

   A full-width strip under the topbar, inside .topbar-stack so it is stuck to
   the bar and on screen at any scroll position. It was a floating pill until it
   turned out to be sitting on top of the page heading and whatever button the
   page had put in the corner: an element that cannot be dismissed has no
   business covering the controls. Inverted rather than coloured — it has to
   read as a different mode of the app, not as one more coloured alert. */
.impersonating {
    display: flex;
    align-items: center;
    gap: .625rem;
    /* Fixed rather than grown from its contents, so --chrome above can be an
       exact number instead of a guess that drifts with the font. */
    height: 32px;
    padding: 0 1.25rem;
    background: var(--ink);
    color: var(--surface);
    font-size: .8125rem;
    font-weight: 600;
    /* Two lines of this would push the page about every time a long name was
       stood in for; the name itself truncates instead. */
    white-space: nowrap;
}

.impersonating strong { font-weight: 750; }

.impersonating-stop {
    flex: none;
    padding: .1875rem .75rem;
    border-radius: 999px;
    background: var(--surface);
    color: var(--ink);
    font-size: .75rem;
    font-weight: 750;
    text-decoration: none;
}

.impersonating-stop:hover { background: var(--surface-2); text-decoration: none; }

/* ---------- floating nav (installed app only) ---------- */
/* A standalone window has no browser chrome, so there is no way back out of a
   page without this. In a tab it would only duplicate controls already there. */
.pwa-nav { display: none; }

@media (display-mode: standalone), (display-mode: minimal-ui), (display-mode: fullscreen) {
    .pwa-nav { display: block; }
}

/* iOS reports standalone on the navigator rather than answering the query. */
.pwa-nav.is-standalone { display: block; }

.pwa-nav {
    position: fixed;
    left: 50%;
    bottom: calc(1rem + env(safe-area-inset-bottom));
    transform: translateX(-50%);
    z-index: 80;
    touch-action: none;
}

.pwa-nav.is-dragging { transition: none; }

.pwa-nav-puck,
.pwa-nav-bar {
    background: color-mix(in srgb, var(--brand) 94%, transparent);
    color: var(--brand-ink);
    box-shadow: var(--shadow-lg);
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
}

.pwa-nav-puck {
    display: grid;
    place-items: center;
    width: 52px;
    height: 52px;
    border: 0;
    border-radius: 50%;
    cursor: grab;
}

.pwa-nav-bar {
    display: none;
    align-items: center;
    gap: .125rem;
    padding: .375rem .5rem;
    border-radius: 999px;
}

.pwa-nav.is-expanded .pwa-nav-puck { display: none; }
.pwa-nav.is-expanded .pwa-nav-bar { display: flex; }

.pwa-nav-bar button,
.pwa-nav-bar a {
    display: grid;
    place-items: center;
    width: 44px;
    height: 44px;
    border: 0;
    border-radius: 50%;
    background: none;
    color: inherit;
    cursor: pointer;
}

.pwa-nav-bar button:hover:not(:disabled),
.pwa-nav-bar a:hover { background: rgb(255 255 255 / .18); }

.pwa-nav-bar button:disabled { opacity: .35; cursor: default; }

.pwa-nav-grip {
    cursor: grab;
    border-right: 1px solid rgb(255 255 255 / .25) !important;
    border-radius: 0 !important;
    width: 34px !important;
    margin-right: .25rem;
}

.pwa-nav.is-dragging .pwa-nav-puck,
.pwa-nav.is-dragging .pwa-nav-grip { background: rgb(255 255 255 / .22); cursor: grabbing; }

/* ---------- screen lock ---------- */

/*
 * The glass. `backdrop-filter` is doing the real work here: the till behind
 * stays where it is — mid-order, streaming updates — and is simply not legible
 * any more. The tint underneath it is what stops a large flat colour (a full
 * white card, a photo) reading through the blur.
 */
.lockscreen {
    position: fixed;
    inset: 0;
    z-index: 200;
    display: grid;
    place-items: center;
    padding: 1.25rem;
    padding-top: max(1.25rem, env(safe-area-inset-top));
    padding-bottom: max(1.25rem, env(safe-area-inset-bottom));
    background: color-mix(in srgb, var(--bg) 55%, transparent);
    backdrop-filter: blur(22px) saturate(115%);
    -webkit-backdrop-filter: blur(22px) saturate(115%);
    opacity: 0;
    transition: opacity .22s ease;
}

.lockscreen.is-open { opacity: 1; }
.lockscreen[hidden] { display: none; }

/* No app behind it, so the glass gets something of its own to sit on. */
.lock-page {
    min-height: 100dvh;
    background:
        radial-gradient(70rem 40rem at 15% -10%, var(--brand-soft), transparent 60%),
        radial-gradient(60rem 40rem at 110% 110%, var(--surface-3), transparent 55%),
        var(--bg);
}

.lock-card {
    width: min(23rem, 100%);
    max-height: calc(100dvh - 2.5rem);
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: .875rem;
    padding: 1.5rem 1.375rem 1.25rem;
    border: 1px solid color-mix(in srgb, var(--line-strong) 70%, transparent);
    border-radius: var(--radius-lg);
    background: color-mix(in srgb, var(--surface) 78%, transparent);
    backdrop-filter: blur(28px) saturate(150%);
    -webkit-backdrop-filter: blur(28px) saturate(150%);
    box-shadow: var(--shadow-lg);
    transform: translateY(.5rem) scale(.985);
    transition: transform .22s ease;
}

.lockscreen.is-open .lock-card { transform: none; }
.lock-card:focus { outline: none; }

.lock-clock {
    font-size: 2.5rem;
    font-weight: 300;
    line-height: 1;
    letter-spacing: -.02em;
    font-variant-numeric: tabular-nums;
    color: var(--ink);
}

.lock-who { display: flex; flex-direction: column; align-items: center; gap: .375rem; }

.lock-avatar {
    display: grid;
    place-items: center;
    width: 52px;
    height: 52px;
    border-radius: 50%;
    background: var(--brand);
    color: var(--brand-ink);
    font-size: 1.25rem;
    font-weight: 650;
}

.lock-name { margin: 0; font-size: 1.0625rem; font-weight: 650; }
.lock-sub { margin: 0; font-size: .8125rem; color: var(--ink-3); }

/* Holds its height whether or not there is anything in it, so the keypad does
   not jump down the screen under a finger the moment a PIN is refused. */
.lock-error {
    min-height: 1.25rem;
    font-size: .8125rem;
    font-weight: 550;
    line-height: 1.25rem;
    text-align: center;
    color: var(--danger);
}

.lock-pin {
    width: 100%;
    min-height: var(--tap);
    border: 1px solid var(--line-strong);
    border-radius: var(--radius-sm);
    background: color-mix(in srgb, var(--surface-2) 80%, transparent);
    color: var(--ink);
    font: inherit;
    font-size: 1.5rem;
    text-align: center;
    letter-spacing: .5em;
    text-indent: .5em; /* centres text that letter-spacing pads on the right */
}

.lock-pin::placeholder { color: var(--ink-3); letter-spacing: .5em; }
.lock-pin:focus { outline: none; border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-soft); }

.lock-field { width: 100%; }
.lock-pad { width: 100%; gap: .5rem; }

.lock-key {
    min-height: 56px;
    font-size: 1.25rem;
    font-weight: 550;
    background: color-mix(in srgb, var(--surface) 70%, transparent);
}

.lock-key-soft { color: var(--ink-2); font-size: 1.0625rem; }

.lock-hint { margin: 0; font-size: .75rem; text-align: center; color: var(--ink-3); }

.lock-out {
    font-size: .8125rem;
    font-weight: 550;
    color: var(--ink-3);
    text-decoration: none;
}

.lock-out:hover { color: var(--ink-2); text-decoration: underline; }

.lock-card.is-wrong { animation: lock-shake .4s; }

@keyframes lock-shake {
    0%, 100% { transform: translateX(0); }
    20% { transform: translateX(-7px); }
    40% { transform: translateX(6px); }
    60% { transform: translateX(-4px); }
    80% { transform: translateX(2px); }
}

/* Where the browser cannot blur, fall back to something opaque rather than
   leaving the takings legible through a half-transparent panel. */
@supports not (backdrop-filter: blur(1px)) {
    .lockscreen { background: var(--bg); }
    .lock-card { background: var(--surface); }
}

.lock-choice {
    padding: .625rem .75rem;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    cursor: pointer;
}

.lock-choice:hover { background: var(--surface-2); }
.lock-choice.is-chosen { border-color: var(--brand); background: var(--brand-soft); }
.lock-choice.is-chosen .strong { color: var(--brand-strong); }

@media (prefers-reduced-motion: reduce) {
    .lockscreen, .lock-card { transition: none; }
    .lock-card.is-wrong { animation: none; }
}

/* ---------- category rows ---------- */
/* Editable in place, one form per row: eight categories do not warrant a page
   each, and the whole point is comparing them while you reorder. */
.category-row {
    gap: .5rem;
    padding: .625rem 1rem;
    border-bottom: 1px solid var(--line);
}

.category-row:last-child { border-bottom: 0; }
.category-row .input { min-height: 40px; padding: .375rem .625rem; }

/* A control that narrows the page it sits on, at the top of the content rather
   than up in the topbar. The bar says where you are; this says what you are
   looking at, and putting the second in the first crowded the heading out of a
   phone. Baseline-aligned so the label reads as part of the sentence the
   controls finish. */
.page-filter {
    align-items: center;
    margin: 0 0 1rem;
}

.page-filter .label { margin: 0; }

/* ---------- audit trail ---------- */
.audit-filters .select, .audit-filters .input { min-height: 42px; }

/*
 * The filter, laid out rather than left to wrap.
 *
 * Two columns on a phone and one row on a laptop. Wrapping had been deciding
 * the pairs on its own and getting them wrong — a lone dropdown stranded on its
 * own line, the button orphaned under it. These pairs are chosen: who and what
 * are the same question asked twice, and Show belongs beside the span it is
 * about to apply.
 */
.filter-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: .625rem;
    align-items: end;
}

/* The search takes the width it needs to be typed into. */
.filter-grid .filter-wide { grid-column: 1 / -1; }
.filter-grid .field { margin: 0; min-width: 0; }

.filter-dates {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: .625rem;
}

/* Both of these set `display`, which would otherwise beat the browser's own
   rule for the hidden attribute and leave the date boxes on screen for good. */
.filter-dates[hidden] { display: none; }
.filter-grid[hidden] { display: none; }

/*
 * The fold. On a phone the six controls and seven chips were most of the first
 * screen, and the entries — the thing anybody came for — started below it.
 */
.filter-summary {
    display: flex;
    align-items: center;
    gap: .625rem;
    padding: .875rem 1rem;
    cursor: pointer;
    user-select: none;
    -webkit-user-select: none;
    /* The default triangle is replaced by the caret at the other end, where a
       thumb reaching for it is not covering the words. */
    list-style: none;
}

.filter-summary::-webkit-details-marker { display: none; }
.filter-summary:hover { background: var(--surface-2); }
.filter-summary-mark { font-size: 1.125rem; color: var(--ink-3); flex: none; }

.filter-caret {
    flex: none;
    color: var(--ink-3);
    font-size: 1.125rem;
    line-height: 1;
    transition: transform .15s ease;
}

.filter-fold[open] .filter-caret { transform: rotate(180deg); }

/* Open, the summary is the card's own heading and needs the line under it that
   every other card head has. */
.filter-fold[open] .filter-summary { border-bottom: 1px solid var(--line); }

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

@media (min-width: 900px) {
    /* Nothing to fold away up here — the card was never the thing in the way —
       so the summary goes and the contents stay put whatever the open attribute
       says. An author rule beats the browser's own for a closed <details>. */
    .filter-summary { display: none; }
    .filter-fold > .card-pad { display: block; }

    .filter-grid { grid-template-columns: minmax(11rem, 1.7fr) 1fr 1fr 1fr auto; }
    .filter-grid .filter-wide { grid-column: auto; }

    /* Two boxes' worth, not half the card each. */
    .filter-dates { grid-template-columns: repeat(2, minmax(0, 11rem)); justify-content: start; }
}

/* The little key/value run under an entry: "reason: staff meal · free lines: 1".
   Inline so short ones sit on the row they belong to, and the separator is
   drawn between them rather than typed into the template. */
.audit-detail { display: inline; }
.audit-detail > span + span::before { content: " · "; color: var(--ink-3); }
.audit-detail b { font-weight: 600; color: var(--ink-3); }

/* Who was really at the keyboard. Deliberately not a badge — it is a sentence,
   and a badge would read as a category of entry rather than a caveat on one. */
.audit-behalf { color: var(--warn); }

@media (max-width: 720px) {
    /* Stacked, the amount is the one cell worth reading first after the row's
       own title, and a right-aligned column of one is just a ragged edge. */
    .audit-detail { display: block; text-align: left; }
    .audit-detail > span + span::before { content: none; }
    .audit-detail > span { display: block; }
}

/* ---------- the app's own confirm box ---------- */
/*
 * Sits above everything the app draws — the panel, the POS sheet, the toasts,
 * the impersonation strip — and below the screen lock at 200, which has to be
 * able to cover a question as readily as it covers the takings.
 */
.dialog {
    position: fixed;
    inset: 0;
    z-index: 150;
    display: grid;
    place-items: center;
    padding: 1.25rem;
}

.dialog[hidden] { display: none; }

.dialog-backdrop {
    position: absolute;
    inset: 0;
    background: rgb(0 0 0 / .55);
    animation: dialog-fade .12s ease-out;
}

.dialog-card {
    position: relative;
    width: min(26rem, 100%);
    padding: 1.5rem;
    background: var(--surface);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);
    animation: dialog-rise .16s cubic-bezier(.32, .72, 0, 1);
}

.dialog-title { margin: 0 0 .5rem; font-size: 1.125rem; font-weight: 650; }
.dialog-message { margin: 0; color: var(--ink-2); line-height: 1.5; }

/*
 * Full-width buttons stacked, not a row of two.
 *
 * This is answered with a thumb on a counter, usually in a hurry, and often
 * about something that cannot be undone. Two 48px targets a finger's width
 * apart are how you delete the wrong product; one above the other, with the
 * safe one nearest the thumb, is not.
 */
.dialog-actions {
    display: flex;
    flex-direction: column-reverse;
    gap: .625rem;
    margin-top: 1.5rem;
}

.dialog-actions .btn { width: 100%; }
.dialog-actions .btn[hidden] { display: none; }

@keyframes dialog-fade {
    from { opacity: 0; }
    to { opacity: 1; }
}

@keyframes dialog-rise {
    from { opacity: 0; transform: translateY(8px) scale(.98); }
    to { opacity: 1; transform: none; }
}

/* The page underneath must not scroll away behind an open question. */
body.has-dialog { overflow: hidden; }

@media (prefers-reduced-motion: reduce) {
    .dialog-backdrop, .dialog-card { animation: none; }
}
