/* ==========================================================================
   LΛMΣLΛ CAFE + ROASTERY AND BAKEHOUSE — DESIGN SYSTEM & STYLESHEET
   ========================================================================== */

/* 0. BRAND WEBFONT — Libertinus Serif (headings, brand guide §3.1)
   ---------------------------------------------------------------------------
   Self-hosted because Libertinus is not on Google Fonts. Files, licence and
   the subsetting recipe are in public/assets/fonts/ — see the README there.

   Only 400 and 700 are declared. Headings ask for 700 or 800; with a real 700
   present, 800 resolves to it and the browser does NOT synthesise a fake bold.
   Regular is here so a heading later set at normal weight still gets a drawn
   face rather than a synthesised one.

   font-display: swap — the heading is readable in the fallback serif from the
   first paint rather than invisible while 23 KB arrives. The face is metrically
   unlike Georgia, so there is a visible reflow on swap; that is the better
   trade for a menu you may be reading on café wifi. */
@font-face {
    font-family: 'Libertinus Serif';
    src: url('/assets/fonts/LibertinusSerif-Regular.woff2') format('woff2');
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: 'Libertinus Serif';
    src: url('/assets/fonts/LibertinusSerif-Bold.woff2') format('woff2');
    font-weight: 700;
    font-style: normal;
    font-display: swap;
}

/* 1. CSS CUSTOM PROPERTIES */

/* ---------------------------------------------------------------------------
   BRAND PALETTE — Lamela Brand Guidelines v1.0, August 2026 (Pondr Co.)
   See docs/brand-guidelines.md. These are the raw brand values and are the
   only source of colour in this stylesheet: every semantic token below is
   built from them, and tints are expressed as alpha over a brand colour
   rather than as new hex values, so nothing off-palette creeps back in.
   --------------------------------------------------------------------------- */
:root {
    /* Primary */
    --brand-cream:        #F0E7D6;
    --brand-taupe:        #D0BCA4;
    --brand-warm-brown:   #A58669;
    --brand-espresso:     #70543B;
    --brand-chocolate:    #332923;

    /* Secondary & accent — use selectively, primaries stay dominant */
    --brand-sky-blue:     #78B1C9;
    --brand-sage:         #B8CD80;
    --brand-ochre:        #D7AF67;
    --brand-peach:        #EB867A;
    --brand-dusty-rose:   #D590AD;
    --brand-teal:         #349BB8;
    --brand-moss:         #556C3E;
    --brand-bronze:       #B99459;
    --brand-terracotta:   #D65762;
    --brand-plum:         #B35D8E;
}

/* ---------------------------------------------------------------------------
   SEMANTIC ROLES — what the interface actually references.
   Mapped onto the palette above; contrast ratios noted where they drove the
   choice (WCAG AA is 4.5:1 for body text, 3:1 for large text and UI edges).
   --------------------------------------------------------------------------- */
:root {
    /* Espresso, not the old amber: the brand has no saturated primary, and its
       own website mockup (guidelines p.27) shows brown actions. White-on-amber
       measured 2.11:1 and failed AA outright; white-on-espresso is 6.95:1. */
    --color-primary: var(--brand-espresso);
    /* Hover darkens to the next colour down the primary ramp rather than to an
       invented shade. White-on-chocolate is 14.17:1. */
    --color-primary-hover: var(--brand-chocolate);

    --color-dark: var(--brand-chocolate);
    /* Chocolate is already the darkest brand colour, so its hover lifts toward
       espresso instead of darkening past the palette. */
    --color-dark-hover: var(--brand-espresso);

    --color-text-dark: var(--brand-chocolate);      /* 14.17:1 on white */
    /* Muted Warm Brown is the intuitive pick here and fails at every text size
       (3.38:1 on white, 2.75:1 on cream), so muted copy uses espresso — still
       clearly recessed from chocolate body text, but readable at 6.95:1. */
    --color-text-muted: var(--brand-espresso);

    /* Placeholders. Espresso made them read as typed-in values rather than as
       hints -- a field showing "120.00" in the same brown as a real figure
       looks filled in, and on this admin that is the difference between a
       price somebody set and one they have not. Grey says "not yours yet".
       Warm-neutral rather than a dead grey, so it belongs to a palette built
       out of browns: 5.29:1 on white and 4.31:1 on cream, both clear of AA,
       and plainly lighter than the 14.17:1 of text somebody actually typed. */
    --color-text-placeholder: #6F6B66;

    /* Text and icons sitting ON a brand-coloured fill. Cream, not white: the
       palette has no white in it, and a pure white label read colder than
       everything around it on a page built out of warm browns. It clears AA
       on every fill it lands on -- espresso 5.67:1, chocolate 11.55:1,
       terracotta 4.83:1, moss 4.75:1. This is a foreground role only; white
       page and card BACKGROUNDS are untouched and stay --color-bg-light. */
    --color-text-on-dark: var(--brand-cream);

    --color-bg-light: #FFFFFF;
    --color-bg-cream: var(--brand-cream);
    --color-bg-tan: var(--brand-taupe);
    /* The barely-there off-white used for inset panels and striped rows. A
       wash of brand cream rather than a grey, so it stays in the warm family. */
    --color-bg-soft: rgba(240, 231, 214, 0.38);
    /* The same wash, already flattened over white. A translucent colour on
       <body> propagates to the canvas with nothing opaque behind it, so the
       page picked up whatever the browser painted underneath -- pale in a
       light window, murky in a dark one or inside any embedded view. This is
       what 38% cream over white actually comes to, and it stays that colour
       everywhere. */
    --color-bg-page: #F9F6EF;

    /* How wide admin content is allowed to get. Every screen used to pick its
       own number -- 780, 1000, 1180, or none at all -- so moving between them
       made the content jump width and the panel felt like several products.
       Two measures: a reading width for forms and documents, and a wide one
       for anything with a table in it. Lists take neither and fill the frame. */
    --admin-measure-form: 820px;
    --admin-measure-wide: 1200px;

    --color-border: var(--brand-taupe);
    --color-border-light: rgba(208, 188, 164, 0.45);
    /* Form fields get the darker warm brown rather than taupe. A control's
       edge is meaningful UI, not decoration, so it wants 3:1 — taupe on white
       is only 1.84:1, warm brown clears it at 3.38:1. */
    --color-border-input: var(--brand-warm-brown);

    /* Status colours. The brand book defines none, so these are the closest
       accent colours that carry the conventional meaning. Text-weight variants
       are darkened because the accents themselves are too light to read:
       Terracotta is 3.89:1 on white, below the 4.5:1 body-text bar. */
    --color-success: var(--brand-moss);             /* 5.83:1 on white */
    --color-success-bg: rgba(184, 205, 128, 0.22);
    --color-danger: #B23843;                        /* terracotta, darkened */
    --color-danger-bg: rgba(214, 87, 98, 0.14);
    --color-warning: var(--brand-espresso);
    --color-warning-bg: rgba(215, 175, 103, 0.22);
    --color-info: #256E82;                          /* teal, darkened */
    --color-info-bg: rgba(120, 177, 201, 0.20);

    /* ---- Accent inks ----
       The accent palette is built for fills and illustration, and most of it
       is far too light to set text in: sky blue is 2.35:1 on white, ochre
       2.06:1, sage 1.74:1. Each ink below is its brand accent darkened along
       its own hue and saturation until it clears 4.5:1, so the colour still
       reads as that accent while the label stays legible. Use the brand value
       for a fill or a tint, the ink for anything with words in it. */
    --ink-sky:        #537B8C;   /* 4.59:1 */
    --ink-sage:       #6F7B4D;   /* 4.55:1 */
    --ink-ochre:      #8A7142;   /* 4.64:1 */
    --ink-peach:      #AB6259;   /* 4.54:1 */
    --ink-rose:       #9A687D;   /* 4.52:1 */
    --ink-teal:       #2B7F97;   /* 4.58:1 */
    --ink-moss:       #556C3E;   /* 5.83:1 — already passes, unchanged */
    --ink-bronze:     #8E7144;   /* 4.57:1 */
    --ink-terracotta: #C4505A;   /* 4.52:1 */
    --ink-plum:       #AE5A8A;   /* 4.54:1 */

    /* ---- Meaning-carrying accents ----
       One idea, one colour, everywhere it appears. The order lifecycle is the
       reason this exists: "new" and "confirmed" were previously both the same
       info blue, so the two states a cashier most needs to tell apart at a
       glance looked identical. Primaries still dominate — these are badges,
       rules and small marks, never large surfaces. */
    --accent-new: var(--ink-sky);
    --accent-new-bg: rgba(120, 177, 201, 0.18);
    --accent-confirmed: var(--ink-teal);
    --accent-confirmed-bg: rgba(52, 155, 184, 0.15);
    --accent-active: var(--ink-ochre);
    --accent-active-bg: rgba(215, 175, 103, 0.24);
    --accent-ready: var(--ink-moss);
    --accent-ready-bg: rgba(184, 205, 128, 0.26);

    --accent-dine-in: var(--ink-plum);
    --accent-dine-in-bg: rgba(179, 93, 142, 0.14);
    --accent-pickup: var(--ink-teal);
    --accent-pickup-bg: rgba(52, 155, 184, 0.13);

    --accent-money: var(--ink-bronze);
    --accent-orders: var(--ink-teal);
    --accent-people: var(--ink-plum);
    --accent-reward: var(--ink-ochre);

    /* Typography — brand guide §3.
     *
     * Headings are Libertinus Serif, self-hosted from the @font-face block at
     * the top of this file (it is not on Google Fonts). Playfair Display, the
     * interim stand-in this site used before the real face was available, is
     * gone from both the stack and base.php.
     *
     * Georgia is the fallback rather than a generic `serif` so a machine that
     * fails to load the webfont still gets a real book face — and it is what
     * shows during the font-display: swap window. */
    --font-heading: 'Libertinus Serif', Georgia, serif;
    --font-body: 'Roboto', system-ui, -apple-system, sans-serif;
    /* §3.2 — for order numbers, codes and receipts. Long digit strings are
       easier to read back over a counter when the figures line up. */
    --font-mono: 'Roboto Mono', ui-monospace, 'SF Mono', Menlo, monospace;

    /* The other two accent faces from §3.2. Declared so there is one place to
       change them, but deliberately NOT loaded in base.php — nothing uses
       them yet. Add the family to that <link> before first use, or the token
       silently falls back to a system cursive. Both are for sparing,
       decorative use only: never body copy, never UI labels. */
    --font-accent-display: 'Knewave', cursive;   /* brushy display */
    --font-accent-hand: 'Caveat', cursive;       /* handwritten */

    /* §3.3 type scale, straight from the brand book (p.19). Size/line-height
     * pairs, so a heading never needs a separate line-height alongside it.
     *
     * Note on the body sizes: the book's "Body" is 14px and its "Large Body"
     * is 16px. This site's base stays 16px — i.e. Large Body is the default
     * running text, and 14px is the smaller/secondary size. Dropping the base
     * to 14px would shrink every page at once, which is a much bigger change
     * than a typeface swap and is not what the scale is asking for. */
    --text-display-1: 700 3.5rem/4rem var(--font-heading);      /* 56/64 */
    --text-display-2: 700 3rem/3.5rem var(--font-heading);      /* 48/56 */
    --text-display-3: 700 2.25rem/2.75rem var(--font-heading);  /* 36/44 */
    --text-h1: 700 2rem/2.5rem var(--font-heading);             /* 32/40 */
    --text-h2: 700 1.75rem/2.25rem var(--font-heading);         /* 28/36 */
    --text-h3: 700 1.5rem/2rem var(--font-heading);             /* 24/32 */
    --text-body-lg: 400 1rem/1.5rem var(--font-body);           /* 16/24 */
    --text-body: 400 0.875rem/1.25rem var(--font-body);         /* 14/20 */

    --container-max-width: 1200px;
    --radius-sm: 8px;
    --radius-md: 14px;
    --radius-lg: 20px;
    --radius-pill: 50px;
    
    --shadow-sm: 0 2px 8px rgba(0,0,0,0.06);
    --shadow-md: 0 6px 20px rgba(0,0,0,0.08);
    --shadow-lg: 0 12px 35px rgba(0,0,0,0.12);
    
    --transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}

/* reCAPTCHA v3.
 *
 * The floating badge is hidden because it is fixed to the bottom-right, which
 * is exactly where the cart floater and the live-order pill already sit — three
 * fixed elements stacking in one corner on a phone. Google's terms permit
 * hiding it only if the attribution appears in the form instead, which is what
 * Recaptcha::notice() renders below. Do not remove one without the other. */
.grecaptcha-badge {
    visibility: hidden;
}

/* The checkbox fallback, shown only after a low score. Centred because it sits
   directly above the submit button on forms that are themselves centred. */
.recaptcha-challenge {
    margin: 18px 0 4px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
}

.recaptcha-challenge-note {
    font-size: 0.82rem;
    color: var(--color-text-muted);
}

.recaptcha-notice {
    margin-top: 14px;
    font-size: 0.72rem;
    line-height: 1.5;
    color: var(--color-text-muted);
    text-align: center;
}

.recaptcha-notice a {
    color: var(--color-primary);
    text-decoration: underline;
}

/* 2. GLOBAL RESETS */
*, *::before, *::after {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

html {
    scroll-behavior: smooth;
    font-size: 16px;
}

body {
    font-family: var(--font-body);
    color: var(--color-text-dark);
    background-color: var(--color-bg-light);
    line-height: 1.6;
    -webkit-font-smoothing: antialiased;
}

a {
    color: inherit;
    text-decoration: none;
    transition: var(--transition);
}

img {
    max-width: 100%;
    height: auto;
    display: block;
}

/* Form controls inherit the page's face and size, but NOT its colour -- the
   user agent sets that to its own `fieldtext`, which is pure black. So every
   value a customer typed, on every form on the site, was rendering in a colour
   that is not in the palette at all, next to labels that were. Chocolate is
   the brand's text colour and what the rest of the page is already using.
   Found by auditing computed styles on the contact form, not by eye: at small
   sizes black and chocolate are very hard to tell apart. */
button, input, select, textarea {
    font-family: inherit;
    font-size: inherit;
    color: var(--color-text-dark);
}

/* iOS Safari tints an unstyled <button>'s glyph/text with the system blue
   accent color even when a custom background is set, unless the native
   control appearance is explicitly turned off — this is what made the
   quantity "-" buttons (and any other plain <button> relying only on
   inline background/border, e.g. qty-plus, addon-qty-minus,
   summary-qty-minus) render with a blue "-" on iOS despite the CSS
   elsewhere never mentioning blue at all. */
button {
    -webkit-appearance: none;
    appearance: none;
    -webkit-tap-highlight-color: transparent;
}

/* 3. TYPOGRAPHY & HEADINGS */
h1, h2, h3, h4, h5, h6 {
    font-family: var(--font-heading);
    color: var(--color-text-dark);
    line-height: 1.25;
    font-weight: 700;
}

.section-title {
    font-family: var(--font-heading);
    font-size: 2.2rem;
    letter-spacing: 1.5px;
    margin-bottom: 8px;
    text-transform: uppercase;
}

.section-subtitle {
    font-family: var(--font-body);
    font-size: 1rem;
    color: var(--color-text-muted);
    margin-bottom: 40px;
    font-weight: 400;
}

.text-uppercase { text-transform: uppercase; }
.text-center { text-align: center; }
.text-left { text-align: left; }
.text-right { text-align: right; }

/* 4. LAYOUT CONTAINERS */
.container {
    max-width: var(--container-max-width);
    margin: 0 auto;
    padding: 0 24px;
}

.section {
    padding: 70px 0;
}

.section-bg-cream {
    background-color: var(--color-bg-cream);
}

.grid {
    display: grid;
    gap: 24px;
}

.grid-2 { grid-template-columns: repeat(2, 1fr); }
.grid-3 { grid-template-columns: repeat(3, 1fr); }
.grid-4 { grid-template-columns: repeat(4, 1fr); }
.grid-5 { grid-template-columns: repeat(5, 1fr); }

/* 5. BUTTON SYSTEM (MATCHING MOCKUP) */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 12px 28px;
    border-radius: var(--radius-pill);
    font-size: 0.88rem;
    font-weight: 700;
    letter-spacing: 1px;
    text-transform: uppercase;
    cursor: pointer;
    /* Was 2px. Every button on the site inherits its border width from here,
       including small ones like SIZES/SAVE/HIDE at .btn-sm size, where 2px
       reads as noticeably heavier than the 1px capsules used everywhere else
       on the page (search/sort/category pills). box-sizing is border-box
       globally, so this alone does not shift any button's outer size. */
    border: 1px solid transparent;
    transition: var(--transition);
    white-space: nowrap;
}

.btn-primary {
    background-color: var(--color-primary);
    color: var(--color-text-on-dark);
    border-color: var(--color-primary);
}

.btn-primary:hover {
    background-color: var(--color-primary-hover);
    border-color: var(--color-primary-hover);
    transform: translateY(-2px);
    box-shadow: 0 4px 15px rgba(112, 84, 59, 0.3);
}

/* The solid button for use over photography — currently the hero's ORDER NOW,
   sitting next to the cream outline button. It was a pure white pill, which
   was the last white surface left on a page otherwise built out of warm
   browns, and next to its cream-outlined neighbour the mismatch was the thing
   you noticed first. Cream fill, chocolate label: 11.55:1.

   Hover darkens to taupe (7.71:1) rather than the translucent
   --color-border-light it used before. That was 45% alpha, so on hover the
   hero photograph showed straight through the button. */
.btn-cream {
    background-color: var(--color-bg-cream);
    color: var(--color-text-dark);
    border-color: var(--color-bg-cream);
}

.btn-cream:hover {
    background-color: var(--color-bg-tan);
    border-color: var(--color-bg-tan);
    transform: translateY(-2px);
}

.btn-outline {
    background-color: transparent;
    color: var(--color-text-dark);
    border-color: var(--color-text-dark);
}

/* Kitchen / Barista / Background queue filter (spec V3-17). Matches
   .btn-outline's palette since it sits on the light page background, one row
   below the dark cashier header. */
.queue-filter-btn {
    background-color: transparent;
    color: var(--color-text-dark);
    border: 1px solid var(--color-border-light);
}

.queue-filter-btn:hover {
    border-color: var(--color-text-dark);
}

.queue-filter-btn.active {
    background-color: var(--color-dark);
    color: var(--color-text-on-dark);
    border-color: var(--color-dark);
}

.btn-outline:hover {
    background-color: var(--color-dark);
    color: var(--color-text-on-dark);
    border-color: var(--color-dark);
    transform: translateY(-2px);
}

/* An outline button sitting ON the cream panel, where a transparent fill would
   leave it barely separated from its background.
 *
 * The white resting fill was an inline `style="background: white"`. An inline
 * declaration outranks every stylesheet rule, so it also survived :hover — the
 * fill stayed white while the label turned cream, and the button read as empty.
 * Same shape of bug as the .btn-danger-outline note above.
 *
 * Declared here instead, with an explicit hover at higher specificity than
 * .btn-outline:hover so the two cannot be reordered into disagreeing. Hover is
 * deliberately identical to a plain .btn-outline: the two sit on the same page
 * and should not behave differently under the cursor. */
.btn-outline.btn-on-cream {
    background-color: #FFFFFF;
}

.btn-outline.btn-on-cream:hover {
    background-color: var(--color-dark);
    color: var(--color-text-on-dark);
    border-color: var(--color-dark);
}

.btn-outline-white {
    background-color: transparent;
    color: var(--color-text-on-dark);
    border-color: var(--color-text-on-dark);
}

.btn-outline-white:hover {
    background-color: var(--color-text-on-dark);
    color: var(--color-text-dark);
    transform: translateY(-2px);
}

/* Destructive outline button — "CANCEL THIS ORDER", "CANCEL ORDER".
 *
 * This exists because the obvious spelling does not work. Those buttons used
 * `.btn-outline` with the red as an inline `style="color: #D9534F"`, and an
 * inline declaration outranks any stylesheet rule — including
 * `.btn-outline:hover`, which sets the on-dark text colour. So the hover state got its
 * near-black fill from the hover rule while the text stayed red: red on black,
 * which is what made the button look broken. Worse on a phone, where :hover
 * sticks after a tap, so it was the state customers actually saw.
 *
 * Same trap as the .btn-outline/:hover specificity tie noted further down this
 * file. A variant that declares both of its own states cannot fall into it.
 *
 * Both states clear WCAG AA for normal text: --color-danger (brand terracotta
 * darkened to #B23843) is 5.93:1 on white, and white on it is the same ratio
 * inverted. The raw brand terracotta is kept for the resting border, where it
 * is decoration rather than text — as a hover fill it would only reach 3.89:1,
 * which this bold 0.8rem label is too small to qualify for. */
.btn-danger-outline {
    background-color: transparent;
    color: var(--color-danger);
    border-color: var(--brand-terracotta);
}

.btn-danger-outline:hover {
    background-color: var(--color-danger);
    color: var(--color-text-on-dark);
    border-color: var(--color-danger);
    transform: translateY(-2px);
}

.btn-sm {
    padding: 8px 20px;
    font-size: 0.8rem;
}

/* Voucher row actions (Admin > Vouchers). EDIT and STOP are the same style
   and size class but different text, so at their natural width they measured
   73px and 79px side by side - the same button reading two different sizes
   in the same row. A shared minimum, wide enough for the longer label plus a
   little breathing room, makes every action in the row - now and any added
   later - land on one size instead of each fitting its own word. */
.voucher-action-btn { min-width: 84px; }

.btn-lg {
    padding: 14px 34px;
    font-size: 0.95rem;
}

/* Circular +/− controls: the glyph is DRAWN, not typeset.
 *
 * These buttons flex-centre a "+" or "-" text node, and the centring is
 * correct — the line box lands dead centre. The glyph inside it does not.
 * "+" sits on the font's math axis, which is not the middle of its line box,
 * so on a 36px button the plus rendered about 2px low. Measured rather than
 * eyeballed: canvas actualBoundingBox puts Inter's "+" ink centre 2px below
 * the line-box centre at 22.4px.
 *
 * A translateY nudge would fix it for exactly one typeface. The body face has
 * since moved from Inter to Roboto, which is exactly the re-measuring this
 * avoided: the strokes are drawn with pseudo-elements instead — geometrically
 * centred, identical in any font, and the "-" gets a real bar rather than a
 * short high hyphen. The Inter measurement above is kept as the record of why
 * this is drawn rather than typeset; it no longer describes the live font.
 *
 * font-size: 0 hides the original text node, and needs !important because
 * every one of these buttons sets its font-size inline (several are built in
 * JS), and an inline declaration outranks any stylesheet rule. The text node
 * is left in the markup so nothing needs to change in the templates or the
 * JS that generates them. */
.btn-customize-open,
.qty-plus, .qty-minus,
.addon-qty-plus, .addon-qty-minus,
.summary-qty-plus, .summary-qty-minus {
    position: relative;
    font-size: 0 !important;
}

.btn-customize-open::before,
.qty-plus::before, .qty-minus::before,
.addon-qty-plus::before, .addon-qty-minus::before,
.summary-qty-plus::before, .summary-qty-minus::before,
.btn-customize-open::after,
.qty-plus::after,
.addon-qty-plus::after,
.summary-qty-plus::after {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    background-color: currentColor;
    border-radius: 1px;
}

/* Horizontal arm — both the plus and the minus have one. */
.btn-customize-open::before,
.qty-plus::before, .qty-minus::before,
.addon-qty-plus::before, .addon-qty-minus::before,
.summary-qty-plus::before, .summary-qty-minus::before {
    width: 42%;
    height: 2px;
}

/* Vertical arm — plus only. */
.btn-customize-open::after,
.qty-plus::after,
.addon-qty-plus::after,
.summary-qty-plus::after {
    width: 2px;
    height: 42%;
}

/* 6. FULL NAVBAR */
.navbar {
    position: sticky;
    top: 0;
    background-color: #FFFFFF;
    z-index: 1000;
    border-bottom: 1px solid var(--color-border-light);
    padding: 16px 0;
}

/* Order-flow stepper. Sticky so "Browse the Menu" stays one tap away — the
   checkout page opens scrolled past it, which otherwise made going back a long
   scroll up. `top` is set by stepper.php from the navbar's measured height;
   z-index sits below the navbar's 1000 so it tucks under rather than over. */
