/* GENERATED FILE — DO NOT EDIT.
   Concatenation of css/parts/*.css in cascade order, built by
   scripts/build-css-bundle.py and pinned by CssBundleTest.
   Edit the parts; regenerate this. */
/* ==== 01-tokens.css ==== */
/* ============================================================
   01-tokens.css  (1 of 6)

   Design tokens and the base reset.

   Everything downstream reads the custom properties defined here, so this file
   loads first and nothing else declares a :root token.

   Part of an ordered set. The browser concatenates these in the order they
   are linked in fragments/head.html, and that order IS the cascade. Do not
   reorder the <link> tags, and do not move a rule between files without
   checking what it would then stop overriding.
   ============================================================ */

/* ============================================================
   BiteSite design system — warm, appetizing, not generic-AI-blue.
   Built as overrides on top of Bootstrap 5, so every existing
   .btn / .card / .badge / .form-control class keeps working.

   v2 — Mobile-first redesign inspired by modern food ordering apps.
   ============================================================ */

:root {
    /* ============================================================
       BiteSite — BIG BEEF / burger-landing neo-brutalist system.
       Ported from /Users/yash113gadia/burger-landing/css/style.css.

       Bone canvas, near-black 3px borders, hard offset shadows,
       red + yellow accents, Archivo Black uppercase type.
       ============================================================ */

    /* --- Surfaces. Warm bone canvas, bone cards. --- */
    --color-bg:            #f2ead9;  /* bone canvas */
    --color-surface:       #f2ead9;  /* bone cards */
    --color-field:         #fffdf6;  /* inputs — lighter bone */
    --color-field-border:  #14100d;  /* control edge — near-black */
    --color-field-rule:    #14100d;
    --color-surface-food:  #f2ead9;
    --color-surface-deep:  #e6dcc4;  /* pressed/active */

    --color-ink:       #14100d;  /* near-black */
    --color-ink-muted: #5c5247;  /* warm grey */
    --color-border:    #14100d;

    /* --- Terracotta red is the primary action colour. --- */
    --color-primary:      #e8281e;  /* red */
    --color-primary-dark: #b81f16;  /* red-dark */
    --color-primary-soft: #ff6a5e;
    --color-primary-tint: #ffd9d4;

    /* --- Yellow accents. --- */
    --color-accent:        #14100d;
    --color-accent-strong: #14100d;
    --color-accent-tint:   #ffc918;

    /* --- Status. --- */
    --color-success:      #14100d;
    --color-success-tint: #ffc918;
    --color-warning:      #14100d;
    --color-warning-tint: #ffc918;
    --color-info:         #14100d;
    --color-info-tint:    #ffc918;
    --color-danger:       #e8281e;
    --color-danger-tint:  #ffd9d4;

    --color-nav: #f2ead9;

    /* --- Shape. Brutalist: tight radii, hard edges. --- */
    --radius-sm:   0px;
    --radius-md:   2px;
    --radius-lg:   2px;
    --radius-pill: 2px;

    /* --- Line weight. ---------------------------------------------------------
       Every structural border in the product reads from these, so the weight of
       the whole UI is four numbers rather than ninety literals.

       The original system drew everything at 3px near-black. At the density the
       customer app runs — a few big cards on a phone — that is the look. On the
       staff and admin consoles, where a screen is a table of thirty rows inside a
       bordered panel inside a bordered card, the same weight stacks: a 3px panel
       edge two pixels above a 3px header rule reads as one thick black bar, and
       the page stops looking designed and starts looking heavy.

       So: one structural weight, thin enough to stack without turning into a
       slab, plus a genuinely quiet rule for separators that were never meant to
       be structure at all. The shapes, palette and type are untouched. */
    --border-w:      1.5px;  /* cards, panels, tables, controls */
    --border-w-loud: 2px;    /* the few things that must assert: primary buttons */
    --rule-w:        1px;    /* separators between rows and sections */
    --rule-color:    rgba(20, 16, 13, 0.14);
    --rule-color-strong: rgba(20, 16, 13, 0.28);

    /* --- Depth. Still hard offset shadows, no blur — but cast as translucent ink
       rather than solid near-black, so a card reads as lifted instead of as a
       second black rectangle behind the first. --- */
    --shadow-xs:      1px 1px 0 rgba(20, 16, 13, 0.20);
    --shadow-sm:      2px 2px 0 rgba(20, 16, 13, 0.18);
    --shadow-md:      3px 3px 0 rgba(20, 16, 13, 0.16);
    --shadow-lg:      4px 4px 0 rgba(20, 16, 13, 0.14);
    --shadow-pressed: 1px 1px 0 rgba(20, 16, 13, 0.20);
    --shadow-sticky:  0 -1.5px 0 rgba(20, 16, 13, 0.18);

    --color-coral:      #ffc918;
    --color-coral-deep: #ffb300;

    /* --- Focus ring. --- */
    /* Two rings: a yellow halo that separates the control from the page, and a thin
       ink edge that keeps the contrast ratio. Reads clearly without the 3px slab. */
    --focus-ring: 0 0 0 2px var(--color-accent-tint), 0 0 0 3.5px var(--color-ink);

    /* --- Type. Archivo for body, Archivo Black for display. --- */
    --font-display: 'Archivo Black', system-ui, sans-serif;
    --font-body:    'Archivo', system-ui, -apple-system, sans-serif;

    /* --- Type scale. --- */
    --text-display-lg:  40px;  --lh-display-lg:  40px;
    --text-headline-lg: 28px;  --lh-headline-lg: 32px;
    --text-headline-md: 22px;  --lh-headline-md: 28px;
    --text-body-lg:     16px;  --lh-body-lg:     24px;
    --text-body-md:     14px;  --lh-body-md:     20px;
    --text-label-lg:    14px;  --lh-label-lg:    20px;
    --text-label-sm:    12px;  --lh-label-sm:    16px;

    /* --- Spacing scale. --- */
    --stack-xs: 4px;
    --stack-sm: 12px;
    --stack-md: 24px;
    --stack-lg: 48px;
    --margin-mobile: 24px;
    --gutter-mobile: 20px;
    --hit-area: 48px;

    --bottom-nav-height: 64px;
    /* Two sources on purpose. Android WebView only passes real insets through to
       env() from WebView 140 onward; below that Capacitor's SystemBars plugin instead
       sets --safe-area-inset-* as inline custom properties on the root element, which
       env() never sees. Reading only env() meant every rule below computed a zero on
       older WebViews and the tab bar sat under the gesture bar. max() takes whichever
       source is actually live. On the desktop web neither resolves, both fall back to
       0px, and nothing moves. */
    --safe-area-bottom: max(env(safe-area-inset-bottom, 0px), var(--safe-area-inset-bottom, 0px));
    --safe-area-top: max(env(safe-area-inset-top, 0px), var(--safe-area-inset-top, 0px));
}


/* `text-wrap` is a shorthand for text-wrap-mode + text-wrap-style, so `text-wrap: pretty`
   also sets mode back to its initial `wrap` — on every element in the document. That
   silently defeats any `white-space: nowrap` that relies on inheritance: the parent
   computes nowrap, and this rule then applies `wrap` directly to each child, which beats
   an inherited value. It is why the marquee strip broke each "FRESH · FAST · FIERCE" onto
   three lines despite its track being explicitly nowrap.

   The longhand asks for the thing that was actually wanted — nicer line breaking — and
   leaves wrapping mode alone. */
* {
    text-wrap-style: pretty;
}

/* ==== 02-base.css ==== */
/* ============================================================
   02-base.css — Base and Bootstrap overrides

   Reset, view transitions, the top and bottom navigation, and the overrides that
   retune Bootstrap's buttons, cards, forms, badges, tables and alerts to this
   design system. Everything else builds on what is here.

   Part of an ordered set linked in fragments/head.html. The browser applies these
   in link order and THAT ORDER IS THE CASCADE — a rule here can only override one
   in an earlier file. Do not reorder the links, and do not move a rule between
   files without checking what it then stops overriding.
   ============================================================ */

/* ============================================================
   app.css — BiteSite consolidated component stylesheet.

   This file merges the former 02-base → 08-redesign cascade into a
   single ordered sheet. 01-tokens.css (design tokens) loads before it
   and is the only other stylesheet this app ships.

   The merge is faithful: where the old files overrode each other,
   the winning (later-file) value is preserved here, so rendering is
   unchanged. The content remains in its original cascade order; a
   scoped polish layer is appended at the end.
   ============================================================ */
/* ============================================================
   02-base.css  (2 of 6)

   Shared primitives used by every portal: view transitions, navbar, bottom nav,
   buttons, cards, forms, badges, tables, alerts, and the Bootstrap overrides
   that retone them.

   Part of an ordered set. The browser concatenates these in the order they
   are linked in fragments/head.html, and that order IS the cascade. Do not
   reorder the <link> tags, and do not move a rule between files without
   checking what it would then stop overriding.
   ============================================================ */

/* ============================================================
   CROSS-DOCUMENT VIEW TRANSITIONS
   This is a traditional server-rendered app, not an SPA — every
   link click and form submit is a full page navigation. Without
   this, that's a hard white-flash reload; with it, the browser
   cross-fades outgoing/incoming pages natively, no JS involved.
   Safe progressive enhancement: browsers that don't support it
   (Safari < 18.2, older Firefox) just keep the instant navigation
   they already have.
   ============================================================ */

@media (prefers-reduced-motion: no-preference) {
    @view-transition {
        navigation: auto;
    }
}

::view-transition-old(root) {
    animation: view-transition-fade-out 160ms cubic-bezier(0.4, 0, 1, 1) both;
}

::view-transition-new(root) {
    animation: view-transition-fade-in 260ms cubic-bezier(0.16, 1, 0.3, 1) both;
}

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

@keyframes view-transition-fade-out {
    from { opacity: 1; }
    to   { opacity: 0; }
}

html, body {
    background-color: var(--color-bg);
    /* Flat. The grain texture belonged to the cream canvas; on this near-white one it
       reads as noise, and the design it is ported from has no texture layer at all. */
    color: var(--color-ink);
    font-family: var(--font-body);
    font-size: var(--text-body-lg);
    line-height: var(--lh-body-lg);
    -webkit-font-smoothing: antialiased;
}

/* One family across every role, so hierarchy is carried by weight and tracking rather
   than by a second typeface — see the export's fontFamily config, which maps display
   through label all to Inter. */
h1, h2, h3, h4, h5, h6 {
    font-family: var(--font-display);
    color: var(--color-ink);
}

h1 { font-size: var(--text-display-lg);  line-height: var(--lh-display-lg);  font-weight: 700; letter-spacing: -0.02em; }
h2 { font-size: var(--text-headline-lg); line-height: var(--lh-headline-lg); font-weight: 700; letter-spacing: -0.01em; }
h3 { font-size: var(--text-headline-lg); line-height: var(--lh-headline-lg); font-weight: 700; letter-spacing: -0.01em; }
h4 { font-size: var(--text-headline-md); line-height: var(--lh-headline-md); font-weight: 600; }
h5 { font-size: var(--text-headline-md); line-height: var(--lh-headline-md); font-weight: 600; }
h6 { font-size: var(--text-label-lg);    line-height: var(--lh-label-lg);    font-weight: 600; }

/* Material Symbols, the icon set used across the whole product. */
.material-symbols-outlined {
    /* The font-family used to come from the Google CDN stylesheet; now that the
       font is self-hosted this rule must carry it, or ligatures render as text. */
    font-family: 'Material Symbols Outlined';
    font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
    vertical-align: middle;
    user-select: none;

    /* An icon is chosen by ligature: the element's text is the icon's name, and the
       font substitutes a glyph for that exact lowercase letter sequence. Anything that
       alters the letters or their spacing before shaping stops the substitution
       matching, and the name renders as words instead.
       `.btn` sets `text-transform: uppercase` and `letter-spacing: 0.05em`, so every
       icon inside a button was drawing the literal text — "LOGOUT" beside the word
       Logout. These three lines are load-bearing, not defensive tidying. */
    text-transform: none;
    letter-spacing: normal;
    font-variant-ligatures: normal;
    /* The names contain underscores and can be long; nothing here should ever wrap. */
    white-space: nowrap;
    word-break: normal;
}
.material-symbols-outlined.is-filled { font-variation-settings: 'FILL' 1; }
.ms-20 { font-size: 20px; }
.ms-16 { font-size: 16px; }

a {
    color: var(--color-primary-dark);
    text-decoration: none;
}
a:hover { color: var(--color-primary); }


/* ---------- Layout ---------- */

.container {
    max-width: 1100px;
}

/* On mobile, add bottom padding for the bottom nav */
@media (max-width: 767.98px) {
    body.has-bottom-nav {
        padding-bottom: calc(var(--bottom-nav-height) + var(--safe-area-bottom) + 12px);
    }
}

/* ============================================================
   TOP NAVBAR
   ============================================================ */

/* The dark bar is gone. It was the loudest remaining piece of dashboard chrome —
   a heavy near-black slab pinned above a warm cream page, which is exactly the
   "generic SaaS with an orange button" read. The reference has no dark surfaces
   at all; the canvas simply continues to the top of the screen and the brand mark
   carries the identity. */

/* A phone should never scroll sideways. Anything that overflows its container is a bug,
   but this keeps one from taking the whole page with it.

   `clip`, not `hidden`. They look the same and behave very differently: `hidden` makes
   the element a scroll container, which silently disables `position: sticky` for every
   descendant — the top navbar and the menu's category strip both declared `sticky` and
   neither ever stuck. `clip` crops the overflow without creating a scrollport, so the
   guard stays and sticky works. */
html, body { overflow-x: clip; }

.navbar-toggler-custom {
    font-size: 1.5rem;
    line-height: 1;
    padding: 4px 8px;
    transition: background-color 150ms ease;
}
.navbar-toggler-custom:hover,
.navbar-toggler-custom:active {
    background: var(--color-surface-deep);
}

/* Staff/admin nav links — collapsed behind the hamburger on mobile (no bottom-nav
   fallback for these roles), always inline on desktop. */
.navbar-links-collapse {
    display: none;
}
.navbar-links-collapse.is-open {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 2px;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    border-radius: 0 0 var(--radius-md) var(--radius-md);
    padding: 0.5rem 1rem 1rem;
    z-index: 1030;
    animation: fade-up 200ms cubic-bezier(0.16, 1, 0.3, 1) both;
}
.navbar-links-collapse.is-open .nav-link {
    padding: 0.6rem 0.7rem !important;
}

@media (min-width: 992px) {
    .navbar-toggler-custom {
        display: none;
    }
    .navbar-links-collapse {
        display: flex !important;
        flex-direction: row;
        align-items: center;
        gap: 0.5rem;
        position: static;
        background: none;
        padding: 0;
        box-shadow: none;
        animation: none !important;
    }
}

/* The status bar draws over the page in the Android shells: targetSdk 36 forces
   edge-to-edge and there is no opting out. The chrome carries the inset rather than
   body, so the page background continues up behind the clock instead of the top bar
   starting underneath it. The navbar sets no background of its own, so what shows
   through is body's --color-bg, which is what theme-color already matches. */
.app-chrome {
    padding-top: var(--safe-area-top);
}

/* Terracotta wordmark on cream — the brand carries the top of the page now that
   there's no dark slab doing it. Matches the reference, whose own wordmark is the
   one strongly orange element on the screen. */

.navbar-brand img {
    border-radius: 0;
}

.navbar .nav-link {
    padding: 0.45rem 0.85rem !important;
    transition: color 150ms ease, background-color 150ms ease;
}

.navbar .navbar-text {
    font-size: 0.85rem;
    color: var(--color-ink-muted) !important;
}

/* Hide top-nav user links on mobile (bottom nav takes over) */
@media (max-width: 767.98px) {
    .navbar .top-nav-links {
        display: none !important;
    }
    .navbar {
        padding-top: 0.65rem;
        padding-bottom: 0.65rem;
    }
}

/* ============================================================
   BOTTOM TAB NAVIGATION (mobile only)
   ============================================================ */

.bottom-nav {
    display: none;
}

@media (max-width: 767.98px) {
    /* Floating rounded glassmorphism bar */
    .bottom-nav {
        display: flex;
        position: fixed;
        bottom: 0;
        left: 0;
        right: 0;
        z-index: 1050;
        height: calc(var(--bottom-nav-height) + var(--safe-area-bottom));
        padding-bottom: var(--safe-area-bottom);
        
        
        border: none;
        align-items: stretch;
    }

    .bottom-nav-item {
        flex: 1;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        gap: 3px;
        text-decoration: none;
        color: var(--color-ink-muted);
        font-size: 0.7rem;
        font-weight: 700;
        font-family: var(--font-display);
        letter-spacing: 0.01em;
        transition: all 180ms ease;
        position: relative;
        -webkit-tap-highlight-color: transparent;
    }

    .bottom-nav-item i {
        font-size: 1.45rem;
        line-height: 1;
        transition: transform 180ms cubic-bezier(0.34, 1.56, 0.64, 1);
    }


    .bottom-nav-item.active i {
        transform: scale(1.15) translateY(-2px);
    }

    .bottom-nav-item:active i {
        transform: scale(0.9);
    }

    /* Cart badge on bottom nav */
    .bottom-nav-badge {
        position: absolute;
        top: 5px;
        right: calc(50% - 18px);
        min-width: 18px;
        height: 18px;
        border-radius: 0;
        background: var(--color-accent-strong);
        color: #fff;
        font-size: 0.65rem;
        font-weight: 800;
        display: flex;
        align-items: center;
        justify-content: center;
        padding: 0 5px;
        line-height: 1;
        box-shadow: none;
        animation: badge-pop 320ms cubic-bezier(0.34, 1.56, 0.64, 1);
    }

    @keyframes badge-pop {
        0% { transform: scale(0); }
        80% { transform: scale(1.2); }
        100% { transform: scale(1); }
    }
}

/* ============================================================
   BUTTONS
   ============================================================ */

/* Soft pills. The reference's buttons are fully rounded and generously padded —
   at phone size they're the most tactile thing on screen, which is right for the
   one control that commits an order. */
.btn {
    font-weight: 700;
    font-size: 0.95rem;
    padding: 0.6rem 1.4rem;
}
/* Bootstrap ships .btn-sm, but this file loads after Bootstrap and .btn matches at
   the same specificity, so the padding above was silently beating it and every
   "small" button rendered full size. That is fine on a desktop table row and
   overflows a card on a phone, which is what broke the outlet and admin screens
   at narrow widths. Restated here so the modifier actually modifies. */
.btn-sm {
    padding: 0.35rem 0.8rem;
    font-size: 0.85rem;
}

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

.btn-primary {
    box-shadow: var(--shadow-sm);
}
.btn-primary:hover, .btn-primary:focus {
    box-shadow: var(--shadow-sm);
}


/* Secondary reads as a soft peach chip rather than a hairline outline — an outlined
   button on a peach card is two thin lines competing; a filled tonal chip is calmer. */
.btn-outline-secondary:hover {
    background: var(--color-coral-deep);
    border-color: transparent;
    color: var(--color-ink);
}

/* Terracotta button — for appetite/brand moments, never for confirm/pay.
   Uses the strong tone so white label text clears 5.37:1. */
.btn-accent {
    border-color: var(--color-accent-strong);
}
.btn-accent:hover, .btn-accent:focus {
    border-color: var(--color-ink);
}

.btn-success {border-color: var(--color-primary);}
.btn-success:hover {border-color: var(--color-primary-dark);}

.btn-warning { background: var(--color-warning); border-color: var(--color-warning); color: #fff; }
.btn-warning:hover { background: var(--color-coral-deep); border-color: var(--color-ink); color: #fff; }

.btn-danger, .btn-outline-danger:hover { background: var(--color-danger); border-color: var(--color-danger); }


/* ============================================================
   CARDS

   Generic .card stays a pale-cream block — it's shared with the canteen and
   admin portals, which are deliberately outside this redesign and need dense,
   legible surfaces. The student-facing peach blocking lives on the specific
   component classes further down (.menu-card, .cart-item-card, .order-card,
   .account-section, .outlet-card, .auth-card).
   ============================================================ */

.card {
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-sm);
    transition: box-shadow 180ms ease, transform 180ms ease;
}
.card:hover {
    box-shadow: var(--shadow-md);
}

.card-title {
    font-family: var(--font-display);
    font-weight: 700;
    letter-spacing: -0.01em;
}

/* ============================================================
   FORMS
   ============================================================ */

/* Pale cream fields sitting inside the warmer surfaces around them — the reference
   lights its inputs slightly *brighter* than the card they're on, which is what makes
   a form read as a set of soft slots rather than a stack of outlined boxes. */
.form-control, .form-select {
    border-radius: var(--radius-sm);
    border: 1.5px solid var(--color-field-border);
    padding: 0.8rem 1rem;
    font-size: 0.95rem;
    color: var(--color-ink);
    transition: border-color 150ms ease, background-color 150ms ease, box-shadow 150ms ease;
}
.form-control::placeholder { color: var(--color-ink-muted); }

.form-label {
    font-weight: 600;
    color: var(--color-ink);
    margin-bottom: 0.4rem;
}

.input-group-text {
    border-radius: var(--radius-sm) 0 0 var(--radius-sm) !important;
    border: 1.5px solid var(--color-field-border);
    border-right: none;
    background: var(--color-field);
    color: var(--color-ink-muted);
}
.input-group .form-control {
    border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
    border-left: none;
}

/* Buttons are pills globally, but a pill welded to the end of a field reads as a
   detached blob floating beside it. Inside an input group the trailing button takes
   the field's own corner radius so the two read as one control. */
.input-group > .btn {
    border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
.input-group > .form-control:not(:last-child) {
    border-radius: 0;
}
.input-group:focus-within .input-group-text {
    border-color: var(--color-primary);
    background: #FFFFFF;
}

/* Toggle switches pick up the action color rather than Bootstrap's blue. */
.form-check-input:focus {
    border-color: var(--color-primary);
    box-shadow: 0 0 0 3px var(--color-primary-tint);
}

/* ============================================================
   BADGES (status pills)
   ============================================================ */

.badge {
    font-family: var(--font-body);
    font-weight: 700;
    letter-spacing: 0.01em;
    padding: 0.45em 0.85em;
}

.badge.bg-success { background: var(--color-primary) !important; color: #fff !important; }
.badge.bg-warning { background: var(--color-warning-tint) !important; color: var(--color-warning) !important; }
.badge.bg-info { background: var(--color-info-tint) !important; color: var(--color-info) !important; }
.badge.bg-primary { background: var(--color-primary) !important; color: #fff !important; }
.badge.bg-secondary { background: var(--color-surface-deep) !important; color: var(--color-ink-muted) !important; }
.badge.bg-danger { background: var(--color-danger-tint) !important; color: var(--color-danger) !important; }

/* ============================================================
   TABLES
   ============================================================ */

.table {
    overflow: hidden;
}
.table thead th {
    font-size: 0.82rem;
    letter-spacing: 0.04em;
    border-bottom-width: 1px;
}
.table td, .table th { vertical-align: middle; border-color: var(--color-border); }

/* ============================================================
   ALERTS
   ============================================================ */

.alert-secondary { background: var(--color-surface-food); color: var(--color-ink-muted); }
.alert-success { background: var(--color-primary-tint); color: var(--color-primary-dark); }
.alert-info { background: var(--color-info-tint); color: var(--color-info); }
.alert-primary { background: var(--color-primary-tint); color: var(--color-primary-dark); }

/* ============================================================
   BOOTSTRAP UTILITY OVERRIDES

   The templates lean on .text-muted (39 uses) and .text-danger (30) — Bootstrap
   ships those as cold grays/reds (#6c757d, #dc3545) which read as foreign against
   a cream-and-terracotta page. Repointing them at the warm palette fixes the
   inconsistency everywhere at once instead of per template.
   ============================================================ */

.text-muted { color: var(--color-ink-muted) !important; }
.text-danger { color: var(--color-danger) !important; }
.text-dark   { color: var(--color-ink) !important; }
.bg-light    { background-color: var(--color-surface) !important; }
.border-bottom, .border-top { border-color: var(--color-border) !important; }

/* ============================================================
   NAV DRAWER — every destination for the signed-in role.

   In 02-base because all three portals use it. It slides from the right so the opening
   gesture starts near the button that triggers it, and so it never fights the bottom tab
   bar on a phone.
   ============================================================ */

.nav-drawer-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 42px;
    height: 42px;
    flex: none;
    padding: 0;
    background: var(--color-surface);
    border: var(--border-w) solid var(--color-ink);
    border-radius: 0;
    color: var(--color-ink);
    box-shadow: var(--shadow-sm);
    cursor: pointer;
}

.nav-drawer-btn:hover { background: var(--color-accent-tint); }
.nav-drawer-btn:active { transform: translate(1px, 1px); box-shadow: none; }
.nav-drawer-btn .material-symbols-outlined { font-size: 22px; }

.nav-drawer-backdrop {
    position: fixed;
    inset: 0;
    z-index: 1100;
    background: rgba(20, 16, 13, 0.55);
    opacity: 0;
    transition: opacity 180ms ease;
}
.nav-drawer-backdrop.is-open { opacity: 1; }

.nav-drawer {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    z-index: 1101;
    width: min(340px, 86vw);
    display: flex;
    flex-direction: column;
    background: var(--color-surface);
    border-left: var(--border-w) solid var(--color-ink);
    transform: translateX(100%);
    transition: transform 220ms cubic-bezier(0.16, 1, 0.3, 1);
    overscroll-behavior: contain;
    /* Fixed to top:0/bottom:0, so it escapes the padding .app-chrome carries and has
       to clear the status bar and the gesture bar itself. */
    padding-top: var(--safe-area-top);
    padding-bottom: var(--safe-area-bottom);
}
.nav-drawer.is-open { transform: translateX(0); }

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

/* The page behind must not scroll while the drawer is over it. */
body.has-drawer-open { overflow: hidden; }

.nav-drawer__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    padding: 12px 14px;
    /* Was a solid ink bar. The drawer opens over the page as a tall bone panel, so a
       black cap on it was the loudest thing on screen for a header that only says who
       you are signed in as. */
    border-bottom: var(--rule-w) solid var(--rule-color-strong);
    background: var(--color-surface-deep);
    color: var(--color-ink);
}

.nav-drawer__who {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    min-width: 0;
    font-family: var(--font-display);
    font-size: 0.78rem;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.nav-drawer__who .material-symbols-outlined { font-size: 20px; flex: none; }

.nav-drawer__close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    flex: none;
    padding: 0;
    background: transparent;
    border: 2px solid var(--color-bg);
    border-radius: 0;
    color: var(--color-bg);
    cursor: pointer;
}
.nav-drawer__close:hover { background: var(--color-primary-dark); }

.nav-drawer__body {
    flex: 1 1 auto;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 4px 0 12px;
}

.nav-drawer__group { padding: 8px 0; }
.nav-drawer__group + .nav-drawer__group { border-top: 2px solid var(--color-field-rule); }

.nav-drawer__label {
    margin: 0 0 2px;
    padding: 4px 16px;
    font-family: var(--font-display);
    font-size: 0.62rem;
    text-transform: uppercase;
    letter-spacing: 0.09em;
    color: var(--color-ink-muted);
}

.nav-drawer__link {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 11px 16px;
    min-height: 44px;
    color: var(--color-ink);
    text-decoration: none;
    font-size: 0.9rem;
    font-weight: 500;
}
.nav-drawer__link .material-symbols-outlined { font-size: 20px; flex: none; }
.nav-drawer__link:hover { background: var(--color-surface-deep); color: var(--color-ink); }

/* Yellow marks where you are, matching the console strip, so "current" means the same
   thing in both navigations. Hover is a quiet grey underneath it — previously hover was
   yellow and current was an ink slab, which made a moused-over row look selected and the
   selected row look like a button. */
.nav-drawer__link[aria-current="page"] {
    background: var(--color-accent-tint);
    color: var(--color-ink);
    box-shadow: inset 3px 0 0 var(--color-ink);
    font-family: var(--font-display);
    text-transform: uppercase;
    font-size: 0.78rem;
    letter-spacing: 0.03em;
}

/* Counts ride to the right of their row, so cart and order state is visible without
   opening either page. */
.nav-drawer__count {
    margin-left: auto;
    min-width: 22px;
    padding: 1px 6px;
    text-align: center;
    background: var(--color-primary-dark);
    color: #fff;
    border: var(--border-w) solid var(--color-ink);
    font-family: var(--font-display);
    font-size: 0.68rem;
}

/* Legal links are references, not destinations you go to often. */
.nav-drawer__group--quiet .nav-drawer__link {
    font-size: 0.8rem;
    color: var(--color-ink-muted);
    min-height: 38px;
    padding-block: 8px;
}

.nav-drawer__foot {
    flex: none;
    padding: 12px 14px calc(12px + var(--safe-area-bottom));
    border-top: var(--border-w) solid var(--color-ink);
    margin: 0;
}


/* ============================================================
   TOUCH INPUT

   Two rules that only matter on a phone, and both were biting.

   iOS Safari zooms the whole page when a field with a font-size under 16px takes focus,
   and does not zoom back out. .form-control and .form-select were 0.95rem and 0.9rem —
   15.2px and 14.4px — so tapping the email box on the sign-in screen, the menu search, a
   quantity select or the pickup-code field threw the layout sideways and left it there.
   Scoped to coarse pointers rather than a width breakpoint: it is a property of touch
   input, not of screen size, and a narrow desktop window should keep its typography.

   44px is Apple's minimum target and 48 is Material's; the controls below were 40 and 42,
   which is inside the range where a thumb misses. --hit-area has existed as a token since
   the beginning and was never applied to anything. */
@media (pointer: coarse) {
    .form-control,
    .form-select,
    input[type="text"],
    input[type="email"],
    input[type="password"],
    input[type="tel"],
    input[type="search"],
    input[type="number"],
    textarea,
    select {
        font-size: 16px;
    }

    .nav-drawer-btn,
    .navbar-toggler-custom,
    .field__toggle,
    .bottom-nav-item {
        min-width: 44px;
        min-height: 44px;
    }
}

/* ==== 03-customer.css ==== */
/* ============================================================
   03-customer.css — Customer screen components

   The student-facing pieces: category tabs, quantity steppers, menu and deal cards,
   the outlet picker, sticky cart, cart, orders, item detail, account and search.

   Part of an ordered set linked in fragments/head.html. The browser applies these
   in link order and THAT ORDER IS THE CASCADE — a rule here can only override one
   in an earlier file. Do not reorder the links, and do not move a rule between
   files without checking what it then stops overriding.
   ============================================================ */

/* ============================================================
   03-app.css  (3 of 6)

   Customer screens: category tabs, quantity steppers, menu cards, deals rail,
   outlet picker, sticky cart, cart, orders, item detail, account, search, auth.

   Part of an ordered set. The browser concatenates these in the order they
   are linked in fragments/head.html, and that order IS the cascade. Do not
   reorder the <link> tags, and do not move a rule between files without
   checking what it would then stop overriding.
   ============================================================ */

/* ============================================================
   CATEGORY TABS

   The app design replaces the circular illustrated chips with a plain
   underlined tab bar: a scrolling row of labels on a hairline, the active
   one in primary with a 2px rule beneath it. Quieter, and it stops the
   category row competing with the food photography below it.
   ============================================================ */

.category-chips-wrapper {
    margin: 0 0 var(--stack-lg);
}

.category-chips {
    display: flex;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
}
.category-chips::-webkit-scrollbar { display: none; }

.category-chip {
    position: relative;
    flex: none;
    cursor: pointer;
    text-decoration: none;
    white-space: nowrap;
    -webkit-tap-highlight-color: transparent;
}

/* The illustrated pucks have no place in a text tab bar. */
.cat-chip-icon,
.cat-icon-img { display: none; }

.cat-chip-label {
    line-height: var(--lh-label-lg);
    transition: color 150ms ease;
}

.category-chip:hover .cat-chip-label { color: var(--color-ink); }


/* The active rule sits on the wrapper's hairline rather than under the label, so the
   two line up exactly however tall the label happens to be. */
.category-chip.active::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: -1px;
}

/* ============================================================
   QUANTITY STEPPER (+ / - buttons replacing number input)
   ============================================================ */

.qty-stepper,
.cart-control__stepper {
    overflow: hidden;
}

.qty-stepper button,
.cart-control__stepper button {
    font-size: 0.95rem;
    font-weight: 700;
    -webkit-tap-highlight-color: transparent;
}


.qty-stepper .qty-value,
.cart-control__stepper .cart-qty-value {
    user-select: none;
}

/* ---------- Add / stepper swap on menu cards ----------
   The card used to show a stepper reading "1" beside an Add button, on an
   item that was not in the cart at all. Two controls, one of which was
   lying: the 1 described what would be added, but read as what was already
   there. Now the stepper only exists once the item is in the cart, so any
   number on screen is a number in the cart.

   Both controls are always in the DOM and .is-active picks between them.
   That keeps the swap instant and, more importantly, keeps the no-JS path
   honest: the server renders the correct one on load. */
.cart-control__stepper { display: none; }
.cart-control.is-active .cart-control__add { display: none; }
.cart-control.is-active .cart-control__stepper { display: inline-flex; }

/* Only the number changes on +/-, and it changes under the user's finger,
   so it gets a short pop to confirm the tap registered. Anything longer
   would lag behind repeated presses. */
.cart-qty-value.is-bumped { animation: qtyBump 180ms ease; }
@keyframes qtyBump {
    0%   { transform: scale(1); }
    45%  { transform: scale(1.28); }
    100% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
    .cart-qty-value.is-bumped { animation: none; }
}

/* Solid sage pill with lively tactile micro-press */
/* ============================================================
   ADD BUTTON

   In the app design the add control is a small filled square that floats
   over the bottom-right of the food photo, not a pill sitting in a row of
   text. It stays the same object on the detail screen and the deals rail.
   ============================================================ */

.btn-add {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    font-family: var(--font-body);
    font-size: 0;              /* the label is for screen readers; the glyph carries it */
    line-height: 0;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}

.btn-add::before {
    content: 'add';
    /* Ligature-selected, so the inherited uppercase from the button would turn
       'add' into 'ADD' and render the word instead of the glyph. */
    text-transform: none;
    letter-spacing: normal;
    font-family: 'Material Symbols Outlined';
    line-height: 1;
}


/* ============================================================
   MENU CARD

   Two-up grid of square-cropped photos on pure-white cards with a hairline
   and the lightest shadow — the app design's main feed. The previous
   horizontal row layout is gone: at two columns the photo does the work of
   identifying the dish, so the text below it is only name, price, rating.
   ============================================================ */

.menu-card {
    height: 100%;
}


/* The row wrapper survives from the old markup but no longer lays anything out
   sideways — the card is a column now. */
.menu-card-row {
    display: flex;
    flex-direction: column;
    height: 100%;
}

.menu-card-photo {
    position: relative;
    background: var(--color-surface-deep);
    overflow: hidden;
}


/* Generated illustrations are line art, not photography: contain them and sit them
   on the tonal fill rather than cropping them to the square. */
.menu-card-photo.is-illustration {
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--color-surface-food);
}

