/* ==========================================================================
   Sun City Birria — Inventory
   Design system

   Brand: navy #153143, teal #54AEA6, orange #E86F0D.
   Built mobile-first: staff use this on a phone, standing up, one-handed.
   No framework — this replaces Bootstrap, MDB, Font Awesome, select2,
   jQuery UI and Material icons, which together were ~1.5 MB of CDN downloads
   before a single row of inventory appeared.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Tokens
   -------------------------------------------------------------------------- */
:root {
    /* Brand */
    --navy:          #153143;
    --navy-700:      #1d4257;
    --navy-300:      #4a6c81;
    --teal:          #54AEA6;
    --teal-600:      #418983;
    --teal-700:      #31635f;
    --orange:        #E86F0D;
    --orange-700:    #C4600D;

    /* Neutrals */
    --white:         #ffffff;
    --gray-50:       #f7f8f9;
    --gray-100:      #eef0f2;
    --gray-200:      #e3e5e8;
    --gray-300:      #cdd2d7;
    --gray-400:      #a6aeb6;
    --gray-500:      #6b7680;
    --gray-600:      #4d565e;

    /* Semantic */
    --bg:            var(--gray-50);
    --surface:       var(--white);
    --border:        var(--gray-200);
    --border-strong: var(--gray-300);
    --text:          var(--navy);
    --text-muted:    var(--gray-500);
    --primary:       var(--teal-600);
    --primary-hover: var(--teal-700);
    --danger:        #c0392b;
    --danger-bg:     #fdecea;
    --success:       #1e7d5a;
    --success-bg:    #e6f4ee;
    --warning:       var(--orange);
    --warning-bg:    #fdf0e4;

    /* Spacing — 4px scale */
    --s1: 0.25rem;
    --s2: 0.5rem;
    --s3: 0.75rem;
    --s4: 1rem;
    --s5: 1.5rem;
    --s6: 2rem;
    --s7: 3rem;

    --radius:    10px;
    --radius-sm: 6px;
    --radius-lg: 16px;

    /* A wash of the app's own accent, for the one panel on the home page that
       is neither a store nor a count. */
    --tint-accent:      #eaf5f4;
    --tint-accent-edge: #cfe6e3;

    --shadow-sm: 0 1px 2px rgba(21, 49, 67, 0.06);
    --shadow:    0 2px 8px rgba(21, 49, 67, 0.08);
    --shadow-lg: 0 12px 32px rgba(21, 49, 67, 0.16);

    /* Motion — one vocabulary, so nothing invents its own timing.
       Deliberately short. This is a tool somebody uses fifty times in a shift
       with cold hands, and an animation you can WAIT for is an animation that
       has started costing money. Nothing here is longer than a third of a
       second, and the two you touch most (press, hover) are under 150ms so
       they read as the surface responding rather than as a effect playing. */
    --dur-press: 90ms;
    --dur-quick: 140ms;
    --dur-base:  200ms;
    --dur-enter: 280ms;

    /* Decelerating: fast off the mark, settling at the end. Things that
       arrive on screen should look like they are coming to rest, not braking. */
    --ease:       cubic-bezier(0.2, 0, 0, 1);
    --ease-enter: cubic-bezier(0.05, 0.7, 0.1, 1);

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

    --header-height: 56px;
}

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

/* The background goes on <html>, not just <body>.
 *
 * Everything outside the page box is painted from here: the home-indicator
 * strip under a Home Screen app, the rubber-band area when you overscroll,
 * and — on iOS Safari — the strip behind the bottom address bar, which
 * samples this colour. With it left unset, all three showed white against a
 * dark page. */
html {
    -webkit-text-size-adjust: 100%;
    background: var(--bg);
    min-height: 100%;
    /* No double-tap zoom. The meta tag alone does not stop it in the installed
       app, and a double tap while ticking a checklist landed the page at 2x
       with nothing on screen explaining why. "manipulation" keeps panning and
       normal scrolling, and drops only the zoom gesture. */
    touch-action: manipulation;
}

/* The login screen is dark end to end, so <html> matches its gradient's
   bottom colour rather than the app's light grey. */
html.is-login {
    background: #0d2130;
}

body {
    margin: 0;
    font-family: var(--font);
    /* 16px minimum: anything smaller makes iOS Safari zoom on input focus. */
    font-size: 16px;
    line-height: 1.5;
    color: var(--text);
    background: var(--bg);
    -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4 { margin: 0; font-weight: 650; line-height: 1.25; }
h1 { font-size: 1.375rem; }
h2 { font-size: 1.125rem; }
h3 { font-size: 1rem; }
p  { margin: 0 0 var(--s3); }

button, input, select, textarea { font: inherit; color: inherit; }
button { cursor: pointer; }
button:disabled { cursor: not-allowed; opacity: 0.5; }

table { border-collapse: collapse; width: 100%; }

[hidden] { display: none !important; }

:focus-visible {
    outline: 2px solid var(--teal);
    outline-offset: 2px;
}

/* --------------------------------------------------------------------------
   Layout
   -------------------------------------------------------------------------- */
/* Installed on an iPhone Home Screen the web view runs full-bleed, right up
   under the clock and the notch. viewport-fit=cover is what asks for that, and
   env(safe-area-inset-*) is what pays for it — without the top inset the
   header sits underneath the status bar and the brand is unreadable.
   The navy deliberately extends INTO the inset so the status bar looks like
   part of the header rather than a white strip above it. */
.app-header {
    position: sticky;
    top: 0;
    z-index: 50;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
    min-height: calc(var(--header-height) + env(safe-area-inset-top, 0px));
    padding-top: env(safe-area-inset-top, 0px);
    padding-right: max(var(--s4), env(safe-area-inset-right, 0px));
    padding-bottom: 0;
    padding-left: max(var(--s4), env(safe-area-inset-left, 0px));
    background: var(--navy);
    color: var(--white);
    box-shadow: var(--shadow);
}

.app-header__brand {
    display: flex;
    align-items: center;
    gap: var(--s3);
    min-width: 0;
}

.app-header__logo { height: 30px; width: auto; display: block; }

.app-header__title {
    font-size: 0.9375rem;
    font-weight: 600;
    letter-spacing: 0.01em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.app-header__user {
    display: flex;
    align-items: center;
    gap: var(--s3);
    font-size: 0.875rem;
}

/* What a phone header can hold, said properly this time.
   The old note here read "two icons plus Sign out" — and then the bar grew to
   four icons plus Sign out, which is how "Sun City Birria" ended up rendering
   as "Sun Cit…". Sign out has moved into Settings and the icons lost their
   boxes; the rule now is that the NAME gets whatever the icons do not need,
   and the icons sit tight against each other rather than spaced like buttons. */
@media (max-width: 30rem) {
    .app-header { gap: var(--s2); }
    .app-header__user { gap: 2px; }
    /* The name may shrink, but never below something readable — and it wraps
       to a second line rather than becoming an ellipsis, because a truncated
       company name looks like a bug in the app rather than a narrow screen. */
    .app-header__title { font-size: 0.875rem; }
}

/* Below this the logo is doing the identifying, and two lines of name in a
   40px bar is worse than none. */
@media (max-width: 20rem) {
    .app-header__title { display: none; }
}

.app-main {
    max-width: 60rem;
    margin: 0 auto;
    padding: var(--s5)
             max(var(--s4), env(safe-area-inset-right, 0px))
             calc(var(--s7) + env(safe-area-inset-bottom, 0px))
             max(var(--s4), env(safe-area-inset-left, 0px));
}

.page-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--s4);
    flex-wrap: wrap;
    margin-bottom: var(--s5);
}

.page-head__sub {
    margin: var(--s1) 0 0;
    color: var(--text-muted);
    font-size: 0.875rem;
}

.stack   { display: grid; gap: var(--s4); }
.stack-2 { display: grid; gap: var(--s2); }
.row     { display: flex; gap: var(--s2); flex-wrap: wrap; align-items: center; }
.row--end { justify-content: flex-end; }
.grow    { flex: 1 1 auto; min-width: 0; }

/* --------------------------------------------------------------------------
   Buttons
   -------------------------------------------------------------------------- */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--s2);
    /* 44px tall — the minimum comfortable tap target on a phone. */
    min-height: 44px;
    padding: var(--s2) var(--s4);
    border: 1px solid transparent;
    border-radius: var(--radius-sm);
    background: var(--primary);
    color: var(--white);
    font-size: 0.9375rem;
    font-weight: 600;
    text-decoration: none;
    white-space: nowrap;
    transition: background-color 0.15s ease, border-color 0.15s ease;
}
.btn:hover:not(:disabled)  { background: var(--primary-hover); }
.btn:active:not(:disabled) { transform: translateY(1px); }

.btn--secondary {
    background: var(--surface);
    border-color: var(--border-strong);
    color: var(--text);
}
.btn--secondary:hover:not(:disabled) { background: var(--gray-100); }

.btn--ghost {
    background: transparent;
    color: var(--text);
    border-color: transparent;
}
.btn--ghost:hover:not(:disabled) { background: var(--gray-100); }

.btn--danger { background: var(--danger); }
.btn--danger:hover:not(:disabled) { background: #a5312a; }

.btn--warning { background: var(--orange); }
.btn--warning:hover:not(:disabled) { background: var(--orange-700); }

.btn--on-navy {
    background: rgba(255, 255, 255, 0.12);
    border-color: rgba(255, 255, 255, 0.25);
    color: var(--white);
    min-height: 36px;
    padding: var(--s1) var(--s3);
    font-size: 0.875rem;
}
.btn--on-navy:hover:not(:disabled) { background: rgba(255, 255, 255, 0.22); }

.btn--sm    { min-height: 36px; padding: var(--s1) var(--s3); font-size: 0.875rem; }
.btn--block { width: 100%; }

.btn--icon {
    min-height: 40px;
    width: 40px;
    padding: 0;
    font-size: 1.125rem;
    line-height: 1;
}

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

.card__body   { padding: var(--s4); }
.card__header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
    padding: var(--s4);
    border-bottom: 1px solid var(--border);
}
.card__footer {
    padding: var(--s3) var(--s4);
    border-top: 1px solid var(--border);
    background: var(--gray-50);
    border-radius: 0 0 var(--radius) var(--radius);
}

/* Location cards on the dashboard */
.location-card__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--s3);
    flex-wrap: wrap;
    margin-bottom: var(--s3);
}

.location-card__name { font-size: 1.0625rem; font-weight: 650; }

/* The address, and the store name when the heading is already the brand's.
   Deliberately quieter than the name but louder than the footnote — it is
   what tells two stores of the same concept apart. */
.location-card__where {
    font-size: 0.875rem;
    color: var(--gray-600);
    margin-top: 2px;
}

/* Who counted last. A footnote, so it sits under the buttons in a footnote's
   type rather than competing with the name for the top of the card. */
.location-card__meta {
    font-size: 0.8125rem;
    color: var(--text-muted);
    margin-top: var(--s3);
}

/* The low-stock warning on one store's card. Full width and tappable — it is
   the one thing on the card that needs acting on today. */
.location-card__stock {
    display: flex;
    align-items: center;
    gap: var(--s2);
    width: 100%;
    margin-bottom: var(--s3);
    padding: var(--s2) var(--s3);
    border: 1px solid #f6d3ae;
    border-left: 3px solid var(--orange);
    border-radius: var(--radius-sm);
    background: var(--warning-bg);
    color: #8a4308;
    font: inherit;
    font-size: 0.875rem;
    font-weight: 600;
    text-align: left;
    cursor: pointer;
}

.location-card__stock--critical {
    border-color: #f3c6c1;
    border-left-color: var(--danger);
    background: var(--danger-bg);
    color: #8c2018;
}

/* ---- the home page's two cards ----------------------------------------
   People and paperwork, then the concept's box. They are built the same way
   on purpose: a coloured header saying whose it is, and a white sheet under it
   holding what belongs to it.

   That sameness is the point. An earlier pass restyled the concept's box and
   left the HR band as it was, so the page read as two designs sharing a
   screen. Anything that is a card up here uses this one shape. */
.home-card {
    background: var(--surface);
    border-radius: var(--radius-lg);
    overflow: hidden;
    margin-bottom: var(--s4);
    box-shadow: var(--shadow-sm), 0 6px 20px rgba(21, 49, 67, 0.05);
}

.home-card__head {
    display: flex;
    align-items: center;
    gap: var(--s3);
    padding: var(--s3) var(--s4);
    background: var(--brand-header, var(--navy));
    color: var(--brand-header-text, var(--white));
}

.home-card__title {
    margin: 0;
    font-size: 1rem;
    font-weight: 600;
    line-height: 1.3;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.home-card__sub {
    margin: 1px 0 0;
    font-size: 0.8125rem;
    opacity: 0.75;
}

.home-card__naming { min-width: 0; flex: 1 1 auto; }

/* The white sheet under the tabs, outlined in the active tab's colour so the
   tab and its card read as one object.

   This started as a coloured bar along the top edge and went through two
   attempts — floating in white space, then too thin for its own rounding to be
   visible. A plain border all the way round is the thing it kept trying to be. */
.home-card__body { padding: var(--s4); }

.tab-head + .home-card__body {
    /* All four sides, the top included. The active tab is the same colour, so
       the line runs INTO it and the two read as one piece of card — while the
       inactive tabs stay visibly cut off above it. That join is the whole
       point of the border. */
    border: 2px solid var(--brand-header, var(--navy));
    /* Matching the card's own corners, since the card clips to them anyway. */
    border-radius: 0 0 var(--radius-lg) var(--radius-lg);
}

/* ---- the concept's box -------------------------------------------------
   Its own class only for the parts the HR band has no use for. */
/* ---- tabs inside a screen ----------------------------------------------
   These used to be pills, on the argument that the brand tabs ARE a card's top
   edge while these merely sit above a list. That distinction is gone: every
   tab strip in the app now wears the same folder shape, so somebody who has
   learnt what a tab looks like on the home page recognises one anywhere.

   The strips themselves are .tab-head/.tab-head__tab; what each of them needs
   is a joined body underneath (.doc-tab-body) or the border has nothing to run
   into. The pill rules that were here are gone with their last call site. */

.brand-shell__stores {
    display: flex;
    flex-direction: column;
    gap: var(--s3);
}

/* Suppliers and costings. A strip rather than a card: inside the box the
   heading would be the brand name printed twice on top of itself. */
/* Tinted rather than grey. It is the one thing in this card that is not a
   store, so it should not look like a store that lost its contents — and the
   tint is drawn from the app's own accent so it belongs to the palette
   instead of introducing a colour of its own. */
.back-office {
    display: flex;
    align-items: center;
    gap: var(--s3);
    flex-wrap: wrap;
    margin-bottom: var(--s3);
    padding: var(--s3);
    background: var(--tint-accent);
    border: 1px solid var(--tint-accent-edge);
    border-radius: var(--radius);
}

/* Not text-transform: uppercase — the label is written "Back Office" and it
   should read that way rather than being shouted. */
.back-office__label {
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--teal-700);
}

.back-office__actions {
    display: flex;
    gap: var(--s2);
    flex-wrap: wrap;
    margin-left: auto;
}

/* ---- the concept's card, tabbed ----------------------------------------
   The tabs ARE the top of the card, like the tabs on a folder: the active one
   is the card's coloured header and the others sit beside it, waiting. Inside
   the card, under a header of their own, they read as a control that happened
   to be filed in there; up here they read as which folder you are looking at.

   With one concept there is nothing to switch, so the single tab fills the bar
   and is drawn as a plain header — the same shape as the band above it. */
.tab-head {
    display: flex;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    /* No background of its own. It used to be a grey bar with the tabs painted
       on it, which meant the ROW had square outer corners however the tabs
       were rounded — a box with tab-shaped divisions rather than tabs. Each
       tab now carries its own colour and its own rounded top, and the strip is
       just what holds them in a line. */
    background: transparent;
}
.tab-head::-webkit-scrollbar { display: none; }

/* grow: 1 so the tabs share the whole bar between them — a strip that stopped
   short left a stub of grey on the right that looked like a rendering fault
   rather than a design.

   shrink: 0 so they never squeeze below their own text. With enough concepts
   to overflow, the bar scrolls sideways instead of cutting names in half,
   which is what the overflow-x on .tab-head is for. */
.tab-head__tab {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--s2);
    flex: 1 0 auto;
    padding: var(--s3) var(--s4);
    /* The floor the food cost strip used to set for itself, kept when it
       moved onto these tabs. Padding alone clears it on most labels; this
       guarantees it on all of them. */
    min-height: 44px;
    border: 0;
    /* Every tab is rounded at the top, chosen or not — that shape IS what
       makes it a tab. Only the active one used to have it, so the others read
       as rectangles that happened to sit in a row beside a tab. */
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    background: none;
    color: var(--gray-600);
    font: inherit;
    font-size: 0.9375rem;
    font-weight: 600;
    line-height: 1.3;
    cursor: pointer;
    white-space: nowrap;
}

/* Everything not chosen sits UNDER the folder in front of it: darker, dimmer,
   and with a shadow pressed into its top edge as though the active tab were
   lying over it. Telling them apart by colour alone was too quiet — this is
   depth, which the eye reads before it reads anything else. */
.tab-head__tab:not(.is-active) {
    background: var(--gray-300);
    color: var(--gray-600);
    box-shadow: inset 0 4px 6px -4px rgba(21, 49, 67, 0.45);
    /* Slightly flatter than the active one. A folder behind another shows less
       of its own corner, and the difference is one more quiet signal of which
       is in front. */
    border-radius: var(--radius) var(--radius) 0 0;
}

/* And the chosen one sits on top: full colour, both top corners rounded like
   the tab on a folder, and a shadow of its own thrown sideways onto the tabs
   beside it. z-index so that shadow lands ON them rather than under them,
   which is what makes the order read.

   Both corners, not just the outer one — a tab rounded on one side only reads
   as a rendering fault when it is not the first in the row. */
.tab-head__tab.is-active {
    position: relative;
    z-index: 1;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    background: var(--brand-header, var(--navy));
    color: var(--brand-header-text, var(--white));
    box-shadow: 0 0 10px rgba(21, 49, 67, 0.35);
}

/* Nothing to switch to: it is a header, not a choice — so it reads left,
   like the title on the band above it, rather than centred like a tab. */
.tab-head__tab--only {
    justify-content: flex-start;
    cursor: default;
}

/* The concept's own mark, on the left of its name. On a white plate, because
   an uploaded logo may be dark ink and would vanish on a dark tab. */
.tab-head__logo {
    width: 24px;
    height: 24px;
    flex: 0 0 auto;
    object-fit: contain;
    background: var(--white);
    border-radius: 6px;
    padding: 2px;
}

/* How many stores are in the folder you have open. A quiet first line in the
   body rather than a second line in the tab, which would leave the tabs
   different heights depending on which was active. */
.brand-shell__count {
    margin: 0 0 var(--s3);
    font-size: 0.8125rem;
    color: var(--text-muted);
}

/* ---- a store, inside its concept's box --------------------------------
   Still a card with its buttons on it. Those buttons are the reason anybody
   opens this app, and hiding them behind a menu to make the page tidier made
   it worse. Only the frame changed: lighter, since a shadowed card inside a
   shadowed card reads as two boxes fighting. */
.store-card {
    border: 1px solid var(--gray-200);
    border-radius: var(--radius);
    box-shadow: none;
}

.store-card .card__body { padding: var(--s3) var(--s4) var(--s4); }