.order-stepper {
    position: sticky;
    top: 0;
    z-index: 900;
    /* Opaque, not --color-bg-soft. That token is rgba(...,0.38), which is fine
       for an inset panel sitting in the page but not for a sticky bar: the
       product cards scrolled underneath showed straight through it. This is
       the same wash already flattened over white, which is the token that
       exists for exactly this problem.

       The literal fallback is not decoration. --color-bg-page is defined by the
       admin-topbar work, so a branch that carries this rule without that commit
       resolves the whole declaration to transparent -- which is what put the
       order summary straight through the stepper on production. */
    background-color: var(--color-bg-page, #F9F6EF);
    border-bottom: 1px solid var(--color-border-light);
    padding: 30px 0 24px;
    width: 100%;
    transition: padding 0.18s ease;
}

.order-stepper-title {
    font-family: var(--font-heading);
    font-size: 1.8rem;
    font-weight: 700;
    letter-spacing: 2px;
    text-transform: uppercase;
    color: var(--color-text-dark);
    margin-bottom: 25px;
    transition: max-height 0.18s ease, opacity 0.18s ease, margin 0.18s ease;
}

/* Compact once stuck. The title is decoration at that point; the three steps
   are the part that has to stay reachable, and the navbar is already taking
   its share of a phone screen. */
.order-stepper.is-stuck {
    padding: 12px 0 12px;
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.06);
}

.order-stepper.is-stuck .order-stepper-title {
    max-height: 0;
    opacity: 0;
    margin-bottom: 0;
    overflow: hidden;
}

/* Hide-on-scroll-down, reveal-on-scroll-up.
   Only ever applied while .is-stuck — at the top of the page the stepper sits
   in the flow and there is nothing to slide away from. Transform rather than
   `display`/`height` so it cannot reflow the page underneath it mid-scroll,
   which is what makes this kind of bar feel jumpy. */
.order-stepper {
    transition: padding 0.18s ease, transform 0.22s ease;
    will-change: transform;
}

.order-stepper.is-hidden {
    transform: translateY(-100%);
}

/* Step appearance. These were inline styles until the highlight had to be able
   to move at runtime — an inline style cannot be overridden by adding a class,
   so the current-step look has to start here. Values carried over unchanged. */
.stepper-circle {
    width: 38px;
    height: 38px;
    border-radius: 50%;
    background: #FFFFFF;
    border: 1.5px solid var(--color-primary);
    color: var(--color-primary);
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 700;
    font-size: 1rem;
    margin-bottom: 8px;
    transition: background-color 0.18s ease, color 0.18s ease;
}

.stepper-label {
    font-size: 0.78rem;
    font-weight: 500;
    color: var(--color-text-muted);
    text-align: center;
    transition: color 0.18s ease;
}

.stepper-step.is-current .stepper-circle {
    background: var(--color-primary);
    color: #FFFFFF;
}

.stepper-step.is-current .stepper-label {
    font-weight: 700;
    color: var(--color-text-dark);
}

/* Disabled state for anchors styled as buttons. An <a> has no :disabled, so
   this is the whole mechanism — pointer-events stops the click without needing
   a JS handler to swallow it, and aria-disabled carries the same fact to
   assistive tech, which pointer-events alone would not. */
.btn.btn-disabled,
a.btn[aria-disabled="true"] {
    opacity: 0.45;
    cursor: not-allowed;
    pointer-events: none;
    filter: grayscale(35%);
}

/* ...and for a real <button disabled>, which the rule above never covered.
   Its comment reasons about an <a> having no :disabled and stops there, so a
   disabled <button class="btn"> kept the full styling of a live one --
   resting AND hover. On the categories screen the DELETE beside a category
   that still holds products filled solid dark under the cursor, identical to
   the working DELETE on the row below it, and then did nothing. The only
   statement that it was refusing was a title tooltip, which a café tablet
   never shows.

   Deliberately no pointer-events: none, unlike the anchor rule above. A
   disabled button already swallows its own clicks, and killing pointer
   events would take the tooltip with it -- and on that screen the tooltip
   is where the reason is written.

   The hover pair is listed because each variant paints its own :hover fill;
   one blanket rule at the same specificity only wins by source order, and
   .btn-outline.btn-on-cream:hover outranks it outright. */
.btn:disabled,
.btn:disabled:hover {
    opacity: 0.45;
    cursor: not-allowed;
    filter: grayscale(35%);
    transform: none;
    box-shadow: none;
}

.btn-outline:disabled:hover,
.btn-outline.btn-on-cream:disabled:hover {
    background-color: transparent;
    color: var(--color-text-dark);
    border-color: var(--color-text-dark);
}

.btn-primary:disabled:hover {
    background-color: var(--color-primary);
    border-color: var(--color-primary);
}

/* Someone who has asked for less motion still wants the bar out of the way —
   they just do not want it animating past them. Snap instead of slide. */
@media (prefers-reduced-motion: reduce) {
    .order-stepper {
        transition: none;
    }
}

.navbar-container {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: space-between;
    max-width: var(--container-max-width);
    margin: 0 auto;
    padding: 0 24px;
}

/* align-items matters here: this is a column flexbox, so its default
   `stretch` pulled the logo image out to the full width of whatever column
   it sat in. Invisible in the header, where the container is itself sized to
   its content, but in the footer's 2fr grid column it stretched the artwork
   from 168px to 536px at the same 46px height — the one distortion the brand
   guidelines call out by name. Caught by comparing rendered aspect ratio
   against the image's intrinsic one, not by eye. */
.logo-container {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    text-decoration: none;
}

/* The real logo artwork, replacing the wordmark that used to be typeset in
   HTML as "L Λ M Σ L Λ" — Playfair Display with Greek capitals standing in
   for the real letterforms. That was a lookalike, not the logo: the actual
   wordmark is set in Achilles, a licensed face the brand book supplies only
   as finished artwork, and the guidelines explicitly say to use the supplied
   artwork and not to rebuild or restyle any part of it.

   This is the SECONDARY lockup (wordmark with the Cupid to its right), which
   the guidelines nominate for "layouts where the primary logo does not fit
   naturally" — a wide, short header bar being exactly that.

   Height-driven so the aspect ratio is never at risk; width follows. The
   intrinsic width/height on the <img> reserves the right box before the
   image loads, so the header does not reflow. */
.logo-img {
    /* The lockup is 3.65:1 and carries the "CAFE + ROASTERY AND BAKEHOUSE"
       descriptor as part of the artwork, so height has to buy legibility for
       that line, not just for the wordmark. Below ~44px the descriptor stops
       being readable. */
    height: 46px;
    width: auto;
    display: block;
}

.nav-links {
    display: flex;
    align-items: center;
    gap: 36px;
}

.nav-link {
    font-size: 0.85rem;
    font-weight: 600;
    letter-spacing: 1px;
    color: var(--color-text-dark);
    padding: 4px 0;
}

.nav-link.active, .nav-link:hover {
    color: var(--color-primary);
}

.nav-actions {
    display: flex;
    align-items: center;
    gap: 12px;
}

.mobile-menu-toggle {
    display: none;
    flex-direction: column;
    justify-content: center;
    gap: 5px;
    width: 30px;
    height: 24px;
    background: none;
    border: none;
    cursor: pointer;
    padding: 0;
    flex-shrink: 0;
}

.mobile-menu-toggle span {
    display: block;
    width: 100%;
    height: 2px;
    background-color: var(--color-text-dark);
    border-radius: 2px;
    transition: var(--transition);
}

@media (max-width: 768px) {
    .mobile-menu-toggle { display: flex; }

    .nav-links {
        position: absolute;
        top: 100%;
        left: 0;
        right: 0;
        flex-direction: column;
        align-items: stretch;
        gap: 0;
        background: #FFFFFF;
        /* The panel's own chrome is drawn ONLY while it is open.
         *
         * `max-height: 0` collapses the content but cannot collapse a border:
         * a border-box element will not render shorter than its own borders,
         * so a closed menu was still a 1px-tall strip, painting a hairline
         * across the full width just under the logo — plus a drop shadow
         * under nothing. Holding the width at 0 until .show removes the box
         * entirely. The transition stays on max-height alone, so the border
         * and shadow simply appear with the panel rather than animating. */
        border-bottom: 0 solid var(--color-border-light);
        box-shadow: none;
        max-height: 0;
        overflow: hidden;
        transition: max-height 0.3s ease;
    }

    .nav-links.show {
        max-height: 420px;
        padding: 8px 24px 20px;
        border-bottom-width: 1px;
        box-shadow: var(--shadow-md);
    }

    .nav-links .nav-link {
        width: 100%;
        padding: 14px 0;
        border-bottom: 1px solid var(--color-border-light);
    }

    /* The lockup is wide (3.65:1), so it shrinks rather than crowding the
       hamburger on a phone. */
    .logo-img {
        height: 30px;
    }
}

/* 7. HERO SECTION (EXACT MATCH TO MOCKUP) */
.hero {
    position: relative;
    min-height: 520px;
    background: linear-gradient(180deg, rgba(0,0,0,0.65) 0%, rgba(0,0,0,0.45) 100%), 
                url('https://images.unsplash.com/photo-1510591509098-f4fdc6d0ff04?ixlib=rb-4.0.3&auto=format&fit=crop&w=1800&q=80') center/cover no-repeat;
    display: flex;
    align-items: center;
    color: var(--color-text-on-dark);
    padding: 80px 0;
    overflow: hidden;
}

.hero::after {
    content: 'L Λ M Σ L Λ';
    position: absolute;
    bottom: -35px;
    left: 24px;
    font-family: var(--font-heading);
    font-size: 8rem;
    font-weight: 800;
    letter-spacing: 20px;
    color: rgba(240, 231, 214, 0.06);
    pointer-events: none;
    white-space: nowrap;
}

.hero-content {
    max-width: 620px;
}

.hero-title {
    font-family: var(--font-heading);
    font-size: 2.8rem;
    font-weight: 700;
    letter-spacing: 2px;
    line-height: 1.2;
    color: var(--color-text-on-dark);
    margin-bottom: 16px;
}

.hero-subtitle {
    font-size: 1.05rem;
    line-height: 1.6;
    color: rgba(240, 231, 214, 0.9);
    margin-bottom: 30px;
}

.hero-actions {
    display: flex;
    gap: 16px;
}

/* 8. ORDER IN 3 EASY STEPS (EXACT MATCH TO MOCKUP) */
.step-card-wrapper {
    position: relative;
    padding-top: 20px;
    display: flex;
    flex-direction: column;
    height: 100%;
}

.step-card-number {
    width: 34px;
    height: 34px;
    border-radius: 50%;
    background: #FFFFFF;
    border: 1px solid var(--color-text-dark);
    color: var(--color-text-dark);
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 700;
    font-size: 0.9rem;
    margin: 0 auto 12px;
    flex-shrink: 0;
}

.step-card {
    background: #FFFFFF;
    border-radius: var(--radius-md);
    border: 1px solid var(--color-border);
    padding: 20px 18px;
    text-align: center;
    flex: 1;
    display: flex;
    flex-direction: column;
    transition: var(--transition);
}

.step-card:hover {
    transform: translateY(-4px);
    box-shadow: var(--shadow-md);
}

.step-card-img {
    width: 100%;
    height: 190px;
    object-fit: cover;
    border-radius: var(--radius-sm);
    margin-bottom: 16px;
}

.step-card-title {
    font-family: var(--font-heading);
    font-size: 1.2rem;
    margin-bottom: 8px;
}

.step-card-desc {
    font-size: 0.85rem;
    color: var(--color-text-muted);
}

/* 9. CRAFTED FOR EVERY CRAVING CONTAINER (EXACT MATCH TO MOCKUP) */
.craving-box {
    background-color: var(--color-bg-cream);
    border-radius: var(--radius-lg);
    padding: 40px;
}

.category-card {
    background-color: var(--color-bg-tan);
    border-radius: var(--radius-md);
    padding: 16px;
    text-align: center;
    display: flex;
    flex-direction: column;
    height: 100%;
    transition: var(--transition);
}

.category-card:hover {
    transform: translateY(-4px);
    box-shadow: var(--shadow-md);
}

.category-card-img {
    width: 100%;
    height: 170px;
    object-fit: cover;
    border-radius: var(--radius-sm);
    margin-bottom: 16px;
}

.category-card-title {
    font-family: var(--font-heading);
    font-size: 1.2rem;
    margin-bottom: 8px;
}

.category-card-desc {
    font-size: 0.83rem;
    color: var(--color-text-muted);
    line-height: 1.5;
}

/* 10. VALUE CARDS (OUR STORY) */
.value-card {
    background-color: var(--color-bg-tan);
    border-radius: var(--radius-md);
    padding: 16px;
    text-align: center;
    display: flex;
    flex-direction: column;
    align-items: center;
}

.value-card-img {
    width: 100%;
    height: 140px;
    object-fit: cover;
    border-radius: var(--radius-sm);
    margin-bottom: 12px;
}

.value-card-title {
    font-family: var(--font-heading);
    font-size: 1.05rem;
    margin-bottom: 6px;
}

.value-card-desc {
    font-size: 0.8rem;
    color: var(--color-text-muted);
}

/* 11. STEP INDICATOR BAR */
.step-indicator-wrapper {
    background-color: var(--color-bg-soft);
    border-bottom: 1px solid var(--color-border-light);
    padding: 20px 0 15px;
    margin-bottom: 30px;
}

.step-indicator {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0;
    max-width: 650px;
    margin: 0 auto;
}

.step-item {
    display: flex;
    flex-direction: column;
    align-items: center;
}

.step-circle {
    width: 36px;
    height: 36px;
    border-radius: 50%;
    background-color: #FFFFFF;
    border: 1px solid var(--color-text-dark);
    color: var(--color-text-dark);
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 700;
    font-size: 0.95rem;
    margin-bottom: 6px;
}

.step-item.active .step-circle {
    background-color: var(--color-dark);
    color: var(--color-text-on-dark);
    border-color: var(--color-dark);
}

.step-item.completed .step-circle {
    background-color: var(--color-success);
    color: var(--color-text-on-dark);
    border-color: var(--color-success);
}

.step-label {
    font-size: 0.8rem;
    color: var(--color-text-muted);
    font-weight: 500;
}

.step-line {
    flex: 1;
    height: 1px;
    background-color: var(--color-border);
    margin: 0 10px;
    margin-bottom: 22px;
}

.step-line.active {
    background-color: var(--color-text-dark);
}

/* 12. FOOTER */
/* No margin-top on purpose. A 60px external margin left a band of bare
   <body> white between the end of the preceding section and the footer's
   own top border — obvious on any page that ends on a coloured section
   (sign in, register, reset password), where the cream stopped 60px short
   of the rule and the strip read as a gap rather than as spacing.
   Breathing room comes from the footer's own 50px top padding, which sits
   inside the border where it belongs, plus whatever bottom padding the
   preceding section already carries (.section is 70px). */
.footer {
    background-color: #FFFFFF;
    border-top: 1px solid var(--color-border-light);
    padding: 50px 0 30px;
}

.footer-top {
    display: grid;
    grid-template-columns: 2fr 1fr 1fr;
    gap: 40px;
    margin-bottom: 40px;
}

.social-icons {
    display: flex;
    gap: 12px;
    margin-top: 16px;
}

.social-icon {
    width: 36px;
    height: 36px;
    border-radius: 50%;
    background-color: var(--color-primary);
    color: var(--color-text-on-dark);
    display: flex;
    align-items: center;
    justify-content: center;
}

.footer-heading {
    font-family: var(--font-body);
    font-size: 0.9rem;
    font-weight: 700;
    margin-bottom: 16px;
    color: var(--color-text-dark);
}

.footer-links {
    list-style: none;
}

.footer-links li {
    margin-bottom: 8px;
}

.footer-links a {
    font-size: 0.85rem;
    color: var(--color-text-muted);
}

.footer-links a:hover {
    color: var(--color-primary);
}

.footer-bottom {
    text-align: center;
    border-top: 1px solid var(--color-border-light);
    padding-top: 20px;
    font-size: 0.75rem;
    color: var(--color-text-muted);
}

/* CATEGORY PILLS & MENU GRID STYLES */
/* One scrollable row, not wrapped — this café has 16 categories now that the
   drinks menu shipped alongside the food one, and flex-wrap left them
   wrapping across 2-3 lines above the menu even on desktop, most of a
   screen spent on filter chips before any actual product is visible.
   Scrolls horizontally instead, same as almost every food-ordering app's
   category bar, and the same treatment this already had at mobile widths —
   applied everywhere now rather than kept as a narrow-viewport-only fix.
   flex-shrink: 0 on the pills stops flexbox trying to squeeze them to fit
   rather than let them overflow into the scroll area, which is what nowrap
   is asking for in the first place. */
.category-pills {
    display: flex;
    justify-content: flex-start;
    align-items: center;
    gap: 12px;
    flex-wrap: nowrap;
    overflow-x: auto;
    padding-bottom: 6px;
    -webkit-overflow-scrolling: touch;
    margin-bottom: 45px;
}

.category-pills .category-pill {
    flex-shrink: 0;
}

/* Grid items default to min-width: auto, which floors their shrink at the
   content's own intrinsic width. .order-menu-column sits in a 1fr/360px
   grid (.order-layout-grid, inline on /order/customize) and now contains a
   nowrap, horizontally-scrolling .category-pills row — without this, that
   row's full intrinsic width (16 pills, unwrapped) blows out the 1fr track
   instead of being constrained to it and scrolling internally, which drags
   the whole page into horizontal overflow and pushes the 360px order
   summary column off-screen entirely. Not scoped to a mobile breakpoint:
   until the category pills went nowrap-everywhere, desktop's content never
   overflowed here, but the exact same failure mode is now possible at any
   width, not just narrow ones. min-width: 0 removes the floor so the 1fr
   track's actual width wins instead. */
.order-menu-column, .order-summary-column {
    min-width: 0;
}

/* Espresso rather than Dark Chocolate for the selected state. Chocolate is
   the brand's body-text colour and reads as flat black at pill size, which
   made the active filter look like a UI default rather than a brand colour.
   Espresso is the primary action colour and is unmistakably brown; white on
   it is 6.95:1. Same reasoning applies to the stepper's current-step marker. */
.category-pill {
    background-color: #FFFFFF;
    color: var(--color-primary);
    border: 1px solid var(--color-primary);
    border-radius: var(--radius-pill);
    padding: 10px 24px;
    font-weight: 700;
    font-size: 0.85rem;
    letter-spacing: 1px;
    cursor: pointer;
    transition: var(--transition);
    text-transform: uppercase;
}

.category-pill:hover, .category-pill.active {
    background-color: var(--color-primary);
    color: var(--color-text-on-dark);
    border-color: var(--color-primary);
}

/* "You are scrolling through this category" — deliberately NOT the same look
   as .active.
   .active means a filter is applied and the grid is showing only that
   category; this means the grid is unfiltered (ALL is active) and these are
   simply the cards under your thumb right now. Painting it the same amber
   would put two pills in the selected state at once and claim a filter that
   is not on. A tinted fill with the brand border reads as "here" without
   reading as "chosen". */
.category-pill.is-viewing:not(.active) {
    background-color: var(--color-bg-soft);
    border-color: var(--color-primary);
    box-shadow: inset 0 0 0 1px var(--color-primary);
}

/* Sticky menu toolbar on a phone, on /order/customize only.
   The column is long — a dozen categories of cards — and the search, sort and
   category row are the tools for getting around it, which were scrolling away
   the moment you started using them. Pinned under the stepper rather than to
   the top of the viewport: the stepper is already sticky above it, and two
   sticky bars that do not know about each other overlap. */
/* All three menu pages, not just /order/customize: the toolbar is the way
   around a long list wherever that list appears.

   Sticky on a phone or tablet everywhere, and on a desktop ONLY on the pages
   that carry a stepper -- /order and /order/customize. The stepper's script
   writes this bar's offset directly there, so it lands in the right place and
   moves with the stepper.

   /menu has no stepper, and on a desktop it is deliberately left to scroll
   away. Pinned there it is 108px under a 79px navbar, 187px of a 900px
   window, with nothing to slide it out of the way: the hide-on-scroll written
   for it did not work, and a bar that takes the space without earning it is
   worse than one that scrolls off. See front-end-comparison.md. */
@media (max-width: 900px) {
    .menu-toolbar {
        position: sticky;
        /* --sticky-offset (navbar + stepper) is published by stepper.php on
           the order pages. /menu has no stepper, so it falls through to the
           navbar alone, which app.js publishes on every page. */
        top: var(--sticky-offset, var(--navbar-offset, 64px));
        z-index: 850;   /* under the stepper's 900, over the cards */
        /* White, matching the page — body is --color-bg-light, and the pill
           fades below are drawn in that same colour. Using the cream
           --color-bg-page here put a cream band on a white page and left the
           white fades painting over it at both ends of the pill row. */
        background: var(--color-bg-light);
        padding: 10px 0 4px;
        transition: top 0.22s ease;
    }

    /* When the stepper slides away on a downward scroll there is nothing left
       to clear, so the toolbar takes its place against the navbar instead of
       leaving a stepper-shaped gap above itself. Same easing as the stepper so
       the two move as one. */
    html.stepper-hidden .menu-toolbar {
        top: var(--navbar-offset, 64px);
    }
}

/* The desktop case: only where a stepper is on the page to pin it. :has() is
   the whole guard -- a browser without it simply leaves the bar in the flow,
   which is the old behaviour and a safe place to land. */
@media (min-width: 901px) {
    body:has(#order-stepper) .menu-toolbar {
        position: sticky;
        z-index: 850;
        background: var(--color-bg-light);
        padding: 10px 0 4px;
        transition: top 0.22s ease;
    }
}


/* Selectable option cards — contact type, dine-in/pickup, payment method.
   All three are cursor:pointer labels wrapping a hidden or plain radio, and
   two of them (.contact-type-option, .order-type-option) already declared
   `transition: border-color, background` inline for a hover state that
   never existed: nothing in this stylesheet matched them on :hover, so
   the transition had nothing to animate toward and hovering did nothing at
   all despite the cursor promising it would. A neutral border darken
   rather than the brand amber (--color-primary) — that color is reserved
   for "this one is actually selected" (see each page's own JS, which sets
   it directly), and reusing it here would make an unselected option look
   selected the moment the mouse passes over it.

   !important on all three: the same trap as .cashier-header-actions and
   .queue-view-filter earlier — an inline `style="border: ..."` beats an
   external rule at any specificity, whether that inline style is static
   (payment-badges-grid's labels are written with one directly in the PHP
   template) or JS-mutated on every render (contact-type-option and
   order-type-option's own selection-sync scripts). Assumed at first that
   "nothing mutates this one" meant "no inline style to fight" — wrong; a
   plain hardcoded `style="border: 1px solid #E0E0E0"` in the markup is
   just as immovable to a bare :hover rule as one JS keeps rewriting.

   :has(input:not(:checked)) scopes the first two to the UNselected cards
   only — without it, !important also overrides the SELECTED card's inline
   amber border the moment it's hovered, which very nearly shipped:
   hovering "Pick Up" after it was already chosen briefly turned it the
   same grey as an unselected option, reading as if the selection had been
   lost. Payment method has no such amber "selected" treatment (the native
   radio dot is the only indicator), so it needs no equivalent guard. */
.contact-type-option:hover:not(.contact-type-disabled):has(input:not(:checked)),
.order-type-option:hover:has(input:not(:checked)),
.payment-badges-grid label:hover {
    border-color: var(--color-border-input) !important;
}

.product-card {
    position: relative;   /* the bestseller badge anchors to this */
    background-color: #FFFFFF;
    border-radius: var(--radius-md);
    border: 1px solid var(--color-border-light);
    box-shadow: var(--shadow-sm);
    overflow: hidden;
    display: flex;
    flex-direction: column;
    height: 100%;
    transition: var(--transition);
}

.product-card:hover {
    transform: translateY(-4px);
    box-shadow: var(--shadow-md);
}

/* Sits on the photo rather than in the text, so it costs the card no height —
   the cards were just cut from 405px to 307px and a badge in the flow would
   have given some of that back. Ochre on chocolate: the ochre carries on a
   photograph of food where the espresso brown disappears into it, and dark
   text on it measures 6.89:1 against the ochre — the badge is small text, so
   the bar is 4.5:1. Cream on the same ochre is 1.67:1 and fails it outright. */
.product-badge {
    position: absolute;
    top: 10px;
    left: 10px;
    z-index: 1;
    padding: 5px 11px;
    border-radius: var(--radius-pill);
    background: var(--brand-ochre);
    color: var(--brand-chocolate);
    font-size: 0.68rem;
    font-weight: 800;
    letter-spacing: 0.6px;
    text-transform: uppercase;
    box-shadow: var(--shadow-sm);
}

/* Sold out today -------------------------------------------------------------
   Marked, never hidden. A dish that disappears from the menu reads as one the
   cafe stopped serving; a dish marked unavailable reads as one they have run
   out of today, which is what is true. The card stays legible rather than
   being greyed into unreadability -- people still want to know what it is and
   what it costs, so they can come back for it. */