.menu-card-photo.is-illustration img {
    width: 62%;
    height: 62%;
    object-fit: contain;
}

.menu-card:hover .menu-card-photo.is-illustration img { transform: scale(1.06); }


.menu-card-name {
    margin: 0;
    color: var(--color-ink);
    /* One line, clipped — a long dish name must not push the price row out of
       alignment with the card beside it. */
    overflow: hidden;
    text-overflow: ellipsis;
}

.menu-card-price {
    display: flex;
    align-items: center;
    gap: 6px;
    line-height: var(--lh-label-lg);
    color: var(--color-primary);
}

/* The add control floats over the photo, but it cannot live inside it: the photo is a
   link to the dish page and a <form> nested in an <a> is invalid markup that browsers
   recover from unpredictably. So the two are siblings inside a positioned media wrapper
   — the button overlaps the image without ever being a descendant of the anchor. */

.menu-card-actions {
    z-index: 2;
}

/* An item that cannot be ordered shows no control at all; the reason is stated in the
   card body instead. Keeps a dead button off a sold-out card. */
.menu-card-actions:empty { display: none; }


/* ============================================================
   TODAY'S DEALS / SPECIALS (Foochi-style featured specials cards)
   ============================================================ */

.featured-scroll {
    padding: 6px 1rem 12px;
    scroll-snap-type: x mandatory;
}

.deal-card {
    transition: all 200ms cubic-bezier(0.16, 1, 0.3, 1);
    position: relative;
}

.deal-card-photo {
    display: flex;
    align-items: center;
    justify-content: center;
}
.deal-card-photo img {
    display: block;
}
.deal-card-photo.is-illustration {
    padding: 10px;
}
.deal-card-photo.is-illustration img {
    opacity: 1;
}
.deal-card:hover .deal-card-photo.is-illustration img {
    transform: none;
}


.deal-card-name {
    font-family: var(--font-display);
    display: -webkit-box;
    -webkit-line-clamp: 1;
    -webkit-box-orient: vertical;
}

.deal-card-price {
    font-family: var(--font-display);
}

.deal-card-add {
    min-width: 0;
    padding: 6px 12px;
}

/* ============================================================
   OUTLET PICKER (choose-your-canteen screen)
   ============================================================ */

.outlet-card {
    display: flex;
    align-items: center;
    gap: 16px;
    background: var(--color-surface-food);
    border: var(--border-w) solid var(--color-ink);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
    padding: 20px 22px;
    transition: all 180ms ease;
    color: inherit;
}
.outlet-card:hover {
    background: var(--color-surface-deep);
    color: inherit;
    transform: translateY(-2px);
    box-shadow: var(--shadow-md);
}
.outlet-card:active {
    transform: scale(0.98);
}

/* Cream disc on peach — the icon reads as a plate set on the surface */
.outlet-card-icon {
    width: 56px;
    height: 56px;
    border-radius: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--color-surface);
    color: var(--color-accent-strong);
    font-size: 1.6rem;
    flex-shrink: 0;
    box-shadow: none;
}

.outlet-card-name {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 1.15rem;
    color: var(--color-ink);
    margin: 0;
}

.outlet-card-hint {
    font-size: 0.84rem;
    color: var(--color-ink-muted);
    margin: 2px 0 0;
}

.outlet-card-chevron {
    margin-left: auto;
    color: var(--color-ink-muted);
    font-size: 1.3rem;
}

/* ============================================================
   STICKY CART BAR (Glassmorphism & glowing action pill)
   ============================================================ */

.sticky-cart-bar {
    right: 0;
    padding: 10px 16px;
    animation: slide-up 320ms cubic-bezier(0.34, 1.56, 0.64, 1);
}

@media (max-width: 767.98px) {
    .sticky-cart-bar {
        bottom: calc(var(--bottom-nav-height) + var(--safe-area-bottom) + 8px);
    }
}
@media (min-width: 768px) {
}

.sticky-cart-inner {
    max-width: 540px;
    margin: 0 auto;
    
    
    cursor: pointer;
    animation: cart-bar-glow 3s ease-in-out infinite alternate;
}

@keyframes cart-bar-glow {
    0% { box-shadow: 4px 4px 0 rgba(20, 16, 13, 0.35); }
    100% { box-shadow: 6px 6px 0 rgba(20, 16, 13, 0.35); }
}


.sticky-cart-inner:active {
    transform: scale(0.97);
}


.sticky-cart-info strong {
    font-size: 1.05rem;
}

.sticky-cart-action {
    color: #fff;
}

@keyframes slide-up {
    from { transform: translateY(100%); opacity: 0; }
    to   { transform: none; opacity: 1; }
}

/* ============================================================
   CART PAGE (card-based layout)
   ============================================================ */

/* ---------- Cart, ported from the Stitch "Your Order" comp ----------
   The comp introduces two things the page lacked: a named pickup block, so
   you can see which counter you are collecting from before paying, and
   small section labels that break a long scroll into "where" / "what" /
   "how much".

   The comp's address line and its Subtotal / Taxes & Fees rows are not
   here: Outlet has no address column and the app has no fee concept at all
   (Order carries a single totalAmount). Rendering either would mean
   inventing numbers, so the summary shows the one figure that is real.
   ============================================================ */

.pickup-card {
    margin-bottom: 1.75rem;
}
.pickup-card__icon {
    place-items: center;
    flex: 0 0 42px;
    font-size: 1.25rem;
}
.pickup-card__name {
    font-family: var(--font-display);
}

.cart-summary {
    margin-bottom: 1.25rem;
}
.cart-summary__row dt {
    font-family: var(--font-display);
    margin: 0;
}
.cart-summary__row dd {
    font-family: var(--font-display);
    margin: 0;
}


.cart-item-photo {
    flex-shrink: 0;
    box-shadow: inset 0 2px 4px rgba(0,0,0,0.06);
    display: flex;
    align-items: center;
    justify-content: center;
}


.cart-item-photo.is-illustration {
    padding: 8px;
}

.cart-item-info {
    flex: 1;
    min-width: 0;
}

.cart-item-name {
    font-family: var(--font-display);
}


.cart-item-subtotal {
    font-family: var(--font-display);
}

/* Sticky pay button at bottom of cart */
.sticky-pay-bar {
    
    
    border-top: var(--border-w) solid var(--color-ink);
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    box-shadow: var(--shadow-sticky);
    padding-bottom: calc(16px + var(--safe-area-bottom));
}

@media (max-width: 767.98px) {
    .sticky-pay-bar {
        bottom: calc(var(--bottom-nav-height) + var(--safe-area-bottom));
        padding-bottom: 14px;
    }
}


.sticky-pay-total {
    font-family: var(--font-display);
    font-weight: 800;
    font-size: 1.35rem;
    color: var(--color-accent-strong);
}

.sticky-pay-total small {
    display: block;
}

/* ============================================================
   ORDERS PAGE (rich cards instead of list-group)
   ============================================================ */

.order-card {
    margin-bottom: 14px;
    text-decoration: none;
}

.order-card:hover {
    background: var(--color-surface-deep);
}
.order-card:active {
    transform: scale(0.98);
}

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


.order-card-items {
    font-size: 0.88rem;
    color: var(--color-ink-muted);
    margin-bottom: 10px;
    line-height: 1.5;
}

.order-card-footer {
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.order-card-total {
    font-family: var(--font-display);
}


/* ---------- Order history, ported from the Stitch comp ----------
   The card previously ran its items as one truncated line, which on a
   three-item order showed "2 x Veg Biryani, 1 x Masala Ch…" and hid the
   rest. The comp lists them, which is both what a receipt does and the
   thing you actually scan for when finding an old order. */
.order-card-lines {
    list-style: none;
    margin: 0 0 12px;
    padding: 0;
    display: grid;
    gap: 2px;
}
.order-card-lines li {
    font-size: 0.88rem;
    color: var(--color-ink);
    line-height: 1.5;
}
.order-card-lines .qty {
    font-family: var(--font-display);
    font-weight: 700;
    color: var(--color-ink-muted);
    margin-right: 0.35rem;
}
.order-card-lines .more {
    font-size: 0.82rem;
    color: var(--color-ink-muted);
}

/* The whole card is the link, so this is an affordance rather than a
   second target: it tells you the card is clickable without adding a
   nested interactive element inside an anchor. */
.order-card-track {
    display: flex;
    align-items: center;
    gap: 0.3rem;
    margin-top: 12px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 0.85rem;
    color: var(--color-primary);
}
.order-card-track i { font-size: 1rem; transition: transform 180ms ease; }
.order-card:hover .order-card-track i { transform: translateX(3px); }


/* ============================================================
   ITEM DETAIL — ported from the Stitch "Dish Details" comp.

   The comp is mostly modifiers: Add Protein, Extras, Special
   Instructions, dietary tags, a favourite toggle. None of that exists
   on MenuItem, which carries name, category, photo, price and an
   optional discount and nothing else. So this page is the honest
   subset: a bigger photo, the price with its strike-through, where to
   collect it, and the add control. It is the right shape to hang
   modifiers on if they ever get modelled.
   ============================================================ */
/* The card name is now a link; it should not look like one until you
   reach for it, or every card in the grid turns into a wall of blue. */
.menu-card-name__link:hover,
.menu-card-name__link:focus-visible {
    color: var(--color-accent-strong);
    text-decoration: underline;
    text-underline-offset: 2px;
}

.detail-back {
    margin: 0.5rem 0 1rem;
    font-family: var(--font-display);
    text-decoration: none;
}
.detail-back i { font-size: 1.05rem; transition: transform 180ms ease; }
.detail-back:hover i { transform: translateX(-3px); }

.detail-hero img {display: block; }
/* Illustrations are line art on a transparent ground, so cropping them to
   fill would cut the drawing; they get padded and contained instead. */
.detail-hero.is-illustration img {padding: 12%; }

.detail-eyebrow {
    font-family: var(--font-display);
}
.detail-price {
    font-family: var(--font-display);
}
.detail-meta {
    font-weight: 600;
    margin-bottom: 1.75rem;
}
.detail-meta i { color: var(--color-primary); font-size: 1.05rem; }

.detail-unavailable {
    font-weight: 600;
}

.detail-add {
    display: flex;
    align-items: center;
    gap: 0.85rem;
    margin-bottom: 2.5rem;
}
.detail-add__btn {
    flex: 1;
    padding: 0.9rem 1rem;
    font-size: 1rem;
    border-radius: var(--radius-md);
}

/* ============================================================
   ACCOUNT PAGE (sectioned list)
   ============================================================ */

.account-section {
    background: transparent;
    margin-bottom: 24px;
    display: flex;
    flex-direction: column;
    gap: 3px;
}

.account-section-title {
    font-weight: 700;
    font-size: 0.8rem;
    color: var(--color-ink-muted);
    padding: 0 4px 8px;
}

.account-row {
    display: flex;
    align-items: center;
    gap: 14px;
    padding: 16px 20px;
    text-decoration: none;
    color: var(--color-ink);
    background: var(--color-surface-food);
    border-top: none;
    transition: background 120ms ease, transform 120ms ease;
    box-shadow: none;
}

/* Round only the outer corners of the stack so it reads as one grouped block */
.account-section-title + .account-row,
.account-section > .account-row:first-child {
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}
.account-section > .account-row:last-child {
    border-radius: 0 0 var(--radius-lg) var(--radius-lg);
}
.account-section > .account-row:only-of-type {
    border-radius: var(--radius-lg);
}

.account-row:hover {
    color: var(--color-ink);
}
.account-row:active {
    background: var(--color-surface-deep);
}

.account-row i {
    font-size: 1.35rem;
    color: var(--color-accent-strong);
    width: 28px;
    text-align: center;
}

.account-row-content {
    flex: 1;
}

.account-row-label {
    font-weight: 700;
    font-size: 0.95rem;
}

.account-row-detail {
    font-size: 0.82rem;
    color: var(--color-ink-muted);
}

.account-row-chevron {
    color: var(--color-ink-muted);
    font-size: 1rem;
}

/* ============================================================
   SEARCH BAR (menu page — Foochi pill search)
   ============================================================ */


.search-bar input:focus {
    box-shadow: none;
}


.search-bar i {
    position: absolute;
    left: 20px;
    top: 50%;
    transform: translateY(-50%);
    color: var(--color-ink-muted);
    font-size: 1.2rem;
    pointer-events: none;
}

/* ============================================================
   PRICE DISPLAY
   ============================================================ */

.card-price {
    font-family: var(--font-display);
    font-weight: 700;
}

.price-strike {
    opacity: 0.75;
}


.list-group-item {
    border-color: var(--color-border);
}
.list-group-item-action:hover {
    background: var(--color-primary-tint);
}

/* ============================================================
   INVOICE
   The bill on the page, not a PDF. Sized for a phone held at a
   counter, which is where it will actually be read.
   ============================================================ */

.invoice {
    background: var(--color-surface);
    border: var(--border-w) solid var(--color-ink);
    padding: 1rem 1.1rem;
}

.invoice__seller {
    display: flex;
    flex-direction: column;
    padding-bottom: 0.75rem;
    margin-bottom: 0.75rem;
    border-bottom: var(--border-w) dashed var(--color-ink-muted);
}

.invoice__seller-name {
    font-weight: 700;
}

.invoice__gstin {
    font-size: 0.75rem;
    color: var(--color-ink-muted);
    font-variant-numeric: tabular-nums;
}

.invoice__line,
.invoice__row {
    display: flex;
    justify-content: space-between;
    gap: 1rem;
    padding: 0.28rem 0;
    font-size: 0.92rem;
}

.invoice__line-amount,
.invoice__row > span:last-child {
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.invoice__qty {
    color: var(--color-ink-muted);
    font-size: 0.82rem;
    margin-left: 0.35rem;
}

.invoice__lines {
    padding-bottom: 0.5rem;
    margin-bottom: 0.5rem;
    border-bottom: var(--border-w) dashed var(--color-ink-muted);
}

.invoice__row--muted {
    color: var(--color-ink-muted);
    font-size: 0.82rem;
}

.invoice__row--discount {
    color: var(--color-success, #1d7a3c);
    font-weight: 600;
}

.invoice__tax {
    padding: 0.25rem 0 0.5rem;
}

.invoice__row--total {
    margin-top: 0.5rem;
    padding-top: 0.6rem;
    border-top: var(--border-w) solid var(--color-ink);
    font-weight: 700;
    font-size: 1rem;
}

/* The point of the soft launch: the number is legible, and plainly not being charged. */
.invoice__struck {
    color: var(--color-ink-muted);
    margin-right: 0.4rem;
}

.invoice__free {
    color: var(--color-success, #1d7a3c);
    letter-spacing: 0.03em;
}

.invoice__note,
.invoice__footer {
    font-size: 0.78rem;
    color: var(--color-ink-muted);
    margin: 0.5rem 0 0;
}

/* ============================================================
   DEVELOPER COFFEE DIALOG
   ============================================================ */

.tip-dialog {
    border: var(--border-w) solid var(--color-ink);
    background: var(--color-surface);
    padding: 1.5rem;
    max-width: 22rem;
    width: calc(100vw - 2.5rem);
}

.tip-dialog::backdrop {
    background: rgba(0, 0, 0, 0.45);
}

.tip-dialog__title {
    margin: 0 0 0.4rem;
    font-family: var(--font-display);
}

.tip-dialog__message {
    font-size: 0.88rem;
    color: var(--color-ink-muted);
    margin-bottom: 1rem;
}

.tip-dialog__amounts {
    display: flex;
    gap: 0.5rem;
    flex-wrap: wrap;
    margin-bottom: 0.75rem;
}

.tip-dialog__amount {
    flex: 1 1 4.5rem;
    font-variant-numeric: tabular-nums;
}

/* Declining is a full-width, plainly legible button, not a greyed-out corner. */
.tip-dialog__skip {
    display: block;
    width: 100%;
    text-align: center;
    color: var(--color-ink-muted);
}

/* ==== 04-auth.css ==== */
/* ============================================================
   04-auth.css — Sign in and registration

   The split-panel auth layout and its editorial field styling.

   Part of an ordered set linked in fragments/head.html. The browser applies these
   in link order and THAT ORDER IS THE CASCADE — a rule here can only override one
   in an earlier file. Do not reorder the links, and do not move a rule between
   files without checking what it then stops overriding.
   ============================================================ */

/* ============================================================
   AUTH PAGES — split panel on desktop, single column on mobile.

   A 420px card centred in ~1400px of cream left the whole right and left
   thirds doing nothing. The brief asked to use width intentionally at
   large sizes, so desktop gets a peach brand panel beside the form. The
   form itself sits directly on the canvas at every size now — no card —
   which also permanently removes the bug where an input painted the same
   colour as the card it sat on became invisible.
   ============================================================ */

.auth-shell {
    min-height: 100dvh;
    display: grid;
    grid-template-columns: 1fr;
    background: var(--color-bg);
}

/* align-items:center looks right until the content is taller than the viewport, at
   which point centring pushes the top of the card above the scroll origin and it
   cannot be reached. Auto margins centre the same way when there is room and
   collapse to zero when there is not, so nothing is ever stranded off-screen.

   The top padding carries the status-bar inset because these pages have no
   .app-chrome, which is what applies it everywhere else. Without it the first
   thing on the page draws under the clock in the Android shells, where
   targetSdk 36 forces edge-to-edge. */
.auth-main {
    display: flex;
    align-items: flex-start;
    justify-content: center;
    padding: calc(2.5rem + var(--safe-area-top)) 1.25rem 3rem;
}
.auth-main > .auth-card { margin-block: auto; }

/* The welcome screen runs its own hero to the screen edges and carries the inset
   itself, so it opts out of the page padding entirely. */
.auth-main--flush {
    padding: 0;
    align-items: stretch;
}

.auth-card {
    position: relative;
    z-index: 1;
    width: 100%;
    max-width: 400px;
}

/* Compact lockup above the form — carries the brand on mobile, where the
   brand panel is hidden. */
.auth-lockup {
    align-items: center;
    gap: 0.55rem;
}
.auth-lockup img { width: 34px; height: 34px; }

.auth-kicker {
    display: inline-block;
    font-family: var(--font-display);
    font-size: 0.74rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--color-accent-strong);
    background: var(--color-accent-tint);
    padding: 0.35rem 0.85rem;
    border-radius: var(--radius-pill);
    margin-bottom: 0.75rem;
}

/* ============================================================
   EDITORIAL AUTH FIELDS — ported from the Stitch "Sign In" comp.

   The comp's one real idea is that form fields stop being boxes.
   Bootstrap's .input-group draws an icon tile, a 1px border and a white
   fill around every field: three pieces of chrome on a page that only
   asks two questions. The comp replaces all of it with a single hairline
   under the text, so the screen reads as type on cream rather than
   widgets on a card.

   The field icons come off entirely. An envelope glyph beside a field
   labelled "Email" is decoration, and it was the thing forcing the boxed
   treatment in the first place.

   Scoped to .auth-card so the canteen and admin portals — same
   stylesheet, deliberately not part of this redesign — keep their dense
   bordered forms.
   ============================================================ */

/* 0.55 dropped the placeholder to 2.42:1. Every field here already has a
   visible label, so the placeholder is supplementary — but it is still text,
   and 1.4.3 does not exempt it. 0.85 resolves to #806E5D at 4.51:1. */
.field__input::placeholder {opacity: 0.85; }
/* Focus is carried by the rule itself rather than a ring, to keep the
   field flat. Thickening to 2px means it still reads for anyone who
   can't rely on the hue change alone; the padding claws back the 0.5px
   so the baseline doesn't hop. */
/* Chrome paints autofilled inputs pale blue, which on a transparent
   field shows up as a floating blue slab. */
.field__input:-webkit-autofill,
.field__input:-webkit-autofill:focus {
    -webkit-text-fill-color: var(--color-ink);
    -webkit-box-shadow: 0 0 0 100px var(--color-bg) inset;
}
.field__input--pw { padding-right: 2.75rem; }
.field__toggle {
    line-height: 1;
}
.field__toggle:hover, .field__toggle:focus-visible { color: var(--color-ink); }

/* Select gets the same underline treatment. Bootstrap's .form-select paints
   its own border, fill and arrow, so all three are stripped and the chevron
   is redrawn inline in --color-ink-muted to match the field text. */
.field__select {
    width: 100%;
    background-color: transparent;
    border: 0;
    border-bottom: 1.5px solid var(--color-field-rule);
    border-radius: 0;
    padding: 0.5rem 1.75rem 0.5rem 0;
    font-family: var(--font-body);
    font-size: 1.05rem;
    color: var(--color-ink);
    appearance: none;
    -webkit-appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%235c5247' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 0.15rem center;
    background-size: 1rem;
    transition: border-color 0.18s ease;
}
.field__select:focus {
    outline: none;
    box-shadow: none;
    border-bottom-color: var(--color-primary);
    border-bottom-width: 2px;
    padding-bottom: calc(0.5rem - 0.5px);
}
/* The "choose one" placeholder option should read as unfilled, like a
   placeholder does, rather than as a real selection. */
.field__select:has(option[value=""]:checked) { color: var(--color-ink-muted); }

/* "(Optional)" sits with the label rather than in the field, so the field
   itself stays clean. Lowercase against the uppercase label so it reads as
   an aside and not part of the field name. */
.field__optional {
    text-transform: none;
    letter-spacing: 0;
    font-weight: 500;
    font-size: 0.72rem;
    opacity: 0.75;
    margin-left: 0.4rem;
}

.field__hint {
    margin: 0.4rem 0 0;
    font-size: 0.8rem;
    color: var(--color-ink-muted);
}

/* Validation copy. --color-danger is 5.11:1 on cream, so it carries as text
   without a tinted box behind it. */
.field__error {
    margin: 0.4rem 0 0;
    font-size: 0.82rem;
    font-weight: 600;
    color: var(--color-danger);
}
/* A field that failed validation says so on the rule too, not by colour
   alone — the message below it is the non-colour cue. */
.field__input.is-invalid,
.field__select.is-invalid {
    border-bottom-color: var(--color-danger);
    border-bottom-width: 2px;
    padding-bottom: calc(0.5rem - 0.5px);
}

/* The comp runs a ~56px CTA. That height is what makes a two-field form
   feel finished rather than cramped. */

.auth-legal {
    margin-top: 0.85rem;
    text-align: center;
    font-size: 0.78rem;
}


/* ---------- Brand panel (desktop only) ---------- */

.auth-brand { display: none; }

@media (min-width: 992px) {
    .auth-shell { grid-template-columns: 1.05fr 1fr; }

    .auth-brand {
        display: flex;
        align-items: center;
        padding: 4rem 3.5rem;
        background: var(--color-surface-food);
        position: relative;
        overflow: hidden;
    }

    /* Oversized brand mark bled off the corner — the identity as pattern
       rather than a logo sitting politely in a box. */
    .auth-brand::after {
        content: '';
        position: absolute;
        right: -14%;
        bottom: -18%;
        width: 62%;
        aspect-ratio: 1;
        background: url('/img/logo-mark-pastel.png') center / contain no-repeat;
        opacity: 0.32;
        pointer-events: none;
    }

}

.auth-brand__inner { position: relative; z-index: 1; max-width: 420px; }

.auth-brand__lockup {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    margin-bottom: 2.5rem;
}
.auth-brand__lockup img { width: 40px; height: 40px; }
.auth-brand__lockup span {
    font-family: var(--font-display);
    font-weight: 800;
    font-size: 1.55rem;
    color: var(--color-accent-strong);
}

.auth-brand__headline {
    font-family: var(--font-display);
    font-weight: 800;
    font-size: 2.6rem;
    line-height: 1.12;
    color: var(--color-ink);
    margin: 0 0 1rem;
}

.auth-brand__sub {
    font-size: 1.02rem;
    margin: 0 0 2.5rem;
    max-width: 34ch;
}

/* Three dishes on the brand panel, so the first screen of a food app
   actually shows food. */
.auth-brand__dishes { display: flex; gap: 1.5rem; align-items: center; }

/* No tile behind the art. The boxed treatment came from the old peach panel,
   where the illustrations needed a surface to sit on; against the flat
   terracotta they read better free-standing, and dropping the 12px padding
   with it gives the artwork the whole box. */
.auth-brand__dish {
    width: 136px;
    height: 136px;
    display: grid;
    place-items: center;
}
/* Size comes from --dish-size, set per dish below. Applied to BOTH axes so the
   box scales squarely and object-fit: contain shrinks the artwork with it —
   which is what makes the knob work on every image whatever its shape. */
.auth-brand__dish img {
    width: var(--dish-size, 100%);
    height: var(--dish-size, 100%);
    object-fit: contain;
}

/* The middle tile used to be lifted 14px as a staggered flourish, which suited the old
   peach panel. On the flat one it just reads as a tile that failed to line up, so the
   row is level now. */

/* ---- Dish sizing. These three values are the knobs. ----------------------
   Percentage of the tile's 68px content box. Lower = smaller.

   Scales the box on both axes, so it works on every image whatever its shape.
   max-height did not: it is inert on a width-limited image, so it moved the
   chai and did nothing at all to the dosa or the puff.

     image     source     shape       limited by
     -------   ---------  ----------  ----------
     dosa      320x215    landscape   width
     puff      320x193    landscape   width

   Both remaining illustrations are landscape and so width-limited, which is why
   they sit at 100%: the box is square and their height is the short axis. The
   chai used to be the exception — the only portrait artwork, the only one whose
   height filled the box — and it has been removed.

   Tagged per dish rather than by :nth-child, so each value stays attached to the
   artwork it was measured against if the order ever changes. */
.auth-brand__dish--dosa { --dish-size: 100%; }
.auth-brand__dish--puff { --dish-size: 100%; }

/* ---------- Password visibility toggle ---------- */

/* This used to be the right-hand cap of a Bootstrap .input-group: a filled tile with a
   border on three sides, `border-left: none` so it butted against the field, sharing one
   edge with it. That group is long gone — the field is now a single bordered box and the
   button is positioned inside it — so the leftover fill and border rendered as an opaque
   near-white tile sitting on top of the field's own right border, reading as a control
   from a different interface.

   The button is an affordance inside the field, so it carries no surface of its own. The
   !important flags went with the input-group too: nothing needs overriding here now, and
   leaving them meant this block quietly beat every later attempt to restyle it. */
.toggle-password {
    border: none;
    background: transparent;
    color: var(--color-ink-muted);
}
.toggle-password:hover {
    color: var(--color-ink);
    background: transparent;
}
.toggle-password:focus-visible {
    z-index: 3;
}

/* ==== 05-shared.css ==== */
/* ============================================================
   05-shared.css — Cross-portal state, and outlet operations

   Pieces every portal draws on — the order queue, state medallions, empty states,
   entry animations, photo widgets and toasts — followed by the outlet's operational
   controls: daily caps, the pause switch and danger zones.

   Part of an ordered set linked in fragments/head.html. The browser applies these
   in link order and THAT ORDER IS THE CASCADE — a rule here can only override one
   in an earlier file. Do not reorder the links, and do not move a rule between
   files without checking what it then stops overriding.
   ============================================================ */

/* ============================================================
   04-shared.css  (4 of 6)

   Cross-portal UI: state medallions, empty states, icons, entry animations,
   toast, page headers.

   Two outlet-only blocks also live in this range — the queue, and the menu
   photo grid with its upload widget. They are not here because they belong
   here; they are here because they sit at these line numbers in the original,
   and lifting them out would reorder the cascade.

   Part of an ordered set. The browser concatenates these in the order they
   are linked in fragments/head.html, and that order IS the cascade. Do not
   reorder the <link> tags, and do not move a rule between files without
   checking what it would then stop overriding.
   ============================================================ */

/* ============================================================
   CANTEEN QUEUE
   ============================================================ */

.live-dot {
    width: 9px;
    height: 9px;
    border-radius: 0;
    background: var(--color-success);
    display: inline-block;
    box-shadow: 0 0 0 rgba(20, 16, 13, 0.35);
    animation: live-pulse 2s infinite;
}

@keyframes live-pulse {
    0%   { box-shadow: 0 0 0 0 rgba(20, 16, 13, 0.35); }
    70%  { box-shadow: 0 0 0 8px rgba(20, 16, 13, 0.35); }
    100% { box-shadow: 0 0 0 0 rgba(20, 16, 13, 0.35); }
}

/* Brand fallback mark (shown only if the Bito image fails to load) — terracotta,
   since it stands in for the logo rather than for an action. */
.auth-mark {
    width: 48px;
    height: 48px;
    margin: 0 auto 1rem;
    border-radius: 0;
    background: var(--color-accent-strong);
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 1.35rem;
}

/* ============================================================
   BRAND MARK & STATE MEDALLIONS

   Bito the mascot is gone. The identity is now an abstract six-ray
   swirl — pastel at display sizes, solid terracotta wherever it has to
   survive small (favicon, navbar, 16px).

   A mark can't perform "order ready" the way a character could, so every
   empty/error/status moment is a medallion instead: the pastel swirl as
   a soft backdrop, a cream disc on top, one icon carrying the meaning
   and a tone carrying the mood. One shape, many states.
   ============================================================ */

.brand-mark {
    flex-shrink: 0;
}

.state-medallion {
    position: relative;
    display: grid;
    place-items: center;
    animation: medallion-arrive 420ms cubic-bezier(0.34, 1.4, 0.64, 1) 120ms both;
}

/* The swirl sits behind at reduced opacity so it reads as a warm halo
   rather than competing with the icon in front of it. */
.state-medallion::before {
    content: '';
    position: absolute;
    inset: 0;
    background: url('/img/logo-mark-pastel.png') center / contain no-repeat;
    opacity: 0.55;
}

.state-medallion__disc {
    position: relative;
}

.state-medallion__disc i {
    font-size: 2.1rem;
    line-height: 1;
    color: var(--color-accent-strong);
}

.state-medallion--good .state-medallion__disc i { color: var(--color-primary-dark); }
.state-medallion--bad  .state-medallion__disc i { color: var(--color-danger); }

/* Sad/error moments drop the spring overshoot — the same bouncy landing
   that reads as delighted is tone-deaf on a cancelled order. */
.state-medallion--bad {
    animation: medallion-arrive-somber 380ms cubic-bezier(0.22, 1, 0.36, 1) 120ms both;
}

.state-medallion--sm { width: 96px; height: 96px; }
.state-medallion--sm .state-medallion__disc { width: 54px; height: 54px; }
.state-medallion--sm .state-medallion__disc i { font-size: 1.5rem; }

@keyframes medallion-arrive {
    from { opacity: 0; transform: translateY(10px) scale(0.94); }
    to   { opacity: 1; transform: none scale(1); }
}

@keyframes medallion-arrive-somber {
    from { opacity: 0; transform: translateY(8px); }
    to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
    .state-medallion, .state-medallion--bad { animation: none; }
}

/* ============================================================
   EMPTY / ERROR STATES
   ============================================================ */

.state-panel {
    max-width: 440px;
    margin: 0 auto;
}

.state-panel h4 {
    font-size: 1.4rem;
}

.state-panel p {
    font-size: 0.96rem;
    margin-bottom: 1.5rem;
}

/* ============================================================
   ICONS (Phosphor)
   ============================================================ */

[class^="ph-"], [class*=" ph-"] {
    vertical-align: -0.15em;
    line-height: 1;
}

.btn i[class^="ph-"], .btn i[class*=" ph-"],
.nav-link i[class^="ph-"], .nav-link i[class*=" ph-"] {
    margin-right: 0.4em;
    font-size: 1.05em;
}

.icon-circle {
    background: var(--color-accent-tint);
    color: var(--color-accent-strong);
    font-size: 1.2rem;
    flex-shrink: 0;
}

/* ============================================================
   ENTRY ANIMATIONS
   ============================================================ */

/* The end state is `transform: none`, not `translateY(0)`.
   They look identical, but they are not equivalent: these animations run with
   `fill-mode: both`, so whatever the last keyframe says stays applied for ever. Ending on
   translateY(0) leaves a computed `matrix(1,0,0,1,0,0)` on every direct child of
   .container — and *any* transform other than `none` makes that element the containing
   block for its position:fixed descendants, and a scroll container boundary for its
   sticky ones.

   That one line was breaking two things: the cart bar was being positioned against a div
   2,000px down the page instead of the viewport (it came to rest on top of the tab bar),
   and the sticky category strip on the menu never stuck at all. Ending on `none` removes
   the containing block once the animation finishes and fixes both at the source. */
@keyframes fade-up {
    from { opacity: 0; transform: translateY(10px); }
    to   { opacity: 1; transform: none; }
}

/* `backwards`, not `both`.
   `both` keeps the animation filling for ever after it ends, and a filling transform
   animation is still an *animated* transform — Chrome resolves even `transform: none`
   to the identity matrix while the fill is active. Any transform other than `none`
   makes the element a containing block for its position:fixed descendants and a
   boundary for its sticky ones, so every direct child of .container was silently
   trapping whatever it contained. `backwards` applies the opening frame before the
   animation starts and then releases the property, which looks identical and leaves
   the element transform-free once it has landed. */
.container > *,
.auth-card {
    animation: fade-up 380ms cubic-bezier(0.16, 1, 0.3, 1) backwards;
}

.row.g-3 > .col,
.row.row-cols-1 > .col,
.row.row-cols-md-2 > .col,
.row.row-cols-md-3 > .col {
    animation: fade-up 420ms cubic-bezier(0.16, 1, 0.3, 1) backwards;
}
.row > .col:nth-child(2) { animation-delay: 40ms; }
.row > .col:nth-child(3) { animation-delay: 80ms; }
.row > .col:nth-child(4) { animation-delay: 120ms; }
.row > .col:nth-child(5) { animation-delay: 160ms; }
.row > .col:nth-child(n+6) { animation-delay: 200ms; }

/* Staggered menu cards */
.menu-card { animation: fade-up 400ms cubic-bezier(0.16, 1, 0.3, 1) backwards; }
.menu-card:nth-child(2) { animation-delay: 50ms; }
.menu-card:nth-child(3) { animation-delay: 100ms; }
.menu-card:nth-child(4) { animation-delay: 150ms; }
.menu-card:nth-child(5) { animation-delay: 200ms; }
.menu-card:nth-child(n+6) { animation-delay: 250ms; }

@media (prefers-reduced-motion: reduce) {
    .container > *, .auth-card, .row > .col, .menu-card { animation: none; }
}

/* ============================================================
   CARD HOVER LIFT
   ============================================================ */

.card.hoverable {
    cursor: pointer;
}
.card.hoverable:hover {
    transform: translateY(-3px);
    border-color: var(--color-primary-tint);
}
.card.hoverable:active {
    transform: scale(0.98);
}

/* ============================================================
   MENU ITEM PHOTOS (legacy — kept for canteen management grid)
   ============================================================ */

.menu-photo-frame {
    position: relative;
    aspect-ratio: 4 / 3;
    overflow: hidden;
    background: var(--color-surface-deep);
    border-radius: var(--radius-md) var(--radius-md) 0 0;
}

.menu-photo-frame img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    transition: transform 300ms ease;
}

.card:hover .menu-photo-frame img {
    transform: scale(1.04);
}

.menu-photo-frame.is-placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
}

.menu-photo-frame.menu-photo-inline {
    width: 92px;
    aspect-ratio: 1;
    border-radius: var(--radius-md) 0 0 var(--radius-md);
    flex-shrink: 0;
}

.menu-photo-frame.is-placeholder img {
    width: 46%;
    height: 46%;
    object-fit: contain;
    opacity: 0.9;
    transition: none;
}

.menu-photo-frame .badge-unavailable {
    position: absolute;
    inset: 0;
    background: rgba(31, 27, 22, 0.55);
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 0.85rem;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}

/* ============================================================
   PHOTO UPLOAD WIDGET (canteen menu item form)
   ============================================================ */

.photo-dropzone {
    position: relative;
    border: 1.5px dashed var(--color-border);
    border-radius: var(--radius-md);
    padding: 1.25rem;
    display: flex;
    align-items: center;
    gap: 1rem;
    cursor: pointer;
    transition: border-color 150ms ease, background-color 150ms ease;
    background: var(--color-field);
}
.photo-dropzone:hover, .photo-dropzone.is-dragover {
    border-color: var(--color-primary);
    background: var(--color-primary-tint);
}
.photo-dropzone input[type="file"] {
    position: absolute;
    inset: 0;
    opacity: 0;
    cursor: pointer;
}

.photo-dropzone-preview {
    width: 64px;
    height: 64px;
    border-radius: var(--radius-sm);
    overflow: hidden;
    flex-shrink: 0;
    background: var(--color-surface-deep);
    display: flex;
    align-items: center;
    justify-content: center;
}
.photo-dropzone-preview img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}
.photo-dropzone-preview img.is-placeholder-icon {
    width: 60%;
    height: 60%;
    object-fit: contain;
    opacity: 0.85;
}