/* ---- people and paperwork ---------------------------------------------- */
.hr-chips {
    display: flex;
    gap: var(--s2);
    flex-wrap: wrap;
    margin-bottom: var(--s3);
}

/* A count and a label, tappable straight through to the screen that explains
   it. The number leads because it is the part worth reading at a glance. */
.hr-chip {
    display: flex;
    align-items: baseline;
    gap: var(--s2);
    padding: var(--s2) var(--s3);
    border: 0;
    border-radius: 999px;
    background: var(--gray-100);
    font: inherit;
    text-align: left;
    cursor: pointer;
}

.hr-chip__count { font-size: 1.0625rem; font-weight: 700; }
.hr-chip__label { font-size: 0.8125rem; color: var(--gray-600); }

.hr-chip--warn { background: var(--warning-bg); }
.hr-chip--warn .hr-chip__count { color: var(--orange-700); }

.hr-chip--alert { background: var(--danger-bg); }
.hr-chip--alert .hr-chip__count { color: var(--danger); }

/* One line that scrolls, not four that wrap. Six buttons wrapping took about
   a quarter of a phone screen before you reached the first store, which is a
   lot of furniture in front of the thing people came for.

   The counts above them are the part worth seeing without scrolling, and they
   still wrap — a number nobody sees is the whole reason this band exists. */
.hr-band__actions {
    display: flex;
    gap: var(--s2);
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    /* Room for the shadow of a focus ring, and so the last button does not
       look clipped against the card's edge. */
    padding-bottom: 2px;
    margin-right: calc(var(--s4) * -1);
    padding-right: var(--s4);
}

/* The tiny pencil that opens the band's edit mode. It lives in the card's
   TITLE BAR — always on screen — because at the end of the button row it
   was unreachable on a desktop, where the row hides its scrollbar and a
   mouse cannot drag it sideways. Furniture about furniture: it must never
   read louder than the buttons it arranges, hence dimmed header-white. */
.hr-band__edit {
    border: none;
    background: none;
    color: var(--brand-header-text, var(--white));
    opacity: 0.55;
    font-size: 0.9rem;
    padding: 0 var(--s2);
    margin-left: auto;
    cursor: pointer;
    min-height: 0;
}

.hr-band__edit:hover { opacity: 1; }

/* Edit mode: the sideways-scrolling button row becomes a wrapping list. */
.hr-band__actions.is-editing {
    flex-wrap: wrap;
    overflow-x: visible;
}

/* On anything wider than a phone the row WRAPS instead of scrolling.
   Sideways scrolling is a thumb gesture; a mouse has no equivalent here
   (the scrollbar is hidden), so on desktop the buttons past the edge were
   simply unreachable. Wide screens have the height to spare. */
@media (min-width: 768px) {
    .hr-band__actions {
        flex-wrap: wrap;
        overflow-x: visible;
    }
}

.hr-band__edithint { flex-basis: 100%; margin: 0 0 var(--s1); }

.hr-band__editlist { flex-basis: 100%; }

.hr-band__editrow {
    display: flex;
    align-items: center;
    gap: var(--s1);
    padding: 2px 0;
}

.hr-band__editrow.is-off .hr-band__editlabel {
    color: var(--text-muted);
    text-decoration: line-through;
}

.hr-band__editlabel { flex: 1; min-width: 0; }

.hr-band__actions::-webkit-scrollbar { display: none; }

.hr-band__actions .btn { flex: 0 0 auto; }

/* Opens the dashboard: who you are and what you are allowed to do, before
   anything else on the page. */
.signed-in {
    margin: 0 0 var(--s4);
    color: var(--text-muted);
    font-size: 0.875rem;
}

.page-head--bare:empty { display: none; }
.page-head--bare { margin-bottom: var(--s3); }

.location-card__actions {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
    gap: var(--s2);
}

/* --------------------------------------------------------------------------
   Forms
   -------------------------------------------------------------------------- */
/* align-content: start keeps the label and input packed at the top. Without
   it, a field with a hint underneath stretches its siblings' rows and the
   inputs in a row stop lining up with each other. */
.field { display: grid; gap: var(--s1); align-content: start; }

.field__label {
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--gray-600);
}

.field__hint {
    font-size: 0.75rem;
    color: var(--text-muted);
}

/* "Bought by the case, and nobody has said how many are in one."
   Orange rather than red: the item still counts and still reorders, it just
   cannot be costed yet — which is a job to do, not a mistake to fix. */
.pack-warn {
    color: var(--orange-700);
}

.pack-warn[hidden] { display: none; }

/* A field that sits on one line in a toolbar rather than stacking. */
/* A time input has one natural size; stretching it across the screen just
   makes an empty box. Wrapping it lets the label stay full width while the
   control itself stays the size of the thing it holds. */
.time-field { max-width: 10rem; }

.field--inline { flex: 0 0 auto; }
.field--grow   { flex: 1 1 200px; min-width: 0; }

/* ----------------------------------------------------------------------
   Money fields

   The dollar sign lives inside the box, so it is still on screen while the
   number is being typed. The box itself is a plain input — the border is
   drawn by the wrapper so the sign sits inside it.
   ---------------------------------------------------------------------- */
.money-input {
    position: relative;
    display: flex;
    align-items: center;
}

.money-input__sign {
    position: absolute;
    left: var(--s3);
    font-weight: 600;
    color: var(--text-muted);
    pointer-events: none;
}

/* Two classes, because this block sits above the generic .input rule and
   would otherwise lose the padding on source order — which puts the $ on top
   of the first digit. */
.input.input--money { padding-left: 1.6rem; font-variant-numeric: tabular-nums; }

/* Read-only, not disabled: still readable, still selectable to copy, but
   plainly not something you type into. */
.input.input--readonly {
    background: var(--gray-50);
    color: var(--text-muted);
    cursor: default;
}
.input.input--readonly:focus { outline: none; border-color: var(--border-strong); box-shadow: none; }