.product-badge.is-out {
    background: var(--brand-chocolate, #4a3728);
    color: #fff;
}

.product-card.is-soldout .product-image,
.product-card.is-soldout img {
    filter: saturate(0.35);
    opacity: 0.72;
}

.product-card.is-soldout .product-price {
    opacity: 0.7;
}

.product-outnote {
    font-size: 0.72rem;
    font-weight: 800;
    letter-spacing: 0.5px;
    text-transform: uppercase;
    color: var(--color-text-muted, #8a7f77);
    border: 1px dashed var(--color-border-light, #e5ded7);
    border-radius: var(--radius-pill, 999px);
    padding: 6px 12px;
    white-space: nowrap;
}

.product-image {
    width: 100%;
    /* Was 190px. The picture is the largest single contributor to card height,
       and a menu is scanned by name and price far more than by photograph —
       155px still reads as a photo of the dish without pushing the price below
       the fold on a phone. */
    height: 155px;
    object-fit: cover;
    display: block;
}

.product-content {
    padding: 15px 16px 14px;
    display: flex;
    flex-direction: column;
    flex: 1;
}

.product-title {
    font-family: var(--font-heading);
    font-size: 1.25rem;
    font-weight: 700;
    margin-bottom: 6px;
    color: var(--color-text-dark);
}

/* Clamped to two lines.
 *
 * Cards sit in a grid row and stretch to the tallest of them, so ONE item with
 * a four-line description made every card beside it that tall — including the
 * ones with no description at all, which then showed a large empty gap above
 * their price. Two lines is enough to tell dishes apart; the full description
 * is on the product itself, where someone deciding will actually read it.
 *
 * The min-height reserves those two lines whether or not there is text, so a
 * described and an undescribed item are the same height rather than the row
 * being ragged.
 */
.product-desc {
    font-size: 0.82rem;
    color: var(--color-text-muted);
    line-height: 1.45;
    margin-bottom: 12px;
    min-height: calc(2 * 1.45em);
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.product-footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-top: auto;
    padding-top: 4px;
}

.product-price {
    font-family: var(--font-heading);
    font-size: 1.2rem;
    font-weight: 800;
    color: var(--color-text-dark);
}

.btn-add-circle {
    width: 36px;
    height: 36px;
    border-radius: 50%;
    background-color: var(--color-primary);
    color: var(--color-text-on-dark);
    border: none;
    font-size: 1.3rem;
    font-weight: 700;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    transition: var(--transition);
    line-height: 1;
}

.btn-add-circle:hover {
    background-color: var(--color-primary-hover);
    transform: scale(1.1);
}

/* 13. FORMS (LOGIN / REGISTER / GENERIC) */
.form {
    background-color: #FFFFFF;
    border: 1px solid var(--color-border-light);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
    padding: 36px;
}

.form-group {
    margin-bottom: 20px;
}

.form-group label {
    display: block;
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--color-text-muted);
    margin-bottom: 8px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

.form-group input,
.form-group select,
.form-group textarea,
.form input[type="text"],
.form input[type="email"],
.form input[type="tel"],
.form input[type="password"] {
    width: 100%;
    padding: 12px 16px;
    border: 1px solid var(--color-border-input);
    border-radius: var(--radius-sm);
    font-size: 0.9rem;
    color: var(--color-text-dark);
    background-color: #FFFFFF;
    transition: var(--transition);
}

.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
    outline: none;
    border-color: var(--color-primary);
    box-shadow: 0 0 0 3px rgba(112, 84, 59, 0.18);
}

/* Every field, not only the ones inside a .form-group. Half the admin builds
   its inputs inline -- the categories table, the add-on rows, the list filter
   strips -- and those were falling through to the browser's own grey, which
   is a different grey on every browser. One answer, everywhere.
   `opacity: 1` because Firefox dims placeholders on its own, which would
   undo the contrast this colour was chosen for. */
input::placeholder,
textarea::placeholder {
    color: var(--color-text-placeholder);
    opacity: 1;
}

/* The browser's password-mask dot renders visibly larger than a real
   character at the same font-size — reported as "the mask is bigger than
   the text" during phone testing. A smaller font-size for this input type
   only shrinks the dots to match the rest of the form without touching the
   actual typed text, which nobody sees anyway. */
.form-group input[type="password"],
.form input[type="password"] {
    font-size: 0.8rem;
}

/* Password show/hide toggle, and the live confirm-password match hint.
   Scoped to `.password-field` so it never touches an ordinary text input
   that just happens to sit near one. */
.password-field {
    position: relative;
}

.password-field input[type="password"],
.password-field input[type="text"] {
    padding-right: 44px;
}

.password-toggle {
    position: absolute;
    top: 0;
    bottom: 0;
    right: 2px;
    width: 38px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: none;
    border: none;
    padding: 0;
    margin: 0;
    cursor: pointer;
    color: var(--color-text-muted);
}

.password-toggle:hover {
    color: var(--color-text-dark);
}

.password-toggle svg {
    width: 19px;
    height: 19px;
}

/* Two icons ship in the button; only one shows at a time. Toggling a class
   rather than swapping markup means a JS hiccup leaves the masked-password
   icon showing — the safe default — never the reverse. */
.password-toggle .icon-eye-off {
    display: none;
}

.password-toggle.is-visible .icon-eye {
    display: none;
}

.password-toggle.is-visible .icon-eye-off {
    display: block;
}

.password-match-msg {
    display: none;
    font-size: 0.78rem;
    font-weight: 600;
    margin-top: 6px;
}

.password-match-msg.is-match {
    display: block;
    color: var(--color-success);
}

.password-match-msg.is-mismatch {
    display: block;
    color: var(--color-danger);
}

.mb-3 { margin-bottom: 12px; }
.mb-5 { margin-bottom: 30px; }
.mb-6 { margin-bottom: 40px; }
.mt-3 { margin-top: 12px; }

/* NAV CIRCLE ICONS (cart / account) — used by both full and minimal navbars */
/* The three nav actions — basket, bell, account.
 *
 * Each is a hairline ring around a line icon. The ring is what makes them read
 * as one set of controls rather than three loose glyphs floating in the bar,
 * and it gives the badge something to sit on the edge of. Kept to 1px in the
 * border colour (taupe) so it frames without competing with the icon stroke
 * inside it; box-sizing is border-box globally, so the ring costs no layout
 * width and the circles stay on the same 38px grid as before. */
.nav-circle-icon {
    width: 38px;
    height: 38px;
    border-radius: 50%;
    border: 1px solid var(--color-border);
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--color-text-dark);
    position: relative;
    transition: var(--transition);
}

.nav-circle-icon:hover {
    background-color: var(--color-bg-cream);
    border-color: var(--color-primary);
}

/* Count badge on the cart and inbox icons.
 *
 * Sized to its contents rather than locked to an 18px circle. "9+" measures
 * 14.4px, and inside a circle the usable width at the glyphs' own height is
 * narrower still, so both characters sat hard against the curve. min-width
 * keeps a single digit perfectly round; anything wider relaxes into a pill.
 *
 * The white ring is what separates it from the icon underneath — the two
 * overlap by design, and without a ring the badge's edge dissolved into the
 * icon's stroke. Matches the header background, which is white in both the
 * full and minimal navbars. */
.nav-badge {
    display: none;
    position: absolute;
    top: -5px;
    right: -6px;
    background-color: var(--color-primary);
    color: var(--color-text-on-dark);
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    border-radius: var(--radius-pill);
    font-size: 0.68rem;
    font-weight: 700;
    line-height: 1;
    align-items: center;
    justify-content: center;
    box-shadow: 0 0 0 2px var(--color-bg-light);
}

/* FLASH MESSAGES */
.flash-message {
    max-width: var(--container-max-width);
    margin: 16px auto;
    padding: 14px 24px;
    border-radius: var(--radius-sm);
    font-size: 0.9rem;
    font-weight: 600;
    text-align: center;
    transition: opacity 0.3s ease-out;
    /* A failed submit on the admin side redirects back into the SAME
       full-screen modal it came from (receive a delivery, write off waste,
       adjust an ingredient, add a pack) so the person is not dropped back
       at the list behind it. Those overlays are position:fixed at
       z-index 1200, and this message was position:static (or, for
       .flash-error, position:relative with no z-index) -- painted first,
       then covered edge to edge by the modal that reopens on top of it.
       The error was real and on the page; it was just never visible.
       Above every overlay in the app (highest is 2000) so this always wins,
       regardless of which modal is open when it fires. */
    position: relative;
    z-index: 2500;
}

/* Two of them can land together -- a refusal and a confirmation of the part
   that did save -- so they stack rather than sit on top of each other. */
.flash-error {
    background-color: var(--color-danger-bg);
    color: var(--color-danger);
    border: 1px solid var(--color-danger-bg);
    /* Room for the dismiss button, which only errors get — they stay until
       the customer closes them rather than fading after four seconds. */
    position: relative;
    padding-right: 48px;
}

.flash-close {
    position: absolute;
    top: 50%;
    right: 12px;
    transform: translateY(-50%);
    /* 32px square: a genuine tap target, not a 12px glyph. */
    width: 32px;
    height: 32px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: none;
    border: none;
    border-radius: var(--radius-sm);
    color: inherit;
    font-size: 1.4rem;
    line-height: 1;
    cursor: pointer;
    opacity: 0.65;
}

.flash-close:hover,
.flash-close:focus-visible {
    opacity: 1;
    background-color: rgba(183, 28, 28, 0.09);
}

.flash-success {
    background-color: var(--color-success-bg);
    color: var(--color-success);
    border: 1px solid var(--color-success-bg);
}

/* ANIMATE ON SCROLL */
.animate-on-scroll {
    opacity: 0;
    transform: translateY(20px);
    transition: opacity 0.5s ease-out, transform 0.5s ease-out;
}

.animate-on-scroll.visible {
    opacity: 1;
    transform: translateY(0);
}

/* Cashier queue action buttons. .btn is nowrap site-wide, which is fine for
   short labels like "CANCEL ORDER" but "MARK AS READY FOR PICKUP" and
   similar next-step labels are long enough, at this font-size/tracking/
   uppercase, to overflow a 100%-width button on an actual phone (~291px of
   text in ~285px of button on a 375px screen) — the text just spills past
   the pill edges instead of wrapping. Scoped to .order-actions rather than
   changing .btn globally, since most buttons on the site are short enough
   that nowrap is the better look for them. */
.order-actions .btn {
    white-space: normal;
    line-height: 1.3;
}

/* RESPONSIVE */
@media (max-width: 992px) {
    .grid-4, .grid-5 { grid-template-columns: repeat(2, 1fr); }
    .grid-3 { grid-template-columns: repeat(2, 1fr); }
    .hero-title { font-size: 2.2rem; }
}

@media (max-width: 600px) {
    .grid-2, .grid-3, .grid-4, .grid-5 { grid-template-columns: 1fr; }
    .footer-top { grid-template-columns: 1fr; gap: 30px; }
    .hero-title { font-size: 1.8rem; }
    .hero-actions { flex-direction: column; }
    /* All three product grids stay 2-up on a phone rather than collapsing to
       the 1-column default above. The cards are small enough (image, name,
       price and an add button) that two per row halves the scrolling needed
       to see the same number of items — the whole point — without the
       cramping a 2-up grid would cause somewhere like the admin product
       manager's larger cards.

       These are id selectors, so they win over the .grid-3/.grid-4 rules
       above on specificity alone; no !important needed, and nothing else on
       the site uses these ids so nothing else is affected.

         #checkout-menu-grid   step 2, customize your order
         #view-menu-grid       /menu
         #order-products-grid  step 1, browse

       The last two were originally left at one card per row while step 2 was
       2-up. That was a visible inconsistency inside a single flow — the same
       products, laid out two different ways one step apart — and the
       one-per-row version meant scrolling through the whole catalogue a card
       at a time on a phone. */
    #checkout-menu-grid,
    #view-menu-grid,
    #order-products-grid { grid-template-columns: repeat(2, 1fr); gap: 12px; }

    /* .product-image is a fixed 190px regardless of card width — at half
       width that alone makes two tall columns instead of less scrolling,
       which defeats the point. Tightened here rather than for every use of
       .product-card, and the description is hidden rather than shrunk: at
       ~170px card width a full sentence wraps to four cramped lines, and
       name, price and the add button are what someone is actually scanning
       for at this size. */
    #checkout-menu-grid .product-image,
    #view-menu-grid .product-image,
    #order-products-grid .product-image { height: 100px; }

    #checkout-menu-grid .product-content,
    #view-menu-grid .product-content,
    #order-products-grid .product-content { padding: 12px; }

    #checkout-menu-grid .product-title,
    #view-menu-grid .product-title,
    #order-products-grid .product-title { font-size: 0.92rem; margin-bottom: 4px; }

    #checkout-menu-grid .product-desc,
    #view-menu-grid .product-desc,
    #order-products-grid .product-desc { display: none; }

    #checkout-menu-grid .product-price,
    #view-menu-grid .product-price,
    #order-products-grid .product-price { font-size: 0.95rem; }

    #checkout-menu-grid .btn-add-circle,
    #view-menu-grid .btn-add-circle,
    #order-products-grid .btn-add-circle { width: 30px; height: 30px; font-size: 1.1rem; }

    /* ---------------------------------------------------------------------
       The menu reads as a list on a phone, not as a 2-up grid.
       Overrides the block above, which it follows at equal specificity.

       Two columns of 170px cards had to drop the description to fit, which is
       the line that tells two similar dishes apart. A row gives the text the
       full width of the screen instead of half of it, so the description
       comes back, and the name gets read rather than truncated.

       All three grids, as of 2026-09-05. This was /menu only, on the reasoning
       that the order flow is for assembling an order from things you have
       already chosen, so denser rows meant less scrolling between picks. In
       use that traded away the wrong thing: the description is what separates
       two similar dishes, and dropping it on the two steps where you are
       actually deciding what to buy is where it is most needed. The same
       products laid out two different ways one step apart was the other half
       of the problem.

       Every rule below lists all three ids in full. `#a, #b, #c .product-card`
       would apply the descendant part to #c only and match the other two grids
       themselves — the kind of selector-list mistake that silently half-works.
       --------------------------------------------------------------------- */
    #checkout-menu-grid,
    #order-products-grid,
    #view-menu-grid { grid-template-columns: 1fr; gap: 10px; }

    #checkout-menu-grid .product-card,
    #order-products-grid .product-card,
    #view-menu-grid .product-card { flex-direction: row; }

    /* Square-ish thumbnail that grows with the row rather than a fixed height,
       so a two-line and a three-line row both keep a full-bleed picture. */
    #checkout-menu-grid .product-image,
    #order-products-grid .product-image,
    #view-menu-grid .product-image {
        width: 104px;
        height: auto;
        min-height: 104px;
        align-self: stretch;
        flex-shrink: 0;
    }

    #checkout-menu-grid .product-content,
    #order-products-grid .product-content,
    #view-menu-grid .product-content { padding: 11px 13px; }

    #checkout-menu-grid .product-title,
    #order-products-grid .product-title,
    #view-menu-grid .product-title { font-size: 1rem; margin-bottom: 3px; }

    /* Back from display:none, and back to the two-line clamp it has on a
       desktop. min-height is dropped: rows size to their own content here,
       so an item with no description should not reserve two blank lines. */
    #checkout-menu-grid .product-desc,
    #order-products-grid .product-desc,
    #view-menu-grid .product-desc {
        display: -webkit-box;
        -webkit-line-clamp: 2;
        -webkit-box-orient: vertical;
        overflow: hidden;
        min-height: 0;
        margin-bottom: 8px;
        font-size: 0.78rem;
    }

    #checkout-menu-grid .product-footer,
    #order-products-grid .product-footer,
    #view-menu-grid .product-footer { padding-top: 0; }

    #checkout-menu-grid .product-price,
    #order-products-grid .product-price,
    #view-menu-grid .product-price { font-size: 1.05rem; }

    #checkout-menu-grid .btn-add-circle,
    #order-products-grid .btn-add-circle,
    #view-menu-grid .btn-add-circle { width: 34px; height: 34px; }

    /* The badge is anchored to the card, which is now a row starting with the
       picture, so it keeps sitting on the picture without moving. It does have
       to be told how wide the picture is, though: the badge is a sibling of the
       <img>, not a child of it, so its containing block is the whole card and
       nothing stops it running off the 104px thumbnail and across the item's
       name. "Bestseller" was short enough to fit and hid that; "Unavailable
       today" is not. Capped to the picture's own width and allowed to wrap. */
    #checkout-menu-grid .product-badge,
    #order-products-grid .product-badge,
    #view-menu-grid .product-badge {
        top: 6px;
        left: 6px;
        padding: 3px 8px;
        font-size: 0.6rem;
        max-width: calc(104px - 12px);
        white-space: normal;
        line-height: 1.3;
        text-align: center;
    }
}

/* Order flow, cashier & admin layouts: stack fixed multi-column inline grids on tablet/mobile */
@media (max-width: 900px) {
    .order-layout-grid { grid-template-columns: 1fr !important; }
    .two-col-stack { grid-template-columns: 1fr !important; }
    /* Order summary first, menu picker below, on step 2 once the grid above
       has already stacked to one column — reordering only means anything
       once there's a single column to reorder within, so this lives in the
       same breakpoint as the collapse rather than a separate one that could
       drift out of sync with it. A customer can see the running total and
       what's already in the cart without scrolling past the whole menu
       first. Done with `order`, not by moving the HTML: source order (and
       therefore tab order) is untouched, only the visual position changes.
       .order-layout-grid is used on exactly this one page, so nothing else
       is affected. */
    .order-summary-column { order: -1; }
}

@media (max-width: 768px) {
    .dashboard-header { flex-wrap: wrap; gap: 15px; }
    .dashboard-header > div:last-child { width: 100%; flex-wrap: wrap; }
    /* Cashier header: SOUND OFF / ALERTS OFF / SCAN / ORDER HISTORY / LOGOUT.
       A plain flex-wrap row here left every control on its own line — nothing
       gave them a width, so each one just wrapped individually the moment the
       row ran out of space, and a five-item column is most of a phone screen
       before a single order is visible. A 2-column grid puts a fixed shape on
       them instead: the active-order-count pill spans the full row (it isn't a
       button and stretching it into a grid cell would misshape it), the five
       controls fill two columns under it. */
    /* !important on display and grid-template-columns: the element carries an
       inline style="display: flex", and an inline style always wins over an
       external rule for the same property unless that rule is !important —
       so without it this whole block would be silently ignored and the five
       controls would stay one-per-line. */
    .cashier-header-actions { display: grid !important; grid-template-columns: 1fr 1fr !important; gap: 10px; width: 100%; }
    .cashier-header-actions > span:first-child { grid-column: 1 / -1; justify-content: center; }
    .cashier-header-actions > button, .cashier-header-actions > a { width: 100%; text-align: center; }
    /* Same reasoning as the header controls above: ALL / KITCHEN / BARISTA /
       BACKGROUND is 4 items, an odd fit for wrap, and BACKGROUND's longer
       label was the one most likely to end up alone and stretched. A fixed
       2-column grid keeps all four an even, predictable shape. */
    .queue-view-filter { display: grid !important; grid-template-columns: 1fr 1fr !important; }
    /* .btn is white-space: nowrap site-wide (correct for short labels), but
       "BACKGROUND (ALL, READ-ONLY)" is long enough at this width, in this
       font, to run past its grid cell rather than wrap — the same class of
       overflow the cashier action buttons already had a fix for elsewhere on
       this page (see .order-actions .btn above). */
    .queue-view-filter > button { width: 100%; white-space: normal; line-height: 1.3; }
    .queue-item-grid { grid-template-columns: 1fr !important; }
    /* .order-actions (the last div here) is intentionally left in its default
       column layout, not forced into a row.
       It used to be forced to `row`, and that broke on this exact page: its
       children are a mix of bare `<button style="width:100%">` and unstyled
       `<form>` wrapping a `<button style="width:100%">`. In row layout a
       form with no declared width shrinks to fit its button's content, while
       a bare button's width:100% resolves against the full row — two
       different sizing rules landing in the same row, which is what made
       START PREPARING render narrow while CANCEL ORDER stretched past it.
       Column stacking is what the buttons were actually built for (it's also
       how they already render in the 280px desktop column), so there is
       nothing left to override here on mobile. */
    .admin-form-grid { grid-template-columns: 1fr !important; }
    .wrap-row { flex-wrap: wrap; }
    /* Category filter scrolling is now the base .category-pills rule itself
       (not mobile-only), so there is nothing left to override here. */
}

/* The queue title used to be set nowrap inline, to stop it stacking one word
   per line at tablet width, and that overflowed the card at phone width -- so
   this undid it with !important. Both are gone: the heading is .admin-banner's
   now, it wraps normally, and "Live order queue" is short enough not to need
   rescuing at either width. */

@media (max-width: 700px) {
    .checkout-payment-grid { grid-template-columns: 1fr !important; gap: 30px !important; }
    .confirm-summary-grid { grid-template-columns: 1fr !important; text-align: center !important; }
    .confirm-divider { display: none; }
    .confirm-detail-row { flex-direction: column !important; text-align: left !important; gap: 4px !important; }
    .confirm-detail-row span:last-child { text-align: left !important; max-width: none !important; }
}

@media (max-width: 480px) {
    .order-history-row { flex-direction: column !important; align-items: flex-start !important; }
    .order-history-row > div:last-child { text-align: left !important; width: 100%; }
}

@media (max-width: 600px) {
    .checkout-card { padding: 24px !important; }
    .payment-badges-grid { grid-template-columns: repeat(2, 1fr) !important; }
    .confirm-photo-main { width: 100% !important; max-width: 320px; height: auto !important; aspect-ratio: 420 / 260; }
    .confirm-photo-small { width: 100px !important; height: 100px !important; margin-left: -35px !important; }
}

/* FLOATING ACTIVE ORDER WIDGET */
.active-order-widget {
    position: fixed;
    /* Sits above the PWA banner's slot but below modals, and clear of the
       bottom edge so it doesn't collide with mobile browser chrome. */
    bottom: 20px;
    right: 20px;
    z-index: 1900;
    max-width: calc(100vw - 40px);
}

.active-order-widget[hidden] { display: none; }

.active-order-pill {
    display: flex;
    align-items: center;
    gap: 10px;
    background: var(--color-dark);
    color: var(--color-text-on-dark);
    border: none;
    border-radius: var(--radius-pill);
    padding: 12px 18px;
    cursor: pointer;
    box-shadow: var(--shadow-lg);
    font-family: var(--font-body);
    animation: order-widget-in 0.35s ease-out;
}

.active-order-pill[hidden] { display: none; }

@keyframes order-widget-in {
    from { transform: translateY(12px); opacity: 0; }
    to { transform: translateY(0); opacity: 1; }
}

.active-order-dot {
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: var(--color-primary);
    flex-shrink: 0;
    animation: order-dot-pulse 2s ease-in-out infinite;
}

.active-order-dot.is-ready {
    background: var(--color-success);
}

@keyframes order-dot-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.35; }
}

.active-order-pill-text {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    line-height: 1.25;
}

.active-order-pill-text strong {
    font-size: 0.82rem;
    font-family: var(--font-heading);
    letter-spacing: 0.5px;
}

.active-order-pill-text span {
    font-size: 0.7rem;
    opacity: 0.75;
}

.active-order-chevron {
    font-size: 0.6rem;
    opacity: 0.6;
    margin-left: 4px;
}

.active-order-card {
    background: #FFFFFF;
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-lg);
    padding: 20px;
    width: 290px;
    max-width: calc(100vw - 40px);
    animation: order-widget-in 0.25s ease-out;
}

.active-order-card[hidden] { display: none; }

.active-order-card-head {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    margin-bottom: 14px;
}

.active-order-eyebrow {
    font-size: 0.62rem;
    font-weight: 700;
    letter-spacing: 1px;
    color: var(--color-text-muted);
}

/* The one place the mono face earns its keep (§3.2): a ten-digit order
   number being read back across a counter. Fixed-width figures stop the
   digits shifting as the pill updates, and 0/8 and 1/7 stop trading places.
   700 rather than the 800 this had — Roboto Mono tops out at 700, and
   asking for 800 would only get a synthesised fake-bold. */
.active-order-number {
    font-family: var(--font-mono);
    font-size: 1.25rem;
    font-weight: 700;
    letter-spacing: 0.5px;
    line-height: 1.1;
}

.active-order-close {
    background: none;
    border: none;
    font-size: 1.3rem;
    line-height: 1;
    color: var(--color-text-muted);
    cursor: pointer;
    padding: 0 2px;
}

.active-order-close:hover { color: var(--color-text-dark); }

.active-order-status {
    font-family: var(--font-heading);
    font-size: 1.05rem;
    font-weight: 700;
    color: var(--color-text-dark);
    margin-bottom: 2px;
}

.active-order-blurb {
    font-size: 0.8rem;
    color: var(--color-text-muted);
    margin-bottom: 12px;
}