.photo-dropzone-text strong {
    display: block;
    font-size: 0.9rem;
    color: var(--color-ink);
}
.photo-dropzone-text span {
    font-size: 0.8rem;
    color: var(--color-ink-muted);
}

/* ============================================================
   TOAST NOTIFICATION (added to cart, etc.)
   ============================================================ */

.toast-container {
    position: fixed;
    bottom: 90px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 1100;
    pointer-events: none;
}

@media (max-width: 767.98px) {
    .toast-container {
        bottom: calc(var(--bottom-nav-height) + var(--safe-area-bottom) + 20px);
    }
}

.toast-msg {
    background: var(--color-ink);
    color: #fff;
    padding: 12px 24px;
    border-radius: var(--radius-pill);
    font-size: 0.9rem;
    font-weight: 700;
    font-family: var(--font-display);
    white-space: nowrap;
    box-shadow: var(--shadow-md);
    display: flex;
    align-items: center;
    gap: 8px;
    animation: toast-in 320ms cubic-bezier(0.34, 1.56, 0.64, 1), toast-out 300ms ease 2.2s forwards;
    pointer-events: auto;
}

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

@keyframes toast-out {
    from { opacity: 1; transform: translateY(0); }
    to   { opacity: 0; transform: translateY(-12px); }
}

/* ============================================================
   PAGE SECTION HEADERS & FOOCHI WAVE DECOR
   ============================================================ */

.page-header {
    margin-bottom: 24px;
}

.page-header-decor {
    position: absolute;
    top: -20px;
    right: -20px;
    width: 220px;
    height: 140px;
    background-image: url('/img/blob-sage-1.svg');
    background-size: contain;
    background-repeat: no-repeat;
    opacity: 0.8;
    pointer-events: none;
    z-index: 0;
}

.page-header-content {
    position: relative;
    z-index: 1;
}

.page-header-label {
    font-family: var(--font-display);
    font-size: 1.05rem;
    font-weight: 700;
    color: var(--color-accent-strong);
    margin-bottom: 2px;
    letter-spacing: 0.01em;
}


@media (min-width: 768px) {
}

/* Navbar with scroll shadow */
.navbar.has-scrolled {
    box-shadow: none;
}

/* Category header section accent */
.menu-category-section h5 {
    color: var(--color-ink);
}

/* ============================================================
   05-outlet.css  (5 of 6)

   Outlet and admin operational controls: per-item daily caps, the pause switch,
   and the delete danger zones.

   Part of an ordered set. The browser concatenates these in the order they
   are linked in fragments/head.html, and that order IS the cascade. Do not
   reorder the <link> tags, and do not move a rule between files without
   checking what it would then stop overriding.
   ============================================================ */

/* ============================================================
   OPERATIONAL CONTROLS — daily caps, outlet pause, danger zones.

   These belong to the outlet and admin portals, which share this
   stylesheet with the student app but deliberately keep the denser,
   flatter treatment (see the --color-surface note at the top). The
   student-facing piece here is .stock-note, which is why that one
   alone is built as a soft pill rather than a table-ish row.
   ============================================================ */

/* --- Per-item daily cap, on the outlet's own menu screen --- */
.daily-limit-row {
    margin-top: 0.75rem;
}

.daily-limit-bar {
    height: 6px;
    border-radius: 0;
    background: var(--color-field);
    overflow: hidden;
}

.daily-limit-bar > span {
    display: block;
    height: 100%;
    border-radius: 0;
    background: var(--color-primary);
    transition: width 200ms ease;
}

.daily-limit-text {
    display: block;
    margin-top: 0.35rem;
    font-size: 0.78rem;
    font-weight: 600;
    color: var(--color-ink-muted);
}

.daily-limit-text.is-low { color: var(--color-warning); }
.daily-limit-text.is-out { color: var(--color-danger); }

/* --- "Taking orders / paused" switch on the queue screen --- */
.outlet-switch {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    padding: 0.85rem 1.1rem;
    margin-bottom: 1.25rem;
    border-radius: var(--radius-sm);
    background: var(--color-primary-tint);
    /* Left rule rather than a full border: the state is the point, and a coloured
       edge reads at a glance from across a kitchen without shouting on the page. */
    border-left: 4px solid var(--color-primary);
}

.outlet-switch.is-paused {
    background: var(--color-warning-tint);
    border-left-color: var(--color-warning);
}

.outlet-switch__text strong {
    display: block;
    font-family: var(--font-display);
    font-size: 1.02rem;
    color: var(--color-ink);
}

.outlet-switch__text span {
    font-size: 0.85rem;
    color: var(--color-ink-muted);
}

/* --- Disclosure panels: cancel-with-reason, and outlet deletion --- */
.cancel-panel,
.danger-zone {
    margin-top: 0.5rem;
}

.cancel-panel > summary,
.danger-zone > summary {
    cursor: pointer;
    list-style: none;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.4rem;
    padding: 0.4rem 0.7rem;
    border-radius: var(--radius-sm);
    border: 1px solid var(--color-danger);
    color: var(--color-danger);
    font-size: 0.85rem;
    font-weight: 600;
    background: transparent;
    transition: background-color 150ms ease;
}

/* Safari still paints its own disclosure triangle without this. */
.cancel-panel > summary::-webkit-details-marker,
.danger-zone > summary::-webkit-details-marker { display: none; }

.cancel-panel > summary:hover,
.danger-zone > summary:hover { background: var(--color-danger-tint); }

.cancel-panel[open] > summary,
.danger-zone[open] > summary { margin-bottom: 0.6rem; }

.cancel-panel form,
.danger-zone form {
    padding: 0.75rem;
    border-radius: var(--radius-sm);
    background: var(--color-danger-tint);
}

.danger-zone__warning {
    font-size: 0.82rem;
    line-height: 1.5;
    color: var(--color-ink-muted);
    margin-bottom: 0.6rem;
}

/* --- One canteen's row in the admin's tenant screen --- */
.outlet-admin-card {
    padding: 0.9rem;
    margin-bottom: 0.85rem;
    border-radius: var(--radius-sm);
    border: 1px solid var(--color-border);
    background: var(--color-surface);
}

.outlet-admin-card__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    margin-bottom: 0.6rem;
}

.outlet-admin-card__name {
    font-family: var(--font-display);
    font-weight: 700;
    color: var(--color-ink);
}

/* --- Student-facing stock notes on a menu card --- */


/* --- Why an order in the history list was cancelled --- */
.order-card-reason {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    margin-top: 0.5rem;
}

/* ============================================================
   SEPARATOR RULE

   `hr` was never styled here, so it inherited Bootstrap's 1px currentColor at 25%
   opacity — which, on ink text, is a mid-grey line of no particular weight sitting
   directly under whatever it followed. Where that was a bordered table or card, the
   two lines stacked into the "double rule" the console screens were showing.

   One quiet rule, and a modifier for the case where a section genuinely needs a
   harder break.
   ============================================================ */
hr {
    border: 0;
    border-top: var(--rule-w) solid var(--rule-color);
    opacity: 1;
    margin: 1.25rem 0;
}

hr.rule-strong {
    border-top-color: var(--rule-color-strong);
}


/* ============================================================
   OFFLINE BAR

   The service worker serves cached pages when the network drops, so the app keeps
   looking usable — and then a checkout POST fails, because a POST is network-only and
   queueing one would risk charging someone twice. This says so before the tap rather
   than after the failure.
   ============================================================ */
.offline-bar {
    display: none;
    position: sticky;
    top: 0;
    z-index: 1080;
    padding: 0.55rem 1rem;
    background: var(--color-ink);
    color: var(--color-bg);
    font-size: 0.85rem;
    text-align: center;
}

.offline-bar.is-visible {
    display: block;
}

/* ============================================================
   SELECT — the in-house dropdown that replaces the platform one.

   A native <select> draws its open list in the OS, not the page, so every
   picker in this app fell out of its own design language the moment it was
   tapped: Material on Android, a wheel on iOS. This is the same control drawn
   in bone, ink and a 2px rule like everything else.

   The native element is still in the DOM doing the actual work. It is clipped
   rather than display:none because Chrome will not surface a validation
   message for a required field it cannot focus. See initSelects in app.js.
   ============================================================ */

.select {
    position: relative;
    display: block;
    width: 100%;
}

/* Kept rendered, so constraint validation can still focus and report on it, but
   given no size, no paint and no pointer events. */
.select__native {
    position: absolute;
    left: 0;
    bottom: 0;
    width: 1px;
    height: 1px;
    opacity: 0;
    pointer-events: none;
    border: 0;
    padding: 0;
    margin: 0;
}

.select__trigger {
    width: 100%;
    display: flex;
    align-items: center;
    gap: var(--stack-xs);
    text-align: left;
    background: var(--color-field);
    color: var(--color-ink);
    border: 2px solid var(--color-field-border);
    border-radius: var(--radius-sm);
    padding: 0.5rem 2.25rem 0.5rem 0.75rem;
    font-family: var(--font-body);
    font-size: var(--text-body-md);
    line-height: var(--lh-body-md);
    font-weight: 600;
    cursor: pointer;
    position: relative;
}

/* Chevron drawn with borders rather than an SVG or an icon-font glyph: the icon
   font is subset per-glyph, and a shape this small is two lines. */
.select__trigger::after {
    content: '';
    position: absolute;
    right: 0.85rem;
    top: 50%;
    width: 0.45rem;
    height: 0.45rem;
    border-right: 2px solid var(--color-ink);
    border-bottom: 2px solid var(--color-ink);
    transform: translateY(-70%) rotate(45deg);
    transition: transform 120ms ease;
}

.select.is-open .select__trigger::after { transform: translateY(-20%) rotate(-135deg); }

.select__trigger:focus-visible {
    outline: none;
    border-color: var(--color-primary);
    box-shadow: var(--shadow-sm);
}

.select__trigger:disabled {
    opacity: 0.55;
    cursor: not-allowed;
}

/* The "choose one" row should read as unfilled, the way a placeholder does,
   rather than as a real choice already made. */
.select--placeholder .select__value { color: var(--color-ink-muted); font-weight: 500; }

.select__value {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.select__list {
    position: absolute;
    z-index: 60;
    top: calc(100% - 2px);
    left: 0;
    right: 0;
    margin: 0;
    padding: 0;
    list-style: none;
    max-height: 15rem;
    overflow-y: auto;
    background: var(--color-field);
    border: 2px solid var(--color-border);
    box-shadow: var(--shadow-sm);
}

/* Flipped when there is no room below — a filter at the bottom of a phone screen
   would otherwise open into nothing. */
.select--up .select__list {
    top: auto;
    bottom: calc(100% - 2px);
}

.select__option {
    padding: 0.55rem 0.75rem;
    font-size: var(--text-body-md);
    line-height: var(--lh-body-md);
    cursor: pointer;
    border-bottom: 1px solid rgba(20, 16, 13, 0.12);
}

.select__option:last-child { border-bottom: 0; }
.select__option:hover { background: var(--color-bg); }

.select__option[aria-selected="true"] {
    background: var(--color-ink);
    color: var(--color-field);
    font-weight: 700;
}

.select__option[aria-disabled="true"] {
    opacity: 0.45;
    cursor: not-allowed;
}

/* Matches .form-select-sm, which several filter bars use. */
.select--sm .select__trigger {
    padding: 0.3rem 2rem 0.3rem 0.55rem;
    font-size: var(--text-label-sm, 12px);
}
.select--sm .select__option { padding: 0.4rem 0.55rem; }

/* ============================================================
   LOADING STATE
   Three things, all of them about the same problem: this app's
   users are on campus mobile data, and until now nothing in the
   product acknowledged a wait. A tap produced no response until
   the next page painted, which is how you get people tapping
   "Proceed to Pay" twice.
   ============================================================ */

/* Full-page navigation. Deliberately a thin bar rather than an overlay: it says
   "something is happening" without taking the screen away from what is still readable. */
.route-progress {
    position: fixed;
    top: 0;
    left: 0;
    height: 3px;
    width: 0;
    background: var(--color-primary);
    z-index: 3000;
    pointer-events: none;
    opacity: 0;
    transition: width 240ms ease-out, opacity 200ms ease-out;
}

.route-progress.is-active {
    opacity: 1;
}

/* A button that has been pressed and is waiting on the server.
   currentColor is used for the spinner so this works on every button variant —
   primary, outline, danger — without a rule per variant. */
.btn.is-busy {
    pointer-events: none;
    cursor: progress;
}

.btn.is-busy::before {
    content: '';
    display: inline-block;
    flex: none;
    width: 1em;
    height: 1em;
    margin-right: 0.45em;
    vertical-align: -0.15em;
    border: 2px solid currentColor;
    border-right-color: transparent;
    border-radius: 50%;
    animation: bitesite-spin 620ms linear infinite;
}

/* The button's own icon steps aside for the spinner rather than sitting beside it,
   so a busy "Save" does not read as two icons competing. */
.btn.is-busy .material-symbols-outlined {
    display: none;
}

/* Flex buttons already space their children; the margin above would double up. */
.btn.is-busy.d-flex::before,
.btn.is-busy.d-inline-flex::before {
    margin-right: 0;
}

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

/* Menu photos arrive after the HTML does. Without this the card holds a blank
   rectangle that then pops — the shimmer says the space is spoken for. */
.img-loading {
    background-color: var(--color-surface-deep);
    background-image: linear-gradient(
        100deg,
        transparent 20%,
        rgba(255, 255, 255, 0.55) 50%,
        transparent 80%);
    background-size: 220% 100%;
    animation: bitesite-shimmer 1.4s ease-in-out infinite;
}

@keyframes bitesite-shimmer {
    from { background-position: 140% 0; }
    to { background-position: -40% 0; }
}

/* Motion is the decoration here, not the information: the spinner still reads as a
   ring, the bar still fills, the shimmer becomes a plain tint. Nothing is lost. */
@media (prefers-reduced-motion: reduce) {
    .btn.is-busy::before {
        animation-duration: 2s;
    }

    .route-progress {
        transition: opacity 200ms ease-out;
    }

    .img-loading {
        background-image: none;
        animation: none;
    }
}

/* ==== 06-editorial.css ==== */
/* ============================================================
   06-editorial.css — Editorial customer screens

   The Stitch editorial comps for the customer app, plus the responsive and
   interaction refinements layered on top of them.

   Part of an ordered set linked in fragments/head.html. The browser applies these
   in link order and THAT ORDER IS THE CASCADE — a rule here can only override one
   in an earlier file. Do not reorder the links, and do not move a rule between
   files without checking what it then stops overriding.
   ============================================================ */

/* ============================================================
   06-app-editorial.css  (6 of 6)

   Customer app, Stitch Editorial layer.

   Loads last on purpose. It restates several components defined in 03-app.css
   and depends on winning the cascade against them, so its position is
   load-bearing rather than incidental.

   Part of an ordered set. The browser concatenates these in the order they
   are linked in fragments/head.html, and that order IS the cascade. Do not
   reorder the <link> tags, and do not move a rule between files without
   checking what it would then stop overriding.
   ============================================================ */

/* ============================================================================
   CUSTOMER APP — Stitch "Editorial" screens
   ============================================================================

   Everything below re-cuts the student and auth surfaces to the app design in
   Stitch. It sits last on purpose: the outlet and admin portals share this
   stylesheet and are not part of this redesign, so their rules are left intact
   above and only the customer-app components are restated here.

   Three things change shape rather than just colour, and they are the reason
   this is a rewrite and not a re-skin:

     - the food feed goes from horizontal rows to a two-up grid of square crops
     - the category row goes from illustrated pucks to a plain underlined tab bar
     - auth inputs lose their boxes and become ruled lines

   The old system's decorative layer — organic blobs, grain texture, the swirl
   behind the state medallion — has no counterpart in the design and is removed
   rather than re-tinted. On a near-white canvas it read as smudge.
   ============================================================================ */

/* ---------- Page headers ---------- */

/* The blob sat behind the greeting on a cream page. Gone: this design carries
   hierarchy with type size alone. */


.page-header-title {
    line-height: var(--lh-display-lg);
    margin: 0;
}

@media (min-width: 768px) {
    .page-header-title { font-size: var(--text-display-lg); }
}

.page-header-sub {
    margin: var(--stack-xs) 0 0;
    line-height: var(--lh-body-md);
}

/* Uppercase micro-label above each block — the design's main structural device
   on the cart and order screens. */
.section-label {
    font-family: var(--font-body);
    font-size: var(--text-label-sm);
    line-height: var(--lh-label-sm);
    font-weight: 500;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--color-ink-muted);
    margin: 0 0 var(--stack-sm);
}