.input, .select, .textarea {
    width: 100%;
    min-height: 44px;
    padding: var(--s2) var(--s3);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    background: var(--white);
    color: var(--text);
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.input:focus, .select:focus, .textarea:focus {
    outline: none;
    border-color: var(--teal);
    box-shadow: 0 0 0 3px rgba(84, 174, 166, 0.2);
}

.textarea {
    min-height: 6rem;
    resize: vertical;
    font-family: var(--font-mono);
    /* 16px, not the 14px this used to be. Below 16 iOS zooms the page when the
       field takes focus — the count note was the one control in the app that
       still did it — and with zooming now switched off there was no pinching
       back out afterwards. Monospace at 14 was small on a phone anyway. */
    font-size: 1rem;
    line-height: 1.6;
}

/* ---------------------------------------------------------------------------
   Search box with a clear cross

   The installed iOS app draws no native clear button for type="search", so a
   search you have finished with can only be undone one backspace at a time.
   The cross below is ours, hidden until there is something to clear.
   --------------------------------------------------------------------------- */
.search-field { position: relative; }

/* Room for the cross, so a long term does not run underneath it. */
.search-field__input { padding-right: 2.75rem; }

/* Desktop Safari and Chrome DO draw their own; two crosses side by side reads
   as a bug, and only one of them is ours to position. */
.search-field__input::-webkit-search-cancel-button { display: none; }

.search-field__clear {
    position: absolute;
    top: 50%;
    right: var(--s2);
    transform: translateY(-50%);
    display: flex;
    align-items: center;
    justify-content: center;
    /* 32px of tappable area around a small glyph: comfortably hit with a
       thumb without making the cross itself look heavy. */
    width: 32px;
    height: 32px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: var(--surface-2, rgba(0, 0, 0, 0.06));
    color: var(--text-muted);
    font-size: 1.125rem;
    line-height: 1;
    cursor: pointer;
}

.search-field__clear:hover { color: var(--text); }

.search-field__clear[hidden] { display: none; }

.input--error, .input.is-invalid {
    border-color: var(--danger);
    background: var(--danger-bg);
}

.select {
    appearance: none;
    background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%236b7680' d='M1.4 0 6 4.6 10.6 0 12 1.4 6 7.4 0 1.4z'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right var(--s3) center;
    padding-right: var(--s6);
}

.checkbox {
    display: inline-flex;
    align-items: center;
    gap: var(--s2);
    cursor: pointer;
    font-size: 0.9375rem;
}
.checkbox input {
    width: 20px;
    height: 20px;
    accent-color: var(--teal-600);
    cursor: pointer;
}

/* --------------------------------------------------------------------------
   Messages
   -------------------------------------------------------------------------- */
.alert {
    padding: var(--s3) var(--s4);
    border-radius: var(--radius-sm);
    border: 1px solid transparent;
    font-size: 0.9375rem;
}
.alert--error   { background: var(--danger-bg);  border-color: #f3c6c1; color: #8c2018; }
.alert--success { background: var(--success-bg); border-color: #b9e0ce; color: #125b41; }
.alert--warning { background: var(--warning-bg); border-color: #f6d3ae; color: #8a4308; }
.alert--info    { background: #eaf3f7;           border-color: #c3dbe6; color: var(--navy); }
/* "This is not yours to change" — a statement of fact, not a problem, so it
   is deliberately the quietest of the five. */
.alert--muted   { background: var(--surface);     border-color: var(--border); color: var(--text-muted); }

.empty-state {
    padding: var(--s7) var(--s4);
    text-align: center;
    color: var(--text-muted);
}
.empty-state__title {
    font-size: 1rem;
    font-weight: 600;
    color: var(--text);
    margin-bottom: var(--s2);
}

/* --------------------------------------------------------------------------
   Tables
   -------------------------------------------------------------------------- */
/* Wide tables scroll inside their own container so the page body never does. */
.table-scroll {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--surface);
}

.table { font-size: 0.9375rem; }

.table th, .table td {
    padding: var(--s3);
    text-align: left;
    border-bottom: 1px solid var(--border);
    white-space: nowrap;
}

.table thead th {
    position: sticky;
    top: 0;
    background: var(--gray-100);
    font-size: 0.75rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--gray-600);
    white-space: normal;
    vertical-align: bottom;
}

.table tbody tr:last-child td { border-bottom: none; }
.table tbody tr:hover { background: var(--gray-50); }

.table td.numeric, .table th.numeric { text-align: right; font-variant-numeric: tabular-nums; }

/* First column stays put while the rest of a wide history table scrolls. */
.table--pinned th:first-child,
.table--pinned td:first-child {
    position: sticky;
    left: 0;
    background: var(--surface);
    border-right: 1px solid var(--border);
    white-space: normal;
    min-width: 9rem;
}
.table--pinned thead th:first-child { background: var(--gray-100); z-index: 2; }
.table--pinned tbody tr:hover td:first-child { background: var(--gray-50); }

.cell-empty { color: var(--gray-400); }

/* --------------------------------------------------------------------------
   Badges
   -------------------------------------------------------------------------- */
.badge {
    display: inline-block;
    padding: 2px var(--s2);
    border-radius: 999px;
    font-size: 0.6875rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
.badge--muted   { background: var(--gray-100); color: var(--gray-600); }
.badge--warning { background: var(--warning-bg); color: #8a4308; }
.badge--case    { background: #eaf3f7; color: var(--navy-700); }

/* --------------------------------------------------------------------------
   Modal
   -------------------------------------------------------------------------- */
.modal {
    position: fixed;
    inset: 0;
    z-index: 100;
    display: flex;
    align-items: flex-end;
    justify-content: center;
    background: rgba(21, 49, 67, 0.55);
    padding: 0;
}

/* On a phone the panel takes the WHOLE screen. Sized by its content it stopped
   short of top and bottom, and the band that showed through read as dead
   space; installed on a Home Screen that band is viewing room the list needs.
   The header absorbs the notch inset so the title clears the clock.

   The breakpoint is 39.99rem so it ENDS exactly where the centred-modal rule
   below starts at 40rem. They used to overlap between 40 and 44rem, where the
   phone rule won on specificity and a tablet got a full-bleed modal it was
   never meant to have. */
@media (max-width: 39.99rem) {
    /* The panel pins ITSELF to the four edges of the viewport.
       Not 100dvh (a moving target on iOS), not 100% of a flex parent (which
       depends on the parent's alignment resolving the way you expect) — a
       fixed element with inset:0 is measured against the viewport directly,
       with nothing in between to get it wrong. */
    .modal:not(.modal--narrow):not(.modal--photo) .modal__panel {
        position: fixed;
        inset: 0;
        width: auto;
        max-width: none;
        height: auto;
        max-height: none;
        border-radius: 0;
    }

    .modal:not(.modal--narrow):not(.modal--photo) .modal__header {
        padding-top: max(var(--s3), env(safe-area-inset-top, 0px));
    }
}
.modal[hidden] { display: none; }

.modal__panel {
    display: flex;
    flex-direction: column;
    width: 100%;
    max-width: 56rem;
    /* Never taller than the screen minus the notch, or the modal header ends
       up behind the status bar the same way the app header used to. */
    max-height: calc(100dvh - env(safe-area-inset-top, 0px));
    background: var(--surface);
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    box-shadow: var(--shadow-lg);
    overflow: hidden;
}

.modal__header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
    padding: var(--s4);
    border-bottom: 1px solid var(--border);
    background: var(--surface);
}

.modal__title { font-size: 1.0625rem; }
.modal__subtitle { font-size: 0.8125rem; color: var(--text-muted); margin-top: 2px; }

/* "Settings › Checklists › New checklist". Sits above the title because it
   answers "where am I", which you read before you read what the screen is. */
/* Now that every screen has one, a trail can run four deep — Home > Settings
   > Documents > Templates. It scrolls sideways rather than wrapping: a wrapped
   trail changes the header's height as you navigate, which makes the whole
   panel jump. */
.modal__trail {
    font-size: 0.75rem;
    color: var(--text-muted);
    margin-bottom: 3px;
    letter-spacing: 0.01em;
    display: flex;
    align-items: baseline;
    white-space: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
}
.modal__trail::-webkit-scrollbar { display: none; }

/* The current screen never scrolls out of sight: it is the part that answers
   "where am I". */
.modal__trail > span { flex: 0 0 auto; }
.modal__trail__sep { opacity: 0.5; padding: 0 4px; }
.modal__trail__here { color: var(--text); font-weight: 600; }

/* "I have completed these tasks…" — deliberately not styled like the task
   rows above it, because it is a different kind of statement. */
.confirm-box {
    display: flex;
    gap: 10px;
    align-items: flex-start;
    margin-top: 16px;
    padding: 12px 14px;
    border: 1px solid #f6d3ae;
    background: var(--warning-bg);
    border-radius: 10px;
    cursor: pointer;
}
.confirm-box input { margin-top: 2px; flex: none; width: 20px; height: 20px; }
.confirm-box__text { font-size: 0.875rem; line-height: 1.4; }
.confirm-box[hidden] { display: none; }

/* ---------------------------------------------------------------------------
   People (HR)
   --------------------------------------------------------------------------- */

/* Two columns on anything wider than a phone, one column on a phone. An
   employee record is mostly short fields — city, state, ZIP — and stacking
   them all makes a form nobody scrolls to the bottom of. */
.hr-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--s3);
    align-content: start;
}
.hr-grid__wide { grid-column: 1 / -1; }

@media (max-width: 560px) {
    /* minmax(0, 1fr), not 1fr. Plain `1fr` is minmax(AUTO, 1fr), and an auto
       minimum lets the track grow to its widest item — so one control with a
       large intrinsic width drags the whole form out past the panel. The
       two-column rule above already got this right; the phone one did not. */
    .hr-grid { grid-template-columns: minmax(0, 1fr); }
}

/* A bordered block for a self-contained control that is not part of the form
   flow — the account-status panel, the add-a-document form. */
.panel {
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: var(--s3);
    background: var(--gray-50);
}
.panel[hidden] { display: none; }

/* Active / HR only / disabled, and "needs details". Small enough to sit
   inline after a name without pushing the row taller. */
.status-pill {
    display: inline-block;
    margin-left: 8px;
    padding: 1px 8px;
    border-radius: 999px;
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    vertical-align: middle;
}
.status-pill--1 { background: var(--warning-bg); color: #8a4308; }
.status-pill--2 { background: var(--danger-bg); color: var(--danger); }
.status-pill--todo { background: var(--gray-100); color: var(--text-muted); }
.status-pill--ok { background: var(--success-bg); color: var(--success); }

/* An expired permit is not the same kind of news as one expiring soon, so the
   whole row says so rather than just the pill. */
.doc-row--alarm { background: var(--danger-bg); }

/* The colour pickers on Company settings, and the live preview beside them. */
.input--colour { padding: 4px; height: 44px; cursor: pointer; }

/* ---------------------------------------------------------------------------
   The company's colours on the app itself

   app.php writes --brand-header and --brand-panel from the saved settings, so
   the header and the accents follow the same two colours the documents wear.
   Every rule falls back to the app's own navy, which is what a company that
   has never opened Company settings sees.
   --------------------------------------------------------------------------- */
.app-header {
    background: var(--brand-header, var(--navy));
    color: var(--brand-header-text, var(--white));
}

/* No white plate. An uploaded logo now keeps its transparency (LogoService
   stores a PNG for the screen alongside the flattened JPEG the PDF needs), so
   a clear background shows the header colour through it rather than a white
   block sitting on the bar.

   The trade-off, said out loud: a logo drawn in DARK ink on transparency will
   be hard to read against a dark header colour. The fix for that is a lighter
   header colour or a light version of the mark — not a plate behind every
   logo, which is what made the common case look broken. */
.app-header__logo { object-fit: contain; }

/* The white plate is GONE, deliberately. It existed for one reason: the
   bundled fallback used to be the restaurant's taco logo, drawn in dark ink,
   which all but vanished on a dark header. The bundled mark is now the app's
   own clipboard in WHITE ink on transparency (assets/images/mark.svg), so it
   sits straight on the bar — and an uploaded logo never had a plate either. */

/* The page-level accents that used to be hard-coded navy. */
.page-head h1 { color: var(--brand-header, var(--navy)); }
.card__header h2 { color: var(--brand-header, var(--navy)); }

/* Card headers wear the panel colour — the same tint the fact panels on a
   generated document use, so the screen and the printed page look like they
   came from the same business. */
.card__header {
    background: var(--brand-panel, transparent);
    border-radius: var(--radius) var(--radius) 0 0;
}

/* ---- telling one store from another ------------------------------------
   A stripe down the left of each store card, in a colour picked from the
   store itself. NOT the brand's colour: the concept's tab is directly above
   these cards and already says whose they are. What the eye needs here is the
   difference between SCB - 1 and SCB - 2, which are otherwise two identical
   white rectangles with near-identical names.

   The colour is chosen from the location id, so a store keeps the same one
   every time anybody opens the app — a stripe that moved would be worse than
   no stripe. */
.store-card { border-left: 4px solid var(--store-stripe, var(--gray-300)); }


/* The stripe and the headings sit on white, so they stay legible whatever
   colour a company picks — only the header BAR needs the contrast switch,
   which is what --brand-header-text is for. */
#locations .location-card__name { color: var(--brand-header, var(--navy)); }

.brand-preview {
    border: 1px solid var(--border);
    border-radius: 10px;
    overflow: hidden;
}
.brand-preview__bar {
    padding: 6px var(--s3);
    font-size: 0.8125rem;
    font-weight: 700;
    letter-spacing: 0.05em;
    text-transform: uppercase;
}
.brand-preview__panel {
    display: grid;
    grid-template-columns: minmax(0, auto) minmax(0, 1fr);
    gap: 4px var(--s4);
    padding: var(--s3);
}
.brand-preview__label { font-weight: 700; }

/* The preview sits on white deliberately: that is the LETTERHEAD's background,
   and this is where somebody checks the mark will print correctly. A pale logo
   that looks fine on a dark header would come out as a grey smudge on paper,
   and this is the screen that says so. */
.logo-preview {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--s3);
    background: #ffffff;
    border: 1px solid var(--border);
    border-radius: 10px;
}
.logo-preview__img {
    max-width: 100%;
    max-height: 110px;
    width: auto;
    height: auto;
}

/* ---- documents ---------------------------------------------------------- */
.doc-list {
    display: flex;
    flex-direction: column;
    border: 1px solid var(--border);
    border-radius: 10px;
    overflow: hidden;
}
.doc-list:empty { display: none; }

.doc-row {
    display: flex;
    align-items: center;
    gap: var(--s3);
    padding: var(--s3);
    border-bottom: 1px solid var(--border);
    /* Wrap rather than squeeze. A signed row carries View AND Download now,
       and on a phone those two took so much of the width that the title broke
       mid-word — "Employee handboo / k" — and the row grew to half the screen.
       Wrapping drops the buttons onto their own line instead, which costs one
       line and keeps the title a title. */
    flex-wrap: wrap;
}

/* The text takes the whole first line whenever the row has more than one
   action beside it. overflow-wrap: anywhere further down is what saves a long
   unbroken word from overflowing; it should never be what handles an ordinary
   title, and giving the text room is how it stops being asked to. */
.doc-row--actions .doc-row__open { flex: 1 1 100%; }
.doc-row--actions .doc-row__icon { order: -1; }
.doc-row:last-child { border-bottom: 0; }

/* PDF / IMG, so you can tell at a glance what tapping will open. */
.doc-row__icon {
    flex: none;
    width: 38px;
    text-align: center;
    padding: 3px 0;
    border-radius: 6px;
    background: var(--gray-100);
    color: var(--text-muted);
    font-size: 0.625rem;
    font-weight: 700;
    letter-spacing: 0.04em;
}

/* The whole row is the download link — a small "download" word beside a long
   title is a target nobody hits on a phone. */
.doc-row__open {
    display: grid;
    gap: 2px;
    min-width: 0;
    flex: 1;
    color: inherit;
    text-decoration: none;
}
.doc-row__open:hover .doc-row__title { text-decoration: underline; }

.doc-row__title  { font-weight: 600; }
.doc-row__meta   { font-size: 0.75rem; color: var(--text-muted); }
.doc-row__note   { font-size: 0.8125rem; color: var(--text-muted); font-style: italic; }
.doc-row__hidden {
    font-size: 0.6875rem;
    font-weight: 600;
    color: #8a4308;
}

/* ---------------------------------------------------------------------------
   Long unbroken text

   Text only wraps at spaces. A filename like
   Ana_Reyes_Verbal_Warning_2026_07_14_FINAL.pdf, or an email address, or a
   pasted URL has none — so it sets a minimum width for its box, which sets one
   for its parent, all the way up until the modal panel is wider than the
   phone and the whole screen slides sideways.

   Measured: one such filename made My account 212px wider than an iPhone.

   `anywhere` rather than `break-word` because it also shrinks the element's
   min-content width, which is the part that stops the chain — break-word
   wraps the text but leaves the box just as wide.
   --------------------------------------------------------------------------- */
.doc-row__title,
.doc-row__meta,
.doc-row__note,
.doc-row__hidden,
.settings-row__label,
.settings-row__hint,
.run-row__when,
.run-row__who,
.run-row__note,
.field__hint,
.doc-sheet {
    overflow-wrap: anywhere;
}

/* A flex or grid item defaults to min-width: auto, which means "never shrink
   below your content". Every container that holds text somebody typed has to
   opt out of that, or the rule above has nothing to shrink into. */
.doc-row > *,
.doc-row__open > *,
.settings-row > *,
.settings-row__text > *,
.hr-grid > *,
.field > * {
    min-width: 0;
}

.input, .select, .textarea { max-width: 100%; }

/* ---------------------------------------------------------------------------
   Date and time inputs

   iOS gives these an intrinsic width based on the locale's date format plus
   its native chrome, centres the value inside them, and does NOT honour
   width:100% while that native appearance is on. Inside a grid track the
   input then demands more room than the panel has and the whole form slides
   out past its edge — which is exactly what a phone screenshot showed on the
   "Due by" field.

   Turning the appearance off makes them size like every other input. The
   picker is unaffected: iOS still opens its wheel on tap, and the indicator
   below keeps the affordance visible on desktop.
   --------------------------------------------------------------------------- */
input[type="date"],
input[type="time"],
input[type="datetime-local"],
input[type="month"] {
    -webkit-appearance: none;
    appearance: none;
    width: 100%;
    /* Beats the UA minimum, which is what actually forces the overflow. */
    min-width: 0;
    max-width: 100%;
    /* Safari centres the value; everything else here is left-aligned, and a
       centred date among left-aligned boxes reads as a different control. */
    text-align: left;
}

/* The value lives in its own shadow box that keeps an intrinsic width of its
   own even after the appearance is off. */
input[type="date"]::-webkit-date-and-time-value,
input[type="time"]::-webkit-date-and-time-value {
    text-align: left;
    min-width: 0;
}

/* appearance:none hides the calendar button in desktop Chrome; put it back so
   the field still looks like something you tap. */
input[type="date"]::-webkit-calendar-picker-indicator,
input[type="time"]::-webkit-calendar-picker-indicator {
    opacity: 0.55;
    cursor: pointer;
}

/* ---- documents to sign -------------------------------------------------- */

/* ---------------------------------------------------------------------------
   A document, on screen

   Laid out to match the PDF: letterhead, title, a block of facts with a label
   column, section headings, hanging-indent list items. The blocks come from
   the server's parser, so the two cannot drift apart — what somebody reads
   before signing is what the file copy says.
   --------------------------------------------------------------------------- */
.doc-sheet {
    padding: var(--s5) var(--s4);
    border: 1px solid var(--border);
    border-radius: 10px;
    background: var(--surface);
    max-height: 58vh;
    overflow-y: auto;
    line-height: 1.55;
}

/* The manual, which is only ever read — nothing sits under it waiting to be
   agreed to, so there is no reason to keep the bottom of the screen clear.
   Without this the help pages were a half-height box of text with a band of
   empty white beneath them. The modal body scrolls instead of the sheet. */
.doc-sheet--full {
    max-height: none;
    overflow-y: visible;
}

.doc-sheet__head {
    padding-bottom: var(--s3);
    margin-bottom: var(--s4);
    border-bottom: 1px solid var(--border-strong);
}
.doc-sheet__company {
    display: block;
    font-weight: 700;
    font-size: 0.8125rem;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-muted);
}
.doc-sheet__title { margin-top: 4px; font-size: 1.25rem; line-height: 1.25; }

/* Only used when a response arrives without blocks — an older cached copy of
   the app talking to a newer server. */
.doc-sheet__plain { white-space: pre-wrap; }

/* A filled bar, exactly as the PDF draws it. The custom properties are set
   per-sheet from the company's saved colours, and fall back to the app's own
   navy when nothing has been chosen. */
.doc-h {
    margin: var(--s5) calc(var(--s4) * -1) var(--s3);
    padding: 6px var(--s4);
    background: var(--doc-header, var(--navy));
    color: var(--doc-header-text, var(--white));
    font-size: 0.8125rem;
    font-weight: 700;
    letter-spacing: 0.05em;
    text-transform: uppercase;
}
.doc-h:first-child { margin-top: 0; }
.doc-h2 { margin: var(--s4) 0 var(--s2); font-size: 0.9375rem; }

.doc-p { margin: 0 0 var(--s3); }

/* The label column, aligned — what makes a run of facts read as a header. */
.doc-facts {
    display: grid;
    grid-template-columns: minmax(0, auto) minmax(0, 1fr);
    gap: 4px var(--s4);
    margin: 0 0 var(--s4);
}
.doc-facts {
    background: var(--doc-panel, var(--gray-100));
    padding: var(--s3) var(--s4);
    margin-left: calc(var(--s4) * -1);
    margin-right: calc(var(--s4) * -1);
}
.doc-facts dt { font-weight: 700; }
.doc-facts dd { margin: 0; }

@media (max-width: 460px) {
    /* On a narrow phone the two columns squeeze the value into two words a
       line. Stack instead, label above value. */
    .doc-facts { grid-template-columns: 1fr; gap: 0; }
    .doc-facts dt { margin-top: var(--s2); }
}

/* The marker hangs, the text aligns — the same shape as the PDF. */
.doc-item {
    display: grid;
    grid-template-columns: 1.5rem minmax(0, 1fr);
    margin-bottom: var(--s2);
}
.doc-item__marker { color: var(--text-muted); font-variant-numeric: tabular-nums; }

/* A checkbox row on a rendered document — the printed-form look, matching the
   PDF: a drawn box, an X when ticked, all options visible either way. */
.doc-check {
    display: grid;
    grid-template-columns: 1.05rem 1fr;
    gap: 0.45rem;
    align-items: baseline;
    margin: 2px 0;
}
.doc-check__box {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 0.85rem;
    height: 0.85rem;
    border: 1.5px solid #414d55;
    border-radius: 2px;
    font-size: 0.7rem;
    font-weight: 700;
    line-height: 1;
    transform: translateY(1px);
}
.doc-check--on .doc-check__box { color: #1b2b35; }

/* A full-width field inside the two-column send-form grid — a checkbox group
   squeezed into half a phone column wraps one word per line. */
.field--full { grid-column: 1 / -1; }

.doc-note {
    margin: 0 0 var(--s3) var(--s4);
    font-style: italic;
    color: var(--text-muted);
}

.doc-rule { border: 0; border-top: 1px solid var(--border); margin: var(--s4) 0; }

/* A row whose whole body is a button rather than a link — same look, because
   to the reader it is the same thing. */
.doc-row__open--button {
    background: none;
    border: 0;
    padding: 0;
    font: inherit;
    text-align: left;
    cursor: pointer;
}

.textarea--tall { min-height: 16rem; font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace); font-size: 0.875rem; }

/* Tappable {{placeholder}} tokens. Typing one from memory is how you get a
   typo nobody notices until it is in somebody's warning letter. */
.chip-row { display: flex; flex-wrap: wrap; gap: 6px; }
.chip {
    padding: 3px 9px;
    border: 1px solid var(--border-strong);
    border-radius: 999px;
    background: var(--gray-50);
    color: var(--text-muted);
    font-size: 0.75rem;
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    cursor: pointer;
}
.chip:hover { background: var(--gray-100); color: var(--text); }

.settings-row--dim { opacity: 0.55; }

/* ---------------------------------------------------------------------------
   Signatures
   --------------------------------------------------------------------------- */

/* touch-action: none is the whole reason a finger can draw here. Without it
   the browser claims the gesture as a scroll and the stroke stops dead after
   a few pixels. */
.sig-pad__canvas {
    touch-action: none;
    display: block;
    border: 1px dashed var(--border-strong);
    border-radius: 10px;
    background: var(--white);
    /* A signature is drawn edge to edge; a highlight on a stray tap looks
       like a bug. */
    -webkit-tap-highlight-color: transparent;
    cursor: crosshair;
    max-width: 100%;
}

.sig-pad[hidden] { display: none; }

/* The three script styles, shown as what they will actually look like. */
.sig-styles { display: grid; gap: var(--s2); margin-top: var(--s3); }

.sig-style {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
    padding: var(--s3);
    border: 1px solid var(--border);
    border-radius: 10px;
    background: var(--white);
    cursor: pointer;
    text-align: left;
    min-height: 62px;
}
.sig-style--on { border-color: var(--primary); box-shadow: 0 0 0 1px var(--primary) inset; }
.sig-style__name {
    font-size: 1.5rem;
    line-height: 1.1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.sig-style__label { flex: none; font-size: 0.6875rem; color: var(--text-muted); }

/* Adopted signatures, as pickable tiles. */
.sig-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: var(--s2); }

.sig-tile {
    display: grid;
    gap: 4px;
    padding: var(--s2);
    border: 1px solid var(--border);
    border-radius: 10px;
    background: var(--white);
    cursor: pointer;
}
.sig-tile--on { border-color: var(--primary); box-shadow: 0 0 0 1px var(--primary) inset; }
.sig-tile__image { width: 100%; height: 52px; object-fit: contain; }
.sig-tile__meta { font-size: 0.6875rem; color: var(--text-muted); }

/* The mark on an already-signed document. */
.sig-shown {
    display: block;
    max-width: 220px;
    height: auto;
    margin-bottom: var(--s2);
}

/* A two-button segmented control — type it / draw it. */
.seg-row { display: flex; border: 1px solid var(--border-strong); border-radius: 10px; overflow: hidden; }
.seg {
    flex: 1;
    padding: 10px var(--s3);
    border: 0;
    background: var(--white);
    color: var(--text-muted);
    font: inherit;
    font-weight: 600;
    cursor: pointer;
}
.seg--on { background: var(--primary); color: var(--white); }

/* Reorder mode: the row stops being a button and grows a pair of arrows.
   Arrows rather than drag-and-drop, because dragging inside a scrolling modal
   on a phone fights the scroll and loses. */
.settings-row--reorder { cursor: default; }
.reorder-arrows { display: flex; gap: 2px; flex: none; }
.reorder-arrows .btn[disabled] { opacity: 0.25; }

.app-banner--amber { background: var(--warning-bg); border-color: #f6d3ae; color: #8a4308; }

/* The optional note somebody left when they submitted a checklist. */
.run-row__note {
    font-size: 0.8125rem;
    color: var(--text-muted);
    font-style: italic;
    margin-top: 4px;
}

/* Back one level, to the left of the X. Only drawn when there is somewhere
   behind this screen to go — see backTo(). */
.modal__back {
    flex: 0 0 auto;
    display: grid;
    place-items: center;
    width: 36px;
    height: 36px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: none;
    color: var(--text-muted);
    cursor: pointer;
}

.modal__back:hover { background: var(--gray-100); color: var(--text); }

/* The ? beside the close button: the manual for the screen you are on. Same
   size and weight as the back arrow, quieter colour — it is an offer, not an
   instruction, and it sits next to the control that leaves the screen. */
.modal__help {
    flex: 0 0 auto;
    display: grid;
    place-items: center;
    width: 36px;
    height: 36px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: none;
    color: var(--text-muted);
    cursor: pointer;
}

.modal__help:hover { background: var(--gray-100); color: var(--text); }
.modal__help[hidden] { display: none; }

.modal__help svg {
    width: 20px;
    height: 20px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.7;
    stroke-linecap: round;
    stroke-linejoin: round;
}

.modal__back svg {
    width: 20px;
    height: 20px;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* A crumb you can press. Looks like the text beside it until you realise it
   is a button — underlined on hover rather than styled as one, because a row
   of buttons up there would compete with the screen's own controls. */
.modal__trail__step {
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    color: inherit;
    cursor: pointer;
}

.modal__trail__step:hover { text-decoration: underline; color: var(--text); }

.modal__close {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    flex: 0 0 40px;
    border: none;
    border-radius: var(--radius-sm);
    background: transparent;
    font-size: 1.5rem;
    line-height: 1;
    color: var(--gray-500);
}
.modal__close:hover { background: var(--gray-100); color: var(--text); }

.modal__body {
    flex: 1 1 auto;
    overflow-y: auto;
    /* Up and down only. Nothing on any of these screens is meant to be read
       by scrolling sideways, so a stray wide element becomes a clipped
       element rather than a panel that slides off the phone. The rules below
       are what stop anything getting wide in the first place; this is the
       backstop for whatever they miss. */
    overflow-x: hidden;
    -webkit-overflow-scrolling: touch;
    padding: var(--s4);
}

.modal__footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
    padding: var(--s3) var(--s4);
    /* Clears the iPhone home indicator. max(), not a sum: the inset already
       includes room to be clear of it, so adding the normal padding on top
       just wasted a strip of screen. */
    padding-bottom: max(var(--s3), env(safe-area-inset-bottom, 0px));
    border-top: 1px solid var(--border);
    background: var(--gray-50);
}

@media (min-width: 40rem) {
    .modal { align-items: center; padding: var(--s5); }
    .modal__panel { border-radius: var(--radius-lg); max-height: calc(100dvh - 3rem); }
}

/* --------------------------------------------------------------------------
   Count sheet
   -------------------------------------------------------------------------- */
.count-row {
    display: grid;
    /* name | note | quantity, one line per item. The note lived on its own
       full-width second line for a day; half as many items fit on a screen,
       and counting is a walk down a list. minmax(0, ...) on the text columns
       so a long name or note truncates instead of blowing the row open.

       1.4:1 between name and note is measured, not guessed: across this
       company's real 65 item names on a 375px phone it puts 60 of them on
       one line while keeping the note box ~96px. An even split wrapped 7;
       pushing to 2:1 rescues only 2 more while crushing the note to 77px.
       The five longest names (Printer Rolls Carbonless…) wrap to a second
       line whatever the split, and truncating them instead would make
       "Tortilla Maiz A…" ambiguous between Amarilla and Blanca on the one
       screen where picking the wrong one corrupts a count. */
    grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr) 5.5rem;
    gap: var(--s3);
    align-items: center;
    padding: var(--s3) 0;
    border-bottom: 1px solid var(--border);
}
.count-row:last-of-type { border-bottom: none; }

.count-row__main { min-width: 0; }
.count-row__name { font-weight: 550; }
.count-row__type { font-size: 0.8125rem; color: var(--text-muted); }

.count-row__qty {
    text-align: center;
    font-variant-numeric: tabular-nums;
    font-size: 1.0625rem;
    font-weight: 600;
}

.count-row.is-invalid .count-row__qty {
    border-color: var(--danger);
    background: var(--danger-bg);
}

.count-progress {
    position: sticky;
    top: 0;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
    padding: var(--s2) 0 var(--s3);
    margin-bottom: var(--s2);
    background: var(--surface);
    font-size: 0.875rem;
    color: var(--text-muted);
    border-bottom: 1px solid var(--border);
}

.progress-bar {
    flex: 1 1 auto;
    height: 6px;
    border-radius: 999px;
    background: var(--gray-200);
    overflow: hidden;
}
.progress-bar__fill {
    height: 100%;
    width: 0;
    background: var(--teal-600);
    border-radius: 999px;
    transition: width 0.2s ease;
}

/* --------------------------------------------------------------------------
   Shopping list
   -------------------------------------------------------------------------- */
/* Controls above, actions below, in a panel that reads as one group.
   The dropdowns are full-width form controls, so they get their own labelled
   row rather than being flex-wrapped in among the buttons — which is what
   left the "Group by" label stranded on the line above its select. */
.shop-toolbar {
    display: grid;
    gap: var(--s3);
    padding: var(--s3);
    margin-bottom: var(--s4);
    background: var(--gray-50);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.shop-toolbar__controls {
    display: grid;
    /* Side by side when there is room, stacked on a phone. */
    grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
    gap: var(--s3);
}

.shop-toolbar__actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s2);
}

/* Generate is the primary action, so it leads and takes the room it needs;
   the rest sit beside it. */
.shop-toolbar__actions .btn { flex: 0 1 auto; }

@media (max-width: 26rem) {
    .shop-toolbar__actions .btn { flex: 1 1 auto; }
}

.shop-section { margin-bottom: var(--s5); }

.shop-section__title {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--s3);
    padding: var(--s2) var(--s3);
    background: var(--navy);
    color: var(--white);
    border-radius: var(--radius-sm) var(--radius-sm) 0 0;
    font-size: 0.9375rem;
    font-weight: 650;
}
.shop-section__subtotal { font-variant-numeric: tabular-nums; font-weight: 600; }