.active-order-track {
    height: 6px;
    background: var(--color-bg-tan);
    border-radius: 3px;
    overflow: hidden;
    margin-bottom: 14px;
}

.active-order-track-fill {
    height: 100%;
    background: var(--color-primary);
    border-radius: 3px;
    transition: width 0.5s ease;
}

.active-order-meta {
    font-size: 0.8rem;
    color: var(--color-text-muted);
    margin-bottom: 6px;
}

.active-order-meta strong { color: var(--color-text-dark); }

.active-order-cta {
    width: 100%;
    text-align: center;
    justify-content: center;
    margin-top: 10px;
    padding: 11px;
    font-size: 0.78rem;
}

/* Several orders in progress at once. Scrolls rather than growing without
   limit — three or four live orders would otherwise push the card taller than
   a phone screen and put its own close button out of reach. */
.active-order-list {
    display: flex;
    flex-direction: column;
    gap: 14px;
    max-height: 46vh;
    overflow-y: auto;
}

/* Separated only between siblings, so a single order looks exactly as it did
   before this became a list. */
.active-order-row + .active-order-row {
    border-top: 1px solid var(--color-border-light);
    padding-top: 14px;
}

.active-order-row-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 10px;
    margin-bottom: 4px;
}

.active-order-row-head strong {
    font-family: var(--font-heading);
    font-size: 1rem;
    color: var(--color-text-dark);
}

.active-order-row-type {
    font-size: 0.68rem;
    font-weight: 700;
    letter-spacing: 0.5px;
    text-transform: uppercase;
    color: var(--color-text-muted);
}

@media (max-width: 480px) {
    .active-order-widget { bottom: 14px; right: 14px; left: 14px; }
    .active-order-card { width: 100%; }
    .active-order-pill { width: 100%; justify-content: flex-start; }
}

/* Cart widget — same corner as the order-status widget, not the opposite
   one: at mobile widths the order widget already spans edge-to-edge (see
   above), so a left/right split doesn't avoid a collision, only stacking
   does. Sits at the same default bottom offset as the order widget and
   only lifts (.cart-widget--stacked, toggled by JS via a MutationObserver
   on the order widget's [hidden] attribute) when that widget is actually
   showing — a customer can have both an order in progress and a new cart
   building at once. Fixed .cart-widget--stacked offset rather than
   measuring the order widget's real height: it only needs to clear the
   collapsed pill, since a customer is unlikely to leave both expanded at
   once, and this avoids a ResizeObserver for a rare edge case. */
.cart-widget {
    position: fixed;
    bottom: 20px;
    /* Centred, where the original page-local checkout bar sat. In the corner
       it sat next to the order-status widget and read as another
       notification rather than the checkout bar it is. */
    left: 50%;
    transform: translateX(-50%);
    z-index: 1899;
    max-width: calc(100vw - 40px);
    transition: bottom 0.2s ease;
}

/* The bar wears the pill's skin, so the cart summary and the checkout button
   read as one control rather than two things that happen to sit near each
   other. They stay separate elements underneath because an <a> cannot be
   nested inside a <button>, and each needs its own click target. */
.cart-widget-bar {
    display: flex;
    align-items: center;
    gap: 8px;
    background: var(--color-dark);
    border-radius: var(--radius-pill);
    padding: 6px 6px 6px 16px;
    box-shadow: var(--shadow-lg);
    animation: order-widget-in 0.35s ease-out;
}

.cart-widget-bar[hidden] { display: none; }

/* Transparent: the bar around it is what's visible now. */
.cart-widget-summary {
    display: flex;
    align-items: center;
    gap: 10px;
    background: none;
    border: none;
    padding: 6px 2px;
    color: var(--color-text-on-dark);
    cursor: pointer;
    font-family: var(--font-body);
}

/* Cream on espresso, not chocolate. Chocolate text on the espresso pill was
   1.75:1 — the label was barely separable from the button it sat on. Cream is
   5.67:1, so the strongest call to action on the menu page finally reads like
   one. Brand cream specifically (rather than the white the other primary
   buttons use) because this bar floats over the menu photography, where a
   pure white label sits colder than the rest of the page. */
.cart-widget-bar-cta {
    display: inline-block;
    white-space: nowrap;
    background: var(--color-primary);
    color: var(--color-bg-cream);
    border-radius: var(--radius-pill);
    padding: 11px 20px;
    font-size: 0.78rem;
    font-weight: 700;
    letter-spacing: 0.5px;
    text-decoration: none;
    flex-shrink: 0;
}

.cart-widget-bar-cta:hover { filter: brightness(1.05); }

.cart-widget[hidden] { display: none; }

.cart-widget.cart-widget--stacked { bottom: 92px; }

.cart-widget-dot { background: var(--color-text-dark, var(--color-text-dark)); animation: none; }

.cart-widget-lines {
    display: flex;
    flex-direction: column;
    gap: 10px;
    max-height: 220px;
    overflow-y: auto;
    margin: 6px 0 4px;
}

.cart-widget-line-row {
    display: flex;
    justify-content: space-between;
    gap: 10px;
    font-size: 0.85rem;
    font-weight: 600;
}

.cart-widget-line-addons {
    font-size: 0.75rem;
    color: var(--color-text-muted);
    margin-top: 2px;
}

@media (max-width: 480px) {
    /* Edge-to-edge on a phone. transform must be cleared: with both left and
       right set, translateX(-50%) shifts it a further half-width off the
       screen — which is exactly what happened, leaving the bar hanging 153px
       past the left edge. */
    .cart-widget { right: 14px; left: 14px; transform: none; }
    .cart-widget .active-order-card { width: 100%; }
    .cart-widget-bar { width: 100%; }
    .cart-widget-summary { flex: 1; justify-content: flex-start; }
    .cart-widget.cart-widget--stacked { bottom: 84px; }
}

/* STORE CLOSED MODAL */
.store-closed-modal {
    position: fixed;
    inset: 0;
    z-index: 3000;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
}

.store-closed-modal[hidden] { display: none; }

.store-closed-backdrop {
    position: absolute;
    inset: 0;
    background: rgba(51, 41, 35, 0.6);
    backdrop-filter: blur(2px);
}

.store-closed-card {
    position: relative;
    background: #FFFFFF;
    border-radius: var(--radius-lg);
    padding: 36px 32px 28px;
    max-width: 420px;
    width: 100%;
    text-align: center;
    box-shadow: var(--shadow-lg);
    animation: store-closed-in 0.3s ease-out;
}

@keyframes store-closed-in {
    from { transform: translateY(16px) scale(0.98); opacity: 0; }
    to { transform: translateY(0) scale(1); opacity: 1; }
}

.store-closed-dismiss {
    position: absolute;
    top: 14px;
    right: 16px;
    background: none;
    border: none;
    font-size: 1.6rem;
    line-height: 1;
    color: var(--color-text-muted);
    cursor: pointer;
    padding: 4px;
}

/* Scoped to a direct child of the card — the small × icon — not the bare
   class. The earlier fix below (search "Buttons in the modal") already
   removed one collision between this class and .btn-outline:hover
   (they tie at identical specificity, so source order alone decided which
   text color won); this was the OTHER source of the same tie, missed at
   the time: .store-closed-dismiss:hover here, unscoped, matched "JUST
   BROWSING" too and — being declared after .btn-outline:hover — still
   overrode its white text with this near-black one, leaving dark text on
   the dark hover fill. `>` excludes "JUST BROWSING" (nested inside
   .store-closed-actions, not a direct child of the card) from ever
   matching this rule at all, rather than trying to out-rank it. */
.store-closed-card > .store-closed-dismiss:hover { color: var(--color-text-dark); }

.store-closed-icon { font-size: 2.6rem; margin-bottom: 10px; }

.store-closed-title {
    font-family: var(--font-heading);
    font-size: 1.5rem;
    margin-bottom: 10px;
}

.store-closed-message {
    color: var(--color-text-muted);
    font-size: 0.92rem;
    line-height: 1.6;
    margin-bottom: 18px;
}

.store-closed-next {
    background: var(--color-bg-cream);
    border-radius: var(--radius-md);
    padding: 14px;
    margin-bottom: 20px;
}

.store-closed-next-label {
    display: block;
    font-size: 0.68rem;
    font-weight: 700;
    letter-spacing: 1px;
    color: var(--color-text-muted);
    margin-bottom: 4px;
}

.store-closed-next-time {
    font-family: var(--font-heading);
    font-size: 1.15rem;
    font-weight: 800;
    color: var(--color-text-dark);
}

.store-closed-actions {
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin-bottom: 14px;
}

.store-closed-footnote {
    font-size: 0.78rem;
    color: var(--color-text-muted);
}

/* Buttons in the modal are full-width; the dismiss button is also a class
   selector on the close icon, so scope the reset carefully.
   No `color` override here on purpose — .btn-outline already sets a sane
   default and .btn-outline:hover sets white-on-dark. `color: inherit` used
   to sit here too, and because this selector and .btn-outline:hover have
   identical specificity, source order decided the tie: this rule comes
   later in the file, so its `inherit` beat the hover state's white and left
   "JUST BROWSING" as dark text on the dark hover background — readable
   until you actually hovered it. */
.store-closed-actions .store-closed-dismiss {
    position: static;
    font-size: 0.85rem;
    padding: 12px 24px;
}

/* PWA Install Banner */
.pwa-install-banner {
    position: fixed;
    left: 16px;
    right: 16px;
    bottom: 16px;
    z-index: 2000;
    background: #FFFFFF;
    border: 1px solid var(--color-border-light);
    border-radius: 16px;
    box-shadow: var(--shadow-lg);
    padding: 14px 16px;
    display: flex;
    align-items: center;
    /* Wraps so the call-to-action can take its own full-width row below the
       text instead of squeezing it. */
    flex-wrap: wrap;
    gap: 12px;
    max-width: 480px;
    margin: 0 auto;
    animation: pwa-banner-in 0.3s ease-out;
}

.pwa-install-banner[hidden] { display: none; }

@keyframes pwa-banner-in {
    from { transform: translateY(20px); opacity: 0; }
    to { transform: translateY(0); opacity: 1; }
}

.pwa-install-banner-icon {
    width: 44px;
    height: 44px;
    border-radius: 12px;
    flex-shrink: 0;
}

.pwa-install-banner-text {
    display: flex;
    flex-direction: column;
    gap: 2px;
    flex: 1;
    min-width: 0;
}

.pwa-install-banner-text strong {
    font-size: 0.85rem;
    color: var(--color-text-dark);
}

.pwa-install-banner-text span {
    font-size: 0.75rem;
    color: var(--color-text-muted);
    line-height: 1.4;
}

/* Full width on its own row, under the text.
   It used to sit inline with flex-shrink: 0 and white-space: nowrap, so a
   long label — iOS gets "WILL ADD TO HOME SCREEN", not Android's short
   "Install" — claimed its full intrinsic width and gave none of it back.
   The text column collapsed next to it and wrapped one word per line. */
.pwa-install-banner .btn {
    order: 3;
    flex-basis: 100%;
    width: 100%;
    text-align: center;
    white-space: normal;
    margin-top: 2px;
}

/* Stays on the first row, beside the text, rather than being pushed down
   with the button. */
.pwa-install-banner-close {
    order: 2;
}

.pwa-install-banner-close {
    background: none;
    border: none;
    font-size: 1.3rem;
    line-height: 1;
    color: var(--color-text-muted);
    cursor: pointer;
    padding: 4px;
    flex-shrink: 0;
}

.pwa-install-banner-close:hover { color: var(--color-text-dark); }

@media (max-width: 420px) {
    .pwa-install-banner { flex-wrap: wrap; }
    .pwa-install-banner .btn { order: 3; width: 100%; text-align: center; }
}

/* ==========================================================================
   ADMIN SHELL
   --------------------------------------------------------------------------
   Sidebar, top bar and content well for the staff back office. Kept at the end
   of the file and namespaced under .admin-* so it cannot reach into the
   storefront: the two share the palette and the type, and nothing else.

   Every colour here is a brand token. The sidebar is chocolate, the darkest
   value in the palette and the only one that can carry a full-height panel
   without competing with the cream well beside it.
   ========================================================================== */

.admin-body { background: var(--color-bg-page); }

/* A <select> comes out two pixels taller than an <input> carrying identical
   padding, border and font, because the two elements derive their inner
   height differently. Side by side in a form row that reads as one control
   being slightly wrong rather than as a browser default. Pinning the
   line-height makes every control in the panel agree. */
.admin-body input:not([type="checkbox"]):not([type="radio"]),
.admin-body select,
.admin-body textarea { line-height: 20px; box-sizing: border-box; }

/* A date input carries a picker control inside it and comes out two pixels
   taller again. Matching the line box explicitly puts it back on the row. */
.admin-body input[type="date"],
.admin-body input[type="time"],
.admin-body input[type="datetime-local"] { height: calc(20px + 1.4em); }

.admin-shell { display: flex; min-height: 100vh; }

/* ----------------------------- Sidebar ---------------------------------- */

.admin-sidebar {
    width: 232px;
    flex-shrink: 0;
    background: var(--color-dark);
    color: var(--color-text-on-dark);
    display: flex;
    flex-direction: column;
    /* Sticky rather than fixed: fixed would need the content well padded by
       hand to match, and the two would drift apart the first time the width
       changed. */
    position: sticky;
    top: 0;
    height: 100vh;
    overflow-y: auto;
    overscroll-behavior: contain;
}

.admin-brand { display: block; padding: 22px 20px 18px; flex-shrink: 0; }
.admin-brand-full { height: 34px; width: auto; display: block; }
.admin-brand-mark { height: 30px; width: auto; display: none; margin: 0 auto; border-radius: 7px; }

.admin-nav { padding: 4px 12px 24px; }

.admin-nav-group {
    font-size: 0.66rem;
    font-weight: 700;
    letter-spacing: 1.2px;
    text-transform: uppercase;
    /* Taupe on chocolate is 7.70:1 -- recessed from the cream links beside it
       (11.54:1) without dropping to a whisper. */
    color: var(--color-bg-tan);
    padding: 20px 10px 8px;
}

.admin-nav-link {
    display: flex;
    align-items: center;
    gap: 11px;
    padding: 9px 10px;
    border-radius: 9px;
    font-size: 0.855rem;
    font-weight: 500;
    color: var(--color-bg-cream);
    text-decoration: none;
    transition: var(--transition);
}

.admin-nav-link svg { flex-shrink: 0; opacity: 0.75; }
.admin-nav-link:hover { background: rgba(240, 231, 214, 0.10); }

/* The current page. An espresso fill rather than a left rule: at this density
   a 2px marker is easy to miss, and a filled pill survives being glanced at. */
.admin-nav-link.is-active {
    background: var(--color-primary);
    color: var(--color-text-on-dark);
    font-weight: 600;
}

.admin-nav-link.is-active svg { opacity: 1; }

/* ----------------------------- Top bar ---------------------------------- */

.admin-main {
    flex: 1;
    min-width: 0;   /* lets wide tables scroll instead of stretching the shell */
    display: flex;
    flex-direction: column;
}

.admin-topbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    padding: 12px 26px;
    background: var(--color-bg-light);
    border-bottom: 1px solid var(--color-border-light);
    position: sticky;
    top: 0;
    z-index: 40;
}

.admin-burger,
.admin-bell {
    width: 38px;
    height: 38px;
    border-radius: 10px;
    border: 1px solid var(--color-border-light);
    background: none;
    color: var(--color-text-dark);
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    position: relative;
    transition: var(--transition);
}

.admin-burger:hover,
.admin-bell:hover {
    background: var(--color-bg-cream);
    border-color: var(--color-primary);
}

.admin-topbar-right { display: flex; align-items: center; gap: 12px; margin-left: auto; }

/* ---- Workspace switch: dashboard / till / queue ------------------------
   One control in both staff layouts (app/views/layouts/workspace-switch.php).
   A segmented group rather than three loose links, because they are one
   choice with one answer -- and the current one is marked, so it says where
   you are as well as where you can go. */
.ws-switch {
    display: flex;
    align-items: stretch;
    gap: 2px;
    padding: 3px;
    border: 1px solid var(--color-border-light);
    border-radius: 999px;
    background: var(--color-bg-cream);
    margin-right: 4px;
}

.ws-link {
    display: flex;
    align-items: center;
    padding: 0 14px;
    height: 30px;
    border-radius: 999px;
    font-size: 0.78rem;
    font-weight: 700;
    letter-spacing: 0.02em;
    color: var(--color-text-muted);
    text-decoration: none;
    white-space: nowrap;
    transition: var(--transition);
}

.ws-link:hover { color: var(--color-text-dark); background: rgba(0, 0, 0, 0.04); }

/* The one you are on. Filled, so it reads at arm's length on a till tablet
   the way the sidebar's active row does. */
.ws-link.is-here {
    background: var(--color-primary);
    color: var(--color-text-on-dark);
}
.ws-link.is-here:hover { background: var(--color-primary); }

.ws-link:focus-visible {
    outline: 2px solid var(--color-primary);
    outline-offset: 2px;
}

/* The cashier row is round-buttoned; the group already is, so it only needs
   the same border weight to sit level with the bell beside it. */
.cashier-shell .ws-switch { border-color: var(--color-border); }

/* Tighter on a narrow screen, never hidden. This is the only route between
   the three workspaces, and a phone is exactly where retyping a URL is
   worst — so it gives up padding rather than existence. */
@media (max-width: 720px) {
    .ws-link { padding: 0 10px; font-size: 0.72rem; }
}
@media (max-width: 480px) {
    .ws-switch { margin-right: 0; }
    .ws-link { padding: 0 8px; height: 28px; font-size: 0.68rem; }
}

.admin-bell-badge {
    position: absolute;
    top: -5px;
    right: -6px;
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    border-radius: var(--radius-pill);
    background: var(--color-primary);
    color: var(--color-text-on-dark);
    font-size: 0.65rem;
    font-weight: 700;
    line-height: 18px;
    text-align: center;
    box-shadow: 0 0 0 2px var(--color-bg-light);
}

.admin-user { position: relative; }

.admin-user-btn {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 5px 8px 5px 5px;
    border: 1px solid transparent;
    border-radius: 10px;
    background: none;
    cursor: pointer;
    color: var(--color-text-dark);
    transition: var(--transition);
}

.admin-user-btn:hover { background: var(--color-bg-cream); }

.admin-avatar {
    width: 32px;
    height: 32px;
    border-radius: 50%;
    background: var(--color-primary);
    color: var(--color-text-on-dark);
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}

.admin-user-text { display: flex; flex-direction: column; line-height: 1.25; text-align: left; }
.admin-user-text strong { font-size: 0.85rem; font-weight: 600; }
.admin-user-text small { font-size: 0.72rem; color: var(--color-text-muted); }

.admin-user-menu {
    position: absolute;
    top: calc(100% + 8px);
    right: 0;
    min-width: 210px;
    background: var(--color-bg-light);
    border: 1px solid var(--color-border-light);
    border-radius: 12px;
    box-shadow: var(--shadow-md);
    padding: 6px;
    z-index: 50;
}

.admin-user-menu a {
    display: block;
    padding: 9px 12px;
    border-radius: 8px;
    font-size: 0.85rem;
    color: var(--color-text-dark);
    text-decoration: none;
}

.admin-user-menu a:hover { background: var(--color-bg-cream); }
.admin-user-menu a.is-danger { color: var(--color-danger); }

/* ----------------------------- Content ---------------------------------- */

.admin-content { padding: 26px; flex: 1; }

/* The seventeen pages that predate this shell still wrap their content in
   the storefront's .section > .container, which centres a 1200px column and
   adds page padding. Inside the shell that is padding on top of padding and a
   column floating in the middle of a well that is already the right width.
   Neutralised here rather than edited out of every page: the markup is
   harmless, only its storefront sizing is wrong in this context. */
.admin-content > .section,
.admin-content .section {
    padding: 0;
}

.admin-content .container {
    max-width: none;
    padding: 0;
    margin: 0;
}

.admin-page-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 20px;
    flex-wrap: wrap;
    margin-bottom: 24px;
}

.admin-page-head h1 { font-family: var(--font-heading); font-size: 1.8rem; margin: 0 0 4px; }

/* The page banner. Five screens were built with one and twelve without, from
   whichever artboard each came from, so the header changed shape twice as you
   moved through Inventory. One definition here rather than a sixth copy in
   each file -- that divergence is exactly how the first five drifted apart.
   The blurb belongs inside it: it is part of the heading, not the first
   paragraph of the content. */
.admin-banner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 20px;
    flex-wrap: wrap;
    background: var(--color-dark);
    color: var(--color-text-on-dark);
    border-radius: 16px;
    padding: 20px 26px;
    margin-bottom: 18px;
}
.admin-banner h1 {
    font-family: var(--font-heading);
    font-size: 1.8rem;
    margin: 0;
    color: var(--color-text-on-dark);
}
.admin-banner-eyebrow {
    font-size: 0.72rem;
    font-weight: 800;
    letter-spacing: 1.2px;
    text-transform: uppercase;
    color: var(--brand-ochre);
    margin-bottom: 5px;
}
/* Held to a readable measure so a long description does not run the full
   width of a wide screen. */
.admin-banner p { margin: 7px 0 0; font-size: 0.88rem; line-height: 1.55; opacity: 0.82; max-width: 82ch; }
.admin-banner-actions { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
@media (max-width: 700px) { .admin-banner { padding: 18px 20px; } .admin-banner h1 { font-size: 1.5rem; } }
.admin-page-head p { color: var(--color-text-muted); font-size: 0.9rem; margin: 0; }

.admin-card {
    background: var(--color-bg-light);
    border: 1px solid var(--color-border-light);
    border-radius: 14px;
    box-shadow: var(--shadow-sm);
}

.admin-section-title { font-family: var(--font-heading); font-size: 1.2rem; margin: 0 0 14px; }

/* Stat tiles. The circular wash carries the accent; the figure stays
   chocolate, because a number in a light accent is a number nobody can read. */
.admin-stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: 18px;
    margin-bottom: 30px;
}

.admin-stat { display: flex; align-items: flex-start; gap: 15px; padding: 20px; }

.admin-stat-icon {
    width: 46px;
    height: 46px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}

.admin-stat-label {
    font-size: 0.68rem;
    font-weight: 700;
    letter-spacing: 1px;
    text-transform: uppercase;
    color: var(--color-text-muted);
    margin-bottom: 6px;
}

.admin-stat-value {
    font-family: var(--font-heading);
    font-size: 1.75rem;
    font-weight: 700;
    line-height: 1.1;
    margin-bottom: 4px;
}

.admin-stat-sub { font-size: 0.76rem; color: var(--color-text-muted); }

/* Attention tiles and module tiles share a shape: a row that is entirely a
   link, with a chevron promising where it goes. */
.admin-tiles { display: grid; gap: 16px; margin-bottom: 30px; }
.admin-tiles--4 { grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); }
.admin-tiles--modules { grid-template-columns: repeat(auto-fit, minmax(270px, 1fr)); }

.admin-tile {
    display: flex;
    align-items: center;
    gap: 14px;
    padding: 17px 18px;
    text-decoration: none;
    color: inherit;
    transition: var(--transition);
}

.admin-tile:hover { border-color: var(--color-primary); transform: translateY(-2px); }

.admin-tile-icon {
    width: 42px;
    height: 42px;
    border-radius: 12px;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    background: var(--color-bg-cream);
    color: var(--color-primary);
}

.admin-tile-body { flex: 1; min-width: 0; }

.admin-tile-title {
    font-weight: 600;
    font-size: 0.92rem;
    margin-bottom: 2px;
    display: flex;
    align-items: center;
    gap: 8px;
}

.admin-tile-desc { font-size: 0.78rem; color: var(--color-text-muted); line-height: 1.45; }
.admin-tile-chevron { color: var(--color-text-muted); flex-shrink: 0; }

.admin-tile-count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 20px;
    height: 20px;
    padding: 0 6px;
    border-radius: var(--radius-pill);
    background: var(--color-primary);
    color: var(--color-text-on-dark);
    font-size: 0.68rem;
    font-weight: 700;
}

/* .admin-table and .admin-table-wrap used to be defined here, in their own
   slightly different shape -- 18px cells, a 700-weight heading, a bottom border
   per row. They are names for THE ADMIN LIST PAGE now, further down, and the
   definition here was deleted rather than left above it.

   Leaving it would have been the whole problem in miniature: the dashboard
   would have kept its own table design while every other screen moved, and the
   two would have been a couple of pixels apart forever with nothing saying
   which was meant. */