/* ---------- Search ---------- */

.search-bar {
    position: relative;
    margin-bottom: var(--stack-md);
}

.search-bar input {
    width: 100%;
    border-radius: var(--radius-lg);
    font-family: var(--font-body);
    line-height: var(--lh-body-md);
    color: var(--color-ink);
    box-shadow: none;
    transition: border-color 150ms ease, background-color 150ms ease;
}


.search-bar input:focus {
    outline: none;
    border-color: var(--color-field-rule);
}

.search-bar > .material-symbols-outlined,
.search-bar > i {
    position: absolute;
    left: 16px;
    top: 50%;
    transform: translateY(-50%);
    color: var(--color-ink-muted);
    font-size: 22px;
    pointer-events: none;
}

/* ---------- "Popular today" rail ---------- */

.featured-scroll {
    display: flex;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    padding-bottom: var(--stack-sm);
    margin: 0 calc(-1 * var(--margin-mobile));
    padding-left: var(--margin-mobile);
    padding-right: var(--margin-mobile);
}
.featured-scroll::-webkit-scrollbar { display: none; }

/* Not a card in this design — an image with three lines under it, no fill,
   no border, no shadow. */
.deal-card {
    scroll-snap-align: start;
}

.deal-card-photo {
    position: relative;
    width: 100%;
    aspect-ratio: 4 / 3;
    margin-bottom: var(--stack-sm);
    overflow: hidden;
}

.deal-card-photo img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform 500ms cubic-bezier(0.16, 1, 0.3, 1);
}
.deal-card:hover .deal-card-photo img { transform: scale(1.05); }

.deal-card-photo.is-illustration {
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--color-surface-food);
}
.deal-card-photo.is-illustration img {
    width: 60%;
    height: 60%;
    object-fit: contain;
}

.deal-card-body {
    justify-content: space-between;
    align-items: flex-start;
    padding: 0;
}

.deal-card-name {
    line-height: var(--lh-label-lg);
    color: var(--color-ink);
}

.deal-card-price {
    line-height: var(--lh-label-lg);
}

/* ---------- Discount + stock flags ---------- */

.discount-tag {
    line-height: var(--lh-label-sm);
}

.stock-note {
    margin-top: 2px;
    background: none;
    line-height: var(--lh-label-sm);
    letter-spacing: 0.02em;
}

/* ---------- Quantity stepper ---------- */

.qty-stepper,
.cart-control__stepper {
    display: inline-flex;
    align-items: center;
    gap: 2px;
    padding: 2px;
}

.qty-stepper button,
.cart-control__stepper button {
    width: 28px;
    height: 28px;
    display: flex;
    align-items: center;
    justify-content: center;
    border: none;
    cursor: pointer;
    transition: background-color 150ms ease, transform 120ms ease;
}
.qty-stepper button:active,
.cart-control__stepper button:active { transform: scale(0.9); }
.qty-stepper button .material-symbols-outlined,
.cart-control__stepper button .material-symbols-outlined { font-size: 18px; }

.qty-stepper .qty-value,
.cart-control__stepper .cart-qty-value {
    min-width: 20px;
    text-align: center;
    font-family: var(--font-body);
    font-size: var(--text-label-lg);
    font-weight: 600;
    color: var(--color-ink);
}

/* ---------- State medallion ---------- */

/* The swirl behind the disc belonged to the warm palette. Here it is just the
   icon on a tonal disc. */
.state-medallion::before { display: none; }

.state-medallion {
    width: 72px;
    height: 72px;
    margin: 0 auto var(--stack-md);
}

.state-medallion__disc {
    width: 72px;
    height: 72px;
    border-radius: var(--radius-lg);
    background: var(--color-field);
    display: grid;
    place-items: center;
    box-shadow: none;
    border: none;
}

.state-medallion__disc .material-symbols-outlined { font-size: 34px; }

/* The three tones must be told apart at a glance, and two of them were not: after the
   palette moved to the burger-landing system, --color-primary-tint and --color-danger-tint
   both resolved to #ffd9d4, and --color-primary and --color-danger both to #e8281e. So a
   "neutral" medallion and a "bad" one rendered pixel-identical — an empty cart looked
   exactly like a cancelled order, and both looked like an error.

   Neutral is now the quiet one it was always meant to be. Only the colours change; the
   disc shape and size are untouched. */
.state-medallion--neutral .state-medallion__disc { background: var(--color-surface-deep); }
.state-medallion--neutral .state-medallion__disc .material-symbols-outlined { color: var(--color-ink); }
.state-medallion--good    .state-medallion__disc { background: var(--color-accent-tint); }
.state-medallion--good    .state-medallion__disc .material-symbols-outlined { color: var(--color-accent); }
.state-medallion--bad     .state-medallion__disc { background: var(--color-primary); }
.state-medallion--bad     .state-medallion__disc .material-symbols-outlined { color: #fff; }


/* ---------- Bottom navigation ---------- */

.bottom-nav {
    height: var(--bottom-nav-height);
    padding: 8px 16px calc(8px + var(--safe-area-bottom));
}

.bottom-nav-item {
    border-radius: var(--radius-sm);
    line-height: var(--lh-label-sm);
}
.bottom-nav-item:active { transform: scale(0.9); }
.bottom-nav-item.active {
    background: transparent;
}

.bottom-nav-badge {
    border: 2px solid var(--color-surface);
}

/* ---------- Sticky cart bar ---------- */


.sticky-cart-inner:hover { background: var(--color-primary-dark);}

.sticky-cart-info,
.sticky-cart-action {
    font-size: var(--text-label-lg);
    line-height: var(--lh-label-lg);
    font-weight: 600;
    letter-spacing: 0.01em;
}

/* ---------- Order cards ---------- */

.order-card {
    display: block;
    background: var(--color-surface);
    padding: var(--stack-sm) var(--stack-md);
    color: inherit;
    transition: box-shadow 200ms ease;
}
.order-card:hover {color: inherit; }

.order-card-token {
    font-size: var(--text-headline-md);
    line-height: var(--lh-headline-md);
    font-weight: 700;
    color: var(--color-ink);
}

.order-card-total {
    font-size: var(--text-label-lg);
    font-weight: 600;
    color: var(--color-primary);
}

.order-card-date {
    font-size: var(--text-label-sm);
    color: var(--color-ink-muted);
}

.order-card-reason {
    font-size: var(--text-label-sm);
    line-height: var(--lh-label-sm);
    color: var(--color-danger);
}
.order-card-reason .material-symbols-outlined { font-size: 16px; }

/* ---------- Cart rows ---------- */

/* Rows on a rule, not floating cards — the design lists items as a ledger. */

.cart-item-photo.is-illustration { display: grid; place-items: center; background: var(--color-surface-food); }
.cart-item-photo.is-illustration img { width: 62%; height: 62%;}

.cart-item-name {
    line-height: 1.25;
    color: var(--color-ink);
}

.cart-item-price {
    margin-top: 2px;
}


.pickup-card {
    box-shadow: none;
}
.pickup-card__icon .material-symbols-outlined { font-size: 20px; }
.pickup-card__name { font-size: var(--text-body-lg);color: var(--color-ink); }

.cart-summary {
    border-top: 1px solid var(--color-border);
    padding-top: var(--stack-sm);
}
.cart-summary__row dt { font-size: var(--text-body-md); color: var(--color-ink-muted); font-weight: 500; }
.cart-summary__row dd { font-size: var(--text-headline-md); font-weight: 700; color: var(--color-ink); }

/* ---------- Dish detail ---------- */

.detail-hero {
    border-radius: var(--radius-md);
    background: var(--color-surface-deep);
    aspect-ratio: 4 / 3;
    overflow: hidden;
    position: relative;
    margin-bottom: var(--stack-md);
}
.detail-hero img { width: 100%; height: 100%; object-fit: cover; }
.detail-hero.is-illustration { display: grid; place-items: center; background: var(--color-surface-food); }
.detail-hero.is-illustration img { width: 58%; height: 58%; object-fit: contain; }

.detail-eyebrow {
    font-size: var(--text-label-sm);
    letter-spacing: 0.1em;
    text-transform: uppercase;
    font-weight: 500;
    color: var(--color-ink-muted);
    margin: 0 0 var(--stack-xs);
}

.detail-name {
    font-family: var(--font-display);
    font-size: var(--text-display-lg);
    line-height: var(--lh-display-lg);
    letter-spacing: -0.02em;
    font-weight: 700;
    margin: 0 0 var(--stack-sm);
    color: var(--color-ink);
}

.detail-price {
    display: flex;
    align-items: baseline;
    gap: 10px;
    font-size: var(--text-headline-lg);
    line-height: var(--lh-headline-lg);
    font-weight: 700;
    color: var(--color-primary);
    margin-bottom: var(--stack-sm);
}

.detail-meta {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: var(--text-body-md);
    color: var(--color-ink-muted);
}
.detail-meta .material-symbols-outlined { font-size: 20px; color: var(--color-accent); }

.detail-back {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    margin-bottom: var(--stack-md);
    font-size: var(--text-label-lg);
    font-weight: 600;
    color: var(--color-ink-muted);
}
.detail-back:hover { color: var(--color-primary); }

.detail-unavailable {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 14px 16px;
    border-radius: var(--radius-md);
    background: var(--color-field);
    border: 1px solid var(--color-border);
    color: var(--color-ink-muted);
    font-size: var(--text-body-md);
    margin-bottom: var(--stack-sm);
}


/* ---------- Token badge ---------- */

.token-badge {
    font-family: var(--font-body);
    font-weight: 700;
    letter-spacing: -0.01em;
    color: var(--color-ink);
    background: none;
    padding: 0;
}

/* ---------- Auth ---------- */

/* The design puts sign-in on plain white rather than the app canvas, opens with
   the wordmark set large in primary, and rules its fields with a single line
   instead of boxing them. */

.auth-shell,
.auth-main { background: var(--color-surface); }

.auth-card {
    background: var(--color-surface);
    border-radius: 0;
    padding: var(--stack-lg) var(--margin-mobile);
}

/* The lockup becomes the design's oversized wordmark. The mark itself is dropped
   on mobile — at this size the word is the logo. */
.auth-lockup {
    display: block;
    margin-bottom: var(--stack-md);
}
.auth-lockup img { display: none; }
.auth-lockup span {
    font-family: var(--font-display);
    font-size: var(--text-display-lg);
    line-height: var(--lh-display-lg);
    font-weight: 900;
    letter-spacing: -0.03em;
    color: var(--color-primary);
}

.auth-head { margin-bottom: var(--stack-lg); }

.auth-head__title {
    font-family: var(--font-display);
    font-size: 22px;
    line-height: 28px;
    font-weight: 700;
    letter-spacing: -0.01em;
    color: var(--color-ink);
    margin: 0 0 var(--stack-xs);
}

.auth-head__sub {
    font-size: var(--text-body-md);
    line-height: var(--lh-body-md);
    color: var(--color-ink-muted);
    margin: 0;
}

.field { margin-bottom: var(--stack-md); }

.field__label {
    display: block;
    line-height: var(--lh-label-sm);
    font-weight: 500;
    color: var(--color-ink-muted);
    margin-bottom: var(--stack-xs);
}

/* Ruled, not boxed. The rule uses --color-field-rule rather than the design's
   own #E5E5E5 hairline: at 1.2:1 on this canvas that hairline is invisible as a
   control boundary, which WCAG 1.4.11 asks to clear 3:1. This tone is the same
   line, dark enough to actually be one. */
.field__input {
    width: 100%;
    padding: 8px 0;
    border-bottom: 1px solid var(--color-field-rule);
    background: transparent;
    font-family: var(--font-body);
    font-size: var(--text-body-lg);
    line-height: var(--lh-body-lg);
    color: var(--color-ink);
    box-shadow: none;
    transition: border-color 150ms ease;
}

.field__input::placeholder { color: var(--color-ink-muted); }

.field__input:focus {
    border-bottom-color: var(--color-primary);
    border-bottom-width: 2px;
    padding-bottom: 7px;   /* keeps the baseline still as the rule thickens */
    background: transparent;
}

.field__wrap { position: relative; }

.field__toggle {
    position: absolute;
    right: 0;
    top: 50%;
    transform: translateY(-50%);
    border: none;
    background: none;
    color: var(--color-ink-muted);
    padding: 8px;
    cursor: pointer;
}
.field__toggle .material-symbols-outlined { font-size: 20px; }

.btn-auth {
    width: 100%;
    padding: 16px;
    border: none;
    border-radius: var(--radius-md);
    background: var(--color-primary);
    color: #fff;
    font-family: var(--font-body);
    font-size: var(--text-label-lg);
    line-height: var(--lh-label-lg);
    font-weight: 600;
    letter-spacing: 0.01em;
    margin-top: var(--stack-sm);
    transition: background-color 150ms ease, transform 120ms ease;
}
.btn-auth:hover  { background: var(--color-primary-dark); color: #fff; }
.btn-auth:active { transform: scale(0.98); }

/* The welcome screen uses anchors rather than submit buttons for these, so the shape
   has to be stated rather than inherited from <button>'s defaults. Harmless on the
   real buttons, which were already full-width and centred. */
.btn-auth {
    display: block;
    text-align: center;
    text-decoration: none;
}

/* Secondary action on the welcome screen. Outlined rather than filled so "Sign in"
   stays the single obvious primary, and it inverts on hover so it never reads as
   disabled. */
.btn-auth--ghost {
    background: transparent;
    color: var(--color-primary);
    border: 2px solid var(--color-primary);
}
.btn-auth--ghost:hover,
.btn-auth--ghost:focus { background: var(--color-primary); color: #fff; }

/* ============================================================
   WELCOME SCREEN

   The landing card for signed-out visitors. Built to read like a product page
   rather than a form: a hero band, a grid of what the canteens actually serve,
   then how the thing works. Uses the existing food and mascot art so it carries
   the same illustration style as the rest of the app.
   ============================================================ */

/* Welcome is a full-bleed page, not a card. At this length a bordered box that
   scrolls for three screens reads as a boxed document rather than an app screen,
   so the border and shadow the brutalist layer gives .auth-card are dropped here
   and the sections carry the page margin themselves. */
.welcome {
    border: none;
    box-shadow: none;
    padding: 0 0 var(--stack-lg);
    max-width: 520px;
}

/* Runs to the screen edges and carries the status-bar inset itself, so the yellow
   continues up behind the clock instead of starting underneath it. */
.welcome-hero {
    margin: 0 0 var(--stack-md);
    padding: calc(var(--stack-md) + var(--safe-area-top)) var(--margin-mobile) var(--stack-md);
    background: var(--color-accent-tint);
    border-bottom: 2px solid var(--color-border);
    display: flex;
    align-items: flex-end;
    gap: var(--stack-sm);
}

/* Everything below the hero gets the horizontal margin the card used to provide. */
.welcome-actions,
.welcome-tiles,
.welcome-steps,
.welcome .welcome-section,
.welcome .auth-foot,
.welcome .auth-legal {
    padding-left: var(--margin-mobile);
    padding-right: var(--margin-mobile);
}

.welcome-hero__copy { flex: 1 1 auto; min-width: 0; }

.welcome-hero__mark {
    display: block;
    font-family: var(--font-display);
    font-size: 22px;
    line-height: 24px;
    font-weight: 900;
    letter-spacing: -0.03em;
    color: var(--color-primary);
    margin-bottom: var(--stack-xs);
}

.welcome-hero__title {
    font-family: var(--font-display);
    font-size: 21px;
    line-height: 23px;
    font-weight: 700;
    letter-spacing: -0.02em;
    color: var(--color-ink);
    margin: 0 0 var(--stack-xs);
}

.welcome-hero__sub {
    font-size: var(--text-body-md);
    line-height: var(--lh-body-md);
    color: var(--color-ink);
    margin: 0;
}

/* Sits on the band's baseline so it reads as standing on the border. */
.welcome-hero__mascot {
    flex: none;
    width: 30%;
    max-width: 112px;
    height: auto;
    margin-bottom: calc(var(--stack-md) * -1);
    align-self: flex-end;
}

/* The two actions sit directly under the hero, not after the supporting content:
   someone arriving to sign in should not have to scroll past an explanation aimed
   at people who have never heard of this. */
.welcome-actions { margin-bottom: var(--stack-md); }
.welcome-actions .btn-auth:first-child { margin-top: 0; }

.welcome-section {
    font-family: var(--font-display);
    font-size: 14px;
    line-height: 18px;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--color-ink);
    margin: 0 0 var(--stack-sm);
}

.welcome-tiles {
    list-style: none;
    margin: 0 0 var(--stack-md);
    /* padding-block, not the shorthand: the shared rule above supplies the inline
       page margin and `padding: 0` here would silently wipe it. */
    padding-block: 0;
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: var(--stack-sm);
}

.welcome-tile {
    background: var(--color-field);
    border: 2px solid var(--color-border);
    box-shadow: var(--shadow-sm);
    padding: var(--stack-sm) 4px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--stack-xs);
    text-align: center;
}

/* Fixed box with contain, because the source art has wildly different aspect
   ratios (a shake is portrait, a dosa is landscape) and a free-flowing height
   would make the grid rows ragged. */
.welcome-tile img {
    width: 100%;
    max-width: 68px;
    height: 52px;
    object-fit: contain;
}

.welcome-tile span {
    font-size: var(--text-label-sm);
    line-height: var(--lh-label-sm);
    font-weight: 700;
    color: var(--color-ink);
}

.welcome-steps {
    list-style: none;
    margin: 0 0 var(--stack-md);
    padding-block: 0;
    display: grid;
    gap: var(--stack-sm);
}
.welcome-steps__item { display: flex; gap: var(--stack-sm); align-items: flex-start; }

/* Yellow chip with the same hard border as the kickers elsewhere, so the list
   reads as part of the same system rather than a stack of loose icons. */
.welcome-steps__icon {
    flex: none;
    width: 34px;
    height: 34px;
    display: grid;
    place-items: center;
    background: var(--color-accent-tint);
    border: 2px solid var(--color-border);
    color: var(--color-ink);
}
.welcome-steps__icon .material-symbols-outlined { font-size: 19px; }

.welcome-steps__text {
    font-size: var(--text-body-md);
    line-height: var(--lh-body-md);
    color: var(--color-ink-muted);
}
.welcome-steps__text strong {
    display: block;
    color: var(--color-ink);
    font-weight: 700;
}

.auth-foot {
    margin-top: var(--stack-md);
    font-size: var(--text-body-md);
    color: var(--color-ink-muted);
    text-align: center;
}
.auth-foot a { color: var(--color-primary); font-weight: 600; }

/* Desktop keeps the brand panel — the design has a desktop sign-in too — but it
   re-tones to the new palette rather than the old peach. */
@media (min-width: 992px) {
    .auth-card { padding: var(--stack-lg); }

    /* The brand panel sits immediately to the left at this width carrying the wordmark,
       this same headline and the same sub, in much larger type. Only the hero's wordmark
       was hidden for that reason, which left the identical sentence on screen twice, side
       by side in two different treatments. The whole band goes and the column opens on
       the two actions, which is what a visitor is here to press. */
    .welcome-hero { display: none; }

    /* The card supplies the margin at this width (padding above), so the sections must not
       add their own on top of it. Two lots of --stack-lg squeezed the tiles and the steps
       into the middle of a column that was already too narrow. */
    .welcome-actions,
    .welcome-tiles,
    .welcome-steps,
    .welcome .welcome-section,
    .welcome .auth-foot,
    .welcome .auth-legal {
        padding-left: 0;
        padding-right: 0;
    }

    /* 520px is a phone column, and it stayed 520px inside a half-screen desktop pane:
       the content sat marooned with dead gutters either side of it. */
    .welcome { max-width: 640px; }

    /* Four stacked steps ran the page long while width went spare beside them. */
    .welcome-steps {
        grid-template-columns: 1fr 1fr;
        column-gap: var(--stack-md);
    }

    /* The panel carries the identity at this width, so the card's own lockup would be
       the second one on screen. The pre-redesign layout hid it here for that reason;
       the unscoped `.auth-lockup { display: block }` above silently undid that, because
       a media query adds no specificity and this file's later rule simply won. */
    .auth-lockup { display: none; }
}

/* One step down the same red. The vivid --color-primary carries white text at 4.42:1,
   under AA for anything below 24px, and this panel is mostly body copy. --color-primary-dark
   is the same hue at 6.47:1, so the panel reads identically and the text becomes legible.
   The sub-heading was additionally at 82% opacity, which took it lower still. */
.auth-brand {
    background: var(--color-primary-dark);
    color: #fff;
}
.auth-brand__headline,
.auth-brand__lockup span { color: #fff; }
.auth-brand__sub { color: #fff; }
/* The translucent tile is gone with the boxed treatment — the illustrations sit
   directly on the panel now. Kept as an empty-intent note rather than silently
   deleted, because this file re-tones the brand panel and its absence is the
   deliberate choice, not an oversight. */

/* ---------- Buttons, app-wide within the customer surfaces ---------- */

.btn-primary {
    border-color: var(--color-primary);
    font-family: var(--font-body);
    font-size: var(--text-label-lg);
    letter-spacing: 0.01em;
}
.btn-primary:hover,
.btn-primary:focus {
    border-color: var(--color-primary-dark);
}

.btn-lg { padding: 14px 20px; font-size: var(--text-label-lg); }


/* ---------- Pickup code ---------- */

/* Read across a counter, often aloud, sometimes one-handed while holding a tray. It
   gets the largest type on the page and the widest tracking, because legibility at a
   glance is the entire job. */
.pickup-code-panel {
    text-align: center;
    padding: var(--stack-md);
    border-radius: var(--radius-md);
    background: var(--color-primary-tint);
    border: 1px solid var(--color-primary-soft);
}

.pickup-code-panel__label {
    margin: 0 0 var(--stack-xs);
    font-size: var(--text-label-sm);
    line-height: var(--lh-label-sm);
    font-weight: 500;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--color-primary-dark);
}

.pickup-code-panel__code {
    margin: 0;
    font-family: var(--font-display);
    font-size: 48px;
    line-height: 1.1;
    font-weight: 800;
    /* Wide tracking so adjacent digits cannot be misread as one number. */
    letter-spacing: 0.18em;
    /* Tabular figures: the code must not reflow as digits change between orders. */
    font-variant-numeric: tabular-nums;
    color: var(--color-primary);
}

.pickup-code-panel__hint {
    margin: var(--stack-xs) 0 0;
    font-size: var(--text-body-md);
    color: var(--color-ink-muted);
}

/* Counter-side entry. Matches the code's tracking so a mistyped digit is obvious. */
.pickup-input {
    font-family: var(--font-display);
    font-size: 1.1rem;
    font-weight: 700;
    letter-spacing: 0.15em;
    text-align: center;
    font-variant-numeric: tabular-nums;
}
/* ============================================================
   07 — UX POLISH
   Responsive and interaction refinements layered after 06 so they
   win the cascade. Touch targets, sticky chrome, keyboard focus,
   and desktop density. Loaded last in head.html.
   ============================================================ */

/* ---------- Sticky top navbar ----------
   The menu is long; the navbar should stay in reach so the user can
   switch outlet or sign out without scrolling back to the top. On
   desktop this also keeps the brand anchored while browsing. */

/* ---------- Touch targets ----------
   The add control floats over the photo; it must be easy to hit.
   40px is the floor for a thumb target on a food feed. */

/* Stepper +/- buttons on menu cards and cart page */
.cart-control__stepper button,
.qty-stepper button {
    width: 36px;
    height: 36px;
    font-size: 1.05rem;
}

/* ---------- Keyboard focus ----------
   Cards and controls get a clear focus ring so the whole feed is
   navigable by keyboard, not just the name link. */
.menu-card:focus-within,
.outlet-card:focus-within,
.cart-item-card:focus-within {
    box-shadow: 0 0 0 3px var(--color-primary-tint), var(--shadow-md);
}

.btn-add:focus-visible,
.cart-control__stepper button:focus-visible,
.qty-stepper button:focus-visible,
.category-chip:focus-visible,
.sticky-cart-inner:focus-visible {
    outline: var(--border-w-loud) solid var(--color-primary);
    outline-offset: 2px;
}

/* ---------- Desktop density ----------
   The 1100px container is roomy; let the menu breathe with a touch
   more max width and lift cards on hover for a tactile feed. */
@media (min-width: 992px) {
    .menu-card {
        transition: transform 180ms ease, box-shadow 180ms ease;
    }
    .menu-card:hover {
        transform: translateY(-3px);
        box-shadow: var(--shadow-lg);
    }
}

/* ---------- Sticky category chips (mobile) ----------
   On a phone the category list is the fastest way to jump a long
   menu. Keep it pinned under the navbar so it never scrolls away. */
@media (max-width: 767.98px) {
    /* Active chip highlight while scrolling (driven by JS) */
    .category-chip.is-scroll-active {
        background: var(--color-primary);
        color: #fff;
        border-color: var(--color-primary);
    }
}

/* ---------- Sticky add-to-cart bar (item detail, mobile) ----------
   Long descriptions push the add control out of reach. A slim bar
   pinned above the bottom nav keeps the primary action thumb-close. */
@media (max-width: 767.98px) {
    .detail-add {
        z-index: 1020;
        background: var(--color-surface-food);
        border: 1px solid var(--color-border);
        border-radius: var(--radius-md);
        padding: 10px 12px;
        margin-top: 1rem;
        box-shadow: var(--shadow-md);
    }
}

/* ---------- Cart page polish ----------
   The old cart page shrank the stepper with an inline transform and
   used a bare text delete. Replace those with clean, full-size
   controls and a proper remove button. */
.cart-item-card .qty-stepper {
    transform: none !important;
    transform-origin: initial !important;
}

.cart-item-remove {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 1.1rem;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}
.cart-item-remove:active {
    transform: scale(0.9);
}

/* ---------- Reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
    .navbar,
    .category-chips-wrapper,
    .detail-add {
        position: static;
    }
    .menu-card,
    .btn-add,
    .cart-control__stepper button,
    .qty-stepper button,
    .cart-item-remove {
        transition: none;
    }
}

/* ============================================================
   PROGRESS RAIL — where an order has got to.

   A vertical run of steps rather than a horizontal stepper: the labels carry times and
   real words, and horizontal steppers force those into abbreviations that then need
   decoding. Vertical also survives a 320px screen without a media query.
   ============================================================ */
.rail {
    display: flex;
    flex-direction: column;
    margin: 0 0 1.5rem;
}

.rail__step {
    display: grid;
    grid-template-columns: 22px 1fr;
    align-items: center;
    gap: 0.7rem;
    padding: 0.45rem 0;
    position: relative;
    font-size: 0.92rem;
}

/* The connecting line, drawn from each step to the next rather than as a separate
   element — one rule, and it cannot fall out of alignment with the pips. */
.rail__step:not(:last-child)::before {
    content: '';
    position: absolute;
    left: 10.25px;
    top: 50%;
    width: var(--rule-w);
    height: 100%;
    background: var(--rule-color-strong);
}

.rail__pip {
    width: 13px;
    height: 13px;
    justify-self: center;
    border: var(--border-w) solid var(--color-ink);
    background: var(--color-bg);
    position: relative;
    z-index: 1;
}

.rail__label {
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
    flex-wrap: wrap;
}

.rail__at {
    font-size: 0.8rem;
    color: var(--color-ink-muted);
    font-variant-numeric: tabular-nums;
}

.rail__step.is-done .rail__pip { background: var(--color-ink); }

/* The current step is the only one that changes shape. Yellow means work in progress
   here exactly as it does on the console and in the order strip. */
.rail__step.is-now .rail__pip {
    background: var(--color-accent-tint);
    box-shadow: 0 0 0 3px var(--color-primary-tint);
}

.rail__step.is-now { font-weight: 700; }

.rail__step.is-todo { color: var(--color-ink-muted); }
.rail__step.is-todo .rail__pip { border-color: var(--rule-color-strong); }

/* ==== 07-brutalist.css ==== */
/* ============================================================
   07-brutalist.css — The BIG BEEF language

   3px near-black borders, hard offset shadows, square corners and uppercase Archivo
   Black. This is where the burger-landing reference is actually applied to the
   components defined earlier.

   Part of an ordered set linked in fragments/head.html. The browser applies these
   in link order and THAT ORDER IS THE CASCADE — a rule here can only override one
   in an earlier file. Do not reorder the links, and do not move a rule between
   files without checking what it then stops overriding.
   ============================================================ */

/* ============================================================
   08 — REDESIGN
   The visual overhaul layer. Loads last, wins the cascade.

   Art direction: modern food-ordering app — expressive rounded
   surfaces, pill categories with illustrated pucks re-enabled,
   gradient hero header, deeper shadows, warmer accents, bolder
   type. Markup and behaviour are untouched; this file only
   restyles what is already in the DOM.
   ============================================================ */

/* ---------- Foundation ----------

   This block used to re-inflate the geometry that 01-tokens.css had just flattened:
   radii back to 16/20/999px, two soft blurred shadows, a terracotta gradient and a
   peach-and-mint haze behind the whole page. That was the *previous* art direction —
   this file carries both it and the brutalist pass that replaced it, and the old
   foundation was never removed. So every surface reading these tokens kept rendering
   in the soft system: the cart's pickup card, its summary panel and the sticky pay bar
   were rounded and gradient-filled on a page of square black-bordered cards.

   The radius overrides are simply gone, so 01-tokens (0–2px) wins as intended. The
   gradient and shadow tokens are kept as names but repointed at flat brand values,
   because eleven rules still reference them and repointing is safer than editing all
   eleven — anything asking for "the brand fill" now gets the brand's flat red. */
:root {
    --gradient-brand: var(--color-primary);
    --gradient-warm: var(--color-surface);
    --shadow-card: var(--shadow-sm);
    --shadow-lift: var(--shadow-md);
    --navbar-height: 60px;
}

/* ---------- Navbar ---------- */


.top-nav-links .nav-link {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: 8px 14px;
    border-radius: var(--radius-pill);
    font-weight: 600;
    font-size: 0.92rem;
    color: var(--color-ink-muted);
    transition: color 150ms ease, background-color 150ms ease;
}

.top-nav-links .nav-link .material-symbols-outlined { font-size: 20px; }

.top-nav-links .nav-link:hover {
    color: var(--color-ink);
    background: var(--color-field);
}

.top-nav-links .nav-link.active {
    color: var(--color-primary);
    background: var(--color-primary-tint);
}

.navbar .btn-outline-secondary {
    padding-inline: 14px;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    transition: background-color 150ms ease, color 150ms ease;
}
.navbar .btn-outline-secondary:hover {
    border: none;
}

/* ---------- Bottom nav (mobile) ---------- */
.bottom-nav {
    background: rgba(255, 255, 255, .92);
    
    
    border-top: 1px solid rgba(26, 26, 26, .06);
    box-shadow: 0 -8px 30px rgba(26, 26, 26, .08);
    padding-bottom: var(--safe-area-bottom);
}

.bottom-nav-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    padding: 8px 0 10px;
    color: var(--color-ink-muted);
    font-size: 0.66rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    text-decoration: none;
    position: relative;
    transition: color 150ms ease;
}

.bottom-nav-item .material-symbols-outlined { font-size: 24px; transition: transform 180ms ease; }

.bottom-nav-item.active { color: var(--color-primary); }
.bottom-nav-item.active .material-symbols-outlined {
    font-variation-settings: 'FILL' 1;
    transform: translateY(-2px);
}

.bottom-nav-item.active::before {
    content: '';
    position: absolute;
    top: 0;
    width: 28px;
    height: 3px;
    border-radius: 0;
    background: var(--gradient-brand);
}

.bottom-nav-badge {
    position: absolute;
    top: 4px;
    right: calc(50% - 20px);
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    border-radius: 0;
    background: var(--gradient-brand);
    color: #fff;
    font-size: 0.64rem;
    font-weight: 700;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 2px 6px rgba(20, 16, 13, 0.35);
}

/* ---------- Page header → hero ---------- */
.page-header {
    position: relative;
    padding: 28px 28px 26px;
    margin-top: 8px;
    overflow: hidden;
}

.page-header::before {
    content: '';
    position: absolute;
    right: -70px;
    top: -70px;
    width: 220px;
    height: 220px;
    border-radius: 0;
    background: none;
    pointer-events: none;
}

.page-header-decor { display: none; }

.page-header-title {
    font-size: clamp(1.5rem, 3.5vw, 1.9rem);
    color: var(--color-ink);
}

.page-header-sub {
    color: var(--color-ink-muted);
    font-size: 0.98rem;
    margin-top: 2px;
}

/* ---------- Search ---------- */
.search-bar {
    display: flex;
    align-items: center;
    gap: 10px;
    /* Was `4px 8px 4px 18px`: the 18px left inset dated from when the icon was absolutely
       positioned over the field and needed clearing. The icon is a flex item now, so the
       inset is doing nothing except making the bar visibly lopsided. */
    padding: 4px 14px;
    transition: box-shadow 150ms ease, border-color 150ms ease;
}
.search-bar .material-symbols-outlined { color: var(--color-ink-muted); font-size: 22px; }
/* This layer turns .search-bar into a flex row, so the icon should be a flex item and
   the gap above should separate it from the field. It was still carrying
   `position: absolute; left: 16px` from the editorial layer, which took it out of the
   flow — the input then started at the container's own padding and the placeholder ran
   underneath the icon. Returning it to the flow is the fix the flex layout assumed. */
.search-bar > .material-symbols-outlined,
.search-bar > i {
    position: static;
    transform: none;
    flex: none;
}
.search-bar input {
    flex: 1;
    outline: none;
    /* Right padding so a long query does not run into the bar's own border. The left is
       handled by the flex gap after the icon. */
    padding: 12px 4px 12px 0;
    font-size: 0.98rem;
    font-weight: 500;
}
.search-bar input::placeholder { color: var(--color-ink-muted); }

/* ---------- Category chips — illustrated pills, re-enabled ---------- */
.category-chips-wrapper {
    margin-bottom: var(--stack-md);
}
.category-chips {
    gap: 10px;
    padding: 4px 2px 8px;
    margin-bottom: 0;
}

.category-chip {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    padding: 0;
    min-width: 72px;
}

.cat-chip-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 62px;
    height: 62px;
    background: var(--color-surface);
    overflow: hidden;
}
.cat-icon-img {
    display: block !important;
    width: 100%;
    height: 100%;
    object-fit: cover;
}