/* Which count the numbers below came from. Quiet, but present — a list built
   off a three-week-old count looks identical to one built this morning, and
   the difference is the whole value of the list. */
.shop-section__source {
    padding: var(--s2) var(--s3);
    background: var(--gray-50);
    border-bottom: 1px solid var(--border);
    font-size: 0.75rem;
    color: var(--text-muted);
}

/* Grouped by vendor there are no store sections to attach it to, so the same
   line sits above the list, one per store. */
.shop-source-list {
    margin-bottom: var(--s3);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    overflow: hidden;
}
.shop-source-list .shop-section__source:last-child { border-bottom: 0; }

/* Rows are deliberately compact — a full shopping trip should fit on one
   screen rather than needing constant scrolling. Padding is tight, the meta
   line sits inline with the name, and the whole row is a <label> so tapping
   anywhere on it ticks the box. */
.shop-item {
    display: grid;
    grid-template-columns: auto 1fr auto;
    gap: var(--s3);
    align-items: center;
    padding: var(--s2) var(--s3);
    border: 1px solid var(--border);
    border-top: none;
    background: var(--surface);
    cursor: pointer;
    min-height: 44px;
}
.shop-item:last-child { border-radius: 0 0 var(--radius-sm) var(--radius-sm); }
.shop-item:hover { background: var(--gray-50); }

.shop-item.is-checked { background: var(--gray-100); }
.shop-item.is-checked .shop-item__name { text-decoration: line-through; color: var(--text-muted); }
.shop-item.is-checked .shop-item__figures { opacity: 0.55; }

.shop-item__check {
    width: 22px;
    height: 22px;
    accent-color: var(--teal-600);
    flex: 0 0 auto;
}

.shop-item__main {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--s1) var(--s2);
    min-width: 0;
}

.shop-item__name { font-weight: 550; line-height: 1.3; }

/* What whoever counted wrote about this item, in full at the bottom of the
   row. flex-basis 100% pushes it onto its own line inside the wrapping
   __main block; pre-wrap keeps any line breaks the person typed. */
.shop-item__note {
    flex-basis: 100%;
    font-size: 0.8125rem;
    color: var(--text-muted);
    font-style: italic;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

.shop-item__note::before { content: '✎ '; font-style: normal; }

.shop-item__meta {
    display: inline-flex;
    align-items: center;
    gap: var(--s2);
    font-size: 0.75rem;
    color: var(--text-muted);
    white-space: nowrap;
}

.shop-item__figures {
    display: flex;
    align-items: baseline;
    gap: var(--s2);
    text-align: right;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.shop-item__qty  { font-weight: 700; font-size: 1rem; }
.shop-item__cost { font-size: 0.75rem; color: var(--text-muted); }

/* On a phone, two lines per item and no more.
 *
 *   [✓]  Item name                    ×8
 *        on hand 5/8 · Store       $112.00
 *
 * The quantity stays beside the name because it is the number you are reading
 * the list for. The unit price is dropped — it is still on screen in the
 * item's own settings, and hiding it is what makes room for the name.
 * Everything is nowrap and ellipsised, so a long name shortens rather than
 * pushing the row to a third line. */
@media screen and (max-width: 34rem) {
    .shop-item {
        grid-template-columns: auto minmax(0, 1fr) auto;
        column-gap: var(--s2);
        row-gap: 0;
        padding: var(--s1) var(--s3);
        align-items: center;
    }

    .shop-item__main {
        grid-column: 2;
        display: grid;          /* name over meta, not a wrapping flex row */
        gap: 0;
    }

    .shop-item__figures {
        grid-column: 3;
        display: grid;          /* qty over total, right-aligned */
        justify-items: end;
        gap: 0;
    }

    .shop-item__name,
    .shop-item__meta,
    .shop-item__cost {
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .shop-item__name { font-size: 0.9375rem; line-height: 1.35; }
    .shop-item__meta { font-size: 0.6875rem; line-height: 1.35; display: block; }
    .shop-item__qty  { font-size: 0.9375rem; line-height: 1.35; }
    .shop-item__cost { font-size: 0.6875rem; line-height: 1.35; }

    /* Both the unit price and the line total stay: the unit price is how you
       spot that something has gone up since last week, which is worth more
       while shopping than the extra width it costs. */
    .shop-item__cost { font-variant-numeric: tabular-nums; }

    /* Badges shrink to fit the meta line rather than claiming their own. */
    .shop-item__meta .badge {
        padding: 0 4px;
        font-size: 0.625rem;
        vertical-align: baseline;
    }

    /* Tighter section headings too — six of them on a list of thirty items
       was costing as much room as the items. */
    .shop-section { margin-bottom: var(--s3); }
    .shop-section__title { padding: var(--s2) var(--s3); font-size: 0.875rem; }
}

/* Narrow phones: stack the price under the quantity rather than letting the
   name get squeezed to two characters. */
@media (max-width: 26rem) {
    .shop-item__figures { flex-direction: column; align-items: flex-end; gap: 0; }
}

.shop-progress {
    position: sticky;
    top: 0;
    z-index: 1;
    padding: var(--s2) 0;
    margin-bottom: var(--s3);
    background: var(--surface);
    border-bottom: 1px solid var(--border);
}

.shop-totals {
    display: grid;
    gap: var(--s2);
    padding: var(--s4);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}
.shop-totals__row {
    display: flex;
    justify-content: space-between;
    gap: var(--s3);
    font-variant-numeric: tabular-nums;
}
.shop-totals__row--grand {
    padding-top: var(--s2);
    border-top: 2px solid var(--navy);
    font-size: 1.125rem;
    font-weight: 700;
}
.shop-totals__row--card { color: var(--orange-700); font-weight: 600; }

/* --------------------------------------------------------------------------
   Notes

   A note someone left while counting is the one piece of free text in the
   whole system, so it gets the orange accent to pull the eye. It is only ever
   rendered when there is something to show — the server sends an empty string
   otherwise.
   -------------------------------------------------------------------------- */
.note-card {
    padding: var(--s3);
    background: var(--warning-bg);
    border: 1px solid #f6d3ae;
    border-left: 4px solid var(--orange);
    border-radius: var(--radius-sm);
}

.note-card--attached {
    border-radius: 0;
    border-top: none;
    border-right-color: var(--border);
    border-bottom-color: var(--border);
}

.note-card__label {
    font-size: 0.6875rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--orange-700);
    margin-bottom: 2px;
}

.note-card__body {
    color: #6b3708;
    white-space: pre-wrap;
    word-break: break-word;
}

/* --------------------------------------------------------------------------
   Item photos
   -------------------------------------------------------------------------- */
/* The little camera next to an item that has a picture. Sized as a proper tap
   target even though it sits inline with the item name. */
.photo-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    margin-left: var(--s2);
    padding: 0;
    vertical-align: middle;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    background: var(--white);
    font-size: 0.9375rem;
    line-height: 1;
}
.photo-btn:hover { background: var(--teal); border-color: var(--teal); }
.photo-btn:active { transform: scale(0.94); }

/* The frame is a FIXED shape — 4:3, full width — and every photo is centred
   inside it untouched. Sizing the frame from the image meant the viewer
   jumped taller and shorter as you flicked between portrait and landscape
   shots, and the arrows underneath moved with it. */
.photo-viewer {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    aspect-ratio: 4 / 3;
    /* Never taller than the screen minus the header and nav — on a short
       phone the fixed shape gives way rather than pushing the arrows off. */
    max-height: calc(100dvh - 10rem);
    padding: 0;
    background: var(--navy);
    overflow: hidden;
}

.photo-viewer__image {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: contain;
}

/* Tapped once: the photo owns the whole screen. The frame's fixed 4:3 gives
   way to "all the room there is", the nav stays reachable at the bottom, and
   the image stays centred and untouched — bigger, never cropped. */
.modal--photo.is-full .modal__panel {
    max-width: none;
    width: 100%;
    height: 100dvh;
    max-height: 100dvh;
    border-radius: 0;
}

.modal--photo.is-full .photo-viewer {
    aspect-ratio: auto;
    flex: 1;
    max-height: none;
    min-height: 0;
}

.modal--photo.is-full .photo-viewer__image { cursor: zoom-out; }
.photo-viewer__image { cursor: zoom-in; }

.photo-viewer__state {
    margin: 0;
    padding: var(--s7) var(--s4);
    color: rgba(255, 255, 255, 0.75);
    font-size: 0.9375rem;
}

.modal--photo .modal__panel { max-width: 44rem; background: var(--navy); }
.modal--photo .modal__header { background: var(--navy); border-bottom-color: rgba(255, 255, 255, 0.15); }
.modal--photo .modal__title { color: var(--white); }
.modal--photo .modal__close { color: rgba(255, 255, 255, 0.7); }
.modal--photo .modal__close:hover { background: rgba(255, 255, 255, 0.12); color: var(--white); }

/* Gallery navigation under the photo */
.photo-viewer__nav {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
    padding: var(--s2) var(--s3);
    padding-bottom: calc(var(--s2) + env(safe-area-inset-bottom, 0px));
    background: var(--navy);
    border-top: 1px solid rgba(255, 255, 255, 0.15);
}

.photo-viewer__arrow {
    width: 44px;
    height: 44px;
    border: 1px solid rgba(255, 255, 255, 0.25);
    border-radius: var(--radius-sm);
    background: rgba(255, 255, 255, 0.1);
    color: var(--white);
    font-size: 1.5rem;
    line-height: 1;
}
.photo-viewer__arrow:hover { background: rgba(255, 255, 255, 0.2); }

.photo-viewer__count {
    color: rgba(255, 255, 255, 0.8);
    font-size: 0.875rem;
    font-variant-numeric: tabular-nums;
}

/* A small count on the camera button when there is more than one to see */
.photo-btn__count {
    position: absolute;
    top: -5px;
    right: -5px;
    min-width: 16px;
    height: 16px;
    padding: 0 3px;
    border-radius: 999px;
    background: var(--orange);
    color: var(--white);
    font-size: 0.625rem;
    font-weight: 700;
    line-height: 16px;
    text-align: center;
}
.photo-btn { position: relative; }

/* Thumbnails in the item editor */
.photo-strip {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s3);
}

.photo-thumb {
    position: relative;
    width: 7rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    overflow: hidden;
    background: var(--gray-100);
}
.photo-thumb.is-first { border-color: var(--teal-600); border-width: 2px; }

.photo-thumb__img {
    display: block;
    width: 100%;
    height: 5.5rem;
    /* contain, not cover: a tall bottle cropped to its middle slice is not
       recognisable, and recognising the item is the entire point. The frame
       stays the same size either way; the grey shows through the gaps. */
    object-fit: contain;
    background: var(--gray-100);
}

.photo-thumb__badge {
    position: absolute;
    top: var(--s1);
    left: var(--s1);
    padding: 1px var(--s1);
    border-radius: var(--radius-sm);
    background: var(--teal-600);
    color: var(--white);
    font-size: 0.5625rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.photo-thumb__actions {
    display: flex;
    border-top: 1px solid var(--border);
    background: var(--white);
}

.photo-thumb__btn {
    flex: 1 1 0;
    min-height: 32px;
    border: none;
    border-right: 1px solid var(--border);
    background: none;
    font-size: 0.875rem;
    color: var(--gray-600);
}
.photo-thumb__btn:last-child { border-right: none; }
.photo-thumb__btn:hover:not(:disabled) { background: var(--gray-100); }
.photo-thumb__btn:disabled { opacity: 0.3; }
.photo-thumb__btn--danger { color: var(--danger); }

/* Upload controls in the items editor */
.photo-controls {
    display: grid;
    gap: var(--s1);
    flex: 1 1 100%;
}

.photo-controls__row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s2);
    align-items: center;
}

.photo-status {
    font-size: 0.75rem;
    color: var(--text-muted);
}
.photo-status--set { color: var(--success); font-weight: 600; }

/* --------------------------------------------------------------------------
   Vendors
   -------------------------------------------------------------------------- */
/* Contact details under a shopping-list section header, so someone can ring
   ahead without leaving the list. */
.shop-section__vendor {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s1) var(--s3);
    align-items: baseline;
    padding: var(--s2) var(--s3);
    background: var(--navy-700);
    color: rgba(255, 255, 255, 0.75);
    font-size: 0.8125rem;
}
.shop-section__vendor .muted { color: rgba(255, 255, 255, 0.6); }

.shop-section__phone {
    color: var(--teal);
    font-weight: 600;
    text-decoration: none;
    white-space: nowrap;
}
.shop-section__phone:hover { text-decoration: underline; }

.vendor-picker { display: grid; gap: var(--s2); }

.vendor-row {
    display: flex;
    align-items: center;
    gap: var(--s2);
    padding: var(--s2);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--white);
}

.vendor-row__name {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-width: 0;
    font-size: 0.9375rem;
}

/* --------------------------------------------------------------------------
   Food cost
   -------------------------------------------------------------------------- */
/* The food cost strip is .tab-head now, like every other. Its old underline
   rules lived here; the one thing worth carrying over was the 44px tap floor,
   which is applied to .tab-head__tab globally below. */

.fc-list {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--surface);
}

/* A tappable row: name on the left, figures on the right, one line each.
 *
 * The column widths are FIXED rather than auto. Each row is its own grid, so
 * with auto columns every row sized itself differently and no header could
 * line up with them. Same template on .fc-head — change one, change both. */
.fc-row,
.fc-head {
    grid-template-columns: minmax(0, 1fr) 7rem 5rem 4.25rem;
}

.fc-row {
    display: grid;
    gap: var(--s2);
    align-items: baseline;
    width: 100%;
    padding: var(--s3);
    border: none;
    border-bottom: 1px solid var(--border);
    background: none;
    text-align: left;
    font-size: 0.9375rem;
}

/* Everything after the name is a figure, so it reads right-aligned under its
   heading. Anything too long is clipped rather than pushing the grid open. */
.fc-row > * + * {
    text-align: right;
    justify-self: end;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 100%;
}
.fc-row:last-child { border-bottom: none; }
.fc-row:hover { background: var(--gray-50); }
.fc-row--error { background: var(--warning-bg); }

.fc-row__name   { font-weight: 550; min-width: 0; }
.fc-row__meta   { font-size: 0.8125rem; color: var(--text-muted); white-space: nowrap; }
.fc-row__figure { font-variant-numeric: tabular-nums; font-weight: 600; white-space: nowrap; }

/* Food cost percentage, coloured against the usual kitchen thresholds. */
.fc-pct {
    font-variant-numeric: tabular-nums;
    font-weight: 700;
    padding: 2px var(--s2);
    border-radius: var(--radius-sm);
    white-space: nowrap;
}
.fc-pct--good { background: var(--success-bg); color: var(--success); }
.fc-pct--ok   { background: var(--warning-bg); color: var(--orange-700); }
.fc-pct--bad  { background: var(--danger-bg);  color: var(--danger); }

/* Column titles. Shares .fc-row's template (declared with it above) so the
   headings sit exactly over the figures they name. */
.fc-head {
    display: grid;
    gap: var(--s2);
    padding: var(--s2) var(--s3);
    font-size: 0.75rem;
    font-weight: 650;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    color: var(--text-muted);
}
.fc-head__cell {
    text-align: right;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.fc-head__cell--name { text-align: left; }


/* Search on the left, sort on the right; stacked on a phone. */
.fc-toolbar {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s3);
    align-items: flex-end;
    margin-bottom: var(--s3);
}

/* The three price points side by side. Scrolls rather than squashing on a
   narrow phone — five columns of numbers do not usefully compress. */
.fc-analysis { overflow-x: auto; }

.fc-analysis__grid {
    display: grid;
    grid-template-columns: max-content repeat(4, minmax(72px, 1fr));
    gap: var(--s2) var(--s3);
    align-items: center;
    min-width: 380px;
    padding: var(--s3);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--surface);
}

.fc-analysis__head {
    font-size: 0.6875rem;
    font-weight: 650;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    color: var(--text-muted);
    text-align: right;
}
.fc-analysis__label { font-weight: 600; white-space: nowrap; }
.fc-analysis__value { text-align: right; font-variant-numeric: tabular-nums; }

/* The percentage pill needs to stay a pill inside a right-aligned cell. */
.fc-analysis__value.fc-pct { justify-self: end; }

/* On a phone the five columns become one block per price point: the name on
   its own line, then the four figures two-up, each labelled by the data-label
   its cell carries. Scrolling sideways to find Profit is not a design. */
@media (max-width: 34rem) {
    .fc-analysis { overflow-x: visible; }

    .fc-analysis__grid {
        grid-template-columns: 1fr 1fr;
        min-width: 0;
        gap: var(--s2) var(--s3);
        padding: var(--s2) var(--s3) var(--s3);
    }

    /* The column headings are replaced by the per-cell ones below. */
    .fc-analysis__head { display: none; }

    .fc-analysis__label {
        grid-column: 1 / -1;
        margin-top: var(--s2);
        padding-top: var(--s2);
        border-top: 1px solid var(--border);
        font-size: 0.875rem;
    }
    /* No rule above the first block — it would float under the panel edge.
       The first label follows the last (hidden) heading, which is what this
       selector keys off; :first-child would never match it. */
    .fc-analysis__head + .fc-analysis__label {
        margin-top: 0;
        padding-top: 0;
        border-top: none;
    }

    .fc-analysis__value {
        display: flex;
        align-items: baseline;
        justify-content: space-between;
        gap: var(--s2);
        text-align: right;
        font-size: 0.9375rem;
    }

    .fc-analysis__value::before {
        content: attr(data-label);
        font-size: 0.6875rem;
        font-weight: 650;
        letter-spacing: 0.03em;
        text-transform: uppercase;
        color: var(--text-muted);
    }

    /* No room for a pill once the label shares the line, so the percentage
       keeps its colour (from .fc-pct--good/ok/bad) and drops the background. */
    .fc-analysis__value.fc-pct {
        justify-self: stretch;
        background: none;
        padding: 0;
    }
}

/* ----------------------------------------------------------------------
   Vendor directory

   Closed, each vendor is one line. The full nine-field record only appears
   when you open it — otherwise a list of twenty vendors is a wall of inputs.
   ---------------------------------------------------------------------- */
#vendors-list,
#users-list {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--surface);
}

.vendor-entry { border-bottom: 1px solid var(--border); }
.vendor-entry:last-child { border-bottom: none; }
.vendor-entry[open] { background: var(--gray-50); }

.vendor-entry__summary {
    display: flex;
    align-items: center;
    gap: var(--s3);
    flex-wrap: wrap;
    padding: var(--s3);
    min-height: 44px;
    cursor: pointer;
    list-style: none;
    user-select: none;
}
.vendor-entry__summary::-webkit-details-marker { display: none; }
.vendor-entry__summary:hover { background: var(--gray-100); }

.vendor-entry__summary::before {
    content: "▸";
    color: var(--text-muted);
    transition: transform 0.15s ease;
}
.vendor-entry[open] > .vendor-entry__summary::before { transform: rotate(90deg); }

.vendor-entry__name { font-weight: 600; min-width: 0; }