/* ===========================================================================
   THE ADMIN LIST PAGE
   ---------------------------------------------------------------------------
   One shape for every screen that lists things, because there were nine. Each
   page had grown its own prefix -- pl-, inv-, ig-, ad-, or-, rl-, fc-, pp-,
   sh- -- for the same table, the same status pill and the same row of buttons,
   with the paddings and weights drifting apart a little more each time one was
   copied. A person moving between two admin screens was reading two designs.

   The parts, in the order they appear, are:

     .admin-banner            the dark header
       .admin-banner-eyebrow    which GROUP the screen belongs to
       h1                       its NAME
       .admin-banner-actions    ADD ENTRY, and anything else that acts on the
                                whole screen rather than on one row
     .list-controls           SEARCH and FILTERS  (components/list-controls.php)
     .admin-card              the white slab the table sits in
       .admin-table             HEADING row, then the rows
         .admin-num               a figure column, right-aligned and tabular
         .admin-pill              a status
         .admin-row-actions       the buttons for that one row
       .admin-empty             what to say when there is nothing yet
     .list-pager              ROWS, the count, and the way on (components/)

   Written against /admin/products, which is the screen this was drawn from.
   =========================================================================== */

/* The slab. No padding of its own: the table's cells set theirs, so a row can
   run the full width and the heading still lines up with the first column.
   Anything in the card that is NOT a table row therefore has to say its own
   padding -- .admin-empty below does, which is the one that kept forgetting. */
.admin-card {
    background: var(--color-bg-light);
    border: 1px solid var(--color-border-light);
    border-radius: 16px;
    box-shadow: var(--shadow-sm);
    overflow: hidden;
    margin-bottom: 22px;
}

/* A card that holds prose or a form rather than a table, and therefore does
   want padding. */
.admin-card-pad { padding: 22px 24px 26px; }
.admin-card > h2,
.admin-card-pad > h2 {
    font-family: var(--font-heading);
    font-size: 1.15rem;
    margin: 0 0 14px;
}
/* 24px, not 18px: the standard insets the first column by 24px, so at 18px
   the heading sat six pixels to the left of the name under it -- the exact
   thing the comment above says this card exists to avoid. */
.admin-card > h2 { padding: 20px 24px 0; }

/* A wide table scrolls inside its own card rather than pushing the page
   sideways. The wrapper is not optional on a table with more than about five
   columns: .admin-card is overflow:hidden, so an unwrapped wide table is CUT
   OFF on a phone rather than scrolled, and the page reports no overflow
   precisely because the card swallowed it. That exact bug has been found four
   times in this admin (W-F5, W-F17, W-F18 and the stock take before them). */
.admin-scroll { overflow-x: auto; }

/* ONE TABLE, MANY NAMES.
   Each screen had grown its own prefix for the same table -- ad-, ig-, rl-,
   pp-, sh-, rcp-, fc-, rc-, po-, pk-, sup-, take-, dlv-, inv- -- and the
   paddings and weights drifted a little further apart each time one was
   copied. Rather than rewrite the markup on twenty-three pages, every one of
   those names is listed here against ONE set of declarations. Change the table
   in this block and it changes everywhere, which is the whole point.

   New work should use the .admin-* names. The rest are kept so the pages that
   already exist did not all have to be edited at once to get the same design;
   they are names for this design, not designs of their own, and a page must
   not redefine them in its own <style> block. */
.admin-list-table,
.admin-table,
.inv-table,
.ad-table,
.ig-table,
.rl-table,
.pp-table,
.sh-table,
.rcp-table,
.fc-table,
.rc-table,
.po-table,
.pk-table,
.sup-table,
.take-table,
.dlv-table {
    width: 100%;
    min-width: 680px;
    border-collapse: collapse;
    font-size: 0.88rem;
}
.admin-list-table thead tr,
.admin-table thead tr,
.inv-table thead tr,
.ad-table thead tr,
.ig-table thead tr,
.rl-table thead tr,
.pp-table thead tr,
.sh-table thead tr,
.rcp-table thead tr,
.fc-table thead tr,
.rc-table thead tr,
.po-table thead tr,
.pk-table thead tr,
.sup-table thead tr,
.take-table thead tr,
.dlv-table thead tr {
    background: var(--color-bg-soft);
    text-align: left;
}
.admin-list-table th,
.admin-table th,
.inv-table th,
.ad-table th,
.ig-table th,
.rl-table th,
.pp-table th,
.sh-table th,
.rcp-table th,
.fc-table th,
.rc-table th,
.po-table th,
.pk-table th,
.sup-table th,
.take-table th,
.dlv-table th {
    text-align: left;
    padding: 12px 16px;
    font-size: 0.72rem;
    font-weight: 800;
    letter-spacing: 0.8px;
    text-transform: uppercase;
    color: var(--color-text-muted);
    white-space: nowrap;
    border-bottom: 1px solid var(--color-border-light);
}
.admin-list-table td,
.admin-table td,
.inv-table td,
.ad-table td,
.ig-table td,
.rl-table td,
.pp-table td,
.sh-table td,
.rcp-table td,
.fc-table td,
.rc-table td,
.po-table td,
.pk-table td,
.sup-table td,
.take-table td,
.dlv-table td {
    padding: 12px 16px;
    border-top: 1px solid var(--color-border-light);
    vertical-align: middle;
}
.admin-list-table thead + tbody tr:first-child td,
.admin-table thead + tbody tr:first-child td,
.inv-table thead + tbody tr:first-child td,
.ad-table thead + tbody tr:first-child td,
.ig-table thead + tbody tr:first-child td,
.rl-table thead + tbody tr:first-child td,
.pp-table thead + tbody tr:first-child td,
.sh-table thead + tbody tr:first-child td,
.rcp-table thead + tbody tr:first-child td,
.fc-table thead + tbody tr:first-child td,
.rc-table thead + tbody tr:first-child td,
.po-table thead + tbody tr:first-child td,
.pk-table thead + tbody tr:first-child td,
.sup-table thead + tbody tr:first-child td,
.take-table thead + tbody tr:first-child td,
.dlv-table thead + tbody tr:first-child td { border-top: 0; }

/* Wider or narrower than the default, said on the table rather than in a page's
   own stylesheet. */
/* THE OUTER COLUMNS SIT IN FROM THE EDGE.
   The row's background still runs the full width of the card -- that is what
   makes the heading read as the top of the table rather than a band lying
   across it -- but the first and last columns are inset, so a name does not
   start hard against the border and a figure does not finish on it.

   Three pages had written this out for themselves at 24px and lost it when
   their rules were merged here: the delivery lines, whose ITEM heading ended
   up clipped outside its card, and the stock take sheet, whose edge margin had
   been added a week earlier for the same complaint. Putting it in the standard
   is the only version that cannot be removed from one screen by tidying
   another. */
.admin-list-table th:first-child,
.admin-table th:first-child,
.inv-table th:first-child,
.ad-table th:first-child,
.ig-table th:first-child,
.rl-table th:first-child,
.pp-table th:first-child,
.sh-table th:first-child,
.rcp-table th:first-child,
.fc-table th:first-child,
.rc-table th:first-child,
.po-table th:first-child,
.pk-table th:first-child,
.sup-table th:first-child,
.take-table th:first-child,
.dlv-table th:first-child,
.admin-list-table td:first-child,
.admin-table td:first-child,
.inv-table td:first-child,
.ad-table td:first-child,
.ig-table td:first-child,
.rl-table td:first-child,
.pp-table td:first-child,
.sh-table td:first-child,
.rcp-table td:first-child,
.fc-table td:first-child,
.rc-table td:first-child,
.po-table td:first-child,
.pk-table td:first-child,
.sup-table td:first-child,
.take-table td:first-child,
.dlv-table td:first-child { padding-left: 24px; }
.admin-list-table th:last-child,
.admin-table th:last-child,
.inv-table th:last-child,
.ad-table th:last-child,
.ig-table th:last-child,
.rl-table th:last-child,
.pp-table th:last-child,
.sh-table th:last-child,
.rcp-table th:last-child,
.fc-table th:last-child,
.rc-table th:last-child,
.po-table th:last-child,
.pk-table th:last-child,
.sup-table th:last-child,
.take-table th:last-child,
.dlv-table th:last-child,
.admin-list-table td:last-child,
.admin-table td:last-child,
.inv-table td:last-child,
.ad-table td:last-child,
.ig-table td:last-child,
.rl-table td:last-child,
.pp-table td:last-child,
.sh-table td:last-child,
.rcp-table td:last-child,
.fc-table td:last-child,
.rc-table td:last-child,
.po-table td:last-child,
.pk-table td:last-child,
.sup-table td:last-child,
.take-table td:last-child,
.dlv-table td:last-child { padding-right: 24px; }

.admin-list-table.is-wide { min-width: 860px; }
.admin-list-table.is-narrow, .inv-table.is-narrow { min-width: 0; }

/* Figures line up under their own heading. A table's th sets text-align:left at
   a higher specificity than a bare .admin-num, so the heading has to be named
   with its element or every figure column gets a left-aligned title over
   right-aligned numbers -- which reads as two unrelated columns. */
.admin-num,
.inv-num,
.ad-num,
.ig-num,
.rl-num,
.pp-num,
.sh-num,
.rcp-num,
.fc-num,
.rc-num { text-align: right; font-variant-numeric: tabular-nums; }
.admin-list-table th.admin-num,
.admin-list-table th.ad-num,
.admin-table th.admin-num,
.admin-table th.ad-num,
.inv-table th.admin-num,
.inv-table th.inv-num,
.ad-table th.admin-num,
.ad-table th.ad-num,
.ig-table th.admin-num,
.ig-table th.ig-num,
.rl-table th.admin-num,
.rl-table th.rl-num,
.pp-table th.admin-num,
.pp-table th.pp-num,
.sh-table th.admin-num,
.sh-table th.sh-num,
.rcp-table th.admin-num,
.rcp-table th.rcp-num,
.rcp-table th.rc-num,
.fc-table th.admin-num,
.fc-table th.fc-num,
.rc-table th.admin-num,
.rc-table th.rcp-num,
.rc-table th.rc-num,
.po-table th.admin-num,
.pk-table th.admin-num,
.sup-table th.admin-num,
.take-table th.admin-num,
.dlv-table th.admin-num { text-align: right; }
.admin-muted, .inv-muted { color: var(--color-text-muted); }

/* The first column of a list: a picture, a name, and a line of detail. */
.admin-item { display: flex; align-items: center; gap: 12px; }
.admin-item img { width: 42px; height: 42px; object-fit: cover; border-radius: 8px; flex-shrink: 0; }
.admin-item-noimg {
    width: 42px; height: 42px; border-radius: 8px;
    background: var(--color-bg-soft);
    display: inline-flex; align-items: center; justify-content: center;
    color: var(--color-border); flex-shrink: 0;
}
.admin-item a, .admin-item strong { font-weight: 700; color: var(--color-text-dark); }
.admin-item a:hover { color: var(--color-primary); }
.admin-sub {
    font-size: 0.78rem;
    color: var(--color-text-muted);
    margin-top: 2px;
    max-width: 42ch;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* A status, in a word. The colour carries the same meaning on every screen:
   green is fine, amber wants attention, red is stopped, grey is switched off
   rather than broken. */
.admin-pill,
.inv-pill,
.ad-pill,
.ig-pill,
.rl-pill,
.sh-pill,
.po-pill,
.pos-pill,
.pe-pill {
    display: inline-block;
    font-size: 0.7rem;
    font-weight: 800;
    letter-spacing: 0.5px;
    text-transform: uppercase;
    padding: 4px 11px;
    border-radius: var(--radius-pill);
    white-space: nowrap;
}
.admin-pill.is-ok,
.inv-pill.is-ok,
.ad-pill.is-ok,
.ig-pill.is-ok,
.rl-pill.is-ok,
.sh-pill.is-ok,
.po-pill.is-ok,
.pos-pill.is-ok,
.pe-pill.is-ok,
.admin-pill.on,
.inv-pill.on,
.ad-pill.on,
.ig-pill.on,
.rl-pill.on,
.sh-pill.on,
.po-pill.on,
.pos-pill.on,
.pe-pill.on,
.admin-pill.ok,
.inv-pill.ok,
.ad-pill.ok,
.ig-pill.ok,
.rl-pill.ok,
.sh-pill.ok,
.po-pill.ok,
.pos-pill.ok,
.pe-pill.ok { background: var(--color-success-bg); color: var(--color-success); }
.admin-pill.is-low,
.inv-pill.is-low,
.ad-pill.is-low,
.ig-pill.is-low,
.rl-pill.is-low,
.sh-pill.is-low,
.po-pill.is-low,
.pos-pill.is-low,
.pe-pill.is-low,
.admin-pill.low,
.inv-pill.low,
.ad-pill.low,
.ig-pill.low,
.rl-pill.low,
.sh-pill.low,
.po-pill.low,
.pos-pill.low,
.pe-pill.low,
.admin-pill.warn,
.inv-pill.warn,
.ad-pill.warn,
.ig-pill.warn,
.rl-pill.warn,
.sh-pill.warn,
.po-pill.warn,
.pos-pill.warn,
.pe-pill.warn { background: var(--color-warning-bg); color: var(--brand-espresso); }
.admin-pill.is-out,
.inv-pill.is-out,
.ad-pill.is-out,
.ig-pill.is-out,
.rl-pill.is-out,
.sh-pill.is-out,
.po-pill.is-out,
.pos-pill.is-out,
.pe-pill.is-out,
.admin-pill.out,
.inv-pill.out,
.ad-pill.out,
.ig-pill.out,
.rl-pill.out,
.sh-pill.out,
.po-pill.out,
.pos-pill.out,
.pe-pill.out,
.admin-pill.bad,
.inv-pill.bad,
.ad-pill.bad,
.ig-pill.bad,
.rl-pill.bad,
.sh-pill.bad,
.po-pill.bad,
.pos-pill.bad,
.pe-pill.bad { background: var(--color-danger-bg); color: var(--color-danger); }
.admin-pill.is-off,
.inv-pill.is-off,
.ad-pill.is-off,
.ig-pill.is-off,
.rl-pill.is-off,
.sh-pill.is-off,
.po-pill.is-off,
.pos-pill.is-off,
.pe-pill.is-off,
.admin-pill.off,
.inv-pill.off,
.ad-pill.off,
.ig-pill.off,
.rl-pill.off,
.sh-pill.off,
.po-pill.off,
.pos-pill.off,
.pe-pill.off,
.admin-pill.hidden,
.inv-pill.hidden,
.ad-pill.hidden,
.ig-pill.hidden,
.rl-pill.hidden,
.sh-pill.hidden,
.po-pill.hidden,
.pos-pill.hidden,
.pe-pill.hidden,
.admin-pill.mute,
.inv-pill.mute,
.ad-pill.mute,
.ig-pill.mute,
.rl-pill.mute,
.sh-pill.mute,
.po-pill.mute,
.pos-pill.mute,
.pe-pill.mute { background: var(--color-bg-soft); color: var(--color-text-muted); }

/* The words the pills were already wearing before they shared a definition.
   Recovered from what each page used to say rather than guessed at: a till
   that is SHORT was red, one that is OVER was amber, one that balanced EXACTLY
   was green, and a shift still RUNNING was amber because it is not settled
   yet. Dropped when the nine pill definitions were merged into one, which left
   four screens rendering an unstyled word -- and an unstyled word still reads
   as a word, so nothing looked broken. */
.admin-pill.hot,
.inv-pill.hot,
.ad-pill.hot,
.ig-pill.hot,
.rl-pill.hot,
.sh-pill.hot,
.po-pill.hot,
.pos-pill.hot,
.pe-pill.hot { background: var(--color-danger-bg); color: var(--color-danger); }
.admin-pill.cool,
.inv-pill.cool,
.ad-pill.cool,
.ig-pill.cool,
.rl-pill.cool,
.sh-pill.cool,
.po-pill.cool,
.pos-pill.cool,
.pe-pill.cool { background: var(--color-info-bg); color: var(--color-info); }
.admin-pill.plain,
.inv-pill.plain,
.ad-pill.plain,
.ig-pill.plain,
.rl-pill.plain,
.sh-pill.plain,
.po-pill.plain,
.pos-pill.plain,
.pe-pill.plain { background: var(--color-bg-soft); color: var(--color-text-muted); }
.admin-pill.exact,
.inv-pill.exact,
.ad-pill.exact,
.ig-pill.exact,
.rl-pill.exact,
.sh-pill.exact,
.po-pill.exact,
.pos-pill.exact,
.pe-pill.exact { background: var(--color-success-bg); color: var(--color-success); }
.admin-pill.over,
.inv-pill.over,
.ad-pill.over,
.ig-pill.over,
.rl-pill.over,
.sh-pill.over,
.po-pill.over,
.pos-pill.over,
.pe-pill.over,
.admin-pill.running,
.inv-pill.running,
.ad-pill.running,
.ig-pill.running,
.rl-pill.running,
.sh-pill.running,
.po-pill.running,
.pos-pill.running,
.pe-pill.running { background: var(--color-warning-bg); color: var(--brand-espresso); }
.admin-pill.short,
.inv-pill.short,
.ad-pill.short,
.ig-pill.short,
.rl-pill.short,
.sh-pill.short,
.po-pill.short,
.pos-pill.short,
.pe-pill.short { background: rgba(214, 87, 98, 0.2); color: var(--color-danger); }

/* The buttons for one row. Kept on one line: a row whose actions wrap is a row
   whose height changes, and a table of them stops being scannable. */
.admin-row-actions,
.ad-actions,
.ig-actions,
.fc-actions,
.pp-actions,
.po-actions,
.lb-actions,
.dp-actions,
.ie-actions,
.igm-actions,
.take-actions { white-space: nowrap; text-align: right; }
.admin-row-actions form,
.ad-actions form,
.ig-actions form,
.fc-actions form,
.pp-actions form,
.po-actions form,
.lb-actions form,
.dp-actions form,
.ie-actions form,
.igm-actions form,
.take-actions form,
.pe-actions form { display: inline; }
/* One gap between any two of them. It used to be written as .btn + .btn and
   form + form, which are the two adjacencies that happen to look the same
   -- and missed the one where a plain button sits beside a button in a
   form. On the product list that put 9px between EDIT and SIZES and 3px
   between SIZES and the eye: the same row spaced two different ways. No
   actions cell has two buttons inside ONE form, so the child combinator
   loses nothing. */
.admin-row-actions > * + *,
.ad-actions > * + *,
.ig-actions > * + *,
.fc-actions > * + *,
.pp-actions > * + *,
.po-actions > * + *,
.lb-actions > * + *,
.dp-actions > * + *,
.ie-actions > * + *,
.igm-actions > * + *,
.take-actions > * + *,
.pe-actions > * + * { margin-left: 6px; }

/* Nothing here yet. It sits in a card whose padding belongs to the table's
   cells, so it has to say its own -- and enough of it to read as a state
   rather than a stray line of text. */
.admin-empty,
.inv-empty,
.ad-empty,
.ig-empty,
.rl-empty,
.pp-empty,
.sh-empty,
.fc-empty,
.rc-empty,
.dlv-empty,
.pe-empty {
    color: var(--color-text-muted);
    font-size: 0.88rem;
    line-height: 1.6;
    margin: 0;
    padding: 26px 16px 30px;
}


/* A wide table scrolls inside its own card rather than pushing the page
   sideways. Not optional on a table with more than about five columns:
   .admin-card is overflow:hidden, so an unwrapped wide table is CUT OFF on a
   phone rather than scrolled, and the page reports no overflow precisely
   because the card swallowed it. Found five times in this admin now. */
.admin-scroll,
.inv-scroll,
.ig-scroll,
.sup-scroll,
.admin-table-wrap { overflow-x: auto; }

/* ADD ENTRY, OPENED.
   The form behind the button. Four screens had written this chrome out
   separately -- receive a delivery, write something off, add a pack, add a
   product -- with the same dark head, the same close cross and the same
   full-height behaviour on a phone, drifting apart by a few pixels each time.
   One definition, so an admin modal looks like an admin modal.

   Opened by a real URL (?add=1) as well as by the button, so a refusal can
   send somebody back to the form with what they typed still in it. */
.admin-overlay {
    position: fixed;
    inset: 0;
    background: rgba(51, 41, 35, 0.55);
    display: flex;
    align-items: flex-start;
    justify-content: center;
    padding: 24px 20px;
    z-index: 1200;
    overflow-y: auto;
}
.admin-overlay[hidden] { display: none; }
.admin-modal {
    width: 900px;
    max-width: 100%;
    background: var(--color-bg-light);
    /* Its own text colour, not inherited. The component is included inside
       .admin-banner so the button lands in the header -- and the banner is dark
       and sets cream text. position:fixed moves the BOX out of the banner; it
       does not move the element out of the banner's inheritance, so every label
       in here came out cream on white and could barely be read. A panel that
       paints its own background has to paint its own foreground. */
    color: var(--color-text-dark);
    border-radius: 18px;
    box-shadow: 0 24px 60px rgba(0, 0, 0, 0.28);
    margin: auto;
}
.admin-modal-sm { width: 640px; }
.admin-modal-head {
    background: var(--color-dark);
    color: var(--color-text-on-dark);
    padding: 18px 24px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    border-radius: 18px 18px 0 0;
}
.admin-modal-eyebrow {
    font-size: 0.62rem;
    font-weight: 800;
    letter-spacing: 1.3px;
    text-transform: uppercase;
    color: var(--brand-ochre);
}
.admin-modal-title {
    font-family: var(--font-heading);
    font-size: 1.3rem;
    margin: 4px 0 0;
    color: var(--color-bg-light);
}
.admin-modal-x {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px; height: 32px;
    border: 0; border-radius: 50%;
    padding: 0;
    background: rgba(255, 255, 255, 0.14);
    color: var(--color-bg-light);
    font-size: 1.1rem;
    cursor: pointer;
}
.admin-modal-x:hover { background: rgba(255, 255, 255, 0.26); }
.admin-modal-body { padding: 22px 24px 26px; }
.admin-modal-hint { font-size: 0.82rem; color: var(--color-text-muted); margin: 0; }

/* The row a modal ends on. The action that completes the form belongs at the
   bottom, after everything it acts on -- not in the middle of the first row of
   fields, where "ADD" sat beside Points looking like part of the grid and read
   as though it belonged to the four fields above it rather than to the form. */
.admin-modal-actions {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 12px;
    margin-top: 20px;
    padding-top: 18px;
    border-top: 1px solid var(--color-border-light);
}
/* Anything explanatory sits at the far end, away from the button it explains,
   so a long sentence never pushes the action off the edge. */
.admin-modal-actions .admin-modal-hint { margin-right: auto; text-align: left; max-width: 62ch; }
@media (max-width: 700px) {
    .admin-modal-actions { flex-wrap: wrap; }
    .admin-modal-actions .btn { flex: 1 1 100%; }
}

@media (max-width: 700px) {
    /* Full height on a phone. A centred card with a margin round it wastes the
       only screen where the form is longer than the window. */
    .admin-overlay { padding: 0; }
    .admin-modal { border-radius: 0; min-height: 100vh; }
    .admin-modal-head { border-radius: 0; }
    .admin-modal-body .admin-form-grid { grid-template-columns: 1fr !important; }
}

/* ADD ENTRY. The one action that makes a new thing, in the banner, where it is
   the same distance from the heading on every screen.

   This was lost once already: it sat between the table rules and the strip
   below, and a later edit that replaced "everything from the table down to the
   strip" took it with them. The button fell back to a bare <button> and nobody
   noticed until it was looked at. It lives below them now, after the rules
   most likely to be rewritten. */
.admin-add {
    align-self: center;
    padding: 10px 18px;
    border-radius: var(--radius-pill);
    border: 0;
    background: var(--color-primary);
    color: var(--color-text-on-dark);
    font-size: 0.68rem;
    font-weight: 800;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    cursor: pointer;
    white-space: nowrap;
    text-decoration: none;
    display: inline-block;
    line-height: 1.2;
}
.admin-add:hover { background: var(--color-dark-hover); color: var(--color-text-on-dark); }

.admin-card .list-controls { padding: 18px 16px 0; }
.admin-card .list-pager { margin-top: 0; padding: 16px 16px 18px; }

.admin-status {
    display: inline-block;
    padding: 3px 11px;
    border-radius: var(--radius-pill);
    font-size: 0.68rem;
    font-weight: 700;
    letter-spacing: 0.5px;
    text-transform: uppercase;
}

.admin-scrim { position: fixed; inset: 0; background: rgba(51, 41, 35, 0.5); z-index: 45; }

/* --------------------------- Collapsed rail ------------------------------ */

/* On a wide screen the burger collapses the sidebar to a 68px rail of icons
   rather than removing it: the destinations stay clickable and the current
   position stays visible, which is the whole reason the sidebar exists. The
   labels and group headings are what go.
   Below the breakpoint the same button is a drawer toggle instead, so one
   control does the sensible thing at either size. */
@media (min-width: 1025px) {
    .admin-shell.is-rail .admin-sidebar { width: 68px; }
    .admin-shell.is-rail .admin-nav-link span,
    .admin-shell.is-rail .admin-nav-group { display: none; }

    .admin-shell.is-rail .admin-nav-link { justify-content: center; padding: 11px 0; }
    .admin-shell.is-rail .admin-nav { padding: 8px 10px 24px; }

    .admin-shell.is-rail .admin-brand { padding: 19px 0 15px; }
    .admin-shell.is-rail .admin-brand-full { display: none; }
    .admin-shell.is-rail .admin-brand-mark { display: block; }
}

/* ----------------------------- Responsive -------------------------------- */

@media (min-width: 1025px) {
    .admin-scrim { display: none; }   /* the scrim exists only for the drawer */
}

@media (max-width: 1024px) {
    /* Off-canvas below the breakpoint. Translated rather than hidden so it
       animates, and made invisible so it is not in the tab order while shut. */
    .admin-sidebar {
        position: fixed;
        top: 0;
        left: 0;
        z-index: 50;
        transform: translateX(-100%);
        transition: transform 0.22s ease;
        visibility: hidden;
    }

    .admin-shell.is-sidebar-open .admin-sidebar { transform: translateX(0); visibility: visible; }

    .admin-content { padding: 20px 16px; }
    .admin-topbar { padding: 10px 16px; }
    .admin-user-text { display: none; }
}

@media (max-width: 560px) {
    .admin-stat-value { font-size: 1.5rem; }
    .admin-page-head h1 { font-size: 1.5rem; }

    /* The bar came to 422 in a 390 phone -- 358 of content, 32 of gaps and 32
       of padding -- so every admin page scrolled sideways by the difference.
       Nothing here needs to be clipped or hidden; the spacing was simply set
       for a screen with room to spare. min-width lets the workspace pills give
       way first, since they are the compressible part. */
    .admin-topbar { gap: 8px; padding-left: 12px; padding-right: 12px; }
    .admin-topbar .ws-switch { min-width: 0; }
    .admin-topbar-right { gap: 8px; }
}

/* ==========================================================================
   CASHIER SHELL
   --------------------------------------------------------------------------
   Shares the admin tokens and top-bar components; adds the pieces particular
   to a till screen. No sidebar: a cashier works one screen at speed on a
   tablet, and a permanent rail of admin destinations would cost a fifth of the
   width to navigation they never use mid-service.
   ========================================================================== */

.cashier-shell { min-height: 100vh; }

.cashier-brand img { height: 34px; width: auto; display: block; }

/* Scanning is the most-used action at the counter, so it is the one filled
   control in the bar. At arm's length on a tablet an outline reads as one more
   grey box. */
/* Round rather than the admin layout's 10px-radius square, to match the
   circular outlined icon buttons already used in the site header
   (.nav-circle-icon) — this row is the cashier's equivalent of that toolbar.
   Scoped to .cashier-shell rather than changed on .admin-bell itself: that
   class is shared with the admin dashboard's own bell, which was not asked
   to change and keeps its own square treatment. */
.cashier-shell .admin-bell {
    border-radius: 50%;
    border-color: var(--color-border);
}

.cashier-scan-btn {
    width: 38px;
    height: 38px;
    /* Round, matching the bell and print buttons beside it. Stays filled —
       that choice is unrelated to the shape and is explained above. */
    border-radius: 50%;
    background: var(--color-primary);
    color: var(--color-text-on-dark);
    display: flex;
    align-items: center;
    justify-content: center;
    transition: var(--transition);
}

.cashier-scan-btn:hover { background: var(--color-dark); }

/* Controls inside the dark queue header. Pills rather than rectangles, and
   cream-on-chocolate at 11.54:1 — these are read across a counter, not from
   reading distance. */
.cashier-pill {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: 8px 16px;
    border-radius: var(--radius-pill);
    border: 1px solid rgba(240, 231, 214, 0.28);
    background: rgba(240, 231, 214, 0.07);
    color: var(--color-bg-cream);
    font-size: 0.78rem;
    font-weight: 600;
    letter-spacing: 0.4px;
    cursor: pointer;
    white-space: nowrap;
    transition: var(--transition);
}

.cashier-pill:hover {
    background: rgba(240, 231, 214, 0.16);
    border-color: var(--color-bg-cream);
}

/* The one pill that is a destination rather than a toggle. */
.cashier-pill.is-primary {
    background: var(--color-primary);
    border-color: var(--color-primary);
}

.cashier-pill.is-primary:hover { background: var(--color-bg-cream); color: var(--color-dark); }

/* A toggle that is currently ON. Moss rather than espresso so "this is armed"
   is not the same colour as "this is a button". */
.cashier-pill.is-on {
    background: var(--accent-ready);
    border-color: var(--accent-ready);
    color: var(--color-text-on-dark);
}

/* ----------------------------- Toolbar ----------------------------------- */

.cashier-toolbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    flex-wrap: wrap;
    margin-bottom: 20px;
}