.cat-chip-label {
    font-weight: 600;
    color: var(--color-ink-muted);
    max-width: 78px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.category-chip.active .cat-chip-label {font-weight: 700; }

/* kill the old underline rule in every state */
.category-chip.active::after { display: none; }

/* mobile sticky strip keeps gradient mask */
@media (max-width: 767.98px) {
    .category-chip.is-scroll-active .cat-chip-icon {
        border-color: var(--color-primary);
        box-shadow: 0 0 0 4px var(--color-primary-tint);
    }
    .category-chip.is-scroll-active .cat-chip-label { color: var(--color-primary); }
}

/* ---------- Section headings ---------- */
.menu-category-section h5 {
    font-weight: 800;
    margin-bottom: 1rem !important;
}

.cat-bullet {
    width: 10px;
    height: 10px;
    border-radius: 2px;
    background: var(--gradient-brand);
    transform: rotate(45deg);
    display: inline-block;
    box-shadow: 0 2px 6px rgba(20, 16, 13, 0.35);
}

/* ---------- Menu cards ---------- */


.menu-card-photo {
    aspect-ratio: 4 / 3;
}

.menu-card-photo img { transition: transform 600ms cubic-bezier(0.16, 1, 0.3, 1); }
.menu-card:hover .menu-card-photo img { transform: scale(1.07); }

.menu-card-media { border-radius: var(--radius-lg) var(--radius-lg) 0 0;}

.menu-card-body {gap: 6px; }

.menu-card-name {
    font-weight: 700;
    white-space: normal;
    display: -webkit-box;
    -webkit-line-clamp: 1;
    -webkit-box-orient: vertical;
}
.menu-card-name__link { text-decoration: none; color: inherit; }
.menu-card-name__link:hover { color: var(--color-primary); }

.menu-card-price {
    letter-spacing: -0.01em;
}

.price-strike {
    font-size: 0.82rem;
}

.discount-tag {
    position: absolute;
    top: 10px;
    left: 10px;
    z-index: 2;
    background: var(--gradient-brand);
    color: #fff;
    font-size: 0.68rem;
    font-weight: 800;
    letter-spacing: 0.04em;
    padding: 4px 9px;
    border-radius: 0;
    box-shadow: 0 4px 12px rgba(20, 16, 13, 0.35);
}

.stock-note {
    font-weight: 700;
    border-radius: 0;
    width: fit-content;
}
.stock-note.is-low { background: var(--color-warning-tint); color: var(--color-warning); }
.stock-note.is-out { background: var(--color-danger-tint); color: var(--color-danger); }

/* Add control: floating disc over the photo */
.btn-add {
    width: 38px;
    height: 38px;
    transition: transform 160ms cubic-bezier(0.16, 1, 0.3, 1), background-color 150ms ease, color 150ms ease;
}
.btn-add::before { font-size: 22px; font-weight: 500; }

/* The button must stay inside the media wrapper: .menu-card-media has
   overflow:hidden to round the photo corners, so a negative bottom here
   pushed the disc below that boundary and clipped it into a half-pill. */

/* Stepper swap: solid brand stepper */
.cart-control__stepper {
    background: var(--color-primary);
    border: none;
    border-radius: 0;
    padding: 3px;
    box-shadow: 0 6px 18px rgba(20, 16, 13, 0.35);
}
.cart-control__stepper button {
    width: 30px;
    height: 30px;
    background: transparent;
    color: #fff;
    font-size: 1rem;
}
.cart-control__stepper button:hover { background: rgba(255, 255, 255, .18); }
.cart-control__stepper .cart-qty-value { color: #fff; min-width: 24px; font-size: 0.92rem; }
.cart-control__stepper button:active { transform: scale(0.85); }

/* ---------- Deals rail ---------- */
.featured-scroll { gap: 14px; margin-bottom: 4px; }

/* Deal add button: full-width footer bar in normal flow — no overlay, no
   collision with the price. */
.deal-card-add:hover { background: var(--color-primary-dark); color: #fff; }

.deal-card {
    padding: 12px;
}
.deal-card:hover {
    background: var(--color-surface);
}

.deal-card-photo {
    background: var(--gradient-warm);
    box-shadow: none;
}

.deal-card-name {
    font-weight: 700;
    letter-spacing: -0.01em;
}

.deal-card-price {
    font-weight: 800;
    font-size: 1.05rem;
    color: var(--color-primary);
}

/* Superseded by the full-width footer bar defined above — kept as a no-op
   so the later cascade position doesn't resurrect the tiny square disc. */

/* ---------- Specials heading ---------- */
h5 .material-symbols-outlined.is-filled {
    font-variation-settings: 'FILL' 1;
    color: var(--color-primary);
}

/* ---------- Sticky cart bar (menu page) ---------- */
.sticky-cart-bar {
    position: fixed;
    left: 50%;
    transform: translateX(-50%);
    bottom: calc(var(--bottom-nav-height) + var(--safe-area-bottom) + 12px);
    width: min(680px, calc(100% - 2rem));
    z-index: 1035;
}

.sticky-cart-inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    background: var(--gradient-brand);
    color: #fff;
    border-radius: var(--radius-pill);
    padding: 12px 12px 12px 22px;
    box-shadow: var(--shadow-md);
    text-decoration: none;
    transition: transform 180ms cubic-bezier(0.16, 1, 0.3, 1), box-shadow 180ms ease;
}
.sticky-cart-inner:hover { transform: translateY(-2px); box-shadow: var(--shadow-sm); color: #fff; }
.sticky-cart-inner:focus-visible { outline: var(--border-w) solid var(--color-ink); outline-offset: 3px; }

.sticky-cart-info { font-weight: 600; font-size: 0.95rem; }
.sticky-cart-info strong { font-weight: 800; }

.sticky-cart-action {
    display: inline-flex;
    align-items: center;
    gap: 2px;
    background: rgba(255, 255, 255, .18);
    border-radius: 0;
    padding: 8px 16px;
    font-weight: 700;
    font-size: 0.9rem;
}

/* desktop: dock bottom-right, out of the content's way */
@media (min-width: 768px) {
    .sticky-cart-bar {
        left: auto;
        right: 2rem;
        transform: none;
        width: 340px;
        bottom: 2rem;
    }
}

/* ---------- CART PAGE ---------- */
.pickup-card {
    display: flex;
    align-items: center;
    gap: 14px;
    background: var(--color-surface);
    border: var(--border-w) solid var(--color-ink);
    border-radius: 0;
    box-shadow: var(--shadow-sm);
    padding: 14px 16px;
}
.pickup-card__icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    flex: none;
    border-radius: 0;
    border: var(--border-w) solid var(--color-ink);
    background: var(--color-accent-tint);
    color: var(--color-ink);
    box-shadow: none;
}
.pickup-card__name { margin: 0; font-weight: 800; letter-spacing: -0.01em; }
.pickup-card__meta { margin: 0; font-size: 0.84rem; color: var(--color-ink-muted); }

.cart-item-card {
    display: flex;
    align-items: center;
    gap: 14px;
    background: var(--color-surface);
    border: none;
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-card);
    padding: 12px;
    margin-bottom: 12px;
    transition: box-shadow 200ms ease;
}
.cart-item-card:hover { box-shadow: var(--shadow-lift); }

.cart-item-photo {
    width: 76px;
    height: 76px;
    flex: none;
    overflow: hidden;
    background: var(--color-surface-food);
}
.cart-item-photo img { width: 100%; height: 100%; object-fit: cover; }
.cart-item-photo.is-illustration img { object-fit: contain; padding: 8px; }

.cart-item-name {
    margin: 0;
    font-weight: 700;
    letter-spacing: -0.01em;
    font-size: 0.98rem;
}
.cart-item-price { color: var(--color-ink-muted); font-size: 0.86rem; font-weight: 600; }
.cart-item-subtotal { font-weight: 800; font-size: 1rem; color: var(--color-ink); }

.cart-item-right { margin-left: auto; text-align: right; display: flex; flex-direction: column; align-items: flex-end; gap: 8px; }

.qty-stepper {
    border: 1px solid var(--color-border);
    border-radius: 0;
    background: var(--color-field);
    padding: 3px;
    box-shadow: none;
}
.qty-stepper button {
    width: 30px;
    height: 30px;
    background: var(--color-surface);
    color: var(--color-primary);
    font-size: 1rem;
    box-shadow: 0 1px 3px rgba(26, 26, 26, .12);
}
.qty-stepper button:hover { background: var(--color-primary); color: #fff; box-shadow: none; }

.cart-item-remove {
    border: none;
    background: transparent;
    color: var(--color-ink-muted);
    width: 34px;
    height: 34px;
    border-radius: 0;
    transition: background-color 150ms ease, color 150ms ease;
}
.cart-item-remove:hover { background: var(--color-danger-tint); color: var(--color-danger); }

/* Summary */
.cart-summary {
    background: var(--color-surface);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-card);
    padding: 18px 20px;
    margin: 0;
}
.cart-summary__row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 4px 0;
}
.cart-summary dt { color: var(--color-ink-muted); font-weight: 600; }
.cart-summary dd { margin: 0; font-weight: 800; font-size: 1.15rem; letter-spacing: -0.02em; }
/* A saving should look like a saving. Same weight as the total, in the success colour,
   because it is the one number on this screen a student actually wants to read. */