/* Staff rows reuse the vendor directory wholesale; this only marks them so
   they can be told apart in the inspector and styled separately later. */
.user-entry .vendor-entry__count { font-variant-numeric: normal; }

.vendor-entry__meta {
    display: inline-flex;
    align-items: center;
    gap: var(--s3);
    margin-left: auto;
    font-size: 0.8125rem;
    color: var(--text-muted);
}
.vendor-entry__phone { font-variant-numeric: tabular-nums; white-space: nowrap; }
.vendor-entry__count { white-space: nowrap; }

.vendor-entry__body {
    padding: 0 var(--s3) var(--s3);
    border-top: 1px solid var(--border);
    padding-top: var(--s3);
}

/* --------------------------------------------------------------------------
   Header icon buttons

   The envelope and the cog. Sized to the 44px touch target even though the
   glyph inside is small, because they sit next to Sign out on a phone.
   -------------------------------------------------------------------------- */
/* No border. Four bordered boxes in a row read as four heavy buttons crowding
   the company's name; the same icons at the same 40px tap target, drawn plain,
   read as a row of icons and give the name back the room it was missing. The
   hover and press tints stay, so the target is still obvious under a finger. */
.icon-btn {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    flex: 0 0 auto;
    padding: 0;
    border: 0;
    border-radius: var(--radius-sm);
    /* Transparent at rest. The border went first and the bar still read as a
       row of buttons, because each icon kept a tinted plate behind it — and
       four plates beside a company name is the crowding, whether the edges are
       drawn or not. The press tint below is the feedback; on a phone there is
       no hover to lose. */
    background: transparent;
    color: var(--white);
    cursor: pointer;
}
.icon-btn:hover  { background: rgba(255, 255, 255, 0.18); }
.icon-btn:active { background: rgba(255, 255, 255, 0.26); }

.icon-btn svg {
    width: 20px;
    height: 20px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.7;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* Gear teeth: thick and squared off, sitting on the rim. Thin round rays
   floating outside the circle is what made the first attempt read as a sun. */
.icon-btn__teeth {
    stroke-width: 3;
    stroke-linecap: butt;
}

/* Unread count. Sits on the corner of the envelope. */
.icon-btn__badge {
    position: absolute;
    top: -5px;
    right: -5px;
    min-width: 18px;
    height: 18px;
    padding: 0 4px;
    border-radius: 9px;
    background: var(--danger);
    color: var(--white);
    font-size: 0.6875rem;
    font-weight: 700;
    line-height: 18px;
    text-align: center;
}

/* --------------------------------------------------------------------------
   Settings menu
   -------------------------------------------------------------------------- */
.settings-list {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--surface);
}

.settings-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
    width: 100%;
    min-height: 56px;
    padding: var(--s3);
    border: none;
    border-bottom: 1px solid var(--border);
    background: none;
    text-align: left;
    font: inherit;
    color: var(--text);
    cursor: pointer;
}
.settings-row:last-child { border-bottom: none; }
.settings-row:hover { background: var(--gray-50); }

.settings-row__text { display: grid; gap: 2px; min-width: 0; }
.settings-row__label { font-weight: 600; }
.settings-row__hint  { font-size: 0.8125rem; color: var(--text-muted); }

.settings-row__chevron {
    font-size: 1.5rem;
    line-height: 1;
    color: var(--text-muted);
    flex: 0 0 auto;
}

/* The destructive entry reads as such before it is opened, not after. */
.settings-row--danger .settings-row__label { color: var(--danger); }

.settings-section {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
    flex-wrap: wrap;
    margin-top: var(--s4);
    padding: var(--s3);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--surface);
}
.settings-section__title { font-weight: 600; }

/* --------------------------------------------------------------------------
   Notification toggles

   A labelled switch per event. The whole row is the label element, so tapping
   anywhere on it flips the box — 44px of target instead of 20.
   -------------------------------------------------------------------------- */
.toggle-list {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--surface);
}

.toggle-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
    min-height: 56px;
    padding: var(--s3);
    border-bottom: 1px solid var(--border);
    cursor: pointer;
}
.toggle-row:last-child { border-bottom: none; }
.toggle-row:hover { background: var(--gray-50); }

.toggle-row__text  { display: grid; gap: 2px; min-width: 0; }
.toggle-row__label { font-weight: 600; }
.toggle-row__hint  { font-size: 0.8125rem; color: var(--text-muted); }

/* The two columns bolted onto the right of the History table on "Add items".
   Narrow on purpose: the amount holds a small whole number, and the numbers
   being DECIDED FROM are the count columns to the left — these must not
   out-weigh them. */
.input--qty {
    width: 4rem;
    text-align: right;
    padding-left: var(--s2);
    padding-right: var(--s2);
}

/* PINNED TO THE RIGHT EDGE, the way ITEM is pinned to the left.
   This is the History table, which scrolls sideways as soon as you ask it for
   more than about two counts. The columns you are READING can go off the edge
   and be scrolled back to; the two you are ACTING on cannot, or the screen
   stops working on a phone the moment somebody presses "+".
   The tick is outermost because it is the last thing the thumb does. */
.shopadd-amount,
.shopadd-pick {
    position: sticky;
    width: 1%;
    white-space: nowrap;
    background: var(--surface);
}

.shopadd-amount { right: 3rem; }
.shopadd-pick   { right: 0; text-align: center; }

/* The left border only on the first of the pair, so the two read as one block
   fixed to the edge rather than as two stray columns. */
.shopadd-amount { border-left: 1px solid var(--border); }

thead .shopadd-amount,
thead .shopadd-pick { background: var(--gray-100); z-index: 2; }

tbody tr:hover .shopadd-amount,
tbody tr:hover .shopadd-pick { background: var(--gray-50); }

/* A coloured count cell must not bleed under the pinned pair. */
.hist-low { position: relative; }

.shopadd-pick input[type="checkbox"] {
    width: 22px;
    height: 22px;
    accent-color: var(--teal-600);
    cursor: pointer;
    vertical-align: middle;
}

@media (max-width: 420px) {
    .input--qty { width: 3.25rem; }
    .shopadd-amount { right: 2.5rem; }
}

.toggle-row input[type="checkbox"] {
    width: 22px;
    height: 22px;
    flex: 0 0 auto;
    accent-color: var(--teal-600);
    cursor: pointer;
}

/* --------------------------------------------------------------------------
   Saved counts, for deleting
   -------------------------------------------------------------------------- */
.run-list {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--surface);
}

.run-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
    padding: var(--s3);
    border-bottom: 1px solid var(--border);
}
.run-row:last-child { border-bottom: none; }

.run-row__text { display: grid; gap: 2px; min-width: 0; }
.run-row__when { font-weight: 600; font-variant-numeric: tabular-nums; }
.run-row__who  { font-size: 0.8125rem; color: var(--text-muted); }

/* The sheet under the documents tabs.
   The same join the concept cards use: the border runs all the way round,
   INTO the chosen tab, which is the same colour — so the tab and the list read
   as one piece of card while the other tab stays visibly cut off above it.
   That join is what makes them folders rather than two buttons. */
/* Lists that carry their own frame lose it inside a folder body — otherwise a
   1px bordered list sits inside a 2px bordered box and reads as a double
   frame. The folder IS the frame here. */
.doc-tab-body > .settings-list,
.doc-tab-body > .fc-list {
    border: 0;
    border-radius: 0;
}

.doc-tab-body {
    padding: var(--s4) var(--s3);
    border: 2px solid var(--brand-header, var(--navy));
    border-radius: 0 0 var(--radius-lg) var(--radius-lg);
    margin-bottom: var(--s3);
}

/* Urgency down the left edge of a document row.
   A list where every row looks the same makes somebody read six lines of small
   print to find the one that is late. The stripe answers it at arm's length,
   which is how a phone gets read in a kitchen. Colour is never the ONLY signal
   — the icon, the pill and the meta line all still say it in words. */
.doc-row--late    { border-left: 4px solid var(--danger); }
.doc-row--due     { border-left: 4px solid var(--warning); }
.doc-row--open    { border-left: 4px solid var(--border-strong); }
.doc-row--refused { border-left: 4px solid var(--gray-400); }
.doc-row--done    { border-left: 4px solid var(--success); }

/* A button that fills its row. Used for Sign out at the bottom of the settings
   menu, where a small button floating left would read as an afterthought. */
.btn--wide { width: 100%; justify-content: center; }

/* A screenshot in the manual. Bordered and inset so it reads as a picture OF
   the app rather than as more app — without the frame, a screenshot of a card
   sitting inside a card is genuinely confusing. */
.doc-shot {
    margin: var(--s3) 0;
}
.doc-shot__img {
    display: block;
    max-width: 100%;
    height: auto;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    /* Shots are taken at phone width. On a desktop the manual is wide, and a
       375px image stretched across it looks broken rather than large. */
    width: min(100%, 380px);
    background: var(--surface);
}
.doc-shot__cap {
    margin-top: var(--s1);
    font-size: 0.8125rem;
    color: var(--text-muted);
}

/* The redacted request body on an activity row. Folded away by default: the
   answer to "who changed the price" is the row itself, and the JSON is only
   for the rarer "what exactly did they send". */
.act-detail { margin-top: var(--s2); }
.act-detail summary {
    font-size: 0.8125rem;
    color: var(--text-muted);
    cursor: pointer;
}
.act-detail__body {
    margin: var(--s2) 0 0;
    padding: var(--s2);
    background: var(--gray-100);
    border-radius: var(--radius-sm);
    font-size: 0.75rem;
    /* The one place in the app horizontal scroll is right: JSON must not be
       reflowed into something that reads differently from what was sent. */
    overflow-x: auto;
    white-space: pre;
}

/* A field that is deliberately locked until somebody confirms they mean to
   edit it. Greyed rather than disabled: a disabled input cannot take focus, and
   focus is exactly what triggers the question. */
.input--locked {
    background: var(--gray-100);
    color: var(--text-muted);
    cursor: pointer;
}

/* A run that carries its ticked lines is taller than one line of text, so the
   right-hand chevron sits at the top rather than floating in the middle of a
   twelve-item list. */
.run-row--stacked { align-items: flex-start; }

.run-row__ticks {
    margin: var(--s2) 0 0;
    padding-left: 1.1em;
    display: grid;
    gap: 2px;
    font-size: 0.8125rem;
    color: var(--text);
}
/* Ticks rather than bullets: every line in this list was ticked, and the mark
   is the record. list-style-type cannot draw one, so the marker is content. */
.run-row__ticks li { list-style: none; position: relative; }
.run-row__ticks li::before {
    content: '✓';
    position: absolute;
    left: -1.1em;
    color: var(--success);
    font-weight: 700;
}

/* A run row that opens something. A button, so it is reachable by keyboard and
   announced as pressable, but styled as the row it replaces. */
.run-row--go {
    width: 100%;
    background: none;
    border: none;
    border-bottom: 1px solid var(--border);
    font: inherit;
    color: inherit;
    text-align: left;
    cursor: pointer;
}
.run-row--go:hover { background: var(--gray-50); }
.run-row--go:last-child { border-bottom: none; }
.run-row__go { color: var(--text-muted); font-size: 1.25rem; line-height: 1; }

/* The same treatment for a document row that leads somewhere — the unsigned
   rows on Needs attention, which open the person they name. A <button> needs
   the browser's own styling undone before it looks like the rows beside it;
   .doc-row supplies the layout, this supplies the reset. */
.doc-row--go {
    width: 100%;
    background: none;
    border: none;
    border-bottom: 1px solid var(--border);
    font: inherit;
    color: inherit;
    text-align: left;
    cursor: pointer;
}
/* ---------------------------------------------------------------------------
   Note popup

   A count's note, over the history table. Its own layer rather than the main
   modal, because the table underneath is what the reader is working in and
   must still be there when the note is closed.
   --------------------------------------------------------------------------- */
.note-pop {
    position: fixed;
    inset: 0;
    /* Above the modal (100), below nothing else. */
    z-index: 200;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--s4);
    background: rgba(21, 49, 67, 0.55);
}

.note-pop__sheet {
    width: 100%;
    max-width: 30rem;
    max-height: 80dvh;
    overflow-y: auto;
    padding: var(--s4);
    border-radius: var(--radius-lg);
    background: var(--white);
    box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
}

.note-pop__title { margin-bottom: var(--s2); font-size: 1rem; }

/* Somebody typed this into a phone at closing time — keep their line breaks
   and let a long unbroken word wrap rather than widen the sheet. */
.note-pop__body { white-space: pre-wrap; overflow-wrap: anywhere; }

/* The Read button in the history head. Compact, because it sits in a column
   heading that is otherwise a date and a name. */
.hist-note { padding: 2px var(--s2); min-height: 0; font-size: 0.75rem; }

/* The pencil inside a count cell: a note was written about this item on THIS
   count. In the cell rather than by the name, so which count the note belongs
   to is answered by where the pencil is. Tight padding — it shares a numeric
   cell with the count itself. */
.hist-item-note {
    border: none;
    background: none;
    color: var(--teal);
    font-size: 0.85rem;
    padding: 0 2px 0 4px;
    cursor: pointer;
    min-height: 0;
    vertical-align: baseline;
}

/* ------------------------------------------------------------------ */
/* Per-item notes on the count sheet                                   */
/* ------------------------------------------------------------------ */

/* Always visible, one per item (Erick's call — a box behind an icon is a
   feature nobody finds), in the middle column between the name and the
   quantity so each row stays one line tall.

   NO font-size here on purpose: it inherits .input's 16px, and below 16px
   iOS Safari zooms the whole page the moment the box is focused — the exact
   bug the base .input rule exists to prevent. This class had 14px for one
   day and reintroduced it. Height likewise inherits .input's 44px, matching
   the quantity box beside it so the row reads as one line. */
.count-row__note {
    min-width: 0;
}

/* The eraser that deletes a count, tucked into the corner of that count's own
   column heading. Small, because it is rare and irreversible; visible, because
   the heading itself no longer does anything and nothing else on the screen
   would tell you the column can be removed at all. */
.hist-run { position: relative; }

.hist-run__erase {
    position: absolute;
    top: 0;
    right: 0;
    /* 28px of tappable area around a small glyph — big enough for a thumb,
       small enough not to compete with the date it sits beside. */
    width: 28px;
    height: 28px;
    padding: 0;
    border: 0;
    border-radius: var(--radius-sm);
    background: none;
    color: var(--text-muted);
    line-height: 1;
    cursor: pointer;
}

/* 17px inside a 28px target: the drawing stays quiet while the thing your
   thumb has to hit stays comfortable. Below about 16 the seam across the
   eraser closes up and it stops reading as one. */
.hist-run__erase svg {
    display: block;
    width: 17px;
    height: 17px;
    margin: 0 auto;
}

.hist-run__erase:hover,
.hist-run__erase:focus-visible {
    color: var(--danger, #c0392b);
    background: var(--danger-bg, #fdecea);
}

/* Keep the date clear of the eraser so a long timestamp does not run under it. */
.hist-run > div:first-of-type { padding-right: 26px; }
.hist-note-row th, .hist-note-row td { padding-top: 2px; padding-bottom: var(--s2); }

.doc-row--go:hover { background: var(--gray-50); }
.doc-row--go:last-child { border-bottom: none; }
.doc-row__go { margin-left: auto; color: var(--text-muted); font-size: 1.25rem; line-height: 1; }

/* Percentage change on the price rollout list. Deliberately not one of the
   fc-pct pills — those mean something else on the same screen. */
.run-row__delta {
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    color: var(--text-muted);
}

/* Spreadsheet import */
.import-status { margin-top: var(--s3); display: grid; gap: var(--s2); }

.import-issue {
    padding: var(--s2) var(--s3);
    border-left: 3px solid var(--danger);
    background: var(--danger-bg);
    border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
    font-size: 0.8125rem;
    color: #8c2018;
    font-variant-numeric: tabular-nums;
}

.import-issue--warn {
    border-left-color: var(--orange);
    background: var(--warning-bg);
    color: #8a4308;
}

.import-divider {
    border: none;
    border-top: 1px solid var(--border);
    margin: var(--s4) 0 var(--s3);
}

.fc-lines { display: grid; gap: var(--s2); }

.fc-line {
    display: grid;
    grid-template-columns: 8rem 1fr 5rem 6rem auto auto;
    gap: var(--s2);
    align-items: center;
    padding: var(--s2);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface);
}

.fc-line .select,
.fc-line .input { min-height: 38px; padding: var(--s1) var(--s2); font-size: 0.875rem; }


.fc-line__cost {
    font-variant-numeric: tabular-nums;
    font-size: 0.875rem;
    font-weight: 600;
    text-align: right;
    white-space: nowrap;
}

.fc-line__error {
    grid-column: 1 / -1;
    font-size: 0.75rem;
    color: var(--danger);
}

.fc-margin { margin-top: var(--s4); }

.footer-total { font-weight: 700; }

/* Recipe lines need room; below this the grid squeezes the pickers to
   uselessness, so it stacks two-up instead: the pair of pickers, then the
   amount and its unit, then the cost and the remove button. That is already
   the order of the markup, so nothing moves in the DOM. */
@media (max-width: 44rem) {
    .fc-line { grid-template-columns: 1fr 1fr; padding: var(--s3) var(--s2); }
    .fc-line__cost { text-align: left; align-self: center; }
    .fc-line .btn--icon { justify-self: end; }
    .fc-line__error { grid-column: 1 / -1; }
}

/* Narrower than this, four columns of figures leaves nothing for the name, so
   rows go two-up with the details wrapping underneath.
   The column titles are hidden with them: headings that do not sit over their
   own figures are worse than no headings, and .fc-head cannot follow this
   reflow — "Price / Cost / Food %" has nowhere to go once the row is two
   columns wide. Above this width both share one template and line up. */
@media (max-width: 34rem) {
    .fc-head { display: none; }

    /* Two columns, filled in DOM order — so odd children land in the left
       column and even ones in the right, and each can be aligned to its own
       edge without pinning anything to a fixed cell. */
    .fc-row { grid-template-columns: minmax(0, 1fr) auto; row-gap: 2px; }
    .fc-row > *:nth-child(odd)  { justify-self: start; text-align: left; }
    .fc-row > *:nth-child(even) { justify-self: end;   text-align: right; }
}

/* --------------------------------------------------------------------------
   Banners

   Full-width strips at the top of the dashboard. Each colour pairs a wash
   with a dark-enough text tone; the map lives here so the database only ever
   stores a name.
   -------------------------------------------------------------------------- */
.app-banner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
    width: 100%;
    padding: var(--s3) var(--s4);
    margin-bottom: var(--s3);
    border: none;
    border-left: 4px solid transparent;
    border-radius: var(--radius-sm);
    font: inherit;
    font-weight: 600;
    text-align: left;
}

.app-banner__text { min-width: 0; overflow-wrap: break-word; }
.app-banner__chevron { font-size: 1.4rem; line-height: 1; flex: 0 0 auto; opacity: 0.7; }

button.app-banner { cursor: pointer; }
button.app-banner:hover { filter: brightness(0.97); }