.queue-view-filter {
    display: flex;
    gap: 4px;
    background: var(--color-bg-light);
    border: 1px solid var(--color-border-light);
    border-radius: 12px;
    padding: 4px;
    flex-wrap: wrap;
}

/* Segmented tabs, not buttons: these are four views of one queue, and a row of
   equal-weight buttons gives no clue that picking one unpicks the others. */
.queue-filter-btn {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: 9px 15px;
    border: none;
    border-radius: 9px;
    background: none;
    color: var(--color-text-muted);
    font-size: 0.82rem;
    font-weight: 600;
    font-family: inherit;
    cursor: pointer;
    white-space: nowrap;
    transition: var(--transition);
}

.queue-filter-btn:hover { background: var(--color-bg-cream); color: var(--color-text-dark); }

.queue-filter-btn.active {
    background: var(--color-dark);
    color: var(--color-text-on-dark);
}

.cashier-sort {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    font-size: 0.82rem;
    color: var(--color-text-muted);
    white-space: nowrap;
}

.cashier-sort select {
    padding: 9px 12px;
    border: 1px solid var(--color-border-input);
    border-radius: 10px;
    background: var(--color-bg-light);
    color: var(--color-text-dark);
    font-size: 0.82rem;
    font-family: inherit;
    font-weight: 600;
    cursor: pointer;
}

/* --------------------------- Queue header -------------------------------- */

/* The queue's dark card was its own component with its own radius and padding
   -- 18px and 24px/28px against the shared banner's 16px and 20px/26px. Same
   colour, different metrics, on screens staff move between all day. It uses
   .admin-banner now, and this is only what is left of the header block. */

@media (max-width: 700px) {
    .cashier-toolbar { align-items: stretch; }
    .queue-view-filter { width: 100%; }
    .queue-filter-btn { flex: 1; justify-content: center; }
}

/* --------------------------- Order slip card ----------------------------- */

/* The card reads top to bottom as one slip: who and what kind, how long it has
   been waiting, what to make, what to do, what to collect. */

.queue-card-head {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 10px;
    padding-bottom: 14px;
    margin-bottom: 14px;
    border-bottom: 1px solid var(--color-border-light);
}

.queue-order-no {
    font-family: var(--font-mono);
    font-size: 1.3rem;
    font-weight: 700;
    letter-spacing: 0.5px;
    color: var(--color-text-dark);
}

.queue-chip {
    display: inline-flex;
    align-items: center;
    font-weight: 700;
    font-size: 0.7rem;
    letter-spacing: 0.5px;
    padding: 4px 11px;
    border-radius: var(--radius-pill);
    white-space: nowrap;
}

/* Customer, phone, time. Pushed right so the pills that say what to DO stay
   nearest the number, and identity sits where you look only when handing over. */
.queue-card-meta {
    display: flex;
    align-items: center;
    gap: 18px;
    flex-wrap: wrap;
    margin-left: auto;
}

.queue-meta-item {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: 0.84rem;
    color: var(--color-text-muted);
    white-space: nowrap;
}

.queue-meta-item svg { color: var(--color-primary); opacity: 0.7; flex-shrink: 0; }

.queue-asap {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-weight: 700;
    color: var(--accent-ready);
}

.queue-elapsed {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    margin-left: 16px;
    padding-left: 16px;
    border-left: 1px solid var(--color-border-light);
    font-family: var(--font-mono);
    font-weight: 700;
    color: var(--color-text-muted);
}

/* Ten minutes on an ASAP order. Terracotta, not red-for-its-own-sake: it is the
   palette's alarm colour and already means this elsewhere. */
.queue-elapsed.is-late { color: var(--color-danger); }

/* ------------------------------ Items ------------------------------------ */

.queue-items-panel {
    list-style: none;
    margin: 0 0 12px;
    padding: 6px;
    background: var(--color-bg-soft);
    border: 1px solid var(--color-border-light);
    border-radius: 12px;
}

/* Every row is the tick target. Cursor and hover say so before it is tried. */
.order-item-line {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    padding: 10px 12px;
    border-radius: 9px;
    cursor: pointer;
    transition: background 0.12s ease;
    -webkit-tap-highlight-color: transparent;
    user-select: none;   /* a double-tap on a tablet should tick, not select text */
}

.order-item-line + .order-item-line { border-top: 1px solid var(--color-border-light); }
.order-item-line:hover { background: var(--color-bg-cream); }

.order-item-line:focus-visible {
    outline: 2px solid var(--color-primary);
    outline-offset: -2px;
}

.order-item-line.is-done { opacity: 0.5; }
.order-item-line.is-done .queue-item-name { text-decoration: line-through; }

/* Beans need fetching from somewhere else, so they are called out. */
.order-item-line.is-bean {
    border: 1.5px solid var(--brand-warm-brown);
    background: var(--color-bg-light);
}

.order-item-line .item-done-toggle {
    width: 17px;
    height: 17px;
    margin-top: 2px;
    flex-shrink: 0;
    accent-color: var(--color-primary);
    pointer-events: none;   /* the row owns the click; see the delegated handler */
}

.queue-item-name { font-weight: 700; font-size: 0.98rem; }

.queue-notes {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    padding: 12px 14px;
    background: var(--color-bg-light);
    border: 1px solid var(--color-border-light);
    border-radius: 11px;
}

.queue-notes svg { color: var(--color-primary); opacity: 0.7; flex-shrink: 0; margin-top: 2px; }
.queue-notes strong { display: block; font-size: 0.84rem; margin-bottom: 2px; }
.queue-notes p { font-size: 0.82rem; color: var(--color-text-muted); margin: 0; }

/* ------------------------------ Actions ---------------------------------- */

.queue-actions-panel {
    background: var(--color-bg-soft);
    border: 1px solid var(--color-border-light);
    border-radius: 12px;
    padding: 16px;
    align-self: start;
}

.queue-actions-title {
    font-size: 0.9rem;
    font-weight: 700;
    font-family: var(--font-heading);
    margin-bottom: 12px;
}

/* ------------------------------- Footer ---------------------------------- */

.queue-card-foot {
    display: flex;
    align-items: center;
    gap: 9px;
    flex-wrap: wrap;
    margin-top: 16px;
    padding-top: 14px;
    border-top: 1px solid var(--color-border-light);
    font-size: 0.85rem;
    color: var(--color-text-muted);
}

.queue-card-foot svg { color: var(--color-primary); opacity: 0.7; flex-shrink: 0; }
.queue-card-foot strong { color: var(--color-text-dark); }
.queue-foot-sep { opacity: 0.5; }

@media (max-width: 820px) {
    /* The actions column stops being a column. */
    .queue-item-grid { grid-template-columns: 1fr !important; }
    .queue-card-meta { margin-left: 0; width: 100%; gap: 12px; }
    .queue-elapsed { margin-left: 12px; padding-left: 12px; }
}

/* ==========================================================================
   MY ACCOUNT
   --------------------------------------------------------------------------
   The customer's own page. Cards on cream, brand type, and one clear next
   action per block — the opposite density to the staff screens.
   ========================================================================== */

.account-wrap { max-width: 980px; margin: 0 auto; }

/* ------------------------------- Hero ------------------------------------ */

.account-hero {
    position: relative;
    overflow: hidden;
    background: var(--color-bg-cream);
    border: 1px solid var(--color-border-light);
    border-radius: 20px;
    padding: 32px;
    margin-bottom: 22px;
    display: flex;
    align-items: center;
    gap: 26px;
    flex-wrap: wrap;
}

/* The logo mark, very faint, bleeding off the right edge. Decoration that is
   already part of the brand rather than a stock flourish. Behind everything
   and non-interactive so it can never intercept a click. */
.account-hero-mark {
    position: absolute;
    right: -40px;
    top: 50%;
    transform: translateY(-50%);
    width: 320px;
    opacity: 0.05;
    pointer-events: none;
    user-select: none;
}

.account-avatar {
    width: 116px;
    height: 116px;
    border-radius: 50%;
    background: var(--color-primary);
    color: var(--color-text-on-dark);
    display: flex;
    align-items: center;
    justify-content: center;
    font-family: var(--font-heading);
    font-size: 2.5rem;
    font-weight: 700;
    letter-spacing: 1px;
    flex-shrink: 0;
    /* A cream ring separates the disc from the cream panel it sits on. */
    box-shadow: 0 0 0 6px var(--color-bg-light);
}

.account-hero-body { flex: 1; min-width: 220px; position: relative; }

.account-eyebrow {
    font-size: 0.76rem;
    font-weight: 700;
    letter-spacing: 1.2px;
    text-transform: uppercase;
    color: var(--color-primary);
}

.account-name {
    font-family: var(--font-heading);
    font-size: 2rem;
    margin: 4px 0 2px;
}

.account-email { color: var(--color-text-muted); font-size: 0.9rem; }

.account-chip {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    margin-top: 12px;
    padding: 7px 15px;
    border-radius: var(--radius-pill);
    background: var(--color-bg-light);
    border: 1px solid var(--brand-ochre);
    color: var(--color-primary);
    font-size: 0.8rem;
    font-weight: 700;
}

.account-hero-actions {
    display: flex;
    flex-direction: column;
    gap: 9px;
    position: relative;
    flex-shrink: 0;
}

.account-action {
    display: inline-flex;
    align-items: center;
    gap: 9px;
    padding: 10px 18px;
    border-radius: 11px;
    border: 1px solid var(--color-border);
    background: var(--color-bg-light);
    color: var(--color-text-dark);
    font-size: 0.85rem;
    font-weight: 600;
    text-decoration: none;
    white-space: nowrap;
    transition: var(--transition);
}

.account-action:hover { border-color: var(--color-primary); background: var(--color-bg-cream); }
.account-action svg { color: var(--color-primary); flex-shrink: 0; }

/* --------------------------- Profile completion -------------------------- */

.account-completion {
    display: flex;
    align-items: center;
    gap: 22px;
    flex-wrap: wrap;
    background: var(--color-bg-light);
    border: 1px solid var(--color-border-light);
    border-radius: 16px;
    padding: 20px 26px;
    margin-bottom: 22px;
}

/* A ring rather than a bar: this is one figure about one thing, and a ring
   reads as a state at a glance where a bar invites comparison with the
   rewards bar below it, which measures something entirely different. */
.account-ring { position: relative; width: 74px; height: 74px; flex-shrink: 0; }
.account-ring svg { transform: rotate(-90deg); }
.account-ring-track { stroke: var(--color-border-light); }
.account-ring-fill { stroke: var(--brand-ochre); stroke-linecap: round; transition: stroke-dashoffset 0.6s ease; }

.account-ring-label {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-family: var(--font-heading);
    font-weight: 700;
    font-size: 1.05rem;
}

.account-completion-body { flex: 1; min-width: 200px; }
.account-completion-body strong { font-family: var(--font-heading); font-size: 1.1rem; display: block; }
.account-completion-body p { color: var(--color-text-muted); font-size: 0.86rem; margin: 2px 0 0; }

/* ------------------------------ Rewards ---------------------------------- */

.account-rewards {
    background: var(--color-bg-light);
    border: 1px solid var(--color-border-light);
    border-radius: 18px;
    padding: 28px;
    margin-bottom: 22px;
}

.account-rewards-top {
    display: flex;
    align-items: center;
    gap: 24px;
    flex-wrap: wrap;
    margin-bottom: 18px;
}

.account-rewards-art {
    width: 96px;
    height: 96px;
    border-radius: 50%;
    background: var(--color-bg-cream);
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}

.account-rewards-body { flex: 1; min-width: 220px; }
.account-rewards-body h2 { font-family: var(--font-heading); font-size: 1.45rem; margin: 0 0 4px; }
.account-rewards-body p { color: var(--color-text-muted); font-size: 0.87rem; margin: 0; }

.account-points { text-align: right; white-space: nowrap; }
.account-points b {
    font-family: var(--font-heading);
    font-size: 2.4rem;
    font-weight: 700;
    color: var(--color-primary);
    line-height: 1;
}
.account-points span { font-size: 1rem; color: var(--color-text-muted); }

.account-bar {
    width: 100%;
    height: 14px;
    background: var(--color-border-light);
    border-radius: var(--radius-pill);
    overflow: hidden;
}

.account-bar-fill {
    height: 100%;
    border-radius: var(--radius-pill);
    background: linear-gradient(90deg, var(--brand-bronze), var(--brand-ochre));
    transition: width 0.6s ease;
}

.account-bar-note {
    font-size: 0.84rem;
    font-weight: 600;
    color: var(--color-primary);
    text-align: right;
    margin: 10px 0 0;
}

/* ------------------------------- Tiles ----------------------------------- */

.account-tiles {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
    gap: 18px;
    margin-bottom: 22px;
}

.account-tile {
    display: flex;
    align-items: center;
    gap: 18px;
    padding: 22px;
    background: var(--color-bg-light);
    border: 1px solid var(--color-border-light);
    border-radius: 16px;
    text-decoration: none;
    color: inherit;
    transition: var(--transition);
}

.account-tile:hover { border-color: var(--color-primary); transform: translateY(-2px); }

.account-tile-icon {
    position: relative;
    width: 56px;
    height: 56px;
    border-radius: 15px;
    background: var(--color-bg-cream);
    color: var(--color-primary);
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}

.account-tile-badge {
    position: absolute;
    top: -6px;
    right: -6px;
    min-width: 22px;
    height: 22px;
    padding: 0 6px;
    border-radius: var(--radius-pill);
    background: var(--color-primary);
    color: var(--color-text-on-dark);
    font-size: 0.7rem;
    font-weight: 700;
    line-height: 22px;
    text-align: center;
    box-shadow: 0 0 0 2px var(--color-bg-light);
}

.account-tile-body { flex: 1; min-width: 0; }
.account-tile-body h3 { font-family: var(--font-heading); font-size: 1.15rem; margin: 0 0 3px; }
.account-tile-body p { color: var(--color-text-muted); font-size: 0.84rem; margin: 0; }
.account-tile-chevron { color: var(--color-text-muted); flex-shrink: 0; }

/* ----------------------------- Newsletter -------------------------------- */

.account-news {
    display: flex;
    align-items: center;
    gap: 20px;
    flex-wrap: wrap;
    background: var(--color-bg-light);
    border: 1px solid var(--color-border-light);
    border-radius: 16px;
    padding: 24px;
}

.account-news-icon {
    width: 52px;
    height: 52px;
    border-radius: 50%;
    background: var(--color-primary);
    color: var(--color-text-on-dark);
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}

.account-news-body { flex: 1; min-width: 240px; }
.account-news-body h3 { font-family: var(--font-heading); font-size: 1.2rem; margin: 0 0 3px; }
.account-news-body p { color: var(--color-text-muted); font-size: 0.85rem; margin: 0; }

.account-news-form { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }

/* Read-only, and shown only to say WHICH address this subscribes. Editable it
   would imply you can subscribe someone else from your own account, which the
   opt-in flag on your row cannot do. */
.account-news-email {
    padding: 11px 15px;
    border: 1px solid var(--color-border-light);
    border-radius: 11px;
    background: var(--color-bg-soft);
    color: var(--color-text-muted);
    font-size: 0.86rem;
    font-family: inherit;
    min-width: 220px;
}

@media (max-width: 720px) {
    .account-hero { padding: 24px; gap: 18px; }
    .account-avatar { width: 88px; height: 88px; font-size: 1.9rem; }
    .account-name { font-size: 1.6rem; }
    .account-hero-actions { flex-direction: row; flex-wrap: wrap; width: 100%; }
    .account-points { text-align: left; width: 100%; }
    .account-hero-mark { display: none; }
}

/* ==========================================================================
   CONTACT — TYPE CARDS, RESERVATION FIELDS
   --------------------------------------------------------------------------
   The selected-card treatment lives here rather than being written as inline
   style by the page's own script. The long note further up this file about
   inline styles defeating :hover on .contact-type-option was describing that
   script; with the styling in CSS, :has() decides it and there is nothing to
   fight.
   ========================================================================== */

.contact-type-option {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    padding: 16px 10px;
    border: 1px solid var(--color-border);
    border-radius: 13px;
    text-align: center;
    cursor: pointer;
    background: transparent;
    transition: var(--transition);
}

/* The radio still exists and still carries the value — it is only moved out
   of sight, so the group stays keyboard-navigable and screen-reader-legible
   in a way display:none would have broken. */
.contact-type-option input[type="radio"] {
    position: absolute;
    opacity: 0;
    width: 1px;
    height: 1px;
    pointer-events: none;
}

.contact-type-icon { color: var(--color-primary); opacity: 0.75; }
.contact-type-label { font-weight: 700; font-size: 0.85rem; }
.contact-type-hint { color: var(--color-text-muted); font-size: 0.72rem; line-height: 1.35; }

.contact-type-option:has(input:checked) {
    border-color: var(--color-primary);
    background: var(--color-bg-cream);
}

.contact-type-option:has(input:checked) .contact-type-icon { opacity: 1; }

/* Keyboard focus has to be visible on the card, since the radio itself is
   not. Without this, tabbing through the group moved an invisible focus. */
.contact-type-option:has(input:focus-visible) {
    outline: 2px solid var(--color-primary);
    outline-offset: 2px;
}

/* The tick. Colour alone is a weak signal on five near-identical cards, and
   it is the one signal a colour-blind visitor may not get at all. */
.contact-type-tick {
    position: absolute;
    top: -8px;
    right: -8px;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    background: var(--color-primary);
    color: var(--color-text-on-dark);
    display: none;
    align-items: center;
    justify-content: center;
    box-shadow: 0 0 0 2px var(--color-bg-light);
}

.contact-type-option:has(input:checked) .contact-type-tick { display: flex; }

.contact-type-disabled { cursor: not-allowed; opacity: 0.5; }

/* --------------------------- Party stepper -------------------------------- */

.party-stepper {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 6px;
    padding: 5px 6px;
    border: 1px solid var(--color-border);
    border-radius: 8px;
    background: var(--color-bg-light);
}

.party-step {
    width: 30px;
    height: 30px;
    flex-shrink: 0;
    border: 1px solid var(--color-border-light);
    border-radius: 7px;
    background: var(--color-bg-light);
    color: var(--color-primary);
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    transition: var(--transition);
}

.party-step:hover { background: var(--color-bg-cream); border-color: var(--color-primary); }

.party-value {
    display: inline-flex;
    align-items: baseline;
    gap: 4px;
    font-size: 0.9rem;
}

.party-value input {
    width: 2.2em;
    padding: 0;
    border: none;
    background: none;
    text-align: right;
    font-size: 0.9rem;
    font-weight: 700;
    font-family: inherit;
    color: var(--color-text-dark);
    -moz-appearance: textfield;   /* the native spinners duplicate the buttons */
}

.party-value input::-webkit-outer-spin-button,
.party-value input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

.party-value input:focus { outline: none; }
.party-unit { color: var(--color-text-muted); font-size: 0.82rem; }

.reservation-note {
    display: flex;
    align-items: flex-start;
    gap: 7px;
    font-size: 0.78rem;
    color: var(--color-text-muted);
    margin-top: 10px;
}

.reservation-note svg { color: var(--color-primary); opacity: 0.8; flex-shrink: 0; margin-top: 1px; }

.contact-submit {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 9px;
    width: 100%;
}

@media (max-width: 640px) {
    /* Three columns of reservation fields do not fit a phone. */
    #reservation-fields > div:first-child { grid-template-columns: 1fr !important; }
}

/* ==========================================================================
   MENU TOOLBAR — search, sort, category pills
   ========================================================================== */

/* A rule with the brand mark in it, closing the header before the controls. */
.menu-divider {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 14px;
    color: var(--color-primary);
    opacity: 0.5;
    margin-bottom: 22px;
}

.menu-divider span {
    height: 1px;
    width: 70px;
    background: var(--color-border);
}

/* The mark is a PNG of a line drawing, so it cannot take currentColor the way
   the icon it replaced did. It is already drawn in the brand brown; the
   parent's opacity is what softens it to a divider rather than a headline. */
.menu-divider-mark {
    width: 34px;
    height: auto;
    display: block;
}