.cart-summary__row--discount dt,
.cart-summary__row--discount dd { color: var(--color-success, #1a7f45); }
.cart-summary__row--total {
    border-top: 2px solid var(--color-ink);
    margin-top: 6px;
    padding-top: 10px;
}
.cart-summary__row--total dt { color: var(--color-ink); font-weight: 800; }
.cart-summary__row--total dd { font-size: 1.3rem; }

/* Offers */
.promo {
    background: var(--color-surface);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-card);
    padding: 14px 16px;
    margin-bottom: 1.25rem;
}
.promo__form {
    display: flex;
    gap: 8px;
}
/* Codes are read off a poster and typed in capitals; letter-spacing makes an O and a 0
   distinguishable while typing rather than after the code is refused. */
.promo__input {
    text-transform: uppercase;
    letter-spacing: 0.06em;
    font-weight: 700;
}
.promo__applied {
    display: flex;
    align-items: center;
    gap: 10px;
}
.promo__tick { color: var(--color-success, #1a7f45); }
.promo__applied-text {
    display: flex;
    flex-direction: column;
    line-height: 1.2;
    flex: 1;
}
.promo__applied-text span {
    font-size: 0.85rem;
    color: var(--color-ink-muted);
}
.promo__remove {
    color: var(--color-danger);
    padding: 0;
    font-weight: 700;
}
.promo__struck { color: var(--color-ink-muted); font-weight: 500; margin-right: 6px; }
.promo__free { color: var(--color-success, #1a7f45); letter-spacing: 0.04em; }

/* Sticky pay bar */
.sticky-pay-bar {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 1035;
    background: transparent;
    pointer-events: none;
    padding: 0 1rem calc(var(--bottom-nav-height) + var(--safe-area-bottom) + 12px);
}
.sticky-pay-inner {
    pointer-events: auto;
    max-width: 680px;
    margin: 0 auto;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 14px;
    background: var(--color-ink);
    color: var(--color-bg);
    border: var(--border-w) solid var(--color-ink);
    border-radius: 0;
    padding: 12px 14px 12px 18px;
    box-shadow: 5px 5px 0 rgba(20, 16, 13, 0.35);
}
.sticky-pay-total { display: flex; flex-direction: column; line-height: 1.25; }
.sticky-pay-total small { color: rgba(255, 255, 255, .65); font-weight: 600; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em; }
.sticky-pay-total span { font-weight: 800; font-size: 1.25rem; letter-spacing: -0.02em; }

.sticky-pay-inner .btn-primary {
    background: var(--color-primary);
    border: 2px solid var(--color-bg);
    border-radius: 0;
    font-weight: 700;
    padding-block: 12px;
    box-shadow: none;
    transition: transform 160ms ease, filter 160ms ease;
}
.sticky-pay-inner .btn-primary:hover { transform: translateY(-1px); filter: brightness(1.06); }

@media (min-width: 768px) {
    .sticky-pay-bar { padding-bottom: 1.5rem; }
}

/* ---------- Alerts / state panels ---------- */
.alert {
    padding: 16px 18px;
}
.alert-warning { background: var(--color-warning-tint); color: var(--color-warning); }
.alert-danger { background: var(--color-danger-tint); color: var(--color-danger); }

.icon-circle {
    width: 40px;
    height: 40px;
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 0;
}

.state-panel {
    background: var(--color-surface);
    padding: 48px 24px;
    text-align: center;
}
.state-panel h4 {margin-bottom: 6px; }
.state-panel p { color: var(--color-ink-muted); }

.state-panel .btn-primary {
    font-weight: 700;
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding-inline: 22px;
}

/* ---------- Forms / selects ---------- */
.form-select {
    border-radius: 0;
    border: 1px solid var(--color-border);
    font-weight: 600;
    font-size: 0.9rem;
    box-shadow: var(--shadow-sm);
}

/* ---------- Buttons, global ---------- */
.btn-primary {
    border: none;
    border-radius: var(--radius-pill);
    font-weight: 700;
}
.btn-primary:hover, .btn-primary:focus {border: none; }

/* ---------- Item detail ---------- */
.detail-add {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: 0;
    box-shadow: var(--shadow-lift);
}
@media (max-width: 767.98px) {
    .detail-add {
        position: fixed;
        left: 1rem;
        right: 1rem;
        bottom: calc(var(--bottom-nav-height) + var(--safe-area-bottom) + 12px);
    }
}

/* ---------- Desktop density ---------- */
@media (min-width: 992px) {
    .container { max-width: 1140px; }
}

/* ---------- Focus rings ---------- */
.menu-card:focus-within,
.cart-item-card:focus-within {
    box-shadow: 0 0 0 3px var(--color-primary-tint), var(--shadow-lift);
}
.category-chip:focus-visible .cat-chip-icon,
.search-bar:focus-within {
    outline: none;
}
.category-chip:focus-visible .cat-chip-icon {
    box-shadow: 0 0 0 3px var(--color-primary), var(--shadow-md);
}

/* ---------- Reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
    .menu-card, .btn-add, .deal-card, .bottom-nav-item .material-symbols-outlined,
    .sticky-cart-inner, .sticky-pay-inner .btn-primary {
        transition: none;
    }
    .menu-card:hover, .deal-card:hover, .btn-add:hover, .sticky-cart-inner:hover { transform: none; }
}

/* ============================================================
   POLISH LAYER — controlled elevation of the shared surfaces.
   Appended after the consolidated cascade so these win, but each
   rule is scoped and uses existing tokens; nothing here fights the
   base system. This is the "gorgeous" pass: depth, rhythm, focus.
   ============================================================ */

/* ---------- Navbar: soft, floating, warm ---------- */
.navbar .nav-link.active {
    color: var(--color-primary-dark) !important;
    background: var(--color-primary-tint);
}

/* ---------- Buttons: confident, tactile ---------- */
.btn-primary {
    background-image: none;
}
.btn-primary:active {
    transform: scale(0.97);
}

/* ---------- Cards: warm, lifted ---------- */
.card {
    background: var(--color-surface);
    border: var(--border-w) solid var(--color-ink);
}
.menu-card, .outlet-card, .deal-card, .order-card {
    border: var(--border-w) solid var(--color-ink);
}

/* ---------- Forms: soft slots ---------- */
.form-control, .form-select {
    background: var(--color-field);
}

/* ---------- Tables: calm zebra, clear header ---------- */
.table tbody tr:hover {
    background: var(--color-surface-deep);
}

/* ---------- Badges: softer, warmer ---------- */
.badge {
    box-shadow: none;
}

/* ---------- Page headers: rhythm ---------- */

/* ---------- Selection & focus ---------- */

/* ---------- Scrollbar (WebKit) — subtle warm ---------- */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-thumb {
    background: var(--color-ink-muted);
    border-radius: 0;
    border: 2px solid transparent;
    background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover { background: var(--color-ink); background-clip: content-box; }

/* ============================================================
   09-brutalist.css  (scoped BIG BEEF layer)

   Ported from /Users/yash113gadia/burger-landing/css/style.css.

   The tokens in 01-tokens.css already remap colour, radius and shadow to
   the neo-brutalist palette, so most components re-tone automatically.
   This layer adds the structural language that tokens cannot: film grain,
   hard 3px borders, uppercase Archivo Black type, and pressed interactions.

   Every selector below targets a known BiteSite component. Nothing here
   rewrites Bootstrap internals or global layout primitives, so the app
   keeps its flow while the surfaces read as BIG BEEF.
   ============================================================ */

/* ---------- Film grain over the whole canvas ---------- */
body::after {
    content: '';
    position: fixed;
    inset: 0;
    width: 100vw;
    height: 100vh;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch''/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n%23)' opacity='0.06'/%3E%3C/svg%3E");
    pointer-events: none;
    z-index: 999;
    /* Oversized fixed elements (inset:-50% / 200%) create phantom scrollable
       overflow on some browsers and block reaching the page bottom. The overlay
       is exactly the viewport; the grain moves via a pure translate so it never
       extends the scrollable area. */
    overflow: hidden;
    animation: grain 8s steps(10) infinite;
}
@keyframes grain {
    0%, 100% { transform: translate(0, 0); }
    10% { transform: translate(-5%, -10%); }
    20% { transform: translate(-15%, 5%); }
    30% { transform: translate(7%, -25%); }
    40% { transform: translate(-5%, 25%); }
    50% { transform: translate(-15%, 10%); }
    60% { transform: translate(15%, 0%); }
    70% { transform: translate(0%, 15%); }
    80% { transform: translate(3%, 35%); }
    90% { transform: translate(-10%, 10%); }
}
@media (prefers-reduced-motion: reduce) {
    body::after { animation: none; }
}

/* ---------- Display type: Archivo Black, uppercase, tight ---------- */
h1, h2, h3, h4, h5, h6,
.navbar-brand,
.page-header h1,
.page-header h2,
.card-title,
.menu-card-name,
.order-card-token,
.account-section-title,
.auth-card h1,
.auth-card h2,
.state-panel h4,
.bottom-nav-item,
.badge {
    font-family: var(--font-display);
    text-transform: uppercase;
    letter-spacing: 0.02em;
}
h1, h2, h3 {
    font-weight: 400; /* Archivo Black is already heavy */
}

/* ---------- Buttons: hard border + offset shadow + press ---------- */
.btn {
    border: var(--border-w) solid var(--color-ink);
    border-radius: 0;
    box-shadow: var(--shadow-sm);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    font-family: var(--font-display);
    transition: transform 100ms ease, box-shadow 100ms ease, background-color 120ms ease;
}
.btn:hover {
    transform: translate(2px, 2px);
    box-shadow: var(--shadow-xs);
}
.btn:active {
    transform: translate(4px, 4px);
    box-shadow: 0 0 0 var(--color-ink);
}
.btn-sm {
    border-width: 2px;
    box-shadow: var(--shadow-sm);
}
.btn-primary { background: var(--color-primary); color: #fff; }
.btn-primary:hover, .btn-primary:focus { background: var(--color-primary-dark); color: #fff; }
.btn-accent { background: var(--color-accent-tint); color: var(--color-ink); }
.btn-accent:hover, .btn-accent:focus { background: var(--color-coral-deep); color: var(--color-ink); }
.btn-success { background: var(--color-accent-tint); color: var(--color-ink); }
.btn-success:hover { background: var(--color-coral-deep); color: var(--color-ink); }
.btn-outline-secondary { background: var(--color-surface-food); border-color: var(--color-ink); color: var(--color-ink); }
.btn-outline-primary { background: var(--color-surface); border-color: var(--color-ink); color: var(--color-ink); }
.btn-outline-primary:hover { background: var(--color-primary); border-color: var(--color-ink); color: #fff; }

/* ---------- Cards: hard 3px border, offset shadow ---------- */
.card,
.menu-card,
.cart-item-card,
.order-card,
.outlet-card,
.account-section,
.auth-card,
.state-panel {
    border: var(--border-w) solid var(--color-ink);
    border-radius: 0;
    box-shadow: var(--shadow-md);
}
.card:hover,
.menu-card:hover,
.order-card:hover,
.outlet-card:hover {
    box-shadow: var(--shadow-lg);
}

/* Menu cards: keep the photo square, harden the edge */
.menu-card-photo {
    border-bottom: var(--border-w) solid var(--color-ink);
}

/* ---------- Forms: hard field edges ---------- */
.form-control, .form-select, .input-group-text {
    border: var(--border-w) solid var(--color-ink);
    border-radius: 0;
    background: var(--color-field);
}
.form-control:focus, .form-select:focus {
    border-color: var(--color-ink);
    background: #fff;
    box-shadow: var(--shadow-sm);
}
.form-check-input:checked {
    background-color: var(--color-primary);
    border-color: var(--color-ink);
}

/* ---------- Category chips: hard active rule ---------- */
.category-chips-wrapper {
    border-bottom: var(--border-w) solid var(--color-ink);
}
.category-chip.active::after {
    height: 3px;
    background: var(--color-primary);
    border-radius: 0;
}

/* ---------- Quantity stepper: hard square buttons ---------- */
.qty-stepper,
.cart-control__stepper {
    border: var(--border-w) solid var(--color-ink);
    border-radius: 0;
    box-shadow: var(--shadow-xs);
    background: var(--color-surface);
}
.qty-stepper button,
.cart-control__stepper button {
    border-radius: 0;
    background: var(--color-accent-tint);
    color: var(--color-ink);
}
.qty-stepper button:hover,
.cart-control__stepper button:hover {
    background: var(--color-primary);
    color: #fff;
}

/* ---------- Sticky cart bar: hard top edge ---------- */

/* ---------- Tables: hard header ---------- */
.table thead th {
    color: var(--color-ink);
}

/* ---------- Alerts: hard edges ---------- */

/* ---------- Navbar: hard bottom border ---------- */

/* ---------- Bottom nav: hard top border ---------- */
@media (max-width: 767.98px) {
    .bottom-nav {
        border-top: var(--border-w) solid var(--color-ink);
        border-radius: 0;
        background: rgba(242, 234, 217, 0.94);
        box-shadow: 0 -3px 0 var(--color-ink);
    }
    .bottom-nav-item.active {
        color: var(--color-primary-dark);
    }
}

/* ---------- Focus ring: thick black ---------- */
:focus-visible {
    outline: var(--border-w) solid var(--color-ink);
    outline-offset: 2px;
}

/* ---------- Page header: hard rule ---------- */
.page-header {
    border-bottom: var(--border-w) solid var(--color-ink);
}

/* ---------- Selection ---------- */
::selection {
    background: var(--color-accent-tint);
    color: var(--color-ink);
}

/* ============================================================
   09-student-brutalist — page headers, cards, controls in the
   BIG BEEF language: 3px black borders, hard offset shadows,
   square corners, uppercase Archivo Black type.
   ============================================================ */

/* ---------- Page header banner ---------- */
.page-header {
    background: var(--color-surface);
    border: var(--border-w) solid var(--color-ink);
    border-radius: 0;
    box-shadow: var(--shadow-md);
}
.page-header::before { display: none; }
.page-header-title {
    font-family: var(--font-display);
    font-weight: 400;
    text-transform: uppercase;
    letter-spacing: 0;
}

/* ---------- Search bar: hard-edged slot ---------- */
.search-bar input {
    border: none;
    background: transparent;
}
.search-bar input:focus { background: transparent; }
.search-bar:focus-within {
    border-color: var(--color-ink);
    box-shadow: var(--shadow-xs);
}

/* ---------- Category chips: square tiles ---------- */
.cat-chip-icon {
    border: var(--border-w) solid var(--color-ink);
    border-radius: 0;
    box-shadow: var(--shadow-sm);
    transition: transform 120ms ease, box-shadow 120ms ease;
}
.category-chip:hover .cat-chip-icon {
    transform: translate(2px, 2px);
    box-shadow: var(--shadow-pressed);
}
.category-chip.active .cat-chip-icon {
    background: var(--color-accent-tint);
    border-color: var(--color-ink);
    box-shadow: var(--shadow-sm);
}
.cat-chip-label {
    font-family: var(--font-display);
    text-transform: uppercase;
    font-size: 0.7rem;
    letter-spacing: 0.03em;
}
.category-chip.active .cat-chip-label { color: var(--color-ink); }
.category-chip.is-scroll-active .cat-chip-icon {
    border-color: var(--color-ink);
}

/* ---------- Deal cards (featured rail) ---------- */
.deal-card:hover { transform: translate(2px, 2px); box-shadow: var(--shadow-sm); }
.deal-card-photo { border-radius: 0; }
.deal-card-photo img,
.deal-card-photo.is-illustration img { border-radius: 0; }
.deal-card:active { transform: translate(4px, 4px); box-shadow: var(--shadow-pressed); }

/* ---------- Menu cards ---------- */
.menu-card {
    transition: transform 120ms ease, box-shadow 120ms ease;
}
.menu-card:hover {
    transform: translate(2px, 2px);
    box-shadow: var(--shadow-sm);
}
.menu-card:active { transform: translate(4px, 4px); }
.menu-card-photo { border-radius: 0; }
.menu-card-photo img { border-radius: 0; }
.menu-card-price {
    font-family: var(--font-display);
}

/* ---------- Floating add button: square, hard shadow ---------- */
.btn-add {
    border-radius: 0;
    border: var(--border-w) solid var(--color-ink);
    background: var(--color-accent-tint);
    color: var(--color-ink);
    box-shadow: var(--shadow-sm);
}
.btn-add:hover {
    background: var(--color-primary);
    color: #fff;
    transform: none;
    box-shadow: var(--shadow-sm);
}
.btn-add:active {
    transform: translate(3px, 3px);
    box-shadow: 0 0 0 var(--color-ink);
}

/* ---------- Cart rows: ledger lines stay, harden photo ---------- */
.cart-item-card {
    border-bottom: var(--border-w) solid var(--color-ink);
}
.cart-item-photo {
    border-radius: 0;
    border: var(--border-w) solid var(--color-ink);
}

/* ---------- Quantity stepper: square block ---------- */
.cart-control__stepper,
.qty-stepper {
    border: var(--border-w) solid var(--color-ink);
    border-radius: 0;
    box-shadow: var(--shadow-sm);
    background: var(--color-surface);
}
.cart-control__stepper button,
.qty-stepper button {
    border-radius: 0;
    background: transparent;
    color: var(--color-ink);
}
.cart-control__stepper button:hover,
.qty-stepper button:hover {
    background: var(--color-accent-tint);
    color: var(--color-ink);
}
.cart-control__stepper .qty-value,
.qty-stepper .qty-value {
    font-family: var(--font-display);
    font-weight: 400;
}

/* ---------- Sticky cart bar ---------- */
.sticky-cart-bar {
    border-top: var(--border-w) solid var(--color-ink);
    box-shadow: none;
    background: var(--color-accent-tint);
}
#sticky-cart-bar .btn { box-shadow: var(--shadow-xs); }

/* ---------- Order cards ---------- */
.order-card {
    border: var(--border-w) solid var(--color-ink);
    border-radius: 0;
    box-shadow: var(--shadow-md);
}
.order-card:hover {
    transform: translate(2px, 2px);
    box-shadow: var(--shadow-sm);
}
.order-card-token {
    font-family: var(--font-display);
    text-transform: uppercase;
    letter-spacing: 0.01em;
}

/* ---------- Account sections ---------- */
.account-section {
    border: var(--border-w) solid var(--color-ink);
    border-radius: 0;
    box-shadow: var(--shadow-md);
    overflow: hidden;
}
.account-section > .account-row:first-child,
.account-section > .account-row:last-child,
.account-section > .account-row:only-of-type { border-radius: 0; }
.account-row { border-radius: 0; }
.account-row:hover { background: var(--color-surface-deep); }
.account-section-title {
    font-family: var(--font-display);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

/* ---------- State panels (empty states) ---------- */
.state-panel {
    border: var(--border-w) solid var(--color-ink);
    border-radius: 0;
    box-shadow: var(--shadow-md);
}
.state-panel h4 {
    font-family: var(--font-display);
    text-transform: uppercase;
    font-weight: 400;
    letter-spacing: 0.01em;
}
.state-panel .btn-primary {
    background: var(--color-primary);
    border: var(--border-w) solid var(--color-ink);
    border-radius: 0;
    box-shadow: var(--shadow-sm);
    text-transform: uppercase;
    font-family: var(--font-display);
}
.state-panel .btn-primary:hover {
    background: var(--color-primary-dark);
    transform: translate(2px, 2px);
    box-shadow: var(--shadow-pressed);
}

/* ---------- Form selects / inputs: square ---------- */
.form-select, .form-control {
    border-radius: 0;
    border: var(--border-w) solid var(--color-ink);
}
.form-select:focus, .form-control:focus {
    border-color: var(--color-ink);
    box-shadow: var(--shadow-sm);
}
.form-label {
    font-family: var(--font-display);
    text-transform: uppercase;
    font-size: 0.72rem;
    letter-spacing: 0.05em;
}

/* ---------- Badges: square pills ---------- */
.badge {
    border-radius: 0;
    border: var(--border-w) solid var(--color-ink);
    text-transform: uppercase;
    font-size: 0.7rem;
}

/* ---------- Tables: hard frame, yellow head ---------- */
.table {
    border: var(--border-w) solid var(--color-ink);
    border-radius: 0;
    box-shadow: var(--shadow-md);
}
.table thead th {
    background: var(--color-accent-tint);
    border-bottom: var(--border-w) solid var(--color-ink);
    font-family: var(--font-display);
    text-transform: uppercase;
    font-weight: 400;
}

/* ---------- Alerts ---------- */
.alert {
    border: var(--border-w) solid var(--color-ink);
    border-radius: 0;
    box-shadow: var(--shadow-sm);
    font-family: var(--font-display);
    text-transform: uppercase;
    font-size: 0.8rem;
    letter-spacing: 0.03em;
}

/* ---------- Navbar: hard bottom rule ---------- */

/* ---------- Auth card ---------- */
.auth-card {
    border: var(--border-w) solid var(--color-ink);
    box-shadow: var(--shadow-md);
}
/* 04-auth styles these fields as underlines: a hairline beneath the text and
   `padding: 0.5rem 1.75rem 0.5rem 0`, where zero left padding is correct because there
   is no left edge to sit away from. This layer turns them back into bordered boxes but
   never restored the padding, so every value and placeholder rendered flush against the
   new border. The box and its padding have to be introduced together. */
.field__input {
    border: var(--border-w) solid var(--color-ink);
    border-radius: 0;
    padding-left: 0.85rem;
    padding-right: 0.85rem;
}
/* Room for the reveal button, measured from the field's inner edge. */
.field__input--pw { padding-right: 3rem; }

.field__input:focus {
    border-color: var(--color-ink);
    box-shadow: var(--shadow-sm);
    outline: none;
}

/* The reveal button was drawing an opaque near-white tile (--color-field) hard against
   the field's right border, so it read as a separate control bolted onto the input
   rather than an affordance inside it. It belongs *in* the field: no fill, no edge of
   its own, and inset far enough not to sit on the border it was overlapping. */
.field__toggle {
    background: transparent;
    border: none;
    right: 3px;
    padding: 6px 9px;
    color: var(--color-ink-muted);
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.field__toggle:hover,
.field__toggle:focus-visible {
    background: transparent;
    color: var(--color-ink);
}
.field__toggle:focus-visible {
    outline: var(--border-w) solid var(--color-ink);
    outline-offset: -2px;
}
.field__label {
    font-family: var(--font-display);
    text-transform: uppercase;
    font-size: 0.7rem;
    letter-spacing: 0.05em;
}

/* ---------- Queue cards (canteen) ---------- */
.queue-card,
[class*="queue-"] .card {
    border: var(--border-w) solid var(--color-ink);
    border-radius: 0;
    box-shadow: var(--shadow-md);
}


/* ============================================================
   TABULAR FIGURES

   Anywhere digits are read as a quantity rather than as prose. Archivo's proportional
   figures set a 1 narrower than a 7, so a column of prices, a list of order tokens or a
   4-digit pickup code all shift line to line — which reads as sloppy on a price list and
   makes a code genuinely harder to read aloud across a counter.

   Applied by role rather than globally: body copy with a number in it still wants
   proportional figures.
   ============================================================ */
.price,
.token-badge,
.pickup-code-panel__code,
.order-card-token,
.cart-item-price,
.cart-item-subtotal,
.sticky-pay-total,
.bottom-nav-badge,
.nav-drawer__count,
.money,
.num {
    font-variant-numeric: tabular-nums;
}

/* ==== 08-customer-final.css ==== */
/* ============================================================
   08-customer-final.css — Customer menu, final pass

   The last word on the student menu, navbar and deal cards, including the density
   pass for large menus. These rules exist to settle conflicts among the layers
   above and must stay last.

   Part of an ordered set linked in fragments/head.html. The browser applies these
   in link order and THAT ORDER IS THE CASCADE — a rule here can only override one
   in an earlier file. Do not reorder the links, and do not move a rule between
   files without checking what it then stops overriding.
   ============================================================ */

/* ============================================================
   10-student-menu.css — AUTHORITATIVE student menu layer.
   Scoped to the student menu page. This is the final winning
   layer for the hero, ticker, and menu-card layout. It exists to
   neutralize the conflicting earlier menu rules (floating Add
   disc over the photo, absolute .menu-card-actions) and replace
   them with a normal-flow, reserved-footer layout that cannot
   overlap the name, price, or image.
   ============================================================ */

/* ---------- Hero ---------- */
.menu-hero {
    position: relative;
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: 1rem;
    padding: 2rem 0 1.5rem;
}
.menu-hero-title {
    font-family: var(--font-display);
    font-size: clamp(2.4rem, 7vw, 4.5rem);
    line-height: 0.95;
    letter-spacing: -0.03em;
    text-transform: uppercase;
    color: var(--color-ink);
    margin: 0;
}
.menu-hero-title .outline {
    color: transparent;
    -webkit-text-stroke: 2px var(--color-ink);
    display: block;
}
.menu-hero-shout {
    display: block;
    font-family: var(--font-body);
    font-weight: 700;
    font-size: clamp(1rem, 2.6vw, 1.25rem);
    letter-spacing: 0.02em;
    text-transform: none;
    color: var(--color-primary);
    margin-top: 0.9rem;
}
.menu-hero-outlet {
    margin-top: 1.1rem;
    max-width: 300px;
}
.menu-hero-outlet .form-select {
    border: var(--border-w) solid var(--color-ink);
    border-radius: 0;
    box-shadow: var(--shadow-sm);
    font-weight: 700;
    font-family: var(--font-display);
    text-transform: uppercase;
    font-size: 0.85rem;
}
.menu-hero-sticker {
    flex: 0 0 auto;
    width: clamp(84px, 14vw, 128px);
    height: clamp(84px, 14vw, 128px);
    border-radius: 50%;
    background: var(--color-accent-tint);
    border: var(--border-w) solid var(--color-ink);
    box-shadow: var(--shadow-md);
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    transform: rotate(8deg);
}
.menu-hero-sticker span {
    font-family: var(--font-display);
    font-size: clamp(0.9rem, 2.4vw, 1.2rem);
    line-height: 1;
    letter-spacing: 0.02em;
    color: var(--color-ink);
}

/* ---------- Ticker ---------- */
.ticker {
    background: var(--color-primary);
    border-top: var(--border-w) solid var(--color-ink);
    border-bottom: var(--border-w) solid var(--color-ink);
    overflow: hidden;
    margin: 0 0 1.75rem;
}
.ticker-track {
    display: flex;
    white-space: nowrap;
    animation: ticker-scroll 18s linear infinite;
}
.ticker-track span {
    /* Stated directly rather than inherited from the track, so this cannot be undone
       again by a universal rule. */
    white-space: nowrap;
    flex: none;
    font-family: var(--font-display);
    font-size: 0.95rem;
    letter-spacing: 0.05em;
    color: #fff;
    padding: 0.55rem 0;
}
@keyframes ticker-scroll {
    from { transform: translateX(0); }
    to   { transform: translateX(-50%); }
}

/* ---------- Menu card: normal-flow footer controls ---------- */
/* Neutralize the earlier floating-disc and absolute actions rules. */
.menu-card-actions {
    position: static;
    right: auto;
    bottom: auto;
    margin-top: 0.85rem;
    border-top: var(--border-w) solid var(--color-ink);
    padding-top: 0.85rem;
}
.menu-card-actions .btn-add {
    border: var(--border-w) solid var(--color-ink);
}

/* The card itself: keep the photo square-ish and the body stacked,
   so the footer sits below the text and can never overlap it. */
.menu-card {
    display: flex;
    flex-direction: column;
    border: var(--border-w) solid var(--color-ink);
    border-radius: 0;
    box-shadow: var(--shadow-md);
    background: var(--color-surface);
    overflow: hidden;
}
.menu-card-media {
    position: relative;
    overflow: hidden;
    background: var(--color-surface-deep);
}
.menu-card-photo {
    display: block;
    width: 100%;
    height: 100%;
}
.menu-card-photo img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}
.menu-card-body {
    display: flex;
    flex-direction: column;
    flex: 1;
}
.menu-card-name {
    font-family: var(--font-display);
    line-height: 1.15;
    text-transform: uppercase;
    letter-spacing: 0.01em;
}
.menu-card-price {
    font-weight: 700;
}
.price-strike {
    color: var(--color-ink-muted);
    text-decoration: line-through;
    margin-right: 0.4rem;
    font-weight: 600;
}

/* The cart control: a full-width, no-overlap action row. */
.menu-card-actions .cart-control {
    display: flex;
    width: 100%;
}
.menu-card-actions .cart-control__add {
    flex: 1;
    display: flex;
}
.menu-card-actions .btn-add {
    flex: 1;
    width: 100%;
    height: 44px;
    border-radius: 0;
    background: var(--color-primary);
    color: #fff;
    font-family: var(--font-display);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    box-shadow: var(--shadow-sm);
    display: flex;
    align-items: center;
    justify-content: center;
    transition: transform 100ms ease, box-shadow 100ms ease;
}
.menu-card-actions .btn-add:hover {
    background: var(--color-primary-dark);
    transform: translate(2px, 2px);
    box-shadow: var(--shadow-pressed);
}
.menu-card-actions .btn-add:active { transform: translate(3px, 3px); box-shadow: none; }

/* Stepper: replaces the Add button when .is-active. */
.menu-card-actions .cart-control__stepper {
    display: none;
    flex: 1;
    width: 100%;
    height: 44px;
    border: var(--border-w) solid var(--color-ink);
    border-radius: 0;
    background: var(--color-accent-tint);
    box-shadow: var(--shadow-sm);
    align-items: stretch;
    overflow: hidden;
}
.menu-card-actions .cart-control.is-active .cart-control__add { display: none; }
.menu-card-actions .cart-control.is-active .cart-control__stepper { display: flex; }
.menu-card-actions .cart-control__stepper button {
    flex: 1;
    background: transparent;
    border: none;
    color: var(--color-ink);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.3rem;
    line-height: 1;
    transition: background-color 120ms ease;
}
.menu-card-actions .cart-control__stepper button:hover { background: rgba(20, 16, 13, 0.08); }
.menu-card-actions .cart-control__stepper .cart-qty-value {
    flex: 1.4;
    display: flex;
    align-items: center;
    justify-content: center;
    font-family: var(--font-display);
    font-size: 1.05rem;
    border-left: var(--border-w) solid var(--color-ink);
    border-right: var(--border-w) solid var(--color-ink);
}

/* ---------- Deal cards (today's specials) ---------- */
.deal-card {
    border: var(--border-w) solid var(--color-ink);
    border-radius: 0;
    box-shadow: var(--shadow-sm);
    overflow: hidden;
    background: var(--color-accent-tint);
}
.deal-card .deal-card-body { padding: 0.85rem; }
.deal-card .deal-card-name {
    font-family: var(--font-display);
    text-transform: uppercase;
    letter-spacing: 0.02em;
    font-size: 0.95rem;
}
.deal-card .btn-add.deal-card-add {
    width: auto;
    height: 38px;
    padding: 0 1.1rem;
    border-radius: 0;
    border: var(--border-w) solid var(--color-ink);
    background: var(--color-primary);
    color: #fff;
    box-shadow: var(--shadow-sm);
    font-family: var(--font-display);
    text-transform: uppercase;
    font-size: 0.8rem;
    letter-spacing: 0.04em;
}

/* ---------- Search + chips stay tidy ---------- */
.search-bar {
    border: var(--border-w) solid var(--color-ink);
    border-radius: 0;
    box-shadow: var(--shadow-sm);
    background: var(--color-field);
}
/* This layer turned the chip from a borderless icon-and-label into a bordered card, but
   left the sizing it had as a label: `padding: 0` and `min-width: 72px` with no fixed
   width. So the chip sized itself to its text — 83px for "All Dishes", 72px for "Meals" —
   which put the 62px icon at an 11px inset in one chip and a 5px inset in the next, and
   ran "ALL DISHES" flush into its own border. The label also still carried
   `text-overflow: ellipsis`, which is why "Beverages" rendered as "BEVERAG…".

   A fixed width fixes all of it at once: every chip identical, every icon on the same
   rhythm, and the label free to wrap instead of being cut. Category names are authored by
   outlets and can be any length, so the label is clamped to two lines at a fixed height —
   that keeps the row even no matter what a canteen types. */
.category-chip {
    border: var(--border-w) solid var(--color-ink);
    border-radius: 0;
    background: var(--color-surface);
    font-family: var(--font-display);
    text-transform: uppercase;
    font-size: 0.8rem;
    letter-spacing: 0.03em;
    color: var(--color-ink);
    box-shadow: var(--shadow-sm);

    flex: 0 0 100px;
    width: 100px;
    min-width: 100px;
    padding: 8px 8px 10px;
    gap: 8px;
    justify-content: flex-start;
}

.category-chip .cat-chip-icon {
    width: 58px;
    height: 58px;
    flex: none;
}

.category-chip .cat-chip-label {
    max-width: 100%;
    width: 100%;
    text-align: center;
    font-size: 0.62rem;
    line-height: 1.2;
    letter-spacing: 0.02em;
    /* Two lines, always the same height, so a one-word category and a three-word one
       produce chips of identical size. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    white-space: normal;
    overflow: hidden;
    /* Break at spaces only. `anywhere` split "BEVERAGES" into "BEVERAGE" and an orphaned
       "S" on the second line; a multi-word name like "All Dishes" still wraps normally. */
    overflow-wrap: normal;
    word-break: normal;
    hyphens: none;
    text-overflow: clip;
    min-height: 2.4em;
}
.category-chip.active {
    background: var(--color-primary);
    color: #fff;
}

/* ---------- Sticky cart bar ---------- */
#sticky-cart-bar {
    border-top: var(--border-w) solid var(--color-ink);
    box-shadow: 0 -4px 0 var(--color-ink);
    background: var(--color-surface);
}

/* ============================================================
   10-menu-final.css — authoritative menu-page layer.
   Renders LAST, so it wins every earlier menu rule. Scoped to
   the student menu page only. Fixes: deal-card button placement,
   photo placeholder (no pre-made SVG), mobile deal layout, and
   the broken navbar.
   ============================================================ */

/* ---------- Photo placeholder (no pre-made food SVG) ---------- */
.photo-placeholder {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--color-surface-deep);
    color: var(--color-ink-muted);
}
.photo-placeholder .material-symbols-outlined {
    font-size: 34px;
    color: var(--color-ink-muted);
    opacity: 0.75;
}
.deal-card-photo,
.menu-card-photo {
    position: relative;
    overflow: hidden;
    display: block;
}
.deal-card-photo img,
.menu-card-photo img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

/* ---------- Deal card: stacked body, full-width add bar ---------- */
.deal-card {
    flex: 0 0 200px;
    width: 200px;
}
.deal-card-name {
    margin: 0;
    font-size: 0.95rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.deal-card-price {
    margin: 0;
}
.deal-card-add-form {
    margin-top: 4px;
}

/* ---------- Mobile: deal cards fill width, no cramped buttons ---------- */
@media (max-width: 575.98px) {
    .featured-scroll {
        display: grid;
        grid-template-columns: repeat(2, 1fr);
        gap: 12px;
        overflow: visible;
    }
    .deal-card {
        flex: none;
        width: auto;
    }
}

/* ---------- Navbar fixes ---------- */
.navbar .navbar-brand {
    font-family: var(--font-display);
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: -0.01em;
    color: var(--color-ink) !important;
}
.navbar .navbar-toggler-custom {
    border: var(--border-w) solid var(--color-ink);
    border-radius: 0;
    color: var(--color-ink);
}

/* ============================================================
   11-navbar-final.css — authoritative navbar layer.
   Wins the cascade over every earlier .navbar definition.
   ============================================================ */

.navbar {
    position: sticky;
    top: 0;
    z-index: 1030;
    background: var(--color-bg) !important;
    border-bottom: var(--border-w) solid var(--color-ink);
    box-shadow: none;
    
    
    min-height: 60px;
    padding-top: 0.5rem;
    padding-bottom: 0.5rem;
}

.navbar-brand {
    font-family: var(--font-display);
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: -0.01em;
    font-size: 1.05rem;
    color: var(--color-ink) !important;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 55vw;
}

.brand-mark {
    height: 28px;
    width: auto;
    flex: none;
}

/* Bootstrap's .navbar is flex-wrap: wrap and its .container inherits that, so as soon
   as the brand plus the button cluster exceeded one line the cluster dropped onto a
   second row — which is what a phone was showing: a college name on one line, then a
   logout button and a menu button floating on the line beneath it.

   The bar is two things, a brand and its controls, and it is always one row. The brand
   is the part that gives: it already truncates. */
.navbar > .container {
    flex-wrap: nowrap;
}

.navbar .navbar-brand {
    min-width: 0;
    flex: 0 1 auto;
}

/* text-overflow only acts on a block container's own inline content, and .navbar-brand
   is a flex row, so the ellipsis never applied: a long college name was cut mid-word
   with no indication it had been ("NIET GREATER N"). The truncation belongs on the
   element holding the text, not on the flex parent. */
.navbar-brand > span {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

@media (max-width: 767.98px) {
    /* The top links and sign-out are both hidden at this width, so the only thing to the
       brand's right is the drawer button. 55vw was sized for a fuller bar and was
       clipping names that now have room to sit. */
    .navbar .navbar-brand {
        max-width: calc(100vw - 6.5rem);
        font-size: 0.98rem;
    }
}

.navbar-right {
    margin-left: auto;
    min-width: 0;
    flex: 0 0 auto;
    /* The cluster is a menu button, a name, a role switcher and a sign-out control. Left
       to wrap it stacked into two ragged rows on the console, where all four are present
       at once; the brand beside it already truncates, so the space comes from there. */
    flex-wrap: nowrap;
}

@media (max-width: 767.98px) {
    /* Sign-out leaves the bar on a phone. It is not lost — the drawer this hamburger
       opens ends with a full-width "Sign out", so the control is one tap away and the
       header is left with the two things it needs: who you are looking at, and the way
       into everything else. Signed-out visitors keep their Login button, since there is
       no drawer to put it in. */
    .navbar .navbar-logout {
        display: none !important;
    }

    /* A multi-role account's switcher is the one remaining item that can outgrow the
       row. Cap it rather than let it push the brand out. */
    .navbar-right .form-select {
        max-width: 8.5rem;
    }
}

.navbar .navbar-text {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 14rem;
}

.navbar-toggler-custom {
    border: var(--border-w) solid var(--color-ink) !important;
    border-radius: 0 !important;
    background: transparent;
    color: var(--color-ink);
    width: 40px;
    height: 40px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    box-shadow: var(--shadow-sm);
}

.navbar-toggler-custom:active {
    transform: translate(2px, 2px);
    box-shadow: var(--shadow-pressed);
}

.navbar .nav-link {
    color: var(--color-ink) !important;
    font-weight: 600;
    font-size: 0.9rem;
    border-radius: 0;
    white-space: nowrap;
}

.navbar .nav-link:hover {
    background: var(--color-accent-tint);
    color: var(--color-ink) !important;
}

.navbar .top-nav-links .nav-link {
    border-radius: 0;
}


/* Logout / login buttons match the brutalist language */
.navbar .btn-outline-secondary {
    border: var(--border-w) solid var(--color-ink) !important;
    border-radius: 0 !important;
    color: var(--color-ink) !important;
    background: transparent;
    font-weight: 700;
    box-shadow: var(--shadow-sm);
    white-space: nowrap;
    flex: none;
}

.navbar .btn-outline-secondary:hover {
    background: var(--color-accent-tint) !important;
    color: var(--color-ink) !important;
}

.navbar .btn-outline-secondary .material-symbols-outlined {
    font-size: 18px;
    vertical-align: -4px;
}

/* Collapsed staff/admin menu on mobile */
.navbar-links-collapse.is-open {
    border: var(--border-w) solid var(--color-ink);
    border-top: none;
    box-shadow: var(--shadow-md);
    background: var(--color-bg);
}

@media (max-width: 991.98px) {
    .navbar-links-collapse {
        position: absolute;
        left: 0;
        right: 0;
        top: 100%;
        z-index: 1040;
        padding: 8px 16px 12px;
        display: none;
    }
    .navbar-links-collapse.is-open {
        display: flex;
        flex-direction: column;
        align-items: stretch;
    }
    .navbar-links-collapse .nav-link {
        display: flex;
        align-items: center;
        gap: 8px;
        padding: 10px 8px;
    }
}

/* ---------- Student mobile: bottom-nav safe ---------- */
@media (max-width: 991.98px) {
    .navbar .top-nav-links {
        display: none !important;
    }
}

/* ============================================================
   12-deal-cards-final.css — Today's specials, final layer.
   ============================================================ */

.deal-card-body {
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.deal-card-add-form {
    display: block;
    width: 100%;
    margin: 4px 0 0;
}

.deal-card-add {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 40px;
    background: var(--color-primary);
    color: #fff;
    border: none;
    border-radius: 0;
    font-family: var(--font-display);
    font-size: 0.85rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    cursor: pointer;
}

.deal-card-add::before {
    content: 'add';
    /* Ligature-selected, so the inherited uppercase from the button would turn
       'add' into 'ADD' and render the word instead of the glyph. */
    text-transform: none;
    letter-spacing: normal;
    font-family: 'Material Symbols Outlined';
    font-size: 18px;
    margin-right: 6px;
}

/* Very narrow phones: one column so cards never crush */
@media (max-width: 359.98px) {
    .featured-scroll {
        grid-template-columns: 1fr;
    }
}

/* ============================================================
   13-menu-density.css — scale fix for large menus (60+ items).
   Dense cards, tighter grid, compact category headers.
   ============================================================ */

/* Category headers: compact label rows, not big display blocks */
.menu-category-section h5 {
    font-family: var(--font-display);
    font-size: 1rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    padding-bottom: 4px;
    border-bottom: var(--border-w) solid var(--color-ink);
    display: flex;
    align-items: center;
    gap: 8px;
}

/* Grid: 2 cols phone, 3 small tablet, 4 desktop — already set via
   Bootstrap cols, but tighten gutters so more fits per screen. */
.menu-category-section .row { --bs-gutter-x: 12px; --bs-gutter-y: 14px; }

/* Cards: smaller type, tighter padding — density without crowding */
.menu-card-body { padding: 10px 12px 12px; }
.menu-card-name { font-size: 0.9rem; margin-bottom: 0.2rem; }
.menu-card-price { font-size: 0.95rem; margin-bottom: 0.4rem; }

/* Card photos: 4:3 instead of square — shorter cards, more per screen */
.menu-card-media { aspect-ratio: 4 / 3; }

/* Compact stock notes so low-stock/sold-out doesn't inflate card height */
.stock-note {
    font-size: 0.72rem;
    padding: 2px 6px;
    display: inline-flex;
    align-items: center;
    gap: 4px;
}
.stock-note .material-symbols-outlined { font-size: 14px; }

/* Stepper/add controls: full-width but slimmer */
.menu-card-actions .cart-control { min-height: 40px; }
.menu-card-actions .btn-add { font-size: 0.8rem; padding: 6px 0; }
.menu-card-actions .cart-control__stepper button { width: 36px; height: 36px; }

/* Sticky chip strip: solid background so cards sliding under don't show
   through, and a hard bottom border in the design language */
@media (max-width: 767.98px) {
    .category-chips-wrapper {
        position: sticky;
        top: 60px;
        z-index: 1020;
        background: var(--color-bg);
        border-bottom: var(--border-w) solid var(--color-ink);
        margin-inline: -1rem;
        padding: 8px 1rem 10px;
        overflow-x: auto;
        -webkit-overflow-scrolling: touch;
        scrollbar-width: none;
    }
    .category-chips-wrapper::-webkit-scrollbar { display: none; }
}

/* Defensive: never let snap/overflow rules touch vertical page scroll */
html { scroll-behavior: auto !important; }
body { overscroll-behavior-y: auto !important; }


/* ============================================================
   ALERTS — readable sentences, not banners.

   07-brutalist styles .alert in uppercase Archivo Black with letter-spacing. That reads
   well for "SAVED" and badly for "Payment cancelled. Your order is still held — tap Pay
   to try again." — which is what this app actually puts in alerts. 09-console already
   exempts the staff portals; these are the customer-facing ones.
   ============================================================ */

.alert {
    font-family: var(--font-body);
    font-weight: 500;
    text-transform: none;
    letter-spacing: normal;
    font-size: 0.92rem;
    line-height: 1.45;
    text-align: left;
    padding: 0.75rem 0.9rem;
}

.alert-success { background: var(--color-accent-tint); color: var(--color-ink); }
.alert-warning { background: var(--color-accent-tint); color: var(--color-ink); }
.alert-danger  { background: var(--color-primary-tint); color: var(--color-ink); }
.alert-info    { background: var(--color-surface-deep); color: var(--color-ink); }

/* The payment failure notice is injected directly after the pay button, which put it
   flush against the "Cancel and return to cart" link below with no separation at all —
   the two read as one block. */
#pay-notice { margin-bottom: 1.25rem; }


/* ============================================================
   CART PAY BAR + CHECKOUT — squaring-up fallout.
   ============================================================ */

/* The label wrapped to two lines once the bar lost its pill shape, which pushed the
   amount out of the bar entirely — "To Pay" with no total under it. */
.sticky-pay-inner .btn-primary {
    white-space: nowrap;
    padding-inline: 1rem;
    font-size: 0.92rem;
}

/* The amount was inheriting ink from the price styling and rendering near-black on the
   near-black bar — present in the DOM, invisible on screen. The bar is a dark surface, so
   its contents have to state their own colour rather than inherit the page's. */
.sticky-pay-total span {
    white-space: nowrap;
    color: var(--color-bg);
}
.sticky-pay-total small {
    color: rgba(242, 234, 217, 0.7);
}

/* The bar is taller than the pill it replaced, and cart.html reserves a fixed 90px of
   scroll clearance for it, so the closing paragraph was sliding underneath. Reserving
   the space here keeps the number next to the bar's real height rather than in a style
   attribute in the template. */
/* Clearance for the sticky bar. It is taller than it looks: 80px of bar plus the 76px
   of bottom padding that lifts it above the tab nav, so ~157px total. The old inline
   90px predated the bottom nav and left the closing paragraph underneath it. */
.cart-page-body {
    padding-bottom: 176px !important;
}

/* Everything else on these screens is square; this was the last circle. */
.icon-circle {
    border-radius: 0;
    border: var(--border-w) solid var(--color-ink);
    box-shadow: var(--shadow-sm);
}


/* ============================================================
   BOTTOM TAB NAV — one authoritative definition.

   Three separate sheets were each setting part of this, and between them the bar was
   64px tall while its contents only filled 61px: 8px of padding-top, 53px of item, and
   no padding-bottom. That left a 3px strip of bare canvas along the bottom edge of a bar
   that is supposed to sit flush to the screen, which reads as a rendering fault.

   Items were also a fixed 72px each. Five of those plus the bar's own 16px side padding
   comes to 392px against a 390px viewport, so they were pressed together with nothing
   between them. They now divide the width evenly and centre inside the full bar height.
   ============================================================ */

@media (max-width: 991.98px) {
    .bottom-nav {
        display: flex;
        align-items: stretch;
        height: calc(var(--bottom-nav-height) + var(--safe-area-bottom));
        padding: 0 4px var(--safe-area-bottom);
        border-top: var(--border-w) solid var(--color-ink);
        background: var(--color-surface);
    }

    .bottom-nav-item {
        /* Even division rather than a fixed width, so the row fits any viewport and the
           tap target grows on wider phones instead of leaving a gutter. */
        flex: 1 1 0;
        min-width: 0;
        display: flex;
        flex-direction: column;
        align-items: center;
        /* Centred in the full height — this is what closes the strip along the bottom. */
        justify-content: center;
        height: 100%;
        padding: 0 2px;
        gap: 3px;
        font-size: 0.62rem;
        letter-spacing: 0.01em;
        line-height: 1;
    }

    .bottom-nav-item .material-symbols-outlined {
        font-size: 22px;
        line-height: 1;
    }

    /* The active tab was marked twice: a red colour change and a 3px red bar pinned to
       the top edge of the item. The bar sat hard against the bar's own black border and
       added a second horizontal rule to a component that already has one. Colour alone
       carries it. */
    .bottom-nav-item.active::before {
        display: none;
    }

    .bottom-nav-item.active {
        color: var(--color-primary-dark);
    }

    .bottom-nav-item.active .material-symbols-outlined {
        font-variation-settings: 'FILL' 1;
        transform: none;
    }

    /* Long labels ("Account", "Support") must not wrap and shove the icons out of line. */
    .bottom-nav-item > span:not(.material-symbols-outlined) {
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
        max-width: 100%;
    }
}


/* ============================================================
   ACTIVE ORDER STRIP

   Sits under the top bar on every customer screen whenever an order is unfinished. The
   colour states the situation before the text is read: ink for in-progress, yellow while
   cooking, red when money is outstanding, and ink-with-yellow when food is waiting on the
   counter — the one state where a student losing track has a real cost.
   ============================================================ */

.active-order-strip {
    /* Deliberately in normal flow, not sticky. `html, body { overflow-x: hidden }` in
       02-base makes the body the nearest scrollport, and a sticky child of a scrollport
       whose height is its own content has nothing to stick within — the top navbar is
       already inert for the same reason. Rather than leave a declaration that does
       nothing, the strip sits at the top of every customer page (so it is seen on every
       load and every navigation) and the Orders tab badge in the bottom nav carries the
       persistent signal while scrolling. */
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 16px;
    border-bottom: var(--border-w) solid var(--color-ink);
    background: var(--color-ink);
    color: var(--color-bg);
    text-decoration: none;
    font-family: var(--font-body);
}

.active-order-strip:hover,
.active-order-strip:focus {
    color: var(--color-bg);
    text-decoration: none;
}

.active-order-strip__pulse {
    width: 10px;
    height: 10px;
    flex: none;
    border: 2px solid currentColor;
    background: currentColor;
    animation: strip-pulse 1.8s ease-in-out infinite;
}

@keyframes strip-pulse {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.25; }
}

@media (prefers-reduced-motion: reduce) {
    .active-order-strip__pulse { animation: none; }
}

.active-order-strip__text {
    display: flex;
    flex-direction: column;
    min-width: 0;
    line-height: 1.2;
}

.active-order-strip__text strong {
    font-family: var(--font-display);
    font-weight: 400;
    text-transform: uppercase;
    font-size: 0.78rem;
    letter-spacing: 0.03em;
}

.active-order-strip__meta {
    font-size: 0.72rem;
    opacity: 0.75;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.active-order-strip__cta {
    margin-left: auto;
    display: inline-flex;
    align-items: center;
    gap: 4px;
    flex: none;
    font-family: var(--font-display);
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    border: 2px solid currentColor;
    padding: 5px 9px;
}

.active-order-strip__cta .material-symbols-outlined { font-size: 16px; }

/* Ready to collect — the loudest pairing in the palette, and used nowhere else. */
.active-order-strip.is-ready {
    background: var(--color-ink);
    color: var(--color-accent-tint);
}

/* Being prepared. */
.active-order-strip.is-preparing {
    background: var(--color-accent-tint);
    color: var(--color-ink);
}

/* Money outstanding: nothing happens until this is resolved. */
.active-order-strip.is-unpaid {
    background: var(--color-primary-dark);
    color: #fff;
}

/* Confirmed, not yet started. */
.active-order-strip.is-paid {
    background: var(--color-ink);
    color: var(--color-bg);
}

/* ---------- Orders screen grouping ---------- */

.orders-group--active {
    margin-bottom: 1.75rem;
}

.orders-group--active .section-label {
    display: flex;
    align-items: center;
    gap: 0.45rem;
}

.orders-group--active .order-card {
    border-width: 3px;
    box-shadow: var(--shadow-md);
}

/* The category strip offsets 60px for the top navbar, but that bar does not stay put —
   it is `sticky` inside a wrapper exactly its own height, so it scrolls away. Pinning the
   strip below a bar that is not there left it floating with scrolled content showing
   through the gap. On phones it pins flush instead. */
@media (max-width: 991.98px) {
    .category-chips-wrapper { top: 0; }
}

/* ============================================================
   PHONE DENSITY — get food onto the first screen.

   Reordering search and categories above the specials rail moved them from 725px and
   819px up to 370px and 464px, but the first dish still sat at 1029px on an 844px
   screen: the hero, the ticker and a 287px carousel between them account for most of a
   viewport before anything orderable appears.

   Nothing is removed — the greeting, the ticker and the rail all still do their job.
   They are simply sized for a phone rather than a desktop hero.
   ============================================================ */

@media (max-width: 767.98px) {
    .menu-hero {
        margin-bottom: 0.75rem;
    }

    .menu-hero-title {
        font-size: clamp(1.6rem, 9vw, 2.2rem);
        line-height: 1.04;
    }

    .menu-hero-sticker {
        width: 76px;
        height: 76px;
    }

    /* Decorative, so it takes a decorative amount of room. */
    .ticker {
        margin-bottom: 1rem;
    }

    .ticker-track span {
        font-size: 0.8rem;
        padding: 0.35rem 0;
    }

    .search-bar {
        margin-bottom: 0.85rem;
    }

    .category-chips-wrapper {
        margin-bottom: 1rem;
    }

    .category-chips {
        gap: 8px;
        padding: 2px 2px 6px;
    }

    .cat-chip-icon {
        width: 52px;
        height: 52px;
    }

    /* The rail is discovery you pass on the way down, not a gate in front of the menu. */
    .featured-scroll .deal-card {
        width: 150px;
    }

    .featured-scroll .deal-card-photo {
        aspect-ratio: 1;
    }

    .deal-card-name {
        font-size: 0.85rem;
    }
}

/* The deal card's photo is 120px but the card is 279px: the name, price and Add control
   below it were sized for a desktop card. On a phone the rail is a glance, not a
   destination. */
@media (max-width: 767.98px) {
    .featured-scroll .deal-card-photo { margin-bottom: 8px; }
    .featured-scroll .deal-card-body { gap: 2px; }
    .featured-scroll .deal-card-name { font-size: 0.8rem; line-height: 1.15; }
    .featured-scroll .deal-card-price { font-size: 0.9rem; }
    .featured-scroll .deal-card-add {
        padding: 0.3rem 0.6rem;
        font-size: 0.72rem;
        min-height: 0;
    }
    .featured-scroll .deal-card-add-form { margin-top: 4px; }

    /* Section heading above the rail. */
    .featured-scroll ~ *, .featured-scroll { margin-top: 0; }
}

@media (max-width: 767.98px) {
    .cat-chip-icon { width: 46px; height: 46px; }
    .cat-chip-label { font-size: 0.62rem; }
    .category-chip { min-width: 58px; }
}

/* ============================================================
   SWIPE TO ADD UNDO TOAST
   ============================================================ */
.undo-toast-container {
    position: fixed;
    bottom: 100px; /* Above the bottom nav */
    left: 0;
    right: 0;
    display: flex;
    justify-content: center;
    pointer-events: none;
    z-index: 1060; /* Above bottom nav */
}

.undo-toast {
    display: flex;
    align-items: center;
    gap: 8px;
    background: var(--color-surface);
    border: var(--border-w) solid var(--color-ink);
    color: var(--color-ink);
    padding: 12px 16px;
    border-radius: 0;
    box-shadow: var(--shadow-md);
    font-family: var(--font-display);
    font-size: 0.9rem;
    text-transform: none;
    letter-spacing: normal;
    pointer-events: all;
    max-width: 90%;
    width: auto;
}

.undo-toast .material-symbols-outlined {
    font-variation-settings: 'FILL' 1;
    font-size: 1.2rem;
}

.undo-button {
    background: transparent;
    border: var(--border-w) solid var(--color-ink);
    color: var(--color-ink);
    padding: 4px 8px;
    font-family: var(--font-display);
    font-size: 0.8rem;
    text-transform: uppercase;
    letter-spacing: 0.02em;
    border-radius: 0;
    cursor: pointer;
    transition: background-color 120ms ease, color 120ms ease;
}

.undo-button:hover {
    background: var(--color-primary);
    color: #fff;
}

.undo-button:active {
    background: var(--color-primary-dark);
    color: #fff;
}

/* ==== 09-console.css ==== */
/* ============================================================
   09-console.css — the outlet and admin portals.

   Loads after app.css and is the last sheet in the set. It is NOT another
   override layer for the customer app: almost everything here is scoped to
   `body.console`, which only the staff templates carry. The handful of
   deliberately unscoped rules at the top are correctness fixes that were wrong
   product-wide, and each says so.

   The design language is the same one the customer app uses (bone canvas,
   near-black 3px borders, hard offset shadows, Archivo Black display type). What
   differs is density and rhythm. A student browses a menu; staff work a queue and
   scan tables. So: smaller type, tighter rows, more per screen, and colour used
   to mean something rather than to decorate.
   ============================================================ */


/* ============================================================
   STATUS COLOUR — unscoped, because it was wrong everywhere.

   The palette is strictly five colours (bone, ink, red, red-dark, yellow) — the
   burger-landing reference has no green, so "success = green" is not available
   and inventing a sixth hue would break the language.

   Before this, `.badge.bg-success` resolved to `--color-primary`, which is red.
   So an ACTIVE college, an ACTIVE user and a COMPLETED order all rendered in the
   same red as a cancellation. Red meant everything, therefore nothing.

   The system below spends the palette on meaning instead:

     red      is reserved for cancelled, refunded, suspended, destructive
     yellow   is work in progress — something is happening right now
     ink fill is a state that wants your attention but is not a problem
     bone     is settled or idle; nothing to do

   Two ink-filled states (PAID, READY_FOR_PICKUP) are told apart by their text
   colour, and the two bone states by their border — dashed reads as provisional.
   ============================================================ */

.badge {
    border: var(--border-w) solid var(--color-ink);
    border-radius: 0;
    font-family: var(--font-display);
    font-weight: 400;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-size: 0.68rem;
    padding: 0.32em 0.6em;
    white-space: nowrap;
}

/* Awaiting payment — provisional, so a dashed edge. Given its own class rather than
   reusing bg-secondary: that class also labels categories and item counts, and a
   dashed border on a category name says "not settled yet" about something that is
   simply a fact. */
.badge.badge-awaiting {
    background: var(--color-bg);
    color: var(--color-ink-muted);
    border-style: dashed;
}

/* A neutral tag: category names, counts, anything that classifies rather than
   reports a state. */
.badge.bg-secondary {
    background: var(--color-surface-deep) !important;
    color: var(--color-ink-muted) !important;
}

/* Paid — money is in, this needs working on. */
.badge.bg-info {
    background: var(--color-ink) !important;
    color: var(--color-bg) !important;
}

/* In progress. */
.badge.bg-warning {
    background: var(--color-accent-tint) !important;
    color: var(--color-ink) !important;
}

/* Ready for pickup — the loudest non-error state in the product. */
.badge.bg-primary {
    background: var(--color-ink) !important;
    color: var(--color-accent-tint) !important;
}

/* Settled: completed orders, active colleges, active accounts. Quiet on purpose —
   the normal case should not compete with the exceptions. */
.badge.bg-success {
    background: var(--color-bg) !important;
    color: var(--color-ink) !important;
    border-style: solid;
}

/* Cancelled, refunded, suspended. The only red badge. */
.badge.bg-danger {
    background: var(--color-primary) !important;
    color: #fff !important;
}

/* `bg-white` was never remapped, so 14 staff tables and panels painted stark
   #fff onto the bone canvas. Bone is the surface colour in this system. */
.bg-white {
    background-color: var(--color-surface) !important;
}

/* Icons sat flush against their labels everywhere ("<icon>Queue", "<icon>Colleges")
   because nothing gave the pairing a gap. Fixing it on the shared components
   rather than per template. */
.nav-link,
.btn,
.dropdown-item {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
}

.btn .material-symbols-outlined,
.nav-link .material-symbols-outlined {
    font-size: 18px;
    line-height: 1;
    flex: none;
}

/* Buttons that carry only an icon should be square, not a wide slab with a
   centred glyph. */
.btn.is-icon-only {
    padding-left: 0.5rem;
    padding-right: 0.5rem;
}


/* ============================================================
   APP CHROME — the top bar and the console strip travel together.

   `.navbar` is sticky in app.css. With a second bar underneath, stickiness has to
   move up to the wrapper or the two would pin independently and overlap.
   ============================================================ */

body.console .app-chrome {
    position: sticky;
    top: 0;
    z-index: 1030;
    background: var(--color-bg);
}

body.console .app-chrome .navbar {
    position: static;
    border-bottom: var(--border-w) solid var(--color-ink);
    min-height: 52px;
    padding-top: 0.35rem;
    padding-bottom: 0.35rem;
}

/* The bars used to carry `mb-4`. Spacing now lives here so the sticky wrapper
   isn't the thing holding a margin. */
body.console .app-chrome + * {
    margin-top: 1.5rem;
}


/* ============================================================
   CONSOLE NAV — one scrollable strip of destinations.

   Scrollable rather than collapsible: with ten places to be, a hamburger adds a
   tap to every single navigation. The strip keeps them all one tap away at every
   width, and simply scrolls when they don't fit.
   ============================================================ */

/* Bone, not ink.

   This strip runs across the top of every staff and admin page, directly under a
   navbar that is also full width. Painted solid near-black it was the largest
   single block of colour in the console — the first thing your eye landed on was
   a black bar, on every screen, saying nothing. The destinations it holds are
   navigation, not content, and navigation should recede.

   It keeps its shape, its uppercase display type and its yellow current-page
   marker, so nothing about the language changes. It simply stops shouting. */
.console-nav {
    background: var(--color-bg);
    border-bottom: var(--rule-w) solid var(--rule-color-strong);
}

.console-nav__strip {
    display: flex;
    align-items: stretch;
    gap: 0;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    /* The strip is the horizontal scroller, so a swipe along it must not turn into
       a browser back-navigation. */
    overscroll-behavior-x: contain;
}

.console-nav__strip::-webkit-scrollbar {
    display: none;
}

/* Scroll affordance: the last visible link fades into the bar instead of ending in a
   hard cut, because a hard cut reads as "that's all of them" — exactly wrong here.

   `margin-left: auto` is what makes it conditional without measuring anything in JS.
   When the links fit there is free space, so auto absorbs it and parks the gradient
   over empty bar, where ink-fading-into-ink is invisible. When they don't fit there
   is no free space, auto collapses to zero, and sticky pins the gradient to the right
   edge of the scrollport over whichever link is under it. */
.console-nav__strip::after {
    content: '';
    position: sticky;
    right: 0;
    flex: none;
    width: 32px;
    margin-left: auto;
    align-self: stretch;
    background: linear-gradient(to right, rgba(242, 234, 217, 0), var(--color-bg) 85%);
    pointer-events: none;
}

.console-nav__link {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    flex: none;
    padding: 0.6rem 0.85rem;
    color: var(--color-ink-muted);
    text-decoration: none;
    font-family: var(--font-display);
    font-weight: 400;
    font-size: 0.76rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    white-space: nowrap;
    border-right: var(--rule-w) solid var(--rule-color);
    transition: background-color 120ms ease, color 120ms ease;
}

.console-nav__link .material-symbols-outlined {
    font-size: 17px;
    line-height: 1;
}

.console-nav__link:hover,
.console-nav__link:focus-visible {
    background: var(--color-surface-deep);
    color: var(--color-ink);
    outline: none;
}

/* Where you are. Yellow is not used for any status, so it never reads as a state.
   The underline carries the marker on its own if the fill is ever missed. */
.console-nav__link[aria-current="page"] {
    background: var(--color-accent-tint);
    color: var(--color-ink);
    box-shadow: inset 0 -3px 0 var(--color-ink);
}

@media (max-width: 575.98px) {
    .console-nav .container {
        padding-left: 0;
        padding-right: 0;
    }
    .console-nav__link {
        padding: 0.55rem 0.7rem;
        font-size: 0.72rem;
    }
}


/* ============================================================
   PAGE HEAD — title, optional blurb, actions.

   Replaces the `d-flex justify-content-between` rows the staff screens used. Those
   had no wrap and no gap, so at 390px the heading and the buttons rendered on top
   of each other — the "COLLEGES" title sat underneath its own action stack.
   ============================================================ */

body.console .console-head {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 0.75rem 1rem;
    margin-bottom: 1.25rem;
    padding-bottom: 0.75rem;
    border-bottom: var(--border-w) solid var(--color-ink);
}

/* Works with the markup already in the templates: a heading plus whatever action
   sits beside it (a link, a form, a filter select, a count). The heading takes the
   slack, everything else keeps its natural width. No wrapper divs required, so
   adopting this was a one-class change per screen. */
body.console .console-head__text,
body.console .console-head > h1,
body.console .console-head > h2,
body.console .console-head > h3 {
    min-width: 0;
    flex: 1 1 16rem;
}

body.console .console-head > :not(h1):not(h2):not(h3):not(.console-head__text):not(.console-head__blurb) {
    flex: 0 0 auto;
    margin-bottom: 0;
}

/* A filter select in the header should size to its content, not stretch. */
body.console .console-head .form-select {
    width: auto;
    min-width: 10rem;
}

body.console .console-head h1,
body.console .console-head h2,
body.console .console-head h3 {
    margin: 0;
    font-family: var(--font-display);
    font-weight: 400;
    text-transform: uppercase;
    font-size: 1.55rem;
    line-height: 1.1;
    letter-spacing: -0.01em;
    color: var(--color-ink);
}

/* When the blurb is a direct child (title and description with no actions beside
   them) it belongs on its own line under the title, not next to it. */
body.console .console-head > .console-head__blurb {
    flex: 1 1 100%;
}

body.console .console-head__blurb {
    margin: 0.3rem 0 0;
    font-size: 0.85rem;
    line-height: 1.45;
    color: var(--color-ink-muted);
    max-width: 60ch;
}

body.console .console-head__actions {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
}

/* A count or short fact that belongs to the title rather than to a row. */
body.console .console-head__count {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    margin-left: 0.5rem;
    padding: 0.15em 0.5em;
    background: var(--color-accent-tint);
    border: var(--border-w) solid var(--color-ink);
    font-family: var(--font-display);
    font-size: 0.72rem;
    text-transform: uppercase;
    vertical-align: 0.25em;
}

@media (max-width: 575.98px) {
    body.console .console-head {
        align-items: stretch;
    }
    body.console .console-head h1,
    body.console .console-head h2,
    body.console .console-head h3 {
        font-size: 1.3rem;
    }
    /* Everything that isn't the heading drops to its own full-width line. This is the
       collision fix: the old flex row had no wrap, so at 390px the title and the
       action stack were laid over each other. */
    body.console .console-head > :not(h1):not(h2):not(h3):not(.console-head__text):not(.console-head__blurb) {
        flex: 1 1 100%;
        width: 100%;
    }
    body.console .console-head__actions {
        width: 100%;
    }
    body.console .console-head__actions > * {
        flex: 1 1 auto;
    }
    body.console .console-head .btn,
    body.console .console-head .form-select {
        width: 100%;
        min-width: 0;
        justify-content: center;
    }
    /* Two side-by-side actions stay side by side rather than stacking into a tower. */
    body.console .console-head > div:not(.console-head__text) {
        display: flex;
        gap: 0.5rem;
    }
}


/* ============================================================
   TABLES — the admin portal is mostly tables.
   ============================================================ */

body.console .table-responsive {
    border: var(--border-w) solid var(--color-ink);
    box-shadow: var(--shadow-md);
    background: var(--color-surface);
    overscroll-behavior-x: contain;
}

/* The frame belongs to the scroll container when there is one, or the table would
   carry a second border that scrolls away from the first. */
body.console .table-responsive > .table {
    border: none;
    box-shadow: none;
    margin-bottom: 0;
}

body.console .table {
    --bs-table-bg: transparent;
    background: var(--color-surface);
    font-size: 0.88rem;
    margin-bottom: 0;
}

body.console .table > :not(caption) > * > * {
    background: transparent;
    padding: 0.6rem 0.75rem;
    vertical-align: middle;
    /* A thirty-row table drew thirty near-black rules. Separating rows only needs
       enough contrast to follow one across; at full ink it became a grid of bars. */
    border-bottom-color: var(--rule-color);
}

body.console .table thead th {
    background: var(--color-accent-tint);
    /* Sits a couple of pixels below the panel border above it. Two structural
       weights that close together read as one thick bar rather than as a frame and
       a header, which is what made these tables look banded. */
    border-bottom: var(--rule-w) solid var(--rule-color-strong);
    font-family: var(--font-display);
    font-weight: 400;
    text-transform: uppercase;
    font-size: 0.7rem;
    letter-spacing: 0.06em;
    white-space: nowrap;
    position: sticky;
    top: 0;
    z-index: 1;
}

body.console .table tbody tr:last-child > * {
    border-bottom: none;
}

body.console .table tbody tr:hover > * {
    background: var(--color-surface-deep);
}

/* Numbers line up when they share a width. */
body.console .table .num,
body.console .table .money {
    font-variant-numeric: tabular-nums;
    text-align: right;
    white-space: nowrap;
}

/* The action cell should not stretch — it holds buttons, not prose. */
body.console .table .row-actions {
    display: flex;
    gap: 0.35rem;
    flex-wrap: wrap;
    justify-content: flex-end;
}

body.console .table .row-actions .btn {
    box-shadow: var(--shadow-xs);
}

/* Columns that carry decoration rather than data (a logo thumbnail, say) step aside
   on phones so the columns that matter aren't pushed off the scrollport. */
@media (max-width: 575.98px) {
    body.console .table .col-optional {
        display: none;
    }
    body.console .table > :not(caption) > * > * {
        padding: 0.5rem 0.5rem;
    }
}

/* A short identifier column — token numbers, references, codes. */
body.console .table .mono {
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 0.82rem;
    letter-spacing: -0.01em;
}


/* ============================================================
   PANELS — a bordered block that is not a table.
   ============================================================ */

body.console .console-panel {
    border: var(--border-w) solid var(--color-ink);
    box-shadow: var(--shadow-md);
    background: var(--color-surface);
    padding: 1rem 1.1rem;
    margin-bottom: 1.25rem;
}

body.console .console-panel__title {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    margin: 0 0 0.75rem;
    font-family: var(--font-display);
    font-weight: 400;
    text-transform: uppercase;
    font-size: 0.95rem;
    letter-spacing: 0.02em;
}

/* Metric row — the sales report and any at-a-glance figures. */
body.console .stat-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
    gap: 0.75rem;
    margin-bottom: 1.25rem;
}

body.console .stat {
    border: var(--border-w) solid var(--color-ink);
    box-shadow: var(--shadow-sm);
    background: var(--color-surface);
    padding: 0.75rem 0.9rem;
}

body.console .stat__label {
    display: block;
    font-size: 0.68rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--color-ink-muted);
    font-family: var(--font-display);
}

body.console .stat__value {
    display: block;
    margin-top: 0.2rem;
    font-family: var(--font-display);
    font-size: 1.5rem;
    line-height: 1.1;
    font-variant-numeric: tabular-nums;
    color: var(--color-ink);
}

body.console .stat--accent {
    background: var(--color-accent-tint);
}


/* ============================================================
   FORMS — denser than the customer app's.
   ============================================================ */

body.console .form-label {
    font-family: var(--font-display);
    font-weight: 400;
    text-transform: uppercase;
    font-size: 0.68rem;
    letter-spacing: 0.05em;
    margin-bottom: 0.25rem;
    color: var(--color-ink);
}

body.console .form-control,
body.console .form-select {
    background: var(--color-field);
    border: var(--border-w) solid var(--color-ink);
    border-radius: 0;
    font-size: 0.9rem;
    padding: 0.45rem 0.6rem;
}

body.console .form-control:focus,
body.console .form-select:focus {
    border-color: var(--color-ink);
    box-shadow: var(--shadow-sm);
    outline: none;
}

body.console .form-text {
    font-size: 0.78rem;
    color: var(--color-ink-muted);
}

/* An inline create/filter row that wraps instead of overflowing. */
body.console .form-row {
    display: flex;
    flex-wrap: wrap;
    gap: 0.6rem;
    align-items: flex-end;
}

body.console .form-row > .form-field {
    flex: 1 1 11rem;
    min-width: 0;
}

body.console .form-row > .form-field--narrow {
    flex: 0 1 8rem;
}


/* ============================================================
   OUTLET QUEUE
   ============================================================ */

/* The pause switch read backwards: "taking new orders" — the healthy state — was
   pink on a red rule, and "paused" was calm yellow. Open is now quiet, paused
   shouts. */
body.console .outlet-switch {
    background: var(--color-surface);
    border: var(--border-w) solid var(--color-ink);
    border-left: 4px solid var(--color-ink);
    box-shadow: var(--shadow-sm);
    padding: 0.8rem 1rem;
}

body.console .outlet-switch.is-paused {
    background: var(--color-primary-tint);
    border-left-color: var(--color-primary);
}

body.console .outlet-switch__text strong {
    font-size: 0.95rem;
    text-transform: uppercase;
}

body.console .outlet-switch__text span {
    font-size: 0.82rem;
}

/* The live indicator was a green-era leftover: it read --color-success, which the
   brutalist palette redefined to near-black, and pulsed a green halo that no longer
   exists in the system. So the "we are polling" cue rendered as a grey smudge. */
body.console .live-dot {
    width: 10px;
    height: 10px;
    border-radius: 0;
    border: var(--border-w) solid var(--color-ink);
    background: var(--color-accent-tint);
    box-shadow: none;
    margin-right: 0.45rem;
    vertical-align: 0.15em;
    animation: console-live-pulse 2s ease-in-out infinite;
}

@keyframes console-live-pulse {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.25; }
}

@media (prefers-reduced-motion: reduce) {
    body.console .live-dot { animation: none; }
}

/* Queue cards: the token is the thing you read from two metres away. */
body.console .token-badge {
    font-family: var(--font-display);
    font-size: 1.6rem;
    line-height: 1;
    letter-spacing: -0.02em;
    color: var(--color-ink);
}

body.console #queue-body .card-body {
    padding: 0.85rem 0.9rem;
}

body.console #queue-body ul {
    list-style: none;
    padding: 0;
    margin: 0 0 0.75rem;
    font-size: 0.85rem;
}

body.console #queue-body ul li {
    padding: 0.15rem 0;
    border-bottom: 1px dashed var(--color-field-rule);
    color: var(--color-ink);
}

body.console #queue-body ul li:last-child {
    border-bottom: none;
}

body.console .pickup-input {
    font-family: var(--font-display);
    font-size: 1.1rem;
    letter-spacing: 0.25em;
    text-align: center;
}


/* ============================================================
   MENU MANAGEMENT — compact rows, not photo heroes.

   The old grid gave every item a 4:3 photo panel. With no photo uploaded that
   became a ~270px block of flat yellow carrying no information, so six items ran
   to 3,225px of scroll on a phone. This is a management list: what staff need is
   name, price, today's count and the stock toggle, many at a time.
   ============================================================ */

body.console .menu-admin {
    list-style: none;
    margin: 0;
    padding: 0;
    border: var(--border-w) solid var(--color-ink);
    box-shadow: var(--shadow-md);
    background: var(--color-surface);
}

body.console .menu-admin__row {
    display: grid;
    grid-template-columns: 56px minmax(0, 1fr) auto;
    align-items: center;
    gap: 0.85rem;
    padding: 0.7rem 0.85rem;
    border-bottom: var(--border-w) solid var(--color-ink);
}

body.console .menu-admin__row:last-child {
    border-bottom: none;
}

/* Switched off items stay legible but visibly stand down. */
body.console .menu-admin__row.is-off {
    background: repeating-linear-gradient(
        135deg,
        var(--color-surface-deep),
        var(--color-surface-deep) 6px,
        var(--color-surface) 6px,
        var(--color-surface) 12px
    );
}

body.console .menu-admin__thumb {
    width: 56px;
    height: 56px;
    border: var(--border-w) solid var(--color-ink);
    background: var(--color-field);
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
    flex: none;
}

body.console .menu-admin__thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

/* No photo: a quiet placeholder glyph, not a saturated panel. */
body.console .menu-admin__thumb.is-placeholder {
    background: var(--color-surface-deep);
}

body.console .menu-admin__thumb.is-placeholder .material-symbols-outlined {
    font-size: 26px;
    color: var(--color-ink-muted);
}

body.console .menu-admin__main {
    min-width: 0;
}

body.console .menu-admin__name {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    flex-wrap: wrap;
    font-family: var(--font-display);
    font-weight: 400;
    text-transform: uppercase;
    font-size: 0.9rem;
    line-height: 1.2;
    color: var(--color-ink);
}

body.console .menu-admin__meta {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
    margin-top: 0.2rem;
    font-size: 0.8rem;
    color: var(--color-ink-muted);
}

body.console .menu-admin__price {
    font-family: var(--font-display);
    font-size: 0.95rem;
    color: var(--color-ink);
    font-variant-numeric: tabular-nums;
}

body.console .menu-admin__price .price-strike {
    color: var(--color-ink-muted);
    text-decoration: line-through;
    font-size: 0.82rem;
    margin-right: 0.3rem;
}

body.console .menu-admin__actions {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    flex: none;
}

body.console .menu-admin__actions .btn {
    box-shadow: var(--shadow-xs);
}

/* The daily-limit bar sits inside the row rather than owning one. */
body.console .menu-admin .daily-limit-row {
    margin-top: 0.35rem;
}

body.console .menu-admin .daily-limit-bar {
    height: 6px;
    border: var(--border-w) solid var(--color-ink);
    background: var(--color-field);
    max-width: 12rem;
}

body.console .menu-admin .daily-limit-bar span {
    display: block;
    height: 100%;
    background: var(--color-accent-tint);
}

body.console .menu-admin .daily-limit-text {
    font-size: 0.76rem;
    color: var(--color-ink-muted);
}

body.console .menu-admin .daily-limit-text.is-low {
    color: var(--color-ink);
    font-weight: 700;
}

body.console .menu-admin .daily-limit-text.is-out {
    color: var(--color-primary);
    font-weight: 700;
}

/* Phones: the action cluster drops to its own line rather than crushing the name. */
@media (max-width: 575.98px) {
    body.console .menu-admin__row {
        grid-template-columns: 48px minmax(0, 1fr);
        row-gap: 0.6rem;
    }
    body.console .menu-admin__thumb {
        width: 48px;
        height: 48px;
    }
    body.console .menu-admin__actions {
        grid-column: 1 / -1;
        justify-content: flex-start;
        flex-wrap: wrap;
    }
}


/* ============================================================
   EMPTY STATES — narrower and calmer than the customer app's.
   ============================================================ */

body.console .state-panel {
    background: var(--color-surface);
    padding: 2rem 1.5rem;
    text-align: center;
    max-width: 32rem;
    margin: 0 auto;
}

/* The medallion had the same problem the badges did: the "neutral" tone was drawn
   with --color-primary-tint on --color-primary, i.e. pink on red. So "All caught
   up", "No sales yet" and "No colleges yet" — the three calmest sentences in the
   product — arrived looking like failures. Scoped to the console because the
   customer app's empty states are signed off as they are. */
body.console .state-medallion__disc {
    border: var(--border-w) solid var(--color-ink);
    border-radius: 0;
}

body.console .state-medallion--neutral .state-medallion__disc {
    background: var(--color-surface-deep);
}

body.console .state-medallion--neutral .state-medallion__disc .material-symbols-outlined {
    color: var(--color-ink);
}

body.console .state-medallion--good .state-medallion__disc {
    background: var(--color-accent-tint);
}

body.console .state-medallion--good .state-medallion__disc .material-symbols-outlined {
    color: var(--color-ink);
}

body.console .state-medallion--bad .state-medallion__disc {
    background: var(--color-primary);
}

body.console .state-medallion--bad .state-medallion__disc .material-symbols-outlined {
    color: #fff;
}

body.console .state-panel h4 {
    font-family: var(--font-display);
    font-weight: 400;
    text-transform: uppercase;
    font-size: 1.1rem;
    margin-bottom: 0.4rem;
}

body.console .state-panel p {
    font-size: 0.88rem;
    color: var(--color-ink-muted);
    margin-bottom: 0.75rem;
}


/* ============================================================
   ALERTS — app.css uppercases these in Archivo Black, which is fine for a
   three-word banner and unreadable for a sentence. Staff alerts carry sentences.
   ============================================================ */

body.console .alert {
    font-family: var(--font-body);
    text-transform: none;
    font-size: 0.88rem;
    letter-spacing: 0;
    padding: 0.7rem 0.9rem;
    box-shadow: var(--shadow-sm);
}

body.console .alert-success {
    background: var(--color-accent-tint);
    color: var(--color-ink);
}

body.console .alert-danger {
    background: var(--color-primary-tint);
    color: var(--color-ink);
}


/* ============================================================
   DANGER ZONE — destructive disclosures (delete an outlet, delete an item).
   Red is load-bearing here and used nowhere else on the page.
   ============================================================ */

body.console .danger-zone > summary,
body.console .cancel-panel > summary {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    cursor: pointer;
    font-family: var(--font-display);
    text-transform: uppercase;
    font-size: 0.78rem;
    letter-spacing: 0.04em;
    color: var(--color-primary);
    padding: 0.35rem 0;
}

body.console .danger-zone[open] > summary,
body.console .cancel-panel[open] > summary {
    margin-bottom: 0.5rem;
}


/* ============================================================
   PRINT — the sales report and the order queue get printed in kitchens.
   ============================================================ */

@media print {
    body.console .app-chrome,
    body.console .console-head__actions,
    body.console .outlet-switch form {
        display: none !important;
    }
    body.console .table-responsive,
    body.console .console-panel,
    body.console .stat {
        box-shadow: none;
    }
}


/* ============================================================
   CONTRAST — unscoped, because it is a WCAG failure product-wide.

   White on the brand red #e8281e is 4.42:1. AA wants 4.5:1 for text below 24px, so
   every filled primary button, and the danger badge, sat just under the line — and
   these are the most important controls in the product (Sign in, Proceed to pay, Add).
   The brand red on the bone canvas is worse at 3.69:1, which is what link text used.

   --color-primary-dark (#b81f16) is already part of the palette and clears both:
   6.47:1 on white, 5.41:1 on bone. It is the same red, one step down, so the brand
   reads unchanged while the text becomes legible. The vivid red is kept for large
   display elements — the ticker, stickers, headings — where 3:1 is the bar and it passes.
   ============================================================ */

.btn-primary,
.deal-card-add,
.sticky-pay-inner .btn-primary,
.sticky-cart-action {
    background: var(--color-primary-dark);
}
.btn-primary:hover,
.btn-primary:focus {
    background: var(--color-primary);
}

/* Link text sitting on the bone canvas. */
a,
.text-danger,
.field__error {
    color: var(--color-primary-dark);
}
a:hover { color: var(--color-primary); }

/* Small white-on-red text. */
.badge.bg-danger {
    background: var(--color-primary-dark) !important;
}

/* ============================================================
   BOOTSTRAP CONTROLS — the checkboxes, switches and list groups that were
   never retuned. Bootstrap ships them rounded, white-filled and blue-checked,
   which is three different design systems away from this one.
   ============================================================ */

.form-check-input {
    border-radius: 0;
    border: var(--border-w) solid var(--color-ink);
    background-color: var(--color-field);
    box-shadow: none;
}
.form-check-input:checked {
    background-color: var(--color-ink);
    border-color: var(--color-ink);
}
.form-check-input:focus {
    border-color: var(--color-ink);
    box-shadow: var(--shadow-xs);
    outline: none;
}
/* The switch variant draws its knob as a background image tinted Bootstrap blue. */
.form-switch .form-check-input {
    border-radius: 0;
    background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2314100d'/%3e%3c/svg%3e");
}
.form-switch .form-check-input:checked {
    background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23f2ead9'/%3e%3c/svg%3e");
}

.list-group,
.list-group-item {
    border-radius: 0;
}

/* ============================================================
   TAP TARGETS — 44px is the minimum comfortable touch size; the console
   nav links and bottom tabs were rendering at 38–42px.
   ============================================================ */

@media (max-width: 991.98px) {
    .console-nav__link {
        min-height: 44px;
    }
    .btn-sm {
        min-height: 40px;
    }
}

/* Adjacent tappable rows were 3px apart — close enough to mis-tap. */
.account-row + .account-row {
    margin-top: 8px;
}


/* ============================================================
   BOOTSTRAP COLOUR TOKENS — cut off at the source.

   Bootstrap ships its own palette through CSS variables, and anything this sheet has
   not explicitly restyled still reads them: body copy came out #212529 rather than ink,
   outline-danger buttons came out #dc3545 rather than the brand red, and borders came
   out #dee2e6. Restating the variables fixes every component that reads them at once,
   including ones not yet built, which is strictly better than chasing each class.
   ============================================================ */

:root {
    --bs-body-color: var(--color-ink);
    --bs-body-color-rgb: 20, 16, 13;
    --bs-body-bg: var(--color-bg);
    --bs-border-color: var(--color-ink);
    --bs-emphasis-color: var(--color-ink);
    --bs-secondary-color: var(--color-ink-muted);
    --bs-link-color: var(--color-primary-dark);
    --bs-link-hover-color: var(--color-primary);
    --bs-danger: #b81f16;
    --bs-danger-rgb: 184, 31, 22;
    --bs-success: #14100d;
    --bs-warning: #ffc918;
    --bs-info: #14100d;
    --bs-primary: #b81f16;
    --bs-primary-rgb: 184, 31, 22;
}

/* The outline variants read their own per-button variables, which the block above does
   not reach. Destructive actions stay red — the palette's red, not Bootstrap's. */
.btn-outline-danger {
    --bs-btn-color: var(--color-primary-dark);
    --bs-btn-border-color: var(--color-ink);
    --bs-btn-hover-color: #fff;
    --bs-btn-hover-bg: var(--color-primary-dark);
    --bs-btn-hover-border-color: var(--color-ink);
    --bs-btn-active-bg: var(--color-primary-dark);
    --bs-btn-active-border-color: var(--color-ink);
}


/* ============================================================
   NAVIGATION: ONE MECHANISM PER WIDTH

   The drawer was added while the console strip stayed, so staff and admin saw both at
   every size: a hamburger and a 19-link black bar offering the same destinations. Two
   controls for one job is the "congested" part, and on a 390px screen the strip alone
   was 47px of permanent chrome above every page.

   Split by width instead of showing both:
     - below 992px the drawer is the whole story, and the strip is gone
     - at 992px and up the strip is genuinely useful — ten destinations, all one click
       away, no tap to open — so it stays and the hamburger steps aside

   The drawer markup is always present either way, so the keyboard and screen-reader
   path never depends on which one is on screen.
   ============================================================ */

@media (max-width: 991.98px) {
    .console-nav { display: none; }
}

@media (min-width: 992px) {
    /* Staff already have every destination on the strip below the bar. */
    body.console .nav-drawer-btn { display: none; }
}


/* ============================================================
   PAGER — the control under every paged list.

   Quiet by design: it is navigation between pages of the same thing, not an action, so
   it takes the rule weight of a separator rather than the border weight of a control.
   ============================================================ */
body.console .pager {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    margin: 1rem 0 2rem;
    font-family: var(--font-display);
    font-size: 0.76rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

body.console .pager__step {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    padding: 0.5rem 0.85rem;
    color: var(--color-ink);
    text-decoration: none;
    border: var(--border-w) solid var(--color-ink);
    background: var(--color-surface);
}

body.console .pager__step:hover {
    background: var(--color-accent-tint);
}

/* Kept in the DOM rather than removed so the row does not reflow as you page — the
   Next button stays under the cursor from one page to the next. */
body.console .pager__step.is-disabled {
    border-color: var(--rule-color);
    color: var(--color-ink-muted);
    opacity: 0.45;
    pointer-events: none;
}

body.console .pager__where {
    padding: 0 0.5rem;
    color: var(--color-ink-muted);
}

body.console .pager .material-symbols-outlined {
    font-size: 16px;
}


/* ============================================================
   STAT TILES — the admin overview.

   Deliberately flat: a number wants to be read, not decorated. The only tile that
   changes appearance is one with a non-zero queue behind it, so a screen with nothing
   to do looks calm and a screen with work on it does not.
   ============================================================ */
body.console .stat-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
    gap: 0.75rem;
    margin-bottom: 0.5rem;
}

body.console .stat {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    padding: 0.9rem 1rem;
    border: var(--border-w) solid var(--color-ink);
    background: var(--color-surface);
    text-decoration: none;
    color: var(--color-ink);
}

body.console .stat__label {
    font-family: var(--font-display);
    font-size: 0.68rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--color-ink-muted);
}

body.console .stat__value {
    font-family: var(--font-display);
    font-size: 1.9rem;
    line-height: 1.1;
}

body.console .stat__note {
    font-size: 0.76rem;
    color: var(--color-ink-muted);
}

body.console a.stat--action:hover {
    background: var(--color-surface-deep);
}

/* Something is actually waiting. Yellow, not red — a queue is work, not a failure. */
body.console .stat--action.is-live {
    background: var(--color-accent-tint);
}

body.console .stat--action.is-live .stat__label,
body.console .stat--action.is-live .stat__note {
    color: var(--color-ink);
}

/* Somebody's money in the wrong place. The only red tile on the overview — red is
   reserved for cancelled and refunded elsewhere, and this is the same category. */
body.console .stat--action.is-urgent {
    background: var(--color-danger-tint);
    box-shadow: inset 4px 0 0 var(--color-primary);
}

/* ============================================================
   CONSOLE FILTER BAR
   Sits under the console head on screens whose list is rendered
   whole. Revealed by initConsoleFilters() in app.js, and only
   when the list is long enough to be worth narrowing — so the
   bar never becomes the clutter it exists to remove.
   ============================================================ */

body.console .console-filter {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    margin-bottom: 1.25rem;
    padding: 0.6rem 0.75rem;
    background: var(--color-surface-deep);
    border: var(--border-w) solid var(--color-ink);
}

body.console .console-filter .form-control,
body.console .console-filter .form-select,
body.console .console-filter .select {
    width: auto;
    min-width: 9rem;
    margin-bottom: 0;
}

/* The search box is the one control that should take the slack. */
body.console .console-filter__search {
    flex: 1 1 14rem;
    min-width: 11rem;
}

body.console .console-filter__count {
    margin-left: auto;
    font-size: 0.75rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--color-ink-muted);
    white-space: nowrap;
}

/* Bootstrap sets an explicit display on tr and on .list-group-item, which outranks
   the [hidden] default. Stated at the same strength so a filtered-out row really goes. */
body.console [data-filter-row][hidden] {
    display: none !important;
}

/* The "nothing matches that" line, shown only while the filter empties the list. */
body.console .console-filter-empty {
    padding: 1.25rem;
    text-align: center;
    color: var(--color-ink-muted);
    background: var(--color-surface);
    border: var(--border-w) dashed var(--color-ink-muted);
    margin-bottom: 1.25rem;
}


/* ============================================================
   ADMIN SIDEBAR — permanent left-rail navigation.

   The admin portal has 14+ destinations and they are still growing.
   A horizontal strip overflows at 1280px; a vertical rail with
   grouped sections surfaces every page, supports nesting, and
   stays readable as the link count grows.

   Activated by `console--has-sidebar` on the body, which only
   admin templates carry. Canteen and techmgr keep the strip.
   ============================================================ */

/* ---- Layout switch ---- */

/* Hide the console-nav strip when the sidebar is active (desktop). */
body.console--has-sidebar .console-nav { display: none; }

/* The sidebar layout: sidebar + content side by side. */
.console-layout {
    display: flex;
    min-height: calc(100vh - 53px);  /* below the top bar */
}

/* ---- Sidebar panel ---- */
.console-sidebar {
    position: sticky;
    top: 53px;  /* height of .app-chrome navbar */
    align-self: flex-start;
    width: 220px;
    flex: none;
    height: calc(100vh - 53px);
    overflow-y: auto;
    overflow-x: hidden;
    scrollbar-width: thin;
    background: var(--color-surface);
    border-right: var(--border-w) solid var(--color-ink);
    z-index: 10;
}

.console-sidebar__nav {
    padding: 0.5rem 0;
}

/* ---- Groups ---- */
.console-sidebar__group {
    padding: 0.5rem 0;
}

.console-sidebar__group + .console-sidebar__group {
    border-top: var(--rule-w) solid var(--rule-color-strong);
}

.console-sidebar__label {
    display: block;
    padding: 0.35rem 1rem 0.25rem;
    font-family: var(--font-display);
    font-size: 0.62rem;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--color-ink-muted);
    user-select: none;
}