.app-banner--navy   { background: #dbe4ec; color: #153143; border-left-color: #153143; }
.app-banner--teal   { background: #d9ecea; color: #235450; border-left-color: #418983; }
.app-banner--green  { background: #dcefd9; color: #2c5a26; border-left-color: #4a8f40; }
.app-banner--yellow { background: #fdf3d1; color: #6d5405; border-left-color: #d9a406; }
.app-banner--orange { background: #fde3c8; color: #8a4207; border-left-color: #e07417; }
.app-banner--red    { background: #fbd2cc; color: #8c2018; border-left-color: #cc4437; }
.app-banner--purple { background: #e6def2; color: #4b3273; border-left-color: #7a5bb5; }

/* The big variant: for the checklist that MUST be done. Twice the presence
   of an ordinary banner — larger type, taller, a second line telling you what
   tapping does. */
.app-banner--big {
    padding: var(--s4);
    border-left-width: 6px;
}

.app-banner--big .app-banner__title {
    display: block;
    font-size: 1.0625rem;
    font-weight: 700;
    line-height: 1.35;
}

.app-banner--big .app-banner__sub {
    display: block;
    margin-top: 2px;
    font-size: 0.8125rem;
    font-weight: 500;
    opacity: 0.75;
}

/* Flashing: a slow pulse, not a strobe — it has to catch the eye across a
   kitchen without being unreadable while you look straight at it. Respects
   the OS setting for people who turn motion off. */
@keyframes banner-pulse {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.45; }
}
.app-banner--flashing { animation: banner-pulse 1.6s ease-in-out infinite; }

/* ---------------------------------------------------------------------------
   Document signatures, coloured by how late they are

   Three states, and the difference between them is the point: a stripe you
   might get to this week, amber for the next few days, red and moving for
   something already late. A settings menu of identical grey rows gave a
   document due yesterday exactly as much weight as the language picker.

   Colour is never the only signal — the hint line underneath still says how
   many and the row still says what it is — so this reads the same to somebody
   who cannot tell the red from the amber.
   --------------------------------------------------------------------------- */
.settings-row--open { border-left: 4px solid var(--teal, #54aea6); }

.settings-row--due {
    border-left: 4px solid var(--warning, #d98324);
    background: var(--warning-bg, #fdf6ec);
}

.settings-row--overdue {
    border-left: 4px solid var(--danger, #c0392b);
    background: var(--danger-bg, #fdecea);
    animation: banner-pulse 1.6s ease-in-out infinite;
}

.settings-row--due .settings-row__label,
.settings-row--overdue .settings-row__label { font-weight: 600; }

@media (prefers-reduced-motion: reduce) {
    .app-banner--flashing,
    .settings-row--overdue { animation: none; }
}

/* Colour swatches on the banner builder. */
.swatch-row { display: flex; gap: var(--s2); flex-wrap: wrap; }

.swatch {
    width: 36px;
    height: 36px;
    border: 2px solid transparent;
    border-radius: 50%;
    cursor: pointer;
    padding: 0;
}
.swatch.is-active { border-color: var(--navy); box-shadow: 0 0 0 2px var(--white) inset; }

.swatch--tiny {
    display: inline-block;
    width: 12px;
    height: 12px;
    vertical-align: baseline;
    cursor: default;
}

.swatch--navy   { background: #153143; }
.swatch--teal   { background: #418983; }
.swatch--green  { background: #4a8f40; }
.swatch--yellow { background: #d9a406; }
.swatch--orange { background: #e07417; }
.swatch--red    { background: #cc4437; }
.swatch--purple { background: #7a5bb5; }

/* Severity pills in the stock pop-up — same three tints as the history. */
.stock-pill {
    padding: 2px var(--s2);
    border-radius: var(--radius-sm);
    font-size: 0.75rem;
    font-weight: 700;
    white-space: nowrap;
    flex: 0 0 auto;
}
.stock-pill--yellow { background: #fdf3d1; color: #6d5405; }
.stock-pill--orange { background: #fde3c8; color: #8a4207; }
.stock-pill--red    { background: #fbd2cc; color: #8c2018; }

.run-row--dim { opacity: 0.55; }

/* --------------------------------------------------------------------------
   Low-stock colouring in history

   The whole cell is tinted, not just the number — scanning a table of sixty
   rows for pale red text does not work, a wash of colour does. Text stays
   dark enough to read on every tint.
   -------------------------------------------------------------------------- */
.hist-low { font-weight: 650; }
.hist-low--yellow { background: #fdf3d1; color: #6d5405; }
.hist-low--orange { background: #fde3c8; color: #8a4207; }
.hist-low--red    { background: #fbd2cc; color: #8c2018; }

/* --------------------------------------------------------------------------
   History toolbar
   -------------------------------------------------------------------------- */
.history-toolbar {
    display: flex;
    gap: var(--s2);
    align-items: center;
    flex-wrap: wrap;
    margin-bottom: var(--s3);
}

.count-picker {
    display: flex;
    align-items: center;
    gap: var(--s1);
    flex: 0 0 auto;
}

.count-picker__input {
    width: 4rem;
    text-align: center;
    font-variant-numeric: tabular-nums;
    /* Steppers do the nudging; the native spinners just add clutter. */
    -moz-appearance: textfield;
}
.count-picker__input::-webkit-outer-spin-button,
.count-picker__input::-webkit-inner-spin-button {
    -webkit-appearance: none;
    margin: 0;
}

/* --------------------------------------------------------------------------
   Messages
   -------------------------------------------------------------------------- */
.message-body {
    font-size: 1.0625rem;
    line-height: 1.6;
    white-space: pre-wrap;
    word-break: break-word;
    margin: 0;
}

/* The "I have read this" tick. Set apart from the message so it does not read
   as part of what was written. */
.checkbox--confirm {
    margin-top: var(--s4);
    padding: var(--s3);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    background: var(--gray-50);
    font-weight: 550;
}

/* My messages: every message this person has been sent, newest first. */
.msg-list {
    display: grid;
    gap: var(--s2);
}

.msg-list__item {
    display: grid;
    gap: var(--s1);
    width: 100%;
    padding: var(--s3);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--surface);
    text-align: left;
    font: inherit;
    cursor: pointer;
}
.msg-list__item:hover { border-color: var(--border-strong); background: var(--gray-50); }
.msg-list__item--unread { border-left: 3px solid var(--warning); }

.msg-list__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
}
.msg-list__from { font-weight: 600; }
.msg-list__when { font-size: 0.8125rem; color: var(--text-muted); white-space: nowrap; }

/* One line of the message as a preview — the rest opens in the reader. */
.msg-list__body {
    color: var(--text-muted);
    font-size: 0.875rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Photos on a message: fixed square frames, image centred inside, same rule
   as everywhere else photos appear. */
.msg-media {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s2);
    margin-top: var(--s3);
}

.msg-media__open {
    padding: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--gray-100);
    overflow: hidden;
    cursor: pointer;
    width: 6rem;
    height: 6rem;
}

.msg-media__thumb {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: contain;
}

/* Links typed into a message body. */
.message-body a { color: var(--teal-600); word-break: break-all; }

.msg-list__action {
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--teal-600);
}

/* Buttons beside the dashboard heading. */
.page-head__actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s2);
    align-items: center;
}

.message-row {
    padding: var(--s3);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--surface);
    margin-bottom: var(--s3);
}

.message-row__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
    margin-bottom: var(--s2);
}

.message-row__to { font-weight: 650; }

.message-row__body {
    white-space: pre-wrap;
    word-break: break-word;
    color: var(--gray-600);
    margin-bottom: var(--s2);
}

.message-row__foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
}

.badge--read { background: var(--success-bg); color: var(--success); }

.modal--narrow .modal__panel { max-width: 28rem; }
.modal__footer--center { justify-content: center; }

/* --------------------------------------------------------------------------
   Language switch
   -------------------------------------------------------------------------- */
/* Lives on a white settings card now, not on the navy header, so the colours
   are the ordinary text ones. The white-on-navy version below is kept for
   anywhere it is still used on a dark background. */
.lang-switch {
    display: inline-flex;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    overflow: hidden;
    flex: 0 0 auto;
}

.lang-switch__option {
    padding: var(--s1) var(--s3);
    min-height: 36px;
    border: none;
    background: transparent;
    color: var(--text-muted);
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-decoration: none;
    display: inline-flex;
    align-items: center;
    cursor: pointer;
}
.lang-switch__option:hover { background: var(--gray-100); color: var(--text); }
.lang-switch__option.is-active { background: var(--teal-600); color: var(--white); }

/* On a dark background: the login screen, and the header if it ever carries
   one again. */
.app-header .lang-switch,
.lang-switch--login { border-color: rgba(255, 255, 255, 0.25); }

.app-header .lang-switch__option,
.lang-switch--login .lang-switch__option { color: rgba(255, 255, 255, 0.75); }

.app-header .lang-switch__option:hover,
.lang-switch--login .lang-switch__option:hover { background: rgba(255, 255, 255, 0.12); color: var(--white); }

.lang-switch--login {
    margin-top: var(--s5);
    border-color: rgba(255, 255, 255, 0.3);
}
.lang-switch--login .lang-switch__option { padding: var(--s2) var(--s4); font-size: 0.8125rem; }

/* --------------------------------------------------------------------------
   Settings editor
   -------------------------------------------------------------------------- */
.setting-item {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--surface);
    margin-bottom: var(--s3);
    overflow: hidden;
}

/* The closed line: name and unit/cost on the left, reorder and delete on the
   right. Everything else waits inside. */
.setting-item__summary {
    display: flex;
    align-items: center;
    gap: var(--s3);
    padding: var(--s2) var(--s3);
    min-height: 48px;
    cursor: pointer;
    list-style: none;
    user-select: none;
}
.setting-item__summary::-webkit-details-marker { display: none; }
.setting-item__summary:hover { background: var(--gray-50); }

.setting-item__summary::before {
    content: "▸";
    color: var(--text-muted);
    flex: 0 0 auto;
    transition: transform 0.15s ease;
}
.setting-item[open] > .setting-item__summary::before { transform: rotate(90deg); }
.setting-item[open] > .setting-item__summary { border-bottom: 1px solid var(--border); }

.setting-item__text {
    display: grid;
    gap: 1px;
    min-width: 0;
    margin-right: auto;
}
.setting-item__title {
    font-weight: 600;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.setting-item__sub {
    font-size: 0.75rem;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

.setting-item__head {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--s2);
    align-items: end;
    padding: var(--s3);
    background: var(--gray-50);
    border-bottom: 1px solid var(--border);
}

.setting-item__body { padding: var(--s3); }

.reorder-buttons { display: flex; gap: 2px; }
.reorder-buttons .btn--icon { min-height: 36px; width: 32px; }

.day-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(6.5rem, 1fr));
    gap: var(--s2);
    margin-bottom: var(--s3);
}

.day-cell {
    padding: var(--s2);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--gray-50);
}

.day-cell__label {
    display: flex;
    align-items: center;
    justify-content: space-between;
    font-size: 0.75rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--gray-600);
    margin-bottom: var(--s1);
}

.day-cell__inputs { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s1); }

.day-cell .input {
    min-height: 38px;
    padding: var(--s1) var(--s2);
    font-size: 0.875rem;
    text-align: center;
}

.mini-label {
    font-size: 0.625rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-muted);
    text-align: center;
    display: block;
}

.copy-days {
    border: 1px solid var(--border-strong);
    background: var(--white);
    border-radius: var(--radius-sm);
    width: 24px;
    height: 20px;
    padding: 0;
    font-size: 0.75rem;
    line-height: 1;
    color: var(--gray-600);
}
.copy-days:hover { background: var(--teal); border-color: var(--teal); color: var(--white); }

.setting-item__footer {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s3);
    align-items: end;
    justify-content: space-between;
    padding: var(--s3);
    border-top: 1px solid var(--border);
    background: var(--gray-50);
}

/* --------------------------------------------------------------------------
   Users editor
   -------------------------------------------------------------------------- */
.user-item {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--surface);
    padding: var(--s4);
    margin-bottom: var(--s3);
}

.user-item__grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
    gap: var(--s3);
    margin-bottom: var(--s3);
}

/* --------------------------------------------------------------------------
   Capability panel

   Collapsed by default. Nine checkboxes per person would make the staff
   screen unreadable, and a preset covers most cases — so the detail is there
   when you want it and out of the way when you do not.
   -------------------------------------------------------------------------- */
.cap-panel {
    margin-bottom: var(--s3);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--gray-50);
}

.cap-panel__summary {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
    flex-wrap: wrap;
    padding: var(--s3);
    min-height: 44px;
    cursor: pointer;
    font-weight: 600;
    font-size: 0.9375rem;
    list-style: none;
    user-select: none;
}
.cap-panel__summary::-webkit-details-marker { display: none; }

/* A rotating caret, so the open/closed state reads at a glance. */
.cap-panel__summary::before {
    content: "▸";
    display: inline-block;
    margin-right: var(--s2);
    color: var(--text-muted);
    transition: transform 0.15s ease;
}
.cap-panel[open] > .cap-panel__summary::before { transform: rotate(90deg); }
.cap-panel__summary:hover { background: var(--gray-100); }

/* The inside of a disclosure panel. Named rather than reusing a utility, so a
   panel opened on a phone gets the same padding as a card body. */
.cap-panel__body { padding: var(--s3); }

/* The label on a collapsible that has no count or tag beside it. */
/* The title takes the free space, so it sits NEXT TO the caret instead of
   being flung to the far right by the summary's space-between — which is
   there for the panels that carry a count on the right, and those still get
   it, pushed out by this. Before this, every fold-away section in the app
   read as "▸                    Send from a template". */
.cap-panel__title { font-weight: 600; flex: 1 1 auto; min-width: 0; }

/* Every problem an import found, with the row it is on. A count alone means
   opening the file and hunting; the row number means fixing it. */
.import-problems {
    margin: var(--s2) 0 0;
    padding: 0 0 0 var(--s4);
    font-size: 0.875rem;
    max-height: 16rem;
    overflow-y: auto;
}

.import-problems li { margin-bottom: var(--s1); }

.import-problems__row {
    display: inline-block;
    min-width: 4.5rem;
    color: var(--text-muted);
}

.cap-panel__meta {
    display: inline-flex;
    align-items: center;
    gap: var(--s2);
    margin-left: auto;
}

.cap-panel__count { font-size: 0.8125rem; font-weight: 500; color: var(--text-muted); }

.cap-panel__hint {
    padding: 0 var(--s3);
    margin: 0 0 var(--s3);
    font-size: 0.8125rem;
    color: var(--text-muted);
}

.cap-group {
    padding: 0 var(--s3) var(--s3);
}

.cap-group__title {
    font-size: 0.6875rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--gray-600);
    padding-bottom: var(--s1);
    margin-bottom: var(--s2);
    border-bottom: 1px solid var(--border);
}

.cap-row {
    display: flex;
    align-items: flex-start;
    gap: var(--s3);
    padding: var(--s2);
    border-radius: var(--radius-sm);
    cursor: pointer;
    /* Comfortable to hit on a phone without being enormous on a laptop. */
    min-height: 44px;
}
.cap-row:hover { background: var(--white); }

.cap-row input {
    width: 20px;
    height: 20px;
    margin-top: 1px;
    accent-color: var(--teal-600);
    flex: 0 0 auto;
}

.cap-row__label { display: block; font-size: 0.9375rem; font-weight: 550; }
.cap-row__hint  { display: block; font-size: 0.75rem; color: var(--text-muted); line-height: 1.4; }

/* A capability the person looking does not hold themselves. Shown, ticked
   truthfully if the employee has it, and not theirs to change — see the
   ceiling in Permissions::limitedTo(). Faded rather than hidden: an admin
   should be able to SEE what somebody can do without being able to hand it
   out, and a row that vanished would make the panel disagree with the count
   beside its heading. */
/* The clear x on a search box.
   type="search" gets a native one on desktop Safari and Chrome and NOTHING on
   iOS, which is where this app is actually used. The native one is hidden so
   there is never two. */
.search-wrap { position: relative; display: block; }
.search-wrap .input--clearable { padding-right: 2.5rem; }

.input--clearable::-webkit-search-cancel-button { display: none; }

.search-clear {
    position: absolute;
    top: 50%;
    right: 0.35rem;
    transform: translateY(-50%);
    width: 30px;
    height: 30px;
    border: 0;
    border-radius: 50%;
    background: transparent;
    color: var(--text-muted);
    font-size: 1.25rem;
    line-height: 1;
    cursor: pointer;
    /* A 30px circle is under the 44px target the rest of the app uses, so the
       tappable area is widened without making the mark itself bigger. */
    padding: 0;
}
.search-clear:hover { background: var(--surface); color: var(--navy); }
.search-clear[hidden] { display: none; }

/* Walking order: the heading above each run of items on a count sheet.
   Sticky, because the whole point is knowing which part of the restaurant you
   are standing in, and on a phone the heading scrolls away after four rows. */
.count-area {
    position: sticky;
    top: 0;
    z-index: 2;
    margin: var(--s4) 0 var(--s2);
    padding: var(--s2) var(--s3);
    background: var(--navy);
    color: #fff;
    border-radius: var(--radius-sm);
    font-size: 0.8125rem;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}
.count-area:first-child { margin-top: 0; }

.cap-row--locked {
    cursor: not-allowed;
    opacity: 0.55;
}
.cap-row--locked:hover { background: transparent; }
.cap-row--locked input { accent-color: var(--text-muted); }

/* A whole record that is not yours: the owner's, or somebody who can already
   do more than you. */
.user-entry--locked > .vendor-entry__summary { opacity: 0.75; }
.user-entry--locked .vendor-entry__body input,
.user-entry--locked .vendor-entry__body select,
.user-entry--locked .vendor-entry__body .cap-row { cursor: not-allowed; }

.location-picker {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s2);
    padding: var(--s2);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    background: var(--white);
}

.location-chip {
    display: inline-flex;
    align-items: center;
    gap: var(--s2);
    padding: var(--s1) var(--s3);
    border-radius: 999px;
    background: var(--gray-100);
    font-size: 0.875rem;
    cursor: pointer;
    user-select: none;
}
.location-chip input { accent-color: var(--teal-600); }
.location-chip:has(input:checked) { background: var(--teal); color: var(--white); }

/* --------------------------------------------------------------------------
   Login
   -------------------------------------------------------------------------- */
.login {
    min-height: 100dvh;
    display: flex;
    align-items: center;
    justify-content: center;
    /* The insets keep the card clear of the notch and the home indicator
       while the gradient itself still runs edge to edge. */
    padding: calc(var(--s5) + env(safe-area-inset-top, 0px))
             max(var(--s4), env(safe-area-inset-right, 0px))
             calc(var(--s5) + env(safe-area-inset-bottom, 0px))
             max(var(--s4), env(safe-area-inset-left, 0px));
    background:
        radial-gradient(ellipse at top, var(--navy-700) 0%, var(--navy) 55%, #0d2130 100%);
}

.login__card {
    width: 100%;
    max-width: 22rem;
    text-align: center;
}

.login__logo {
    width: min(15rem, 70vw);
    height: auto;
    margin: 0 auto var(--s6);
    display: block;
}

.login__title {
    color: var(--white);
    font-size: 1rem;
    font-weight: 500;
    margin-bottom: var(--s5);
    opacity: 0.85;
}

.pin-inputs {
    display: flex;
    justify-content: center;
    gap: var(--s3);
    margin-bottom: var(--s5);
}

.pin-digit {
    width: 3.25rem;
    height: 3.75rem;
    padding: 0;
    text-align: center;
    font-size: 1.75rem;
    font-weight: 600;
    color: var(--white);
    background: rgba(255, 255, 255, 0.08);
    border: 2px solid rgba(255, 255, 255, 0.25);
    border-radius: var(--radius);
    transition: border-color 0.15s ease, background-color 0.15s ease;
}
.pin-digit:focus {
    outline: none;
    border-color: var(--teal);
    background: rgba(255, 255, 255, 0.14);
}
.pin-digit.is-filled { border-color: var(--teal-600); }

.login .btn { width: 100%; }

/* "Remember me on this device" */
.login__remember {
    display: flex;
    align-items: flex-start;
    gap: var(--s3);
    padding: var(--s3);
    margin-bottom: var(--s4);
    border-radius: var(--radius-sm);
    background: rgba(255, 255, 255, 0.06);
    color: rgba(255, 255, 255, 0.85);
    font-size: 0.875rem;
    text-align: left;
    cursor: pointer;
}
.login__remember input {
    width: 20px;
    height: 20px;
    margin-top: 1px;
    accent-color: var(--teal-600);
    flex: 0 0 auto;
}
.login__remember-hint {
    display: block;
    font-size: 0.75rem;
    color: rgba(255, 255, 255, 0.55);
    margin-top: 2px;
}

/* "Not you? Enter a PIN" — a quiet way back to the keypad. */
.btn--link {
    background: none;
    border: none;
    min-height: 40px;
    color: rgba(255, 255, 255, 0.7);
    font-size: 0.875rem;
    font-weight: 500;
    text-decoration: underline;
    text-underline-offset: 3px;
}
.btn--link:hover:not(:disabled) { background: none; color: var(--white); }

#device-panel { margin-bottom: var(--s3); }

/* Confirmation shown once before a device is trusted */
.trust-dialog {
    position: fixed;
    inset: 0;
    z-index: 300;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--s4);
    background: rgba(9, 22, 31, 0.85);
}
.trust-dialog[hidden] { display: none; }

.trust-dialog__panel {
    width: 100%;
    max-width: 22rem;
    padding: var(--s5);
    border-radius: var(--radius-lg);
    background: var(--navy-700);
    box-shadow: var(--shadow-lg);
    text-align: left;
}

.trust-dialog__title { color: var(--white); font-size: 1.125rem; margin-bottom: var(--s3); }

.trust-dialog__body {
    color: rgba(255, 255, 255, 0.8);
    font-size: 0.9375rem;
    margin-bottom: var(--s5);
}
#device-panel .btn { margin-bottom: var(--s2); }

.login__message {
    min-height: 3rem;
    margin-top: var(--s4);
    font-size: 0.9375rem;
    color: #ffb4a8;
}
.login__message--success { color: var(--teal); }

/* Shown when the previous session was closed by the idle timeout, so nobody
   is left wondering why they were signed out. */
.login__notice {
    padding: var(--s3);
    margin-bottom: var(--s5);
    border-radius: var(--radius-sm);
    background: rgba(232, 111, 13, 0.15);
    border: 1px solid rgba(232, 111, 13, 0.4);
    color: #ffd9b3;
    font-size: 0.875rem;
}

.login__footer {
    margin-top: var(--s6);
    font-size: 0.8125rem;
    color: rgba(255, 255, 255, 0.55);
}

/* --------------------------------------------------------------------------
   Diagnostics

   Dense on purpose. The whole value of this screen is that ONE screenshot
   carries the full picture — the old card rows ran off the bottom of a phone
   and the interesting half never made it into the photo that got sent.
   -------------------------------------------------------------------------- */
.diag-title {
    margin: var(--s3) 0 var(--s1);
    font-size: 0.6875rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.diag-grid {
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    overflow: hidden;
}

.diag-row {
    display: grid;
    grid-template-columns: minmax(0, 7.5rem) minmax(0, 1fr);
    gap: var(--s2);
    align-items: baseline;
    padding: 3px var(--s2);
    border-bottom: 1px solid var(--gray-100);
}
.diag-row:last-child { border-bottom: 0; }
.diag-row:nth-child(odd) { background: var(--gray-50); }

.diag-row__label {
    font-size: 0.6875rem;
    color: var(--text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Monospace so fingerprints line up and are easy to read back to me. */
.diag-row__value {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 0.6875rem;
    font-weight: 600;
    color: var(--text);
    overflow-wrap: anywhere;
}

/* --------------------------------------------------------------------------
   Utility
   -------------------------------------------------------------------------- */
.muted     { color: var(--text-muted); }
.small     { font-size: 0.8125rem; }
.numeric   { font-variant-numeric: tabular-nums; }
.text-right{ text-align: right; }

/* Inline result text — "pinned to 8m", "that fix is rough", "denied". */
.text-ok     { color: var(--success); }
.text-warn   { color: #8a4308; }
.text-danger { color: var(--danger); }
.mt-0 { margin-top: 0; }
.mt-2 { margin-top: var(--s2); }
.mt-3 { margin-top: var(--s3); }
.mt-4 { margin-top: var(--s4); }
.mb-0 { margin-bottom: 0; }
.mb-2 { margin-bottom: var(--s2); }
.mb-3 { margin-bottom: var(--s3); }
.mb-4 { margin-bottom: var(--s4); }

.spinner {
    display: inline-block;
    width: 1.125rem;
    height: 1.125rem;
    border: 2px solid currentColor;
    border-right-color: transparent;
    border-radius: 50%;
    animation: spin 0.7s linear infinite;
    vertical-align: -0.2em;
}
@keyframes spin { to { transform: rotate(360deg); } }

.loading-block {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--s3);
    padding: var(--s7);
    color: var(--text-muted);
}

@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        transition-duration: 0.01ms !important;
    }

    /* The view cross-fade (View Transitions) is already skipped in the
       script for these readers — withTransition() calls the swap directly.
       This is the belt to that braces, for any path that reaches
       startViewTransition without going through it. The universal rule above
       does not cover these: the pseudo-elements live outside the document
       tree that `*` matches. */
    ::view-transition-old(root),
    ::view-transition-new(root) { animation: none !important; }
}

/* Toast */
.toast-stack {
    position: fixed;
    left: 50%;
    bottom: calc(var(--s4) + env(safe-area-inset-bottom, 0px));
    transform: translateX(-50%);
    z-index: 200;
    display: grid;
    gap: var(--s2);
    width: min(26rem, calc(100vw - 2rem));
}

.toast {
    padding: var(--s3) var(--s4);
    border-radius: var(--radius-sm);
    background: var(--navy);
    color: var(--white);
    font-size: 0.9375rem;
    box-shadow: var(--shadow-lg);
    animation: toast-in 0.2s ease;
}
.toast--error   { background: var(--danger); }
.toast--success { background: var(--success); }

@keyframes toast-in {
    from { opacity: 0; transform: translateY(0.5rem); }
    to   { opacity: 1; transform: translateY(0); }
}

/* --------------------------------------------------------------------------
   Print — shopping lists get taken to the shop on paper

   Paper gets the list and nothing else: no header, no dashboard behind the
   modal, no buttons, no progress bar, no modal chrome. What survives is the
   heading, the sections, the rows and the total.
   -------------------------------------------------------------------------- */
@media print {
    @page { margin: 1.2cm; }

    html, body {
        background: #fff;
        font-size: 9.5pt;
        line-height: 1.25;
        color: #000;
        /* The screen locks scrolling while a modal is open; on paper that
           would clip the list to one page. */
        overflow: visible !important;
        height: auto !important;
    }

    /* Chrome, in one list. The dashboard sits BEHIND the open modal and used
       to print above the list — .app-main is the important one here. */
    .app-header,
    .modal__header,
    .modal__footer,
    .modal__close,
    .shop-toolbar,
    .shop-progress,
    .import-panel,
    .btn,
    .photo-btn,
    .photo-controls,
    .toast-stack,
    #message-modal,
    #idle-modal,
    #photo-modal,
    .no-print { display: none !important; }

    /* The dashboard sits BEHIND an open modal and used to print above the
       list. Scoped to modal-open so printing from the dashboard still works. */
    body.modal-open .app-main { display: none !important; }

    /* Only the open modal prints, and it prints as a plain document. */
    .modal {
        position: static;
        inset: auto;
        background: none;
        padding: 0;
        display: block;
        z-index: auto;
    }
    .modal[hidden] { display: none !important; }

    .modal__panel {
        max-width: none;
        max-height: none;
        width: auto;
        box-shadow: none;
        border-radius: 0;
        border: none;
        display: block;
        background: none;
    }
    .modal__body { overflow: visible; padding: 0; max-height: none; }

    /* The only heading on the page: shop, day, who saved it. */
    .print-header { display: block !important; margin: 0 0 0.6rem; }
    .print-header h2 { font-size: 13pt; margin: 0 0 0.15rem; }
    .print-header p  { margin: 0; font-size: 8.5pt; color: #000; }

    .shop-section { break-inside: auto; margin-bottom: 0.5rem; }

    .shop-section__title {
        background: none;
        color: #000;
        border-bottom: 1.5px solid #000;
        border-radius: 0;
        padding: 0 0 0.1rem;
        margin: 0.5rem 0 0.2rem;
        font-size: 10pt;
        font-weight: 700;
        /* A vendor heading alone at the foot of a page helps nobody. */
        break-after: avoid;
        break-inside: avoid;
    }

    /* One line per item, tight. */
    .shop-item {
        break-inside: avoid;
        border: none;
        border-bottom: 0.5pt solid #ccc;
        padding: 0.18rem 0;
        gap: 0.4rem;
        align-items: baseline;
    }
    .shop-item__meta { font-size: 8pt; color: #333; }
    .shop-item__cost { font-size: 8pt; color: #333; }
    .shop-item__qty  { font-weight: 700; }

    /* On screen a picked-up item goes grey to fade into the background. On
       paper grey is just harder to read, so it keeps the strikethrough and
       gets its ink back. */
    .shop-item.is-checked,
    .shop-item.is-checked .shop-item__name,
    .shop-item.is-checked .shop-item__meta,
    .shop-item.is-checked .shop-item__cost { color: #000; }

    /* Badges lose their pill so they read as plain text. */
    .badge {
        background: none !important;
        color: #000 !important;
        border: none;
        padding: 0;
        font-size: 8pt;
        font-weight: 400;
    }
    .badge::before { content: "("; }
    .badge::after  { content: ")"; }

    /* An empty box to tick off with a pen — but anything already ticked on
       screen prints ticked, so the printed list matches what has been picked
       up so far. */
    .shop-item__check {
        appearance: none;
        width: 11px;
        height: 11px;
        min-width: 11px;
        border: 1pt solid #000;
        border-radius: 1px;
        position: relative;
        margin: 0;
    }
    .shop-item.is-checked .shop-item__check::after {
        content: "\2713";
        position: absolute;
        inset: -4px 0 0 1px;
        font-size: 11px;
        line-height: 1;
    }

    /* Notes must survive onto paper — they are usually the reason someone
       needs to buy something unusual. */
    .note-card {
        break-inside: avoid;
        background: none;
        border: none;
        border-left: 2pt solid #000;
        padding: 0.1rem 0 0.1rem 0.4rem;
        margin: 0.15rem 0;
        font-size: 8.5pt;
    }
    .note-card__body { color: #000; }

    .shop-totals {
        border: none;
        border-top: 1.5pt solid #000;
        border-radius: 0;
        background: none;
        break-inside: avoid;
        margin-top: 0.5rem;
        padding-top: 0.25rem;
        font-size: 10pt;
    }

    .alert { border: 0.5pt solid #000; background: none; color: #000; padding: 0.3rem; }
    .empty-state { color: #000; }

    .table-scroll { overflow: visible; border: none; }
    .table th, .table td { border-color: #999; padding: 0.15rem 0.3rem; }
    .table thead th { background: none; border-bottom: 1.5pt solid #000; }
}

.print-header { display: none; }

/* ------------------------------------------------------------------ */
/* The staff schedule                                                  */
/* ------------------------------------------------------------------ */

/* The grid scrolls sideways inside its own wrapper on a phone; the modal
   never scrolls horizontally itself. On desktop the columns simply fit. */
.sched-scroll {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
}

.sched-grid {
    display: grid;
    /* Person rail plus seven equal days. The minmax floor is what makes a
       phone scroll instead of crushing seven columns into porridge. */
    grid-template-columns: 8.5rem repeat(7, minmax(8rem, 1fr));
    min-width: 64rem;
    background: var(--white);
}

.sched-corner { background: var(--gray-50); border-bottom: 1px solid var(--border); }

.sched-day {
    display: flex;
    flex-direction: column;
    padding: var(--s2);
    font-weight: 600;
    font-size: 0.8125rem;
    background: var(--gray-50);
    border-bottom: 1px solid var(--border);
    border-left: 1px solid var(--border);
}

.sched-day.is-today { background: rgba(84, 174, 166, 0.12); }
.sched-day__date { font-weight: 400; color: var(--text-muted); font-size: 0.75rem; }

.sched-person {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 2px;
    padding: var(--s2);
    border: none;
    border-bottom: 1px solid var(--border);
    background: var(--gray-50);
    text-align: left;
    cursor: pointer;
    font: inherit;
}

.sched-person__name { font-weight: 600; font-size: 0.8125rem; }
.sched-person__muted { margin-left: 4px; font-size: 0.7rem; }
.sched-person__hours { font-size: 0.7rem; color: var(--text-muted); }
/* What this person costs this week, under their hours. Managers only —
   the money never reaches any other payload. */
.sched-person__pay { font-size: 0.7rem; font-weight: 600; color: var(--text); }
.sched-person--view { cursor: default; }

.sched-cell {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: var(--s1);
    min-height: 3.4rem;
    border-bottom: 1px solid var(--border);
    border-left: 1px solid var(--border);
}

.sched-cell.is-over { background: rgba(84, 174, 166, 0.15); }

/* A thin strip along the bottom of the square, not a floating button.
   Floating it in the corner meant it sat ON TOP of the shift that filled the
   square, so tapping the bottom-right of a pill opened "add a shift" instead
   of editing the one you were pointing at. A strip cannot overlap anything:
   the chips flex into the space above it. */
.sched-cell__add {
    flex: none;
    width: 100%;
    height: 16px;
    line-height: 14px;
    border: none;
    background: none;
    color: var(--gray-400);
    font-size: 0.85rem;
    cursor: pointer;
    padding: 0;
    min-height: 0;
}

@media (hover: hover) {
    .sched-cell__add { opacity: 0; }
    .sched-cell:hover .sched-cell__add { opacity: 1; }
}

/* A chip FILLS its share of the square: flex-grow means one shift takes the
   whole cell, two split it evenly, and nothing floats small in a big box.
   Text is clipped with an ellipsis rather than allowed to bleed across the
   border — the full times are one tap away in the editor. */
.sched-chip {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    justify-content: center;
    gap: 1px;
    flex: 1 1 auto;
    align-self: stretch;
    width: 100%;
    min-width: 0;
    overflow: hidden;
    border: 1px solid var(--border);
    border-left: 3px solid var(--gray-400);
    border-radius: var(--radius-sm);
    background: var(--gray-50);
    padding: 3px 6px;
    font: inherit;
    font-size: 0.75rem;
    text-align: left;
    cursor: grab;
    max-width: 100%;
}

.sched-chip.is-dragging { opacity: 0.4; }

/* Edited since the last push: Square still holds the old version, and Square
   is what decides whether somebody may clock in. */
.sched-chip.is-stale { border-style: dashed; }
.sched-chip--view { cursor: default; }

.sched-chip__time {
    font-weight: 600;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100%;
}

.sched-chip__job {
    color: var(--text-muted);
    font-size: 0.7rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100%;
}

/* A note is the exceptional thing on a chip — "close the patio", "training
   Ana" — so it is the one line that should catch the eye. It used to be the
   faintest: muted grey, italic, and the smallest type on the shift. Now it
   reads like a highlighter mark over the top of it. */
.sched-chip__note {
    align-self: stretch;
    margin-top: 1px;
    padding: 1px 4px;
    border-radius: 3px;
    /* The default, for a shift with no job. A shift WITH one overrides both
       of these inline, tinted from that job's colour — a fixed orange band
       disappeared on an orange pill. */
    background: var(--warning-bg);
    box-shadow: inset 2px 0 0 var(--warning);
    color: var(--text);
    font-style: normal;
    font-weight: 600;
    font-size: 0.7rem;
    overflow-wrap: anywhere;
    overflow: hidden;
    max-width: 100%;
}

.sched-chip__pay {
    font-weight: 600;
    font-size: 0.7rem;
    white-space: nowrap;
}

/* The hours share the money's line, in the muted weight — the figure that
   matters most on a pill is what it costs, the hours are how you got there. */
.sched-chip__hours {
    font-weight: 400;
    font-size: 0.7rem;
    color: var(--text-muted);
}

.sched-off {
    align-self: flex-start;
    font-size: 0.65rem;
    font-weight: 700;
    letter-spacing: 0.04em;
    color: var(--orange-700);
    background: var(--warning-bg);
    border-radius: var(--radius-sm);
    padding: 1px 6px;
}

.sched-total {
    padding: var(--s1) var(--s2);
    font-size: 0.7rem;
    color: var(--text-muted);
    border-left: 1px solid var(--border);
    background: var(--gray-50);
}

/* Hours and money stack in the totals band; the corner carries the week's
   grand total. */
.sched-total > span { display: block; }
.sched-total__pay { font-weight: 600; color: var(--text); }

/* The positions editor: tick a job, name its pay. One row per job. */
.sched-positions__row {
    display: flex;
    align-items: center;
    gap: var(--s2);
    padding: var(--s1) 0;
    cursor: pointer;
}

.sched-positions__colors { padding: 0 0 var(--s2) 1.9rem; }

.sched-positions__rate {
    width: 6.5rem;
    flex: none;
    text-align: right;
}

/* The schedule's own action row: equal columns that reflow by width, so the
   four buttons sit two-by-two on a phone and in one line on a desktop —
   instead of three across with a lone orphan underneath. */
/* The cron command in Diagnostics: long, monospaced, and it must be readable
   and selectable on a phone rather than running off the edge. */
.diag-cron {
    display: block;
    padding: var(--s2);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--gray-50);
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 0.75rem;
    line-height: 1.5;
    overflow-wrap: anywhere;
    user-select: all;
}

.sched-actions {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
    gap: var(--s2);
}

.sched-actions .btn { width: 100%; }

/* Three actions of equal weight, filling the bar they sit in. The store
   name used to sit here as a growing span and squeezed them into a corner. */
.modal--schedule .modal__footer { gap: var(--s2); }
.modal--schedule .modal__footer .btn { flex: 1 1 0; min-width: 0; }

/* The tick for picking several shifts at once. Hidden until the mouse is
   over its chip; shown on every chip once anything is picked, or once Select
   is pressed — a phone has no hover, so that button is how it starts there. */
.sched-chip__tick {
    position: absolute;
    top: 2px;
    right: 2px;
    z-index: 2;
    margin: 0;
    opacity: 0;
    pointer-events: none;
}

.sched-grid.is-picking .sched-chip__tick,
.sched-chip:hover .sched-chip__tick,
.sched-chip__tick:checked {
    opacity: 1;
    pointer-events: auto;
}

/* Only where a mouse can actually hover — on a touch screen :hover sticks
   after a tap and would leave ticks scattered about. */
@media (hover: none) {
    .sched-chip:hover .sched-chip__tick { opacity: 0; pointer-events: none; }
    .sched-grid.is-picking .sched-chip__tick { opacity: 1; pointer-events: auto; }
}

.sched-chip.is-picked { outline: 2px solid var(--teal); outline-offset: -2px; }

.sched-pick {
    display: flex;
    align-items: center;
    gap: var(--s2);
    flex-wrap: wrap;
    margin-top: var(--s2);
    padding: var(--s2) var(--s3);
    border: 1px solid var(--teal);
    border-radius: var(--radius-sm);
    background: rgba(84, 174, 166, 0.12);
    font-size: 0.8125rem;
}

/* Something picked up on a phone: the bar that says so, and says how to put
   it down again. */
.sched-hold {
    display: flex;
    align-items: center;
    gap: var(--s2);
    flex-wrap: wrap;
    margin-top: var(--s2);
    padding: var(--s2) var(--s3);
    border: 1px solid var(--teal);
    border-radius: var(--radius-sm);
    background: rgba(84, 174, 166, 0.12);
    font-size: 0.8125rem;
}

/* While a shift is held, every square is somewhere it could go. */
.sched-grid.is-placing .sched-cell {
    background: rgba(84, 174, 166, 0.06);
    cursor: copy;
}

.sched-toolbar {
    display: flex;
    align-items: center;
    gap: var(--s2);
    margin-bottom: var(--s3);
}

.sched-weeklabel { font-weight: 600; flex: 1; text-align: center; }

.sched-sheet { max-width: 30rem; }

.sched-swatches { display: flex; gap: var(--s2); flex-wrap: wrap; }

.sched-swatch {
    width: 28px;
    height: 28px;
    border-radius: 50%;
    border: 2px solid transparent;
    cursor: pointer;
    min-height: 0;
    padding: 0;
}

.sched-swatch.is-on { border-color: var(--text); }

.sched-jobrow {
    display: grid;
    /* The name column has a FLOOR. As a bare 1fr it was crushed to about
       50px by the colour block and the three buttons beside it — the job
       names were all still there, just in a box too narrow to show a single
       letter of them. */
    grid-template-columns: minmax(11rem, 1fr) auto auto;
    gap: var(--s2) var(--s3);
    align-items: end;
    padding: var(--s2) 0;
    border-bottom: 1px solid var(--border);
}

/* On a phone there is no width to share: one thing per line, name first. */
@media (max-width: 36rem) {
    .sched-jobrow { grid-template-columns: 1fr; }
    .sched-jobrow .row { flex-wrap: wrap; }
}

.sched-dot {
    display: inline-block;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    margin-right: 6px;
}

/* The colour dot in a positions row is pressable — it folds the palette out
   underneath. Declared AFTER .sched-dot so its size wins: same specificity,
   and the later rule is the one that applies. */
.sched-dot--pick {
    /* A tap target, not a bullet: 10px on a phone is a coin toss. */
    width: 18px;
    height: 18px;
    border: 1px solid var(--border);
    padding: 0;
    cursor: pointer;
    flex: none;
}

/* The modifier-drag hint only means something where dragging exists. */
@media (hover: none) {
    .sched-draghint { display: none; }
}

/* The schedule takes the whole screen — it is a board, not a form — and the
   board sits centred in it. Applied by the schedule screens after open();
   modal.open() strips it for everything else. */
/* Full-screen mode: the board and nothing else.
   The header and the footer are the two bands that eat the most of a phone
   screen, and while you are laying out a week you need neither — so they go
   away and the grid gets their room back. One button puts them back. */
.modal--schedule.is-bare .modal__header,
.modal--schedule.is-bare .modal__footer,
.modal--schedule.is-bare .sched-draghint,
.modal--schedule.is-bare .sched-touchhint,
.modal--schedule.is-bare .sched-actions,
.modal--schedule.is-bare .cap-panel,
.modal--schedule.is-bare .sched-setting,
.modal--schedule.is-bare .alert {
    display: none;
}

/* Nothing above the board any more, so the body starts at the top of the
   screen rather than below a header that is no longer drawn. */
.modal--schedule.is-bare .modal__body { padding-top: var(--s2); }

/* The way back out. Small, fixed, and always reachable — a full-screen mode
   you cannot leave is a trap, and on iOS there is no browser chrome to fall
   back on. */
.sched-bare-exit {
    position: fixed;
    right: var(--s3);
    bottom: max(var(--s3), env(safe-area-inset-bottom, 0px));
    z-index: 30;
    box-shadow: var(--shadow-lg);
}

.modal--schedule:not(.is-bare) .sched-bare-exit { display: none; }

.modal--schedule .modal__panel {
    /* The same fixed-inset approach the phone layout already proved out —
       measured against the viewport directly, nothing in between to get it
       wrong. */
    position: fixed;
    inset: 0;
    width: auto;
    max-width: none;
    height: auto;
    max-height: none;
    border-radius: 0;
}

/* The BODY is the scroller, so it stays the full width of the screen and its
   scrollbar sits on the screen's right edge where a scrollbar belongs. Capping
   the body itself put the bar in the middle of a wide monitor, floating beside
   the content instead of at the edge — which reads as a second, inner window.
   The CONTENT is what gets centred and capped. */
.modal--schedule .modal__body { width: 100%; }

.modal--schedule .modal__body > * {
    max-width: 78rem;
    margin-left: auto;
    margin-right: auto;
}

.modal--schedule .sched-toolbar { justify-content: center; }

.modal--schedule .sched-scroll { margin: 0 auto; }

/* On a big screen the grid does not need to force a scroll width. */
@media (min-width: 70rem) {
    .modal--schedule .sched-grid { min-width: 0; }
}

/* The Square setup wizard: name, picker, saved-mark. */
.sched-setup__row {
    display: grid;
    grid-template-columns: minmax(8rem, 1fr) minmax(0, 2fr) 2rem;
    gap: var(--s2) var(--s3);
    align-items: center;
    padding: var(--s2) 0;
    border-bottom: 1px solid var(--border);
}

.sched-setup__name { font-weight: 600; min-width: 0; overflow-wrap: anywhere; }

/* ---------------------------------------------------------------------------
   Time cards

   A payroll table with two live inputs per row. The widths are set on the
   INPUTS rather than the columns because the table scrolls sideways on a
   phone: a percentage column would collapse the adjustment box to nothing at
   the exact width where somebody is trying to type into it.
   --------------------------------------------------------------------------- */
/* Full screen, the same fixed-inset approach the schedule maker proved out:
   measured against the viewport directly, with nothing in between to get it
   wrong. A payroll table is seven columns wide and the week is read ACROSS. */
.modal--timecards .modal__panel {
    position: fixed;
    inset: 0;
    width: auto;
    max-width: none;
    height: auto;
    max-height: none;
    border-radius: 0;
}

/* Everything centred in the room that gives us, rather than pinned left on a
   monitor wide enough to seat the whole table twice over — but centred by
   capping the CONTENT, never the body. The body is the scroller, and a capped
   scroller puts its bar in the middle of the screen. */
.modal--timecards .modal__body {
    width: 100%;
    text-align: center;
}

.modal--timecards .modal__body > * {
    max-width: 60rem;
    margin-left: auto;
    margin-right: auto;
}

/* The tables stay centred as blocks, but their CELLS read left and right the
   way a table has to — a centred money column cannot be scanned. */
.modal--timecards .table-scroll { margin: 0 auto; }
.modal--timecards .table { margin: 0 auto; text-align: left; }
.modal--timecards .table td.numeric,
.modal--timecards .table th.numeric { text-align: right; }
.modal--timecards .sched-toolbar { justify-content: center; }
.modal--timecards .alert { text-align: left; }

.tc-table { min-width: 40rem; width: 100%; }

.tc-table td, .tc-table th { vertical-align: middle; }

/* The employee's name opens their days. It has to look like the link it is —
   a name that silently does something is a name nobody presses. */
.tc-name {
    background: none;
    border: none;
    padding: 0;
    font: inherit;
    font-weight: 600;
    color: var(--navy);
    text-align: left;
    text-decoration: underline;
    text-underline-offset: 2px;
    cursor: pointer;
}

.tc-name:hover { color: var(--teal); }

.tc-adjust {
    width: 4.75rem;
    padding: var(--s1) var(--s2);
    text-align: right;
    font-variant-numeric: tabular-nums;
}

/* The sign lives beside the box, not inside it.
   iOS raises a decimal keypad with NO minus key, and an adjustment here is
   usually money coming off — an advance, a till short — so the commonest
   entry on this screen was the one a phone could not type. */
.tc-adjust-wrap {
    display: flex;
    align-items: center;
    gap: var(--s1);
    justify-content: flex-end;
}

.tc-sign {
    flex: none;
    width: 2.25rem;
    min-height: 2.25rem;
    padding: 0;
    font-size: 1.0625rem;
    font-weight: 700;
    line-height: 1;
    background: var(--surface);
    color: var(--text-muted);
    border-color: var(--border-strong);
}

.tc-sign:hover:not(:disabled) { background: var(--gray-50); }

/* Money coming OFF somebody's pay is worth seeing without reading the number.
   A payroll row that is taking money away should not look identical to one
   that is adding it. */
.tc-sign--minus {
    background: var(--danger-bg);
    color: var(--danger);
    border-color: var(--danger);
}

.tc-sign--minus:hover:not(:disabled) { background: var(--danger-bg); }

.tc-type { width: 7rem; padding: var(--s1) var(--s2); }

/* The totals line is a conclusion, not another row of data. */
.tc-totals td {
    font-weight: 700;
    border-top: 2px solid var(--border-strong);
    background: var(--gray-50);
}

/* The two cash tables are narrow by nature — two and three columns. Their
   SCROLLER has to be narrow with them, or the bordered box stretches the full
   width of a monitor with a small table adrift in the middle of it. */
.tc-money { max-width: 26rem; margin-left: auto; margin-right: auto; }

/* Who gets cash, and what it is made of — one above the other on a phone,
   where there is only ever room for one column. */
.tc-pair { display: grid; gap: var(--s3); justify-content: center; }

/* Side by side once there is room for both AND the gap between them. Below
   this the two would be squeezed to about eleven characters each, which is
   narrower than "Edwin Quezada Alvarado". */
@media (min-width: 56rem) {
    .tc-pair {
        grid-template-columns: repeat(2, minmax(0, 26rem));
        align-items: start;
    }

    /* They are grid cells now, so the auto margins have nothing to centre
       against — the grid does it. */
    .tc-pair .tc-money { margin-left: 0; margin-right: 0; width: 100%; }
}

.tc-cash, .tc-packets { min-width: 0; width: 100%; }

.tc-cash tfoot td,
.tc-packets tfoot td {
    font-weight: 700;
    border-top: 2px solid var(--border-strong);
}

/* One day off somebody's card, in the sheet behind their name. */
.tc-day {
    text-align: left;
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: var(--s3);
    padding: var(--s2) 0;
    border-bottom: 1px solid var(--border);
}

.tc-day:last-of-type { border-bottom: none; }

.tc-day__when { display: flex; flex-direction: column; gap: 2px; min-width: 0; }

.tc-day__nums {
    display: flex;
    flex-direction: column;
    gap: 2px;
    text-align: right;
    font-variant-numeric: tabular-nums;
    flex-shrink: 0;
}

/* The off-screen box the clipboard fallback types into. Not display:none —
   a hidden field cannot be selected, and selecting it is the whole trick. */
.tc-clip {
    position: fixed;
    top: -100rem;
    left: -100rem;
    opacity: 0;
}

/* Somebody whose login is off but who is on this week's payroll. Quiet, next
   to the name, because it explains a name rather than warning about it. */
.tc-former { display: block; }

/* Already paid: struck through and stood down, but still there.

   The row is not removed. The week has to keep reading as the whole week, and
   a name that disappeared when you ticked it would be a name you could not
   find again to untick.

   line-through is set on the CELLS rather than the row, because a <tr> is not
   a box the decoration propagates out of in every engine. The tick's own cell
   is exempt: the one control that has to stay legible is the one that undoes
   this. Colour rather than opacity, so the checkbox and the name button below
   can be put back to full strength — a child cannot be more opaque than its
   parent. */
.tc-paid td { text-decoration: line-through; color: var(--text-muted); }
.tc-paid .tc-name { color: var(--text-muted); }
.tc-paid td.tc-paid-cell { text-decoration: none; }

/* The controls keep working, so they keep looking like controls. Struck-out
   text inside a select box reads as a rendering fault rather than as a state,
   and the copy button's icon has no text to strike at all. */
.tc-paid .input,
.tc-paid .select,
.tc-paid .btn { text-decoration: none; }

.tc-paid-cell { text-align: center; }
.tc-paid-cell input { width: 1.15rem; height: 1.15rem; }

/* A narrative box on a document — "what happened", "what has to change".
   Prose, not a template body, so it reads in the page's own face rather than
   the monospace the template editor wants. Taller than the base too: the size
   of the box is what tells somebody how much they are expected to write. */
.doc-long {
    font-family: inherit;
    font-size: 1rem;
    line-height: 1.5;
    min-height: 9rem;
}

/* --------------------------------------------------------------------------
   Motion

   One section rather than a transition tacked onto forty rules, so the whole
   feel of the app can be read — and reverted — in one place.

   THE DIVISION OF LABOUR. Three mechanisms, each doing the job it is best at,
   and none of them overlapping:

     - Leaving a screen, and the home page redrawing, are View Transitions.
       The browser cross-fades two snapshots; the script drives it
       (withTransition in app.js). Those are whole-view swaps.
     - A screen's CONTENT arriving is a CSS entrance, below. render() puts new
       nodes in the body and a new node runs its animation on insertion, which
       costs nothing and needs no hook.
     - Everything you touch — buttons, rows, folds, fields — is a plain
       transition on a state change.

   Deliberately NOT animated: the dashboard's own cards. That swap already
   goes through a View Transition, and a cross-fade running at the same time
   as the children fading in is two animations fighting over the same pixels.

   ONLY transform and opacity, everywhere. Those two are handed to the
   compositor and cost nothing per frame; animating height, top or width makes
   the browser re-lay-out the page every frame, which on the oldest till in
   the building is the difference between smooth and visibly stepping.

   Reduced motion is handled once, globally, by the
   @media (prefers-reduced-motion: reduce) block above — it forces every
   duration in the app to 0.01ms with !important, so everything here collapses
   to the instant version without a single rule needing to opt out.
   -------------------------------------------------------------------------- */

@keyframes rise-in  { from { opacity: 0; transform: translateY(8px); } }
@keyframes panel-in { from { opacity: 0; transform: translateY(16px); } }
@keyframes veil-in  { from { background-color: rgba(21, 49, 67, 0); } }
@keyframes fold-in  { from { opacity: 0; transform: translateY(-4px); } }

/* The screen arriving ---------------------------------------------------- */

/* The dim behind the panel fades up rather than snapping on, which is most of
   why a sheet reads as "sliding over" instead of "replacing". */
.modal:not([hidden]) { animation: veil-in var(--dur-base) var(--ease) both; }

.modal:not([hidden]) .modal__panel {
    animation: panel-in var(--dur-enter) var(--ease-enter) both;
}

/* A screen's content, every time render() draws it — including navigating
   from one screen to the next inside an already-open panel, which is the
   common case and the one that used to snap.

   Direct children only. A count sheet can hold two hundred rows, and
   animating every row is two hundred animations for one visual event; the
   half-dozen blocks the body is actually made of give the same effect. */
.modal__body > * { animation: rise-in var(--dur-base) var(--ease-enter) both; }

/* A gentle cascade over the first few blocks. Capped on purpose: past the
   sixth the delay is longer than the animation, and the screen starts feeling
   slow rather than considered. */
.modal__body > *:nth-child(2) { animation-delay: 20ms; }
.modal__body > *:nth-child(3) { animation-delay: 40ms; }
.modal__body > *:nth-child(4) { animation-delay: 60ms; }
.modal__body > *:nth-child(5) { animation-delay: 80ms; }
.modal__body > *:nth-child(n+6) { animation-delay: 100ms; }

/* The login card is the first thing anybody sees each shift. */
.login__card { animation: panel-in var(--dur-enter) var(--ease-enter) both; }

/* Things you touch ------------------------------------------------------- */

.btn {
    transition: background-color var(--dur-quick) var(--ease),
                border-color var(--dur-quick) var(--ease),
                color var(--dur-quick) var(--ease),
                box-shadow var(--dur-quick) var(--ease),
                transform var(--dur-press) var(--ease);
}

/* The press already moved the button 1px; this is what makes it feel like a
   button rather than a jump. */
.btn:active:not(:disabled) { transition-duration: var(--dur-press); }

.icon-btn,
.modal__back,
.modal__help,
.modal__close,
.search-clear,
.search-field__clear {
    transition: background-color var(--dur-quick) var(--ease),
                color var(--dur-quick) var(--ease),
                transform var(--dur-press) var(--ease);
}

.icon-btn:active,
.modal__back:active,
.modal__help:active { transform: scale(0.94); }

/* Rows and cards --------------------------------------------------------- */

.card,
.store-card {
    transition: box-shadow var(--dur-base) var(--ease),
                transform var(--dur-base) var(--ease);
}

/* Only where a press actually leads somewhere. A card that gives under the
   finger and then does nothing is a worse lie than a card that sits still. */
.store-card:active { transform: scale(0.994); }

.settings-row,
.run-row,
.shop-item,
.fc-row,
.toggle-row,
.list-row {
    transition: background-color var(--dur-quick) var(--ease);
}

.chip,
.location-chip,
.sched-chip,
.lang-switch__option,
.tab-head__tab {
    transition: background-color var(--dur-quick) var(--ease),
                border-color var(--dur-quick) var(--ease),
                color var(--dur-quick) var(--ease);
}

/* Fields ----------------------------------------------------------------- */

/* Retokenised from a hard-coded 0.15s so the fields move on the same clock as
   everything else. */
.input, .select, .textarea {
    transition: border-color var(--dur-quick) var(--ease),
                box-shadow var(--dur-quick) var(--ease);
}

.pin-digit {
    transition: border-color var(--dur-quick) var(--ease),
                box-shadow var(--dur-quick) var(--ease),
                transform var(--dur-press) var(--ease);
}

/* The one place a little scale is worth it: typing a PIN is four separate
   moments and the box growing slightly confirms each keystroke landed. */
.pin-digit:focus { transform: scale(1.04); }

/* Fold-away panels ------------------------------------------------------- */

/* The chevrons already rotated — they just did it in one frame. */
.vendor-entry__summary::before,
.setting-item__summary::before,
.cap-panel__summary::before {
    transition: transform var(--dur-base) var(--ease);
}

.vendor-entry__summary,
.setting-item__summary,
.cap-panel__summary {
    transition: background-color var(--dur-quick) var(--ease);
}

/* What is inside a fold, when it opens. <details> cannot animate its own
   height without JavaScript, and animating height would be the wrong tool
   anyway — this fades the contents in instead, which reads as the panel
   opening without costing a layout per frame. */
.vendor-entry[open] > *:not(summary),
.setting-item[open] > *:not(summary),
.cap-panel[open] > *:not(summary) {
    animation: fold-in var(--dur-quick) var(--ease-enter) both;
}

/* Toasts ----------------------------------------------------------------- */

.toast { animation: toast-in var(--dur-base) var(--ease-enter); }

/* The per-person warning under every custom box on a bulk send.

   Half the standard templates ask for something that is a fact about ONE
   person — a key holder agreement asks how many keys. Typed once and stamped
   on nine signed documents, that is worse than no document, so the warning is
   on every box rather than a guessed subset, and it is the colour of a warning
   rather than a hint. */
.tc-warn {
    color: var(--danger);
    display: block;
    margin-top: var(--s1);
}