.menu-toolbar {
    /* No card, no inner fills. Panel-inside-a-panel put three nested
       rounded boxes between the heading and the food; the controls carry
       themselves without being boxed. */
    margin-bottom: 30px;
}

.menu-toolbar-row {
    display: flex;
    align-items: center;
    gap: 14px;
    flex-wrap: wrap;
    margin-bottom: 14px;
}

.menu-search {
    position: relative;
    flex: 1;
    min-width: 220px;
    display: flex;
    align-items: center;
}

.menu-search svg {
    position: absolute;
    left: 14px;
    color: var(--color-text-muted);
    pointer-events: none;
}

.menu-search input {
    width: 100%;
    padding: 12px 14px 12px 40px;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-pill);
    background: var(--color-bg-soft);
    font-size: 0.9rem;
    font-family: inherit;
    transition: var(--transition);
}

.menu-search input:focus {
    outline: none;
    /* A ring rather than a border-colour change: 1px of colour is a weak cue
       for keyboard focus, and this reads at a glance. */
    border-color: var(--color-primary);
    box-shadow: 0 0 0 2px var(--color-primary);
    background: var(--color-bg-light);
}

.menu-sort {
    display: inline-flex;
    align-items: center;
    gap: 9px;
    /* Outlined to match the search field beside it and the pills below, but
       unfilled: it is a control you press, not one you type into. */
    padding: 3px 15px 3px 16px;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-pill);
    background: none;
    flex-shrink: 0;
}

/* Now that both are outlined capsules sitting side by side, any difference in
   height reads as misalignment. Their padding and line-height sums land 3px
   apart (44 and 47), so one declared height settles it for both. The phone
   rule below drops the pair to 42 together. */
.menu-search input,
.menu-sort { min-height: 44px; }

.menu-sort svg { color: var(--color-text-muted); flex-shrink: 0; }

.menu-sort select {
    border: none;
    background: none;
    padding: 7px 2px;
    font-size: 0.88rem;
    font-family: inherit;
    font-weight: 600;
    color: var(--color-text-dark);
    cursor: pointer;
}

.menu-sort select:focus { outline: none; }

/* The pill row scrolls sideways rather than wrapping to three lines: there are
   a dozen categories and wrapping pushed the menu itself below the fold. The
   fade on the right edge is what says there is more to the right — a scrollbar
   under a row of pills reads as a stray element, and on a touch device there
   is no scrollbar to read at all. */
/* The pill row scrolls sideways rather than wrapping: there are a dozen
 * categories and wrapping pushed the menu itself below the fold.
 *
 * Hiding the scrollbar without replacing it left the row unusable with a
 * mouse — a wheel scrolls the page, not the row, so the off-screen categories
 * could not be reached at all on a desktop. The arrows are that replacement,
 * and the edge fades are what say there is more in that direction, which on a
 * touch device is the only signal available since there is no scrollbar to
 * read there either.
 */
.pill-scroller {
    display: flex;
    align-items: center;
    gap: 6px;
}

/* Fades live on the viewport, not on the scrolling element: a child positioned
   inside a scroll container scrolls away with the content. */
.pill-viewport {
    position: relative;
    flex: 1;
    min-width: 0;      /* lets the flex item be narrower than the pills inside it */
}

.pill-viewport::before,
.pill-viewport::after {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    width: 40px;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.18s ease;
    z-index: 1;
}

.pill-viewport::before {
    left: 0;
    background: linear-gradient(to right, var(--color-bg-light), transparent);
}

.pill-viewport::after {
    right: 0;
    background: linear-gradient(to left, var(--color-bg-light), transparent);
}

.pill-scroller.can-prev .pill-viewport::before,
.pill-scroller.can-next .pill-viewport::after { opacity: 1; }

.menu-toolbar .category-pills {
    display: flex;
    gap: 9px;
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    scroll-behavior: smooth;
    padding: 2px;
    margin: 0;
}

.menu-toolbar .category-pills::-webkit-scrollbar { display: none; }

/* Smaller than the site-wide pill: this row holds 17 of them and is scrolled
   rather than read, so the label needs to be legible, not prominent. */
.menu-toolbar .category-pill {
    flex-shrink: 0;
    padding: 8px 17px;
    font-size: 0.74rem;
    letter-spacing: 0.7px;
}

/* Both arrows leave together when nothing overflows, so a short category list
   does not carry two dead controls. While the row IS scrollable they keep
   their space and only dim at the ends — arrows that vanish mid-scroll would
   shift the pills sideways under the cursor mid-click. */
/* A chevron, not a button. At 30px circled they were two more bubbles in a
   row that already has 17, and on a phone they cost 72px of a 293px row.
   Bare, they cost 40px and still say which way there is more. */
.pill-nav {
    flex-shrink: 0;
    width: 20px;
    height: 26px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: none;
    background: none;
    color: var(--color-primary);
    opacity: 0.7;
    cursor: pointer;
    transition: var(--transition);
}

.pill-nav:hover:not(:disabled) { opacity: 1; }

.pill-nav:disabled {
    opacity: 0.18;
    cursor: default;
}

.pill-scroller:not(.is-overflowing) .pill-nav { display: none; }

.menu-empty {
    text-align: center;
    color: var(--color-text-muted);
    font-size: 0.92rem;
    padding: 40px 20px 50px;
}

.menu-empty button {
    border: none;
    background: none;
    padding: 0;
    font: inherit;
    color: var(--color-primary);
    font-weight: 600;
    text-decoration: underline;
    cursor: pointer;
}

@media (max-width: 620px) {
    /* Sort sits BESIDE the search rather than full-width beneath it, and drops
       its label to the icon alone. The row is the widest thing competing for a
       phone's width, and "Featured" is a default the customer did not choose —
       spending a whole line announcing it pushed the menu itself further down.
       Still a 42px tap target, which is the floor worth keeping. */
    .menu-toolbar-row { gap: 9px; margin-bottom: 10px; flex-wrap: nowrap; }

    /* One shared height rather than two that land a pixel apart from
       different padding and line-height sums. */
    .menu-search input,
    .menu-sort { min-height: 42px; }

    .menu-search input { padding: 11px 14px 11px 38px; }

    /* Allowed to shrink: its 220px floor is what forced the sort onto its own
       line, and with nowrap above that floor would overflow the row instead. */
    .menu-search { min-width: 0; flex: 1; }

    .menu-sort {
        width: auto;
        flex-shrink: 0;
        gap: 4px;
        padding: 2px 10px;
        /* Anchors the invisible <select> that covers this pill. */
        position: relative;
    }

    /* The select stays exactly where it was in the DOM and keeps its
       aria-label, so it is still the real control for a screen reader and for
       the keyboard — it is only made invisible and stretched over the whole
       pill, so a tap anywhere on the icon opens the native picker. Hiding it
       with display:none or width:0 would have taken the control away rather
       than the label. */
    .menu-sort select {
        position: absolute;
        inset: 0;
        width: 100%;
        height: 100%;
        opacity: 0;
        padding: 0;
        cursor: pointer;
    }

    /* The chevron the native select was drawing for itself, now that it is
       invisible — without it the pill reads as an icon, not as a menu. */
    .menu-sort::after {
        content: '';
        width: 7px;
        height: 7px;
        border-right: 1.6px solid var(--color-text-muted);
        border-bottom: 1.6px solid var(--color-text-muted);
        transform: translateY(-2px) rotate(45deg);
        flex-shrink: 0;
    }

    .menu-divider span { width: 40px; }
    .menu-divider-mark { width: 28px; }
}

/* ==========================================================================
   Dialogs — the app's own alert() and confirm()
   ==========================================================================
   Paired with /assets/js/dialog.js. The browser's versions could not be
   styled at all: system typeface, system grey, pinned to the top of the
   window with the site's address printed above the message. These are the
   same two jobs drawn from the same tokens as everything else, so a question
   from the shop looks like it came from the shop.

   Nothing here introduces a colour. Every value is a var(). */

.dlg-scrim {
    position: fixed;
    inset: 0;
    z-index: 2000;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
    /* Chocolate at 55%, not black: the page behind stays warm rather than
       going grey under a neutral scrim. */
    background: rgba(51, 41, 35, 0.55);
    animation: dlg-in 140ms ease-out;
}

/* [hidden] is display:none at normal specificity, which a display:flex on a
   class silently beats. Said again here so hiding actually hides. */
.dlg-scrim[hidden] { display: none; }

/* The page must not scroll behind the question -- on a phone the scrim covers
   the viewport, and a stray drag scrolled the page underneath it. */
html.dlg-is-open { overflow: hidden; }

.dlg {
    width: min(440px, 100%);
    background: var(--color-bg-light);
    border-radius: var(--radius-lg);
    box-shadow: 0 24px 60px rgba(51, 41, 35, 0.28);
    padding: 26px 30px 24px;
    animation: dlg-rise 160ms ease-out;
}

.dlg-label {
    margin: 0;
    font-family: var(--font-body);
    font-size: 0.7rem;
    font-weight: 800;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--color-text-muted);
}

/* pre-line, because these messages carry their own line breaks: a list of what
   was left out, a reading laid out line by line. */
.dlg-body {
    margin: 10px 0 0;
    font-family: var(--font-body);
    font-size: 0.95rem;
    line-height: 1.6;
    color: var(--color-text-dark);
    white-space: pre-line;
}

.dlg-actions {
    display: flex;
    gap: 10px;
    margin-top: 24px;
}

.dlg-btn {
    flex: 1;
    min-height: 46px;
    padding: 0 18px;
    border-radius: var(--radius-md);
    border: 2px solid transparent;
    font-family: var(--font-body);
    font-size: 0.82rem;
    font-weight: 800;
    letter-spacing: 0.06em;
    cursor: pointer;
    transition: background-color 120ms ease, border-color 120ms ease;
}

.dlg-btn:focus-visible {
    outline: 3px solid var(--brand-ochre);
    outline-offset: 2px;
}

.dlg-yes {
    background: var(--color-dark);
    color: var(--color-text-on-dark);
}
.dlg-yes:hover { background: var(--color-dark-hover); }

.dlg-no {
    background: transparent;
    border-color: var(--color-border);
    color: var(--color-text-muted);
}
.dlg-no:hover { border-color: var(--color-primary); color: var(--color-text-dark); }

/* Destructive. The button carries the warning, not the whole card -- a red
   dialog reads as "something broke", which is not what is being said. */
.dlg.is-danger .dlg-yes { background: var(--color-danger); }
.dlg.is-danger .dlg-yes:hover { background: var(--brand-terracotta); }

/* The till: a thumb on a tablet at a counter, an arm's length away. */
.dlg.is-touch { width: min(520px, 100%); padding: 30px 34px 28px; }
.dlg.is-touch .dlg-body { font-size: 1.05rem; }
.dlg.is-touch .dlg-btn { min-height: 58px; font-size: 0.9rem; }

@keyframes dlg-in   { from { opacity: 0; } }
@keyframes dlg-rise { from { opacity: 0; transform: translateY(10px); } }

@media (prefers-reduced-motion: reduce) {
    .dlg-scrim, .dlg { animation: none; }
    .dlg-btn { transition: none; }
}

@media (max-width: 480px) {
    .dlg { padding: 22px 22px 20px; }
    /* Stacked, with the answer on top: on a narrow screen two buttons side by
       side put the destructive one under the thumb that was already moving. */
    .dlg-actions { flex-direction: column-reverse; }
}

/* ==========================================================================
   Admin lists — cards, tables, and the controls above and below them
   ==========================================================================
   Six inventory screens carried their own copy of the .inv-* rules below.
   That is how the packs table came to be marked up as .inv-table on a page
   that did not define it: the class name existed everywhere else, so it
   looked right and rendered as an unstyled browser default. One copy now.

   Paired with app/helpers/listing.php and the two components in
   app/views/components/. */

.inv-card {
    background: var(--color-bg-light);
    border-radius: 16px;
    border: 1px solid var(--color-border-light);
    padding: 22px 24px;
    box-shadow: var(--shadow-sm);
    margin-bottom: 22px;
}
.inv-card h2 {
    font-family: var(--font-heading);
    font-size: 1.15rem;
    margin: 0 0 14px;
}
/* A ROW OF FIELDS, ENDING IN A BUTTON.
   Lived twice, identically, in inv-counts.php and inv-deliveries.php. Same
   design, two definitions, nothing saying which was meant -- so a fix to one
   left the other where it was. */
.inv-form { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px 16px; align-items: end; }
.inv-field-wide { grid-column: span 2; }
.inv-field label { display: block; font-size: 0.72rem; font-weight: 800; letter-spacing: 0.8px; text-transform: uppercase; color: var(--color-text-muted); margin-bottom: 6px; }
.inv-field input, .inv-field select { width: 100%; padding: 10px 12px; border: 1px solid var(--color-border-light); border-radius: 10px; font-size: 0.9rem; background: #fff; }
.inv-field .btn { width: 100%; }

/* The button is a control on the same line as the fields, so it is the same
   height as them. It was .btn-sm at 33px against a 42px input, sitting 9px
   short of it because the row is bottom-aligned. min-height rather than
   padding: the two have different font sizes, so equal padding would still
   have left them different heights. */
.inv-field input,
.inv-field select,
.inv-field .btn { min-height: 42px; }
@media (max-width: 640px) { .inv-field-wide { grid-column: span 1; } }

.inv-empty {
    color: var(--color-text-muted);
    font-size: 0.88rem;
    line-height: 1.6;
    margin: 0;
}

/* .inv-table, .inv-num and .inv-scroll were defined here in their own older
   shape -- a borderless heading, cell padding only on the right. They are names
   for THE ADMIN LIST PAGE now, and this block is deleted rather than left
   sitting after it.

   It mattered more than the others: this one came LATER in the file than the
   shared definition, so it won. .inv-table is the most-used table name in the
   admin -- deliveries, waste, cash, refunds, stock takes, consumption,
   suppliers -- and every one of them was still drawing the old design while
   the rest of the admin had moved. Nothing said so, because both designs look
   deliberate on their own; you only see it by putting two screens side by
   side, which is exactly what the client did. */

.inv-act { width: 1%; white-space: nowrap; text-align: right; }
.inv-dim { color: var(--color-text-muted); }

/* -- the strip above -------------------------------------------------- */
.list-controls {
    display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-end;
    margin-bottom: 16px;
}
/* FILTER sits at the end of a row of 42px fields. align-items: flex-end put
   its BOTTOM on theirs, which is not the same as lining it up: at 35px it
   read as floating above the row. Same fix as the count form's button. */
.list-controls .btn { min-height: 42px; }
/* A yes/no filter, and the count beside it. Both sit on the same line as the
   fields, so both take the fields' height rather than their own. */
.list-check {
    display: flex; align-items: center; gap: 8px;
    min-height: 42px;
    margin: 0;
    font-size: 0.85rem; text-transform: none; letter-spacing: 0;
    cursor: pointer;
}
.list-check input { width: 17px; height: 17px; accent-color: var(--brand-moss); }
.list-count {
    display: flex; align-items: center;
    min-height: 42px;
    font-size: 0.82rem; color: var(--color-text-muted);
}
.list-field { display: block; flex: 0 1 auto; }
/* The search box takes the slack: it is the control people use most, and the
   thing being typed into it is longer than a filter's name. */
.list-field.is-grow { flex: 1 1 240px; }
.list-field > span {
    display: block;
    font-size: 0.68rem; font-weight: 800; letter-spacing: 1px; text-transform: uppercase;
    color: var(--color-text-muted);
    margin-bottom: 5px;
}
.list-field input, .list-field select {
    width: 100%; box-sizing: border-box;
    padding: 10px 12px;
    border: 1px solid var(--color-border-light); border-radius: 10px;
    font-size: 0.9rem; font-family: inherit;
    background: var(--color-bg-light);
}
.list-field input:focus, .list-field select:focus {
    outline: 2px solid var(--brand-ochre); outline-offset: 1px;
}
/* Sat as bare text beside a pill button, half a head shorter than it and on a
   different baseline. It is an action, so it looks like a small one. */
.list-clear {
    align-self: stretch;
    display: inline-flex; align-items: center;
    padding: 0 14px;
    border: 1px solid transparent; border-radius: 999px;
    font-size: 0.74rem; font-weight: 800; letter-spacing: 0.06em; text-transform: uppercase;
    color: var(--color-text-muted); text-decoration: none;
    white-space: nowrap;
}
.list-clear:hover, .list-clear:focus-visible {
    border-color: var(--color-border); color: var(--color-text-dark);
    background: var(--color-bg-light);
}

/* -- the strip below --------------------------------------------------- */
.list-pager {
    display: flex; align-items: center; justify-content: space-between; gap: 16px;
    margin-top: 18px; padding-top: 16px;
    border-top: 1px solid var(--color-border-light);
}
.list-pager-at { font-size: 0.82rem; color: var(--color-text-muted); text-align: center; }
.list-pager-at small { display: block; font-size: 0.72rem; opacity: 0.85; }

/* Page size. Small enough to ignore, present enough to find -- most people
   never change it, and the ones who do are looking for it. */
.list-pager-size { display: flex; align-items: center; gap: 4px; flex: 1 1 0; }
.list-pager-size > span {
    font-size: 0.66rem; font-weight: 800; letter-spacing: 1.1px; text-transform: uppercase;
    color: var(--color-text-muted); margin-right: 4px;
}
.list-size {
    min-width: 30px; padding: 4px 7px; border-radius: 8px;
    font-size: 0.76rem; font-variant-numeric: tabular-nums; text-align: center;
    color: var(--color-text-muted); text-decoration: none;
}
.list-size:hover { background: var(--color-bg-soft); color: var(--color-text-dark); }
.list-size.is-on {
    background: var(--color-dark); color: var(--color-text-on-dark); font-weight: 700;
}

.list-pager-nav { display: flex; align-items: center; gap: 8px; flex: 1 1 0; justify-content: flex-end; }
/* Drawn even when there is nowhere to go: an empty space where the arrows
   would be reads as a table that cannot page at all. */
.list-page {
    padding: 7px 14px;
    border: 1px solid var(--color-border-light); border-radius: 999px;
    font-size: 0.74rem; font-weight: 800; letter-spacing: 0.05em;
    color: var(--color-text-dark); text-decoration: none; white-space: nowrap;
}
.list-page:hover, .list-page:focus-visible { border-color: var(--color-primary); color: var(--color-primary); }
.list-page.is-off { opacity: 0.32; cursor: default; }

@media (max-width: 720px) {
    .list-controls { gap: 10px; }
    .list-field, .list-field.is-grow { flex: 1 1 100%; }
    .list-clear { align-self: flex-start; padding: 8px 14px; }
    .list-pager { flex-direction: column; gap: 12px; }
    .list-pager-size, .list-pager-nav { flex: none; justify-content: center; }
}

/* ==========================================================================
   Audience mode — the money curtain
   ==========================================================================
   Paired with assets/js/audience.js and the inline guard in the admin layout's
   head. The rule below is what stops the takings being drawn for a frame
   before the script has swapped them: hidden the moment the class is on,
   revealed again by .is-money-ready once the text says nothing. */

html.audience [data-money]:not(.is-money-ready) { visibility: hidden; }

/* The masked figure is deliberately quieter than the number it replaces. A
   row of bullets in the same 1.8rem chocolate as a peso total reads as data;
   recessed, it reads as "not shown", which is what it means. */
html.audience [data-money] {
    color: var(--color-text-muted);
    letter-spacing: 0.06em;
}

.admin-eye { background: none; border: 0; cursor: pointer; font: inherit; }
.admin-eye .eye-shut { display: none; }
.admin-eye[aria-pressed="true"] .eye-open { display: none; }
.admin-eye[aria-pressed="true"] .eye-shut { display: block; }
/* On, it is a state rather than an action -- so it stops looking like the
   bell beside it and starts looking switched on. */
.admin-eye[aria-pressed="true"] {
    background: var(--color-warning-bg);
    color: var(--brand-espresso);
}

/* The typed answer, for Dialog.prompt(). Same box as every other field on the
   admin side, so a question asking for a sentence looks like the forms that
   ask for one. */
.dlg-input {
    width: 100%; box-sizing: border-box;
    margin-top: 14px; padding: 10px 12px;
    border: 1px solid var(--color-border); border-radius: var(--radius-md);
    font-family: inherit; font-size: 0.95rem;
    background: var(--color-bg-light); color: var(--color-text-dark);
}
.dlg-input:focus { outline: 3px solid var(--brand-ochre); outline-offset: 1px; }
.dlg.is-touch .dlg-input { font-size: 1.05rem; padding: 14px 14px; }

/* ===========================================================================
   EDITING ONE PRODUCT  (.pe-*)
   These lived in product-edit.php's own <style> block. That was fine while
   the form only ever appeared on that page -- but the form was pulled out
   into partials/product-form.php so the products list could open it in a
   modal, and a fetched fragment brings no <style> with it. The modal
   rendered the whole form in browser defaults: Times New Roman, square
   inputs, no grid. Same failure as .dlv-scan and .admin-add before it --
   the markup moved and the rules that dressed it did not.

   They belong here anyway: one file to change when this design changes.
   =========================================================================== */
.pe-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; background: var(--color-dark); color: var(--color-text-on-dark); border-radius: 16px; padding: 20px 26px; margin-bottom: 20px; }
.pe-eyebrow { color: var(--brand-ochre); font-size: 0.7rem; font-weight: 800; letter-spacing: 1.4px; text-transform: uppercase; margin-bottom: 4px; }
.pe-head h1 { font-family: var(--font-heading); font-size: 1.8rem; margin: 0; color: var(--color-text-on-dark); }




.pe-grid { display: grid; grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); gap: 18px; align-items: start; }
.pe-col { display: flex; flex-direction: column; gap: 18px; min-width: 0; }
.pe-card { background: #FFF; border: 1px solid var(--color-border-light); border-radius: 16px; padding: 22px 24px; }
.pe-card h2 { font-family: var(--font-heading); font-size: 1.2rem; margin: 0 0 4px; color: var(--color-text-dark); }
.pe-sub { font-size: 0.85rem; color: var(--color-text-muted); margin: 0 0 18px; }
/* Body copy in a card that is telling you something rather than asking. */
.pe-aside { font-size: 0.88rem; color: var(--color-text-muted); line-height: 1.7; margin: 0; }

.pe-card-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
.pe-card-head h2 { margin: 0; }
.pe-tag { font-size: 0.66rem; font-weight: 800; letter-spacing: 1px; text-transform: uppercase; color: var(--color-text-on-dark); background: var(--brand-espresso); padding: 5px 10px; border-radius: 6px; }
.pe-stock { border-color: var(--brand-ochre); border-width: 2px; }

.pe-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 14px; }
/* Three across rather than two. 160px apiece does not fit three in the left
   column, so the third field drops to a row of its own and stretches the
   width of the card -- which reads as a mistake beside the pairs above it. */
.pe-row-3 { grid-template-columns: repeat(auto-fit, minmax(118px, 1fr)); }
/* Controls in a row line up because their LABELS are all one line. A hint
   belongs under the box, not inside the label -- see .pe-hint, and the two
   forms that use it. A second line in a label makes that field taller than
   the one beside it and the boxes stop lining up. */