/* ---- Links ---- */
.console-sidebar__link {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    padding: 0.45rem 1rem;
    color: var(--color-ink-muted);
    text-decoration: none;
    font-family: var(--font-display);
    font-weight: 400;
    font-size: 0.74rem;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    white-space: nowrap;
    border-left: 3px solid transparent;
    transition: background-color 100ms ease, color 100ms ease, border-color 100ms ease;
}

.console-sidebar__link .material-symbols-outlined {
    font-size: 18px;
    line-height: 1;
    flex: none;
}

.console-sidebar__link:hover,
.console-sidebar__link:focus-visible {
    background: var(--color-surface-deep);
    color: var(--color-ink);
    text-decoration: none;
    outline: none;
}

/* Active state — yellow fill + ink left border, same language as the strip. */
.console-sidebar__link.is-active {
    background: var(--color-accent-tint);
    color: var(--color-ink);
    border-left-color: var(--color-ink);
}

/* ---- Nested sub-links (e.g. Billing → Settlements, Promo Codes) ---- */
.console-sidebar__nested {
    padding-left: 0.5rem;
}

.console-sidebar__link--nested {
    padding-left: 1.5rem;
    font-size: 0.70rem;
}

.console-sidebar__link--nested .material-symbols-outlined {
    font-size: 16px;
}