.pe-field { margin-bottom: 14px; }
.pe-field label { display: block; font-size: 0.72rem; font-weight: 800; letter-spacing: 0.8px; text-transform: uppercase; color: var(--color-text-muted); margin-bottom: 6px; }
.pe-field input, .pe-field select, .pe-field textarea { width: 100%; padding: 11px 13px; border: 1px solid var(--color-border-light); border-radius: 10px; font-size: 0.92rem; font-family: inherit; background: #FFF; }
.pe-field input:focus, .pe-field select:focus, .pe-field textarea:focus { border-color: var(--brand-ochre); outline: none; }
.pe-hint { display: block; font-size: 0.72rem; font-weight: 500; letter-spacing: 0; text-transform: none; color: var(--color-text-muted); margin-top: 5px; }

/* Amber, not red: counting a sized product is a mismatch to think about, not
   an error to block. The link is the point of the box. */
.pe-sizewarn { background: #FBF0E4; border: 1px solid var(--brand-ochre); border-radius: 12px;
  padding: 14px 16px; margin-bottom: 16px; font-size: 0.86rem; line-height: 1.55;
  color: var(--color-text-dark); }
/* The heading only. Scoped to the direct first child because the paragraphs
   emphasise words inline, and a blanket `display: block` here put the "1" on
   a line of its own mid-sentence. */
.pe-sizewarn > strong:first-child { display: block; font-size: 0.9rem; margin-bottom: 6px; }
.pe-sizewarn p { margin: 0 0 8px; }
.pe-sizewarn a { display: inline-block; font-weight: 700; color: var(--color-primary);
  text-decoration: none; border-bottom: 1px solid currentColor; }

.pe-toggle { display: flex; align-items: center; justify-content: space-between; gap: 16px; background: var(--color-bg-soft); border-radius: 12px; padding: 14px 16px; margin-bottom: 16px; cursor: pointer; }
.pe-toggle strong { display: block; font-size: 0.95rem; }
.pe-toggle small { color: var(--color-text-muted); font-size: 0.8rem; }
.pe-toggle input { width: 20px; height: 20px; flex: 0 0 auto; accent-color: var(--brand-moss); }
.pe-stock-fields[hidden] { display: none !important; }

.pe-note { background: var(--color-warning-bg); border-radius: 12px; padding: 14px 16px; font-size: 0.84rem; color: var(--color-text-muted); line-height: 1.6; }
.pe-note strong { display: block; color: var(--color-text-dark); }

.pe-today-row { display: flex; justify-content: space-between; gap: 12px; padding: 11px 0; border-bottom: 1px solid var(--color-border-light); font-size: 0.9rem; }
.pe-today-row span { color: var(--color-text-muted); }
.pe-today-row strong { font-weight: 700; }
.pe-today-row strong.bad { color: var(--color-danger); }
.pe-today-row.is-total { border-bottom: 0; font-size: 1rem; }
.pe-today-row.is-total span { color: var(--color-text-dark); font-weight: 700; }

.pe-dark { background: var(--color-dark); border-color: var(--color-dark); color: var(--color-text-on-dark); }
.pe-dark-label { color: var(--brand-ochre); font-size: 0.7rem; font-weight: 800; letter-spacing: 1.2px; text-transform: uppercase; margin-bottom: 12px; }
.pe-dark ul { margin: 0 0 14px; padding-left: 18px; font-size: 0.88rem; line-height: 1.9; }
.pe-dark p { margin: 0; font-size: 0.82rem; opacity: 0.8; line-height: 1.6; border-top: 1px solid rgba(255,255,255,0.15); padding-top: 14px; }

/* What finishes the product form. It was borrowing the rule that right-aligns
   a TABLE row's actions -- a different thing that happened to share the name.
   Centred under the panel above it, which is the width it has to work with. */
.pe-actions { display: flex; justify-content: center; gap: 10px; flex-wrap: wrap; }
.pe-actions .btn { min-width: 132px; }




@media (max-width: 900px) { .pe-grid { grid-template-columns: minmax(0, 1fr); } }

/* A CARD THAT HOLDS A LIST.
   The card keeps its top and bottom padding and gives up its left and right,
   so the table's header bar reaches both borders instead of stopping 24px
   short of them; everything in the card that is NOT the table takes that
   24px back. Several pages had written this out privately -- fc-list,
   rl-list, pp-list, rp-list -- before it was worth a name. New work uses
   this one.

   It sits at the end of the file on purpose: .inv-card sets `padding` as a
   shorthand further up, and at equal specificity the later rule wins, so a
   modifier written above it would have been overruled by the very card it
   modifies. */
.admin-list-card { padding-left: 0; padding-right: 0; overflow: hidden; }
.admin-list-card > h2,
.admin-list-card > p,
.admin-list-card > .admin-empty,
.admin-list-card > .inv-empty,
/* The filter strip and the pager are shared components that expect a card
   with padding round them. Without these two the words SEARCH and ROWS sit
   flush against the border -- which is the same thing the inventory overview
   had to write out privately before this modifier existed. */
.admin-list-card > .list-controls,
.admin-list-card > .list-pager { padding-left: 24px; padding-right: 24px; }
.admin-list-card > .admin-scroll,
.admin-list-card > .inv-scroll { overflow-x: auto; }


/* A FORMULA, AND WHAT IT COSTS.
   Two screens draw these now -- one product's formula, and one add-on's --
   so the rules cannot live in either page's <style> block. The edit-product
   modal already taught this branch what happens when markup moves to a
   second place and the rules that dressed it stay behind in the first: the
   page that fetched it rendered in Times New Roman and nothing complained. */
/* The quantity cell, now that it sets the quantity rather than reporting it.
   Right-aligned and tabular so a column of amounts still reads as a column of
   numbers rather than a column of boxes. */
.rc-qtyform { display: inline-flex; align-items: center; gap: 6px; justify-content: flex-end; }
.rc-qtyform-inline { justify-content: flex-start; }
.rc-qtyin { width: 78px; padding: 5px 8px; border: 1px solid var(--color-border); border-radius: 6px;
    font-size: 0.85rem; font-weight: 700; text-align: right; font-variant-numeric: tabular-nums; }
.rc-qtyunit { font-size: 0.74rem; color: var(--color-text-muted); }
.rc-qtyset { border: 1px solid var(--color-border); background: #FFF; border-radius: 999px;
    padding: 4px 10px; font-size: 0.62rem; font-weight: 800; letter-spacing: 0.06em;
    color: var(--color-text-muted); cursor: pointer; }
.rc-qtyset:hover { border-color: var(--color-primary); color: var(--color-primary); }
.rc-qtynote { display: block; font-size: 0.68rem; color: var(--color-text-muted); margin-top: 4px; }

.rc-head { display: flex; align-items: center; justify-content: space-between; gap: 20px; background: var(--color-dark); color: var(--color-text-on-dark); border-radius: 16px; padding: 20px 26px; margin-bottom: 16px; }
.rc-eyebrow { color: var(--brand-ochre); font-size: 0.7rem; font-weight: 800; letter-spacing: 1.4px; text-transform: uppercase; margin-bottom: 4px; }
.rc-head h1 { font-family: var(--font-heading); font-size: 1.8rem; margin: 0; color: var(--color-text-on-dark); }
.rc-backbone { flex: 0 0 auto; padding: 8px 18px; border: 1px solid var(--brand-ochre); color: var(--brand-ochre); border-radius: 999px; font-size: 0.72rem; font-weight: 800; letter-spacing: 1px; text-transform: uppercase; }

.rc-warn { background: var(--color-warning-bg); border: 2px solid var(--brand-ochre); border-radius: 14px; padding: 16px 20px; margin-bottom: 16px; font-size: 0.88rem; color: var(--color-text-dark); line-height: 1.6; }
.rc-warn strong { display: block; margin-bottom: 3px; }

/* Draft or confirmed. A half-written formula used to look exactly like a
   finished one on every screen that reads it. */
.rc-status { display: flex; align-items: center; justify-content: space-between; gap: 18px;
  border-radius: 14px; padding: 14px 20px; margin-bottom: 16px; flex-wrap: wrap; }
.rc-status strong { display: block; font-size: 0.72rem; font-weight: 800; letter-spacing: 0.12em;
  text-transform: uppercase; margin-bottom: 2px; }
.rc-status span { font-size: 0.84rem; line-height: 1.55; }
.rc-status-draft { background: var(--color-warning-bg); border: 2px solid var(--brand-ochre); }
.rc-status-ready { background: var(--color-success-bg); border: 2px solid var(--color-success); }

/* A size saying it does not use one of the base ingredients. Distinct from
   removing a line, which hands the ingredient straight back to the base. */
.rc-notused { border: 1px solid var(--color-border-light); background: #fff; cursor: pointer;
  border-radius: 999px; padding: 3px 10px; font-size: 0.66rem; font-weight: 800;
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--color-text-muted); }
.rc-notused:hover { border-color: var(--color-danger); color: var(--color-danger); }
.rc-excluded { margin: 16px 0 0; padding: 14px 18px; border-radius: 12px;
  background: var(--color-bg-soft); border: 1px dashed var(--color-border-light); }
.rc-excluded > strong { display: block; font-size: 0.68rem; font-weight: 800; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--color-text-muted); margin-bottom: 8px; }
.rc-excluded ul { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; }
.rc-excluded li { display: flex; align-items: center; gap: 8px; background: #fff;
  border: 1px solid var(--color-border-light); border-radius: 999px; padding: 5px 6px 5px 14px; }
.rc-excluded li span { font-size: 0.84rem; font-weight: 600; }
.rc-excluded li button { border: 0; background: var(--color-bg-soft); border-radius: 999px;
  padding: 4px 10px; font-size: 0.66rem; font-weight: 800; letter-spacing: 0.05em;
  text-transform: uppercase; cursor: pointer; color: var(--color-text-muted); }
.rc-excluded li button:hover { background: var(--brand-ochre); color: #fff; }
.rc-excluded p { margin: 10px 0 0; font-size: 0.78rem; color: var(--color-text-muted); line-height: 1.55; }

.rc-card { background: #FFF; border: 1px solid var(--color-border-light); border-radius: 16px; padding: 22px 24px; margin-bottom: 16px; overflow: hidden; }
/* A table in one of these runs the full width of the card: the header bar
   reaches both borders instead of stopping 24px short of them, and the
   cells take that 24px back themselves (see THE ADMIN LIST PAGE, which
   .rc-table is one of the names for). The card gives up overflow-x for
   this, so the scrolling moves in here -- a wide table left unwrapped is
   CUT OFF by the card rather than scrolled. */
.rc-bleed { margin: 16px -24px 0; overflow-x: auto; }
.rc-card h2 { font-family: var(--font-heading); font-size: 1.2rem; margin: 0 0 4px; }
.rc-sub { font-size: 0.8rem; color: var(--color-text-muted); }

.rc-inherit { font-size: 0.82rem; color: var(--color-text-muted); margin: 0 0 12px; }

.rc-product { display: flex; align-items: center; justify-content: space-between; gap: 20px; flex-wrap: wrap; margin-bottom: 14px; }
.rc-product h2 { margin: 0 0 2px; }
.rc-product p { margin: 0; font-size: 0.85rem; color: var(--color-text-muted); }
.rc-sizes { display: flex; gap: 8px; flex-wrap: wrap; }
.rc-size { padding: 9px 18px; border: 1px solid var(--color-border-light); border-radius: 999px; font-size: 0.84rem; font-weight: 600; color: var(--color-text-dark); }
.rc-size:hover { border-color: var(--color-primary); }
.rc-size.is-on { background: var(--color-dark); color: var(--color-text-on-dark); border-color: var(--color-dark); }






.rc-strong { font-weight: 800; }
.rc-mute { color: var(--color-text-muted); }
.rc-name { font-weight: 700; }
.rc-table tr.is-inherited { background: var(--color-bg-soft); }
.rc-x { width: 28px; height: 28px; border: 1px solid var(--color-border-light); border-radius: 8px; background: #fff; color: var(--color-text-muted); cursor: pointer; }
.rc-x:hover { border-color: var(--color-danger); color: var(--color-danger); }

.rc-figures { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 14px; margin-bottom: 16px; }
.rc-fig { background: #FFF; border: 1px solid var(--color-border-light); border-radius: 14px; padding: 18px 22px; }
.rc-fig.is-dark { background: var(--color-dark); border-color: var(--color-dark); color: var(--color-text-on-dark); }
.rc-fig-label { font-size: 0.7rem; font-weight: 800; letter-spacing: 1px; text-transform: uppercase; color: var(--color-text-muted); margin-bottom: 6px; }
.rc-fig.is-dark .rc-fig-label { color: var(--brand-ochre); }
.rc-fig-value { font-family: var(--font-heading); font-size: 1.9rem; }
.rc-fig-note { font-size: 0.76rem; color: var(--color-text-muted); margin-top: 3px; }

.rc-form { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px 16px; align-items: end; margin-top: 12px; }
.rc-form-tight { grid-template-columns: 2fr 1fr 1fr auto; gap: 8px; }
.rc-field label { display: block; font-size: 0.72rem; font-weight: 800; letter-spacing: 0.8px; text-transform: uppercase; color: var(--color-text-muted); margin-bottom: 6px; }
.rc-form input, .rc-form select { width: 100%; padding: 10px 12px; border: 1px solid var(--color-border-light); border-radius: 10px; font-size: 0.9rem; background: #fff; }
.rc-form .btn { width: 100%; }

.rc-addon { border: 1px solid var(--color-border-light); border-radius: 12px; padding: 14px 18px; margin-top: 12px; }
.rc-addon-head { display: flex; justify-content: space-between; gap: 12px; }
.rc-addon-lines { list-style: none; margin: 10px 0 0; padding: 0; }
.rc-addon-lines li { display: flex; align-items: center; gap: 12px; padding: 7px 0; border-top: 1px solid var(--color-border-light); font-size: 0.86rem; }
.rc-addon-lines li span:first-child { flex-grow: 1; }
.rc-addon-empty { font-size: 0.82rem; color: var(--brand-espresso); margin: 8px 0 0; }

.rc-note { background: var(--color-bg-soft); border-radius: 14px; padding: 16px 20px; margin-bottom: 16px; font-size: 0.86rem; color: var(--color-text-muted); line-height: 1.6; }

@media (max-width: 640px) { .rc-form-tight { grid-template-columns: minmax(0, 1fr); } }

/* PICKING SEVERAL INGREDIENTS AT ONCE.
   The button, the list it opens, and the table of what came back. */
.rc-pick-head { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-top: 12px; }
.rc-pick-count { font-size: 0.82rem; color: var(--color-text-muted); }
/* The amount goes in the cell, so the row stays a row rather than growing a
   form's worth of padding around every box. */
.rc-pick-qty { width: 120px; padding: 7px 10px; border: 1px solid var(--color-border-light);
  border-radius: 8px; font-size: 0.88rem; font-weight: 700; text-align: right;
  font-variant-numeric: tabular-nums; }
.rc-picked select { padding: 7px 10px; border: 1px solid var(--color-border-light);
  border-radius: 8px; font-size: 0.88rem; background: #fff; }
.rc-pick-actions { margin-top: 16px; }

/* The list inside the modal. Scrolls on its own so the modal's own action row
   stays where it is however many ingredients the shop keeps. */
.rc-search input { width: 100%; padding: 11px 14px; border: 1px solid var(--color-border-light);
  border-radius: 10px; font-size: 0.92rem; background: #fff; margin-bottom: 14px; }
.rc-pick-list { border: 1px solid var(--color-border-light); border-radius: 12px; }
.rc-pick-item { display: flex; align-items: center; gap: 12px; padding: 11px 14px;
  border-bottom: 1px solid var(--color-border-light); cursor: pointer; margin: 0;
  text-transform: none; letter-spacing: 0; }
/* `hidden` is display:none from the user agent, and ANY author display beats
   it -- so the flex above quietly un-hides every row the pager put away, and
   all 137 of them laid out behind the ten. Same trap as .pe-stock-fields. */
.rc-pick-item[hidden] { display: none; }
.rc-pick-item:last-child { border-bottom: 0; }
.rc-pick-item:hover { background: var(--color-bg-soft); }
.rc-pick-item input { width: 18px; height: 18px; flex: 0 0 auto; accent-color: var(--brand-moss); }
.rc-pick-name { font-size: 0.92rem; font-weight: 600; flex-grow: 1; }
.rc-pick-unit { font-size: 0.74rem; color: var(--color-text-muted); }
.rc-pick-in { font-size: 0.62rem; font-weight: 800; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--color-text-muted); background: var(--color-bg-soft); border-radius: 999px; padding: 3px 9px; }
.rc-pick-none { font-size: 0.86rem; color: var(--color-text-muted); margin: 14px 0 0; }

/* The questions above the ten. Wrapped rather than squeezed: a supplier's name
   is as long as the supplier decided it would be. */
.rc-pick-filters { display: flex; flex-wrap: wrap; align-items: end; gap: 12px; margin-bottom: 14px; }
.rc-pick-filters > label { display: flex; flex-direction: column; gap: 5px; margin: 0;
  text-transform: none; letter-spacing: 0; }
.rc-pick-filters > label > span { font-size: 0.68rem; font-weight: 800; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--color-text-muted); }
.rc-pick-filters select { padding: 9px 12px; border: 1px solid var(--color-border-light);
  border-radius: 10px; font-size: 0.88rem; background: #fff; max-width: 200px; }
.rc-pick-only { flex-direction: row !important; align-items: center; gap: 8px !important;
  margin-left: auto !important; padding-bottom: 9px; cursor: pointer; }
.rc-pick-only input { width: 17px; height: 17px; accent-color: var(--brand-moss); }
.rc-pick-only > span { font-size: 0.82rem; color: var(--color-text-muted);
  text-transform: none; letter-spacing: 0; font-weight: 600; }

/* Ten at a time, and which ten. */
.rc-pick-pager { display: flex; align-items: center; justify-content: space-between; gap: 12px;
  margin-top: 12px; }
.rc-pick-pager[hidden] { display: none; }
.rc-pick-range { font-size: 0.78rem; color: var(--color-text-muted); font-variant-numeric: tabular-nums; }
.rc-page { border: 1px solid var(--color-border-light); background: #fff; border-radius: 999px;
  padding: 6px 14px; font-size: 0.66rem; font-weight: 800; letter-spacing: 0.06em;
  color: var(--color-text-muted); cursor: pointer; }
.rc-page:hover:not(:disabled) { border-color: var(--color-primary); color: var(--color-primary); }
.rc-page:disabled { opacity: 0.4; cursor: default; }

/* THE ADD BUTTON, ON HOVER.
   It changed colour and nothing else, which on a pill that dark is a change
   you have to be looking for. It lifts now, the way every card on the
   customer side does. */
.admin-add { transition: background var(--transition, 0.2s ease), transform 0.15s ease, box-shadow 0.15s ease; }
.admin-add:hover { transform: translateY(-2px); box-shadow: 0 6px 16px rgba(0, 0, 0, 0.28); }
.admin-add:active { transform: translateY(0); box-shadow: none; }

/* SHOWN OR HIDDEN, AS AN EYE.
   EDIT and SIZES say what they do to the product; HIDE said what would happen
   to it, in the same size and weight, so three words competed for the same
   glance. The state is a picture now and the words are in the title. */
.admin-icon-btn {
    display: inline-flex; align-items: center; justify-content: center;
    width: 38px; height: 38px; padding: 0;
    border: 1px solid var(--color-border); border-radius: 999px;
    background: #FFF; color: var(--color-text-muted); cursor: pointer;
}
.admin-icon-btn:hover { border-color: var(--color-primary); color: var(--color-primary); }
.admin-icon-btn svg { width: 18px; height: 18px; display: block; }
/* A button with no text in it has no text baseline: an inline-flex falls back
   to its bottom margin edge, which then sits ON the baseline of the words in
   the buttons beside it and rides 5px high. Aligned by its middle instead,
   which is what the eye is. */
.admin-icon-btn { vertical-align: middle; }
/* Hidden is the state worth noticing, so it is the one that is coloured. */
.admin-icon-btn.is-hidden { color: var(--color-danger); border-color: var(--color-danger-bg); }

/* NUMBER FIELDS KEEP THEIR ARROWS.
   A browser shows the spinner on hover or focus, so a column of number boxes
   looks like a column of text boxes until you touch one -- and nothing says
   the value is meant to be nudged rather than retyped. */
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button { opacity: 1; margin-left: 2px; }
input[type="number"] { -moz-appearance: number-input; }

/* CLEARING A CHOSEN FILE.
   "No file chosen" is a state you can get back to only by reloading the form,
   which on a modal means losing everything else typed into it. */
.file-field { display: flex; align-items: center; gap: 8px; }
.file-field > input[type="file"] { flex: 1 1 auto; min-width: 0; }
.file-clear {
    flex: 0 0 auto;
    width: 26px; height: 26px; padding: 0;
    display: inline-flex; align-items: center; justify-content: center;
    border: 1px solid var(--color-border-light); border-radius: 999px;
    background: #FFF; color: var(--color-text-muted);
    font-size: 1rem; line-height: 1; cursor: pointer;
}
.file-clear:hover { border-color: var(--color-danger); color: var(--color-danger); }

/* A MESSAGE THAT DOES NOT MOVE THE PAGE.
   It used to sit in the flow above the banner, so every save pushed the whole
   screen down by its height and then, four seconds later, pulled it back up --
   under a cursor already on its way to something. It floats over instead. */
.flash-stack {
    position: fixed;
    /* Under the navbar, not across it. At a flat 16px the message covered the
       logo and the cart -- and the navbar is sticky, so it was not a matter of
       scrolling past it either. --navbar-offset is the header's own measured
       height, published by the header on every page; the literal is only for
       the moment before that runs, and is the desktop navbar's height rather
       than the 64px guess used elsewhere. */
    top: calc(var(--navbar-offset, 79px) + 12px);
    left: 50%;
    transform: translateX(-50%);
    z-index: 2500;
    width: min(var(--container-max-width), calc(100% - 32px));
    display: flex;
    flex-direction: column;
    gap: 10px;
    /* The empty space beside a short message is still the page underneath. */
    pointer-events: none;
}
.flash-stack > .flash-message {
    margin: 0;
    pointer-events: auto;
    box-shadow: 0 10px 30px rgba(51, 41, 35, 0.22);
    /* The tints are rgba at about a fifth: right when the message sat ON the
       page ground, see-through now it floats over the banner. The tint moves
       to a background IMAGE, which paints above background-color, so it still
       composites over the page ground -- only this time the ground travels
       with it. */
    background-image: linear-gradient(var(--flash-tint), var(--flash-tint));
    background-color: var(--color-bg-light);
}
.flash-stack > .flash-error { --flash-tint: var(--color-danger-bg); }
.flash-stack > .flash-success { --flash-tint: var(--color-success-bg); }
/* Arrives rather than appears: a message that is simply there on the next
   frame reads as part of the page and gets skipped. Short and small -- it
   starts 8px high and lands where it belongs, so nothing is still moving by
   the time it is being read. */
.flash-stack > .flash-message {
    animation: flash-arrive 0.22s ease-out both;
}
@keyframes flash-arrive {
    from { opacity: 0; transform: translateY(-8px); }
}
@media (prefers-reduced-motion: reduce) {
    .flash-stack > .flash-message { animation: none; }
}

/* ---------------------------------------------------------------------------
   The customer app's first paint.

   /menu, /order and /order/customize are the three screens React builds from
   scratch, so their server HTML is only a fallback. Until the bundle has
   downloaded and run, that fallback is what the browser paints -- and it was a
   bare <h1> and an unstyled list of every product, which flashed on screen on
   every reload before the real page replaced it.

   The fallback now sits inside <noscript>, where a browser with JS never
   builds it at all, and this stands in its place: the logo on the page's own
   ground, holding the full height so nothing jumps when the real page arrives.

   Drawn immediately, with no delay on it. A delay meant the first thing on
   screen was an empty white page, which is the awkward gap it was supposed to
   prevent -- better to show the mark and let it be brief.

   This lives in the head stylesheet, not the app's own bundle: the bundle's
   <link> sits after #customer-root in the body, which is too late to stop the
   thing above it from painting. --------------------------------------------- */
.customer-boot {
    /* dvh, not vh: on a phone the browser chrome is part of the first view and
       a 100vh box starts the page taller than the screen. */
    min-height: 100dvh;
    display: grid;
    align-content: center;
    justify-items: center;
    gap: 22px;
    padding: 24px;
    background: var(--color-bg-light);
}
.customer-boot-logo {
    width: min(260px, 60vw);
    height: auto;
    /* Breathing rather than spinning. The lockup carries the "CAFE + ROASTERY
       AND BAKEHOUSE" line as part of the artwork, and rotating or pulsing the
       size of it makes that line unreadable. */
    animation: customer-boot-breathe 1.7s ease-in-out infinite;
}
.customer-boot-bar {
    position: relative;
    width: min(170px, 40vw);
    height: 3px;
    border-radius: 3px;
    background: var(--color-border-light);
    overflow: hidden;
}
.customer-boot-bar::after {
    content: "";
    position: absolute;
    inset: 0;
    width: 40%;
    border-radius: inherit;
    background: var(--color-primary);
    /* Indeterminate on purpose: nothing here knows how much of the bundle has
       arrived, and a bar that pretends to would be lying about it. */
    animation: customer-boot-sweep 1.2s ease-in-out infinite;
}
@keyframes customer-boot-breathe {
    50% { opacity: 0.55; }
}
@keyframes customer-boot-sweep {
    from { transform: translateX(-110%); }
    to { transform: translateX(260%); }
}
@media (prefers-reduced-motion: reduce) {
    /* Still reads as "working" -- a filled bar under the mark -- without
       anything moving. */
    .customer-boot-logo { animation: none; }
    .customer-boot-bar::after {
        position: static;
        display: block;
        width: 100%;
        height: 100%;
        animation: none;
    }
}

/* DV-F2: the sort control's select hides its own outline (and on a phone is
   invisible over the pill), so the pill shows where keyboard focus is. */
.menu-sort:has(select:focus-visible) {
    outline: 2px solid var(--color-primary);
    outline-offset: 2px;
}

/* DV-F1: the live-order dot pulsed for ever, and the widget slide-ins and
   smooth scrolling ignored a visitor who asked for less motion. One rule for
   all of them, including anything added later. Durations are cut rather than
   animations removed, so an animation that only DELAYS something (the pay
   page's fallback link) still does its job. */
@media (prefers-reduced-motion: reduce) {
    html { scroll-behavior: auto !important; }
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
}