/* ---- Main content ---- */
.console-main {
    flex: 1 1 0%;
    min-width: 0;  /* flex child can shrink below content size */
}

/* Spacing between top bar and content, replacing the old .app-chrome + * rule
   which doesn't fire here because the layout wrapper sits between them. */
body.console--has-sidebar .console-main > .container {
    padding-top: 1.5rem;
}

/* On sidebar pages the container is already constrained by the sidebar eating
   220px, so let it run wider than the default 1140px to use the space. */
body.console--has-sidebar .console-main > .container {
    max-width: 1200px;
}

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

/* Below 992px: sidebar hidden, drawer takes over. */
@media (max-width: 991.98px) {
    .console-sidebar { display: none; }
    .console-layout { display: block; }
    /* Re-show the drawer button that the console scoping hides at ≥992px. */
    body.console--has-sidebar .nav-drawer-btn { display: inline-flex; }
    /* Content needs spacing again since the layout is now block. */
    body.console--has-sidebar .console-main > .container {
        padding-top: 0;
    }
    body.console--has-sidebar .app-chrome + .console-layout {
        margin-top: 1.5rem;
    }
}

/* Large screens: sidebar is generous. */
@media (min-width: 1400px) {
    .console-sidebar { width: 240px; }
}

/* ---- Print: hide the sidebar ---- */
@media print {
    .console-sidebar { display: none !important; }
    .console-layout { display: block; }
}

/* ============================================================
   ANALYTICS CHARTS & VISUALS
   ============================================================ */

.analytics-chart-viewport {
    height: 190px;
    display: flex;
    align-items: flex-end;
    gap: 8px;
    overflow-x: auto;
    padding-top: 15px;
}

.analytics-chart-col {
    flex: 1;
    min-width: 44px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-end;
    height: 100%;
}

.analytics-bar-track {
    height: 120px;
    width: 100%;
    display: flex;
    align-items: flex-end;
    justify-content: center;
    position: relative;
}

.analytics-baseline {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    height: 2px;
    background: var(--color-ink);
}

.analytics-bar {
    width: 28px;
    max-width: 85%;
    background: var(--color-primary);
    border: 2px solid var(--color-ink);
    box-shadow: 2px -2px 0 var(--color-ink);
    border-radius: 4px 4px 0 0;
    transition: height 0.4s ease;
    z-index: 1;
}

.analytics-bar--rush-peak {
    width: 22px;
    background: var(--color-accent-tint);
    border: 2px solid var(--color-ink);
    box-shadow: 2px -2px 0 var(--color-ink);
    border-radius: 4px 4px 0 0;
    z-index: 1;
}

.analytics-bar--rush-normal {
    width: 22px;
    background: var(--color-ink);
    border: 2px solid var(--color-ink);
    border-radius: 4px 4px 0 0;
    z-index: 1;
}

.analytics-dot-empty {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--color-ink-muted);
    margin-bottom: -3px;
    z-index: 1;
}

.analytics-slot-empty {
    width: 14px;
    height: 6px;
    border-radius: 2px 2px 0 0;
    background: rgba(0, 0, 0, 0.08);
    z-index: 1;
}

.analytics-slot-empty.is-rush {
    background: rgba(255, 201, 24, 0.35);
}
