/*
 * Saigon — the ordering pages.
 *
 * Built to the coffee-shop ordering app it was drawn from: pill-shaped category
 * bar, a grid of photo cards, a totals box shaped like a ticket with two
 * notches, and one solid full-width action button.
 *
 * Mobile-first: every plain rule is written for a phone; a wide screen switches
 * to a different layout altogether (categories down the left, the dish grid in
 * the middle, the cart pinned to the right) in the @media block at the end of
 * each section.
 *
 * Hand-written CSS, no Vite or Tailwind: the shop deploys to shared hosting by
 * copying the folder, and there is no Node there.
 */

:root {
    /*
     * Taken from the shop's own logo rather than chosen, and re-sampled from
     * the file they sent on 11/09: the blue is #002f90 and the gold #fbcd14.
     * Both were a shade out before — close enough to look right on their own
     * and wrong the moment the logo sat on the header beside them.
     *
     * Change the brand on these lines and the whole site follows.
     */
    --brand: #002f90;
    --brand-ink: #002370;
    --brand-soft: #e9eef8;

    /* The gold of the lettering. */
    --gold: #fbcd14;

    /*
     * The same gold with the brightness taken out, for gold text on white.
     *
     * NOT used at the moment. The shop asked for the real gold on the category
     * headings and that is what is there — but #fbcd14 on white measures about
     * 1.5:1, where three is the floor even for large text, so it reads as faded
     * rather than golden and will read worse on a phone outdoors. This is the
     * same hue at 4.6:1, one switch away if they want it.
     *
     * The third answer, and the best-looking one, is gold on the brand blue —
     * bright on dark, which is what the logo itself does and why it works
     * there. See docs/ke-hoach-chinh-sua-theo-gop-y-slide.md.
     */
    --gold-ink: #8a6200;

    --ink: #14181c;
    --ink-2: #6b7379;
    --ink-3: #9aa1a7;

    --bg: #ffffff;
    --soft: #f4f5f6;
    --line: #e8eaec;
    --line-strong: #d8dcdf;

    --ok: #1f7a4d;
    --ok-soft: #eef7f2;
    --bad: #c0392b;
    --bad-soft: #fdefed;

    --r: 12px;
    --r-lg: 18px;
    --shadow-1: 0 1px 3px rgba(20, 24, 28, .06);
    --shadow-2: 0 8px 24px rgba(20, 24, 28, .10);

    /* Tall enough for the mark with "Manager" set under it. The sidebar's
       own offset and the sticky tables all read this, so they follow. */
    --header-h: 84px;
    --tabs-h: 66px;
    --stuck: calc(var(--header-h) + var(--tabs-h));
}

* { box-sizing: border-box; }

html {
    -webkit-text-size-adjust: 100%;
    scroll-behavior: smooth;

    /*
     * The gutter is always reserved, whether or not the page scrolls.
     *
     * Without it the scrollbar appearing and disappearing changes the content
     * width by about 15px, and that is one link in a chain that eats itself:
     *
     *   --footer-band (measured) → .page padding-bottom → page height
     *     → scrollbar appears → width drops 15px → the three policy links
     *     rewrap onto a second line → the footer is taller → footer.js
     *     measures it and writes a new --footer-band → round again.
     *
     * It showed up as the whole page flickering the moment a dish modal was
     * opened, because showModal() takes the page's scrollbar away and starts
     * the cycle. Holding the gutter open means the width never moves, so the
     * footer never rewraps and the loop has nothing to stand on.
     *
     * It costs a permanently reserved 15px on a desktop browser, and buys back
     * every other place a page jumps sideways as it grows past one screen.
     */
    scrollbar-gutter: stable;
}

body {
    margin: 0;
    background: var(--bg);
    color: var(--ink);
    /* Inter, and it carries the Vietnamese marks, so "Phở" and "Pho" are set in
       the same face. See the note in the layout. */
    font: 400 16px/1.5 Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    /* Inter is drawn a little loose at text sizes; this is the tracking its own
       designer recommends for 16px, and it is what stops the page reading as
       wider than it is. */
    letter-spacing: -.011em;
}

/*
 * The footer is pinned to the bottom of the window, at the shop's request, and
 * stays there however far the page is scrolled.
 *
 * It began as a flex column that merely pushed the band down on short pages —
 * which fixed the original complaint (an empty cart left the band floating a
 * third of the way down the screen) without costing anything on long ones. The
 * shop wants it always visible, so it is `position: fixed` now.
 *
 * **That costs 46px of every screen, permanently**, and on the menu page it is
 * not 46 — the cart bar is pinned down there too, so the two together take
 * about 120px off a phone. Everything below is the arithmetic that keeps them
 * from landing on top of each other. One line reverts it: take the `position`
 * off .site-footer and the flex column underneath still does the short-page
 * job on its own.
 *
 * The height is a variable because four other rules have to know it.
 *
 * --footer-h is the guess; --footer-band is the answer.
 *
 * The guess is a number typed here, and on a narrow phone it was wrong: three
 * legal links do not fit on one line at 390px, they wrap to two, and the band
 * grew past the 74px everything else was measuring against. The cart bar landed
 * on the links and the credit line ran off the bottom of the screen.
 *
 * So footer.js measures the band once it is on the page and writes the real
 * height into --footer-band, again whenever it changes — a turned phone, a
 * pinched font size. Everything that has to keep clear reads --footer-band, and
 * its fallback is the old arithmetic, so a browser with the script blocked
 * behaves exactly as before rather than losing the footer altogether.
 */
:root {
    --footer-h: 46px;
    --footer-band: calc(var(--footer-h) + env(safe-area-inset-bottom, 0px));
}

/*
 * The flex column stays even though the footer has left the flow. It costs
 * nothing, and it is what keeps the main tall enough that a three-line page
 * still paints a full screen of white rather than ending in mid-air.
 *
 * dvh after vh: on a phone `100vh` is the window measured with the browser's
 * own bars hidden, so while they are showing the page is taller than the screen
 * — dvh is the space actually visible, and the vh line stays for browsers that
 * do not know dvh.
 *
 * On `body.site` rather than on `body`, because the back office is not built
 * this way: its own shell is already a full-height flex ROW, and turning the
 * body into a column there would stand the sidebar on top of the page.
 */
body.site {
    min-height: 100vh;
    min-height: 100dvh;
    display: flex;
    flex-direction: column;
}

/* The main takes whatever height the content does not. Its bottom padding is
   what stops the last line of a page hiding behind the fixed band. */
body.site .page {
    flex: 1 0 auto;
    padding-bottom: calc(var(--footer-band) + 28px);

    /*
     * A column, so the small print can be pushed to the foot of it.
     *
     * .page already takes whatever height the content leaves over — that is
     * what stops a short page ending half way up the screen. But the policy
     * links sat directly under the content inside it, so an empty cart left
     * them stranded in the middle of the window with a field of white beneath.
     * They belong at the bottom, just above the band. See .page-legal.
     */
    display: flex;
    flex-direction: column;
}

/*
 * The menu page needs the cart bar's height on top of that: it is pinned to the
 * viewport as well and would otherwise cover the last row of dishes.
 */
body.has-floating-cart .page {
    padding-bottom: calc(var(--footer-band) + 88px);
}

a { color: var(--brand-ink); }

/*
 * The page runs on 400, with 500 for anything that has to stand out. <strong>,
 * <b> and <th> default to 700, so left alone they would each be heavier than
 * every heading around them — an emphasised phrase inside a sentence ending up
 * the loudest thing on screen.
 *
 * 500 rather than 600 since the shop asked for lighter type (slide 8). One
 * notch above the rest of the page is still emphasis, and it now matches every
 * heading rather than sitting above them.
 */
strong, b, th { font-weight: 500; }

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

h1, h2, h3 { line-height: 1.25; }

.wrap {
    width: 100%;
    max-width: 1240px;
    margin-inline: auto;
    padding-inline: 16px;
}

.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
}

/* Line icons: they always take the colour and size of the text around them. */
.ico {
    width: 1.35em;
    height: 1.35em;
    flex: none;
    vertical-align: -.28em;
}

/* ============================================================== top bar */

.topbar {
    position: sticky;
    top: 0;
    z-index: 30;
    min-height: var(--header-h);
    display: flex;
    align-items: center;
    background: rgba(255, 255, 255, .94);
    backdrop-filter: saturate(160%) blur(12px);
}

.topbar .wrap {
    display: flex;
    align-items: center;
    gap: 12px;
}

.topbar-place {
    display: flex;
    align-items: center;
    gap: 10px;
    min-width: 0;
    text-decoration: none;
    color: inherit;
    line-height: 1.25;
}

.topbar-place small {
    display: block;
    color: var(--ink-2);
    font-size: .92rem;
    font-weight: 400;
}

/*
 * The logo, worn round, and now the only thing naming the shop on the page.
 *
 * Round because the shop asked: slide 12 draws an arrow from their logo to the
 * round badge beside the search box on the site they are copying. It used to be
 * a rounded square out of a fear that a circle would clip the ends of
 * "VIETNAMESE & ASIAN FOOD" — measured on the file they sent, a circle
 * inscribed in that square clears the arc with room to spare.
 *
 * 56 rather than 44, because the name that used to sit beside it has gone and
 * the artwork now has to carry the lettering on its own. It is the ceiling, not
 * a preference: the header is 72 tall, so a 56px badge leaves 8px of air above
 * and below and anything larger starts pushing the bar open.
 */
.topbar-mark {
    width: 56px;
    height: 56px;
    flex: none;
    border-radius: 999px;
    object-fit: cover;
    display: block;

    /*
     * The badge stays a circle on a narrow phone.
     *
     * `img { max-width: 100% }` at the top of this file is the right default
     * everywhere else — it is what stops a photograph from a wide screen
     * bursting its column. Here it was squashing the logo: on a 320px header
     * the link around the badge is narrower than 56px, max-width capped the
     * WIDTH to the space available while `height` held at 56, and the shop's
     * round logo came out an egg. It was reported from a real phone and it does
     * not show on a laptop, where the header always has room.
     *
     * min-width as well as the override, because the link it sits in is a
     * shrinking flex item and a flex child's automatic minimum is content-based
     * — `flex: none` alone does not settle it in every engine.
     */
    max-width: none;
    min-width: 56px;
    aspect-ratio: 1;
}

/*
 * A phone keeps a smaller badge. 48 and not 56: the bar also has to hold the
 * search button and the cart, and the shop's name is in the artwork rather than
 * beside it now, so the badge is the one thing that cannot be allowed to crowd
 * them off the line.
 */
@media (max-width: 480px) {
    .topbar-mark { width: 48px; height: 48px; min-width: 48px; }
}

/*
 * Narrower still — a 320px phone, which is a real thing people order from.
 *
 * The badge, the two round buttons and their gaps are 168px of the 288 the
 * header has; whatever the address does, it must not come out of the logo. It
 * gives way instead, and at this width shows the street number alone.
 */
@media (max-width: 360px) {
    .topbar-mark { width: 44px; height: 44px; min-width: 44px; }
    .topbar .wrap { gap: 8px; }
    .place { gap: 4px; }
    .place-text b { max-width: 34vw; }
}

/*
 * The shop's name and its opening hours used to be set beside the badge, in
 * .topbar-text. The shop asked for the logo alone, so the markup has gone and
 * the rules that dressed it went with it — see layouts/app.blade.php.
 */

/* ====================================== pickup from ... , ready at ... */

/*
 * Three lines beside the badge: what this is, where it is, and when.
 *
 * It is a <summary>, so the whole block opens the collection panel — the same
 * panel the cart page uses, so there is one place the answer is given and the
 * two can never disagree.
 */
/*
 * It shrinks. The chip it shares markup with does not, and must not.
 *
 * .chip-wrap is `flex: none` because the old header pill was a fixed-size disc
 * that had to keep its shape. This one is a line of text that has to give way
 * to the search and cart buttons — left on `flex: none` it kept its full
 * min-content width and pushed both of them clean off the right of the screen.
 *
 * min-width:0 as well as the flex shorthand: a flex item will not shrink below
 * its min-content size without it, and the whole point here is the ellipsis on
 * a long street name.
 */
.place-wrap {
    position: relative;
    flex: 0 1 auto;
    min-width: 0;
}

.place {
    display: flex;
    align-items: center;
    gap: 6px;
    min-width: 0;
    padding: 2px 4px;
    cursor: pointer;
    list-style: none;
    line-height: 1.25;
    -webkit-tap-highlight-color: transparent;
}

/* Safari draws its own triangle on a summary and it cannot be styled. */
.place::-webkit-details-marker { display: none; }

.place-text { min-width: 0; }

.place-text small {
    display: block;
    color: var(--ink-2);
    font-size: .74rem;
    letter-spacing: .01em;
}

.place-text b {
    display: block;
    font-size: .97rem;
    font-weight: 700;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/*
 * The time, set quieter than the address above it.
 *
 * It is the answer to a question nobody has asked yet — most people are here to
 * look at food — so it stays available without competing with the address for
 * the same glance.
 */
.place-when {
    display: flex;
    align-items: center;
    gap: 4px;
    color: var(--ink-2);
    font-size: .76rem;
    white-space: nowrap;
}

.place-when .ico { width: 13px; height: 13px; flex: none; vertical-align: 0; }

.place .chip-arrow { display: block; }

/*
 * On a narrow phone the address gets whatever is left after the badge and the
 * three round buttons, which is not much. Capped rather than left to shrink to
 * nothing, so what shows is the start of a real street name.
 */
@media (max-width: 480px) {
    .place-text b { max-width: 42vw; }
    .place-text small { font-size: .7rem; }
    .place-when { font-size: .72rem; }
}

.topbar .spacer { flex: 1; }

/*
 * The cart button against the right edge, whatever else is in the bar.
 *
 * The spacer is hidden on wide screens (the search box does the pushing there),
 * so on a page with no search box — the cart, the checkout — nothing pushed and
 * the button sat halfway across, next to the logo. margin-left:auto takes the
 * leftover space wherever there is any, and adds nothing where there is none.
 */
.topbar .wrap > .round-btn:last-child { margin-left: auto; }

/* Thin-outlined round button, as in the mockup. */
.round-btn {
    position: relative;
    display: grid;
    place-items: center;
    width: 48px;
    height: 48px;
    flex: none;
    border: 1px solid var(--line-strong);
    border-radius: 999px;
    background: var(--bg);
    color: var(--ink);
    text-decoration: none;
    font-size: 1rem;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}

.round-btn .ico { width: 21px; height: 21px; vertical-align: 0; }

.round-btn:active { background: var(--soft); }

.round-btn .count {
    position: absolute;
    top: -4px;
    right: -4px;
    min-width: 20px;
    height: 20px;
    padding: 0 5px;
    border-radius: 999px;
    background: var(--brand);
    color: #fff;
    font-size: .72rem;
    /*
     * The one place on the page that keeps a heavy weight, and it earns it: a
     * 0.72rem number reversed out of a coloured disc has almost no ink, and at
     * 500 it half disappears. The sites this was drawn from make exactly this
     * exception and no other.
     */
    font-weight: 700;
    line-height: 20px;
    text-align: center;
}

/* ================================================= the collection chip */

/*
 * "Pickup · Ready in about 15 minutes", in the header, with the times behind it.
 *
 * A <details> rather than a dialog: it opens and closes with no JavaScript at
 * all, which matters for a control that carries a form. The panel posts, the
 * server saves the choice in the session, and the checkout starts from it.
 *
 * There is no shop to choose. The sites this was drawn from put a list of
 * branches here with a map above it, because they have several; this shop has
 * one, and a question with a single answer is a step that costs a tap and
 * returns nothing.
 */
.chip-wrap { position: relative; flex: none; }

.chip {
    display: flex;
    align-items: center;
    gap: 8px;
    height: 48px;
    padding: 0 12px;
    border: 1px solid var(--line-strong);
    border-radius: 999px;
    background: var(--bg);
    cursor: pointer;
    list-style: none;
    -webkit-tap-highlight-color: transparent;
}

/* Safari draws its own triangle on a summary and it cannot be styled. */
.chip::-webkit-details-marker { display: none; }

.chip:hover { border-color: var(--brand); }

.chip-mark {
    display: grid;
    place-items: center;
    width: 30px;
    height: 30px;
    flex: none;
    border-radius: 999px;
    background: var(--brand-soft);
    color: var(--brand);
}

.chip-mark .ico { width: 17px; height: 17px; vertical-align: 0; }

/* On a phone only the disc shows — the words would take the room the shop's
   name needs, and the disc still opens the same panel. */
.chip-text { display: none; min-width: 0; }
.chip-text b { display: block; font-size: .8rem; font-weight: 500; line-height: 1.2; }

.chip-text small {
    display: block;
    color: var(--ink-2);
    font-size: .76rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 180px;
}

.chip-arrow { display: none; width: 16px; height: 16px; color: var(--ink-3); vertical-align: 0; }

[data-collection][open] .chip-arrow { transform: rotate(180deg); }

/*
 * Pinned to the screen, not to the chip.
 *
 * Hung off the chip with right:0 it was 340px wide starting from a disc that
 * sits 48px from the right edge of a 375px phone — so it began 41px off the
 * left of the screen, and the first choice was unreadable and half untappable.
 * A panel this size has nothing to gain from tracking the button that opened
 * it; the screen is the thing it has to fit inside.
 */
.chip-panel {
    position: fixed;
    top: calc(var(--header-h) + 6px);
    left: 12px;
    right: 12px;
    z-index: 40;
    padding: 16px;
    background: var(--bg);
    border-radius: var(--r-lg);
    box-shadow: var(--shadow-2), 0 0 0 1px var(--line);
}

.chip-panel h3 { margin: 0 0 10px; font-size: 1rem; font-weight: 500; }

.chip-note {
    margin: 12px 0 0;
    color: var(--ink-3);
    font-size: .78rem;
}

/* "As soon as possible" chosen: there is no time to pick, so the list goes.
   :has() rather than a script — the panel opens and works with no JavaScript,
   and the list hiding itself should not be the one part that needs some. */
[data-collection]:has([value="asap"]:checked) [data-collection-time] { display: none; }

/* =============================================================== search */

/*
 * The search box lives inside the header now, and takes one of two shapes.
 *
 * On a phone it is the drawer behind the magnifier: pinned across the width of
 * the bar and hanging just under it, because a phone cannot carry the box, the
 * shop's name and the buttons on one line.
 *
 * On a computer it is simply in the bar, filling the middle — see the wide-screen
 * block at the end of the file. That is the arrangement every ordering site of
 * this kind uses, and the reason is that a menu of seventy dishes is searched far
 * more often than it is scrolled.
 */
.searchbar {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    z-index: 2;
    padding: 4px 16px 12px;
    background: rgba(255, 255, 255, .96);
    backdrop-filter: saturate(160%) blur(12px);
}

.searchbar label { display: block; position: relative; }

/*
 * The type selector has to be in here.
 *
 * Further down, the forms section styles input[type=search] along with every
 * other field, and that rule carries exactly the same specificity as a plain
 * `.searchbar input` while coming later in the file — so it won, the padding
 * below was thrown away, and the typing ran underneath the icon.
 */
/*
 * An outlined pill with the magnifier inside it on the left, which is the
 * shape the shop asked for.
 *
 * It was a filled grey box with the icon on the right. Grey-on-white reads as a
 * disabled field at a glance, and the magnifier on the right is the shape a
 * SEARCH BUTTON has rather than a box you type in — put it on the left and the
 * icon is a label for the field instead of something to press.
 */
.searchbar input[type=search] {
    width: 100%;
    height: 48px;
    min-height: 48px;
    border: 1px solid var(--line-strong);
    border-radius: 999px;
    background: var(--bg);
    /* Room on the left for the magnifier, and on the right for the × that
       appears once something is typed. */
    padding: 0 44px 0 46px;
    /* 16px is the threshold: below it, Safari on an iPhone zooms the page in
       when the customer taps the field, and never zooms back out. */
    font-size: 16px;
    color: var(--ink);
    -webkit-appearance: none;
    appearance: none;
}

.searchbar input[type=search]::placeholder { color: var(--ink-3); }

/* WebKit draws its own little × inside a search field. Ours is nicer and is the
   one the script controls, so hide theirs rather than showing two. */
.searchbar input[type=search]::-webkit-search-cancel-button,
.searchbar input[type=search]::-webkit-search-decoration {
    -webkit-appearance: none;
    appearance: none;
}

.searchbar input[type=search]:hover { border-color: var(--ink-3); }

.searchbar input[type=search]:focus {
    outline: none;
    border-color: var(--brand);
    box-shadow: 0 0 0 3px var(--brand-soft);
}

/*
 * The magnifier sits inside the left-hand end and stays there, typed in or not.
 * It is a label for the box, not a button, so it never goes away and never
 * takes a tap — the × that empties the field is a separate thing at the other
 * end.
 */
.searchbar .search-icon {
    position: absolute;
    left: 17px;
    top: 50%;
    transform: translateY(-50%);
    display: grid;
    color: var(--ink-3);
    pointer-events: none;
}

.searchbar .search-icon .ico { width: 19px; height: 19px; vertical-align: 0; }

.searchbar .search-clear {
    position: absolute;
    right: 10px;
    top: 50%;
    transform: translateY(-50%);
    display: grid;
    place-items: center;
    width: 32px;
    height: 32px;
    border: 0;
    border-radius: 999px;
    background: transparent;
    color: var(--ink-3);
    cursor: pointer;
}

.searchbar .search-clear .ico { width: 18px; height: 18px; vertical-align: 0; }

/* ==================================================== categories (pills) */

.tabs {
    position: sticky;
    top: var(--header-h);
    z-index: 20;
    height: var(--tabs-h);
    display: flex;
    align-items: center;
    background: rgba(255, 255, 255, .94);
    backdrop-filter: saturate(160%) blur(12px);
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
}

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

.tabs ul {
    display: flex;
    gap: 10px;
    list-style: none;
    margin: 0;
    padding: 0 16px;
}

.tabs a {
    display: grid;
    place-items: center;
    height: 48px;
    padding: 0 26px;
    white-space: nowrap;
    border-radius: 999px;
    background: var(--soft);
    color: var(--ink-2);
    font-size: .94rem;
    font-weight: 500;
    letter-spacing: .04em;
    text-transform: uppercase;
    text-decoration: none;
    transition: background-color .15s, color .15s;
}

.tabs a[aria-current="true"] {
    background: var(--brand);
    color: #fff;
}

.cat {
    padding-top: 26px;
    scroll-margin-top: calc(var(--stuck) + 4px);
}

/*
 * The shop's gold, because the shop asked for it: slide 10 draws an arrow from
 * the gold SAIGON lettering to these headings.
 *
 * Kept at 500 rather than thinned to 400 with the rest of the page — gold on
 * white has little contrast to spare, and taking weight out as well would leave
 * the heading fainter than the dish names under it.
 */
.cat > h2 {
    margin: 0 0 3px;
    font-size: 1.4rem;
    font-weight: 500;
    letter-spacing: -.01em;
    text-transform: uppercase;
    color: var(--gold);
}

.cat > .cat-sub {
    margin: 0;
    color: var(--ink-2);
    font-size: .88rem;
}

/* ================================================================ dishes */

.dishes {
    list-style: none;
    margin: 14px 0 0;
    padding: 0;
    display: grid;
    /* Two columns on a phone, as in the mockup — photos big enough to know the
       dish by, with four dishes still on one screen. */
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 14px;
}

/*
 * A dish card: square photo with the ＋ on its corner, then name, description
 * and price.
 *
 * The ＋ used to sit on a row of its own at the bottom, opposite the price. It
 * is on the photograph now, which is where every ordering site of this kind
 * puts it — the photograph is the biggest thing on the card and the thumb is
 * already over it, so the button meets the finger where the finger is.
 *
 * margin-top:auto on the price row keeps the prices of two cards side by side
 * on one line even when one name runs to two lines; without it the grid goes
 * ragged.
 */
.dish {
    display: flex;
    flex-direction: column;
    background: var(--bg);
    border-radius: var(--r-lg);
    /* A soft shadow and no border: a 1px border makes the grid look like a
       spreadsheet, while the shadow lifts the card off the white ground the
       way the mockup does. */
    box-shadow: 0 1px 2px rgba(20, 24, 28, .05), 0 8px 20px rgba(20, 24, 28, .07);
    padding: 8px 8px 10px;
}

.dish-link,
.dish-body {
    display: block;
    text-decoration: none;
    color: inherit;
}

/* What the ＋ is positioned against. */
.dish-photo { position: relative; }

.dish-add {
    position: absolute;
    right: 8px;
    bottom: 8px;
    display: flex;
    align-items: center;
}

/*
 * Sold out says so on the photograph, in a pill that can be read against it —
 * grey text on a greyed-out picture cannot.
 */
.dish-add .soldout {
    background: rgba(20, 24, 28, .78);
    color: #fff;
    border-radius: 999px;
    padding: 5px 11px;
    font-size: .74rem;
    font-weight: 500;
    backdrop-filter: blur(4px);
}

.thumb {
    position: relative;
    aspect-ratio: 1 / 1;
    border-radius: var(--r);
    background: var(--soft);
    display: grid;
    place-items: center;
    overflow: hidden;
}

.thumb img { width: 100%; height: 100%; object-fit: cover; }

/* A dish with no photo yet: a faint drawing, not a huge initial letter. */
.thumb-blank {
    width: 38%;
    height: 38%;
    color: var(--line-strong);
}

/*
 * 400, not 500 — "font chữ mảnh như bên Square này", slide 8.
 *
 * The dish name is the most repeated piece of type on the page: fifty of them
 * down a menu, so its weight IS the weight of the site. Taking it to 400 is
 * most of what the shop was asking for, and the price beside it was already
 * 400, so the two now sit at the same weight the way a printed menu sets them.
 */
.dish-name {
    margin: 12px 2px 0;
    font-size: 1.02rem;
    font-weight: 400;
    line-height: 1.32;
    letter-spacing: -.01em;
}

.dish-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    /* Pushes the price row to the bottom of the card — see the note on .dish. */
    margin-top: auto;
    padding: 8px 2px 0;
}

/*
 * A price is ordinary text, and gets ordinary text's weight.
 *
 * It was raised to 500 when the ＋ moved off this row, which was the wrong
 * direction: 500 is what the page uses for the things that have to stand out,
 * and on a card the dish's name is that thing, not its price. Nobody scans a
 * menu for $18.50.
 *
 * No tabular figures either. They belong in a column of amounts that has to
 * line up — the totals box, the cart — and there they earn the extra width they
 * give every digit. One price sitting alone under a photograph only reads as
 * heavier for them.
 */
.dish-price { font-size: 1.02rem; font-weight: 400; }

.dish-desc {
    margin: 4px 2px 0;
    color: var(--ink-2);
    font-size: .84rem;
    /* A long description doubles the height of every card and leaves the
       customer scrolling forever; two lines say enough about a dish. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

/* The label sits on the photo rather than crowding the name. */
.badge {
    position: absolute;
    top: 8px;
    left: 8px;
    background: rgba(20, 24, 28, .72);
    color: #fff;
    border-radius: 999px;
    font-size: .64rem;
    font-weight: 500;
    letter-spacing: .06em;
    text-transform: uppercase;
    padding: 4px 9px;
    backdrop-filter: blur(4px);
}

.dish.is-out .thumb { filter: grayscale(1); opacity: .5; }

/*
|==============================================================================
| Three ways to lay the same menu out
|==============================================================================
|
| Not three designs — three answers to "how much do I already know about this
| menu". Somebody new reads the photographs; a regular ordering their usual
| wants the whole list on one screen and is slowed down by pictures they have
| seen a hundred times.
|
|   list   two dense columns of rows, no photographs. A seventy-dish menu fits
|          in two screens instead of nine.
|   cards  two columns of bordered cards, no photographs, the price on its own
|          line at the foot. Reads like a printed menu: the description gets
|          room to be read rather than being clipped to two lines.
|   tiles  the photographs, five to a row on a computer. For browsing.
|
| Only `tiles` shows pictures, which is the whole point of having it — and the
| reason the other two can be so much denser.
|
| Chosen with the switch above the categories and kept in the browser — see
| menu.js. `cards` is the default, and it is what a body with no data-view gets,
| so a browser with no JavaScript lands on a real layout rather than a broken
| one.
*/

.viewbar {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 8px;
    margin-top: 12px;
}

.viewbar-label {
    font-size: .82rem;
    color: var(--ink-2);
}

.viewswitch {
    display: flex;
    gap: 2px;
    padding: 3px;
    background: var(--soft);
    border-radius: 999px;
}

.viewswitch button {
    display: grid;
    place-items: center;
    width: 36px;
    height: 32px;
    border: 0;
    border-radius: 999px;
    background: transparent;
    color: var(--ink-3);
    cursor: pointer;
}

.viewswitch button .ico { width: 18px; height: 18px; }

.viewswitch button[aria-pressed="true"] {
    background: var(--bg);
    color: var(--ink);
    box-shadow: 0 1px 3px rgba(20, 24, 28, .14);
}

/* ------------------------------------------------------------------ list */

/*
 * No photograph, and the price against the right edge so the column of them
 * can be read straight down. The ＋ moves to the end of the row, since there is
 * no longer a picture for it to sit on.
 */
body[data-view="list"] .dishes {
    grid-template-columns: minmax(0, 1fr);
    gap: 0;
}

body[data-view="list"] .dishes > .dish {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto auto;
    align-items: start;
    column-gap: 14px;
    padding: 14px 2px;
    border-radius: 0;
    box-shadow: none;
    border-bottom: 1px solid var(--line);
}

/*
 * display:contents lifts the ＋ out of the photo wrapper so it can take a
 * column of the row's grid.
 *
 * The link around the photograph has to go entirely, not just the .thumb
 * inside it: an empty link is still a grid item, and an unplaced grid item is
 * auto-placed into a column of its own. Hiding only the picture left a fourth
 * column holding nothing, which pushed the price and the ＋ off the right of a
 * phone screen — the page looked correct and was simply too wide.
 */
body[data-view="list"] .dishes > .dish > .dish-photo { display: contents; }
body[data-view="list"] .dishes > .dish .dish-link { display: none; }

body[data-view="list"] .dishes > .dish > .dish-body {
    grid-column: 1;
    grid-row: 1;
}

body[data-view="list"] .dishes > .dish .dish-name { margin: 0; }
body[data-view="list"] .dishes > .dish .dish-desc { margin-top: 2px; }

body[data-view="list"] .dishes > .dish > .dish-foot {
    grid-column: 2;
    grid-row: 1;
    margin: 0;
    padding: 0;
}

/* Static, not absolute: with the photograph gone there is nothing left to
   position against, and an absolute ＋ would settle over the description. */
body[data-view="list"] .dishes > .dish .dish-add {
    position: static;
    grid-column: 3;
    grid-row: 1;
}

/* Two columns of rows once there is room for them, as the reference site
   does — a list one column wide on a 1400px screen wastes half the page. */
@media (min-width: 900px) {
    body[data-view="list"] .dishes {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        column-gap: 36px;
    }
}

/* ----------------------------------------------------------------- tiles */

/*
 * The photograph and little else: name and price under it, no description.
 *
 * The description is what makes a card tall, and the whole point of this
 * layout is fitting five across. A dish somebody wants to read about is one
 * tap away on its own page.
 */
/*
 * Counted columns rather than auto-fill.
 *
 * auto-fill with a minimum width fits as many as it can, which on a wide screen
 * came out at seven — tiles small enough that the photograph stopped being the
 * point of the layout. Five is what the reference does and what a plate of food
 * survives being shrunk to.
 */
body[data-view="tiles"] .dishes {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 12px;
}

@media (min-width: 600px) {
    body[data-view="tiles"] .dishes { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

@media (min-width: 900px) {
    body[data-view="tiles"] .dishes { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

@media (min-width: 1200px) {
    body[data-view="tiles"] .dishes { grid-template-columns: repeat(5, minmax(0, 1fr)); }
}

body[data-view="tiles"] .dishes > .dish .dish-desc { display: none; }

body[data-view="tiles"] .dishes > .dish .dish-name {
    margin-top: 8px;
    font-size: .92rem;
    /* Clipped to one line so no tile in the row is taller than the others; the
       full name is on the dish's own page. */
    display: -webkit-box;
    -webkit-line-clamp: 1;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

body[data-view="tiles"] .dishes > .dish .dish-price { font-size: .92rem; }

/* Against the right edge, so a row of tiles gives a column of prices that can
   be read straight down. */
body[data-view="tiles"] .dishes > .dish > .dish-foot {
    padding-top: 4px;
    justify-content: flex-end;
}

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

/*
 * Two columns of bordered cards with no photograph, and the price on a line of
 * its own at the foot.
 *
 * The description is NOT clipped here, which is the whole difference between
 * this and the tiles above: with the picture gone the words are all there is to
 * tell two pizzas apart, and half a list of toppings tells you nothing. Cards
 * in a row still line up, because the price row is pushed to the bottom of each.
 */
body[data-view="cards"] .dishes {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 12px;
}

body[data-view="cards"] .dishes > .dish {
    border: 1px solid var(--line);
    border-radius: var(--r);
    box-shadow: none;
    padding: 14px 16px 12px;
}

body[data-view="cards"] .dishes > .dish .thumb { display: none; }
body[data-view="cards"] .dishes > .dish .dish-name { margin: 0; }

body[data-view="cards"] .dishes > .dish .dish-desc {
    margin: 5px 0 0;
    display: block;
    overflow: visible;
}

body[data-view="cards"] .dishes > .dish > .dish-foot { padding: 18px 0 0; }

/*
 * The ＋ has no photograph left to sit on, so it comes back onto the price row,
 * opposite the price — which is where it lived before the pictures arrived.
 *
 * order:2 with a negative top margin rather than moving it in the markup: the
 * markup is shared by all three layouts, and one of them still needs the button
 * on the corner of a picture.
 */
body[data-view="cards"] .dishes > .dish > .dish-photo {
    display: flex;
    order: 2;
    justify-content: flex-end;
    margin-top: -36px;
    pointer-events: none;
}

body[data-view="cards"] .dishes > .dish .dish-add {
    position: static;
    pointer-events: auto;
}

@media (max-width: 620px) {
    body[data-view="cards"] .dishes { grid-template-columns: minmax(0, 1fr); }
}

.soldout { font-size: .84rem; color: var(--ink-3); }

.stepper {
    display: flex;
    align-items: center;
    gap: 2px;
    padding: 0 2px;
}

.stepper form { display: contents; }

.stepper button {
    width: 36px;
    height: 36px;
    border: 0;
    border-radius: 999px;
    background: transparent;
    color: var(--brand-ink);
    line-height: 1;
    cursor: pointer;
    display: grid;
    place-items: center;
    padding: 0;
    -webkit-tap-highlight-color: transparent;
    transition: transform .06s;
}

.stepper button .ico { width: 19px; height: 19px; vertical-align: 0; }

.stepper button:active { transform: scale(.9); }

.stepper .qty {
    min-width: 20px;
    text-align: center;
    font-size: .95rem;
    font-weight: 500;
    font-variant-numeric: tabular-nums;
}

.stepper[data-empty="true"] .btn-minus,
.stepper[data-empty="true"] .qty { display: none; }

/* The admin's own preparation-time stepper — an <input> standing in for the
   customer app's <span>, so it needs its own box and no native spinner. */
[data-minutes-stepper] {
    display: inline-flex;
    border: 1px solid var(--line-strong);
    border-radius: 999px;
    background: var(--bg);
}

[data-minutes-stepper] input.qty {
    width: 48px;
    border: 0;
    background: transparent;
    -moz-appearance: textfield;
}

[data-minutes-stepper] input.qty::-webkit-outer-spin-button,
[data-minutes-stepper] input.qty::-webkit-inner-spin-button {
    -webkit-appearance: none;
    margin: 0;
}

/*
 * On a dish card the stepper is a small pill on the corner of the photograph.
 * With nothing ordered it shrinks to exactly one round button filled with the
 * brand colour — an unmistakable invitation, and small enough to cover almost
 * none of the food.
 *
 * A shadow rather than a border, because it stands on a photograph and a hairline
 * border disappears against a busy one.
 */
.dish-add .stepper {
    background: var(--bg);
    border-radius: 999px;
    box-shadow: 0 1px 3px rgba(20, 24, 28, .3), 0 0 0 1px rgba(255, 255, 255, .55);
}

/*
 * White, as the shop asked on slide 11 — the arrow goes from our blue button to
 * the white one on the site they are copying.
 *
 * The shadow is doing real work here, not decoration. This button stands on a
 * photograph, and about a third of the shop's own photographs have a white or
 * cream ground: Special Fried Rice, Stir-Fried Vegetables, every plate shot on
 * the pale studio backdrop. A plain white circle on those simply is not there.
 * So it carries a hairline of its own plus a shadow underneath, which is what
 * separates it from the plate behind it.
 */
.dish-add .stepper[data-empty="true"] {
    background: #fff;
    box-shadow: 0 1px 4px rgba(20, 24, 28, .3), 0 0 0 1px rgba(20, 24, 28, .12);
    padding: 0;
}

.dish-add .stepper[data-empty="true"] .btn-plus { color: var(--brand-ink); }

/* A dish with options: the button looks the same but opens the dish page. */
.dish-open {
    display: grid;
    place-items: center;
    width: 36px;
    height: 36px;
    flex: none;
    border-radius: 999px;
    background: #fff;
    color: var(--brand-ink);
    text-decoration: none;
    box-shadow: 0 1px 4px rgba(20, 24, 28, .3), 0 0 0 1px rgba(20, 24, 28, .12);
}

.dish-open .ico { width: 19px; height: 19px; vertical-align: 0; }

/*
 * How many of this dish are in the cart, on a dish that has options.
 *
 * The ＋ grows into a pill to hold it, the way the stepper does on a dish with
 * none — so the two kinds of card read the same at a glance and a customer
 * scanning the menu can see what they have already chosen either way.
 */
.dish-count {
    min-width: 18px;
    padding-left: 4px;
    text-align: center;
    font-size: .95rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

.dish-open:has(.dish-count:not([hidden])) {
    width: auto;
    min-width: 36px;
    padding: 0 10px 0 6px;
    gap: 2px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

.stepper.is-busy { opacity: .45; pointer-events: none; }

/* The large outlined stepper — used on the dish page and in the cart. */
.stepper-lg button {
    width: 44px;
    height: 44px;
    border: 1px solid var(--line-strong);
    color: var(--ink);
}

.stepper-lg button .ico { width: 20px; height: 20px; }

.stepper-lg .qty { min-width: 30px; font-size: 1.05rem; font-weight: 500; }

/* The bottom bar of the dish page: the stepper keeps a fixed width and the add
   button takes what is left, so its label is never squeezed onto two lines. */
.actionbar .stepper-lg { padding: 0 2px; }

.actionbar .btn { padding-inline: 18px; }

.btn-split,
.btn-total { gap: 10px; }

/*
 * The amount must never be clipped: it is the most important thing on the
 * button. On a narrow screen the label on the left is ellipsised instead, and
 * the money stays whole.
 */
.btn-split > :first-child,
.btn-total > :first-child { overflow: hidden; text-overflow: ellipsis; }

.btn-split > :last-child,
.btn-total > :last-child { flex: none; }

/* ============================================================ dish page */

.item-page { max-width: 640px; margin-inline: auto; }

/*
 * The photo runs the full width of the screen with the back button floating on
 * top of it, as in the mockup. The negative margin escapes the 16px padding
 * that .wrap puts on.
 */
.item-hero {
    position: relative;
    /*
     * A frame of a definite shape, and the picture shown whole inside it.
     *
     * The ratio is NOT what cropped the drinks — object-fit: cover was. A fixed
     * frame plus `contain` shows every photograph in full; what it costs is a
     * band of --soft beside a picture whose shape is not the frame's.
     *
     * Square, because the photographs are: 73 of the menu's 84 files are
     * 1000x1000. The frame was 4:3, then 10/9, and both were a guess that the
     * pictures then had to be cut to fit. Matching the photography is what
     * makes contain and cover come to the same thing for almost every dish —
     * no crop AND no band beside it.
     *
     * The eleven that are not square get the band, filled by the blurred copy
     * below. That is the right way round: the exception pays, not the rule.
     *
     * Capped so a square picture cannot take the whole screen. A cap clamps the
     * height while the width stays, so the frame stops being square and the
     * picture is contained inside it — which is the wanted behaviour and not a
     * special case.
     */
    aspect-ratio: 1 / 1;
    max-height: 62dvh;
    margin-inline: -16px;
    overflow: hidden;
    background: var(--soft);
    display: grid;
    place-items: center;
}

/* The whole photograph, never a piece of it. */
.item-hero img {
    display: block;
    width: 100%;
    height: 100%;
    /*
     * contain, not cover. This is the line that stopped the cropping.
     *
     * cover fills the frame and throws away what will not fit, which sliced a
     * drink photographed standing up clean through the glass — the "bị cắt
     * ngang" of slide 171. contain fits the whole picture inside the frame and
     * leaves --soft beside it, which is the same ground the frame shows while
     * the image is still loading.
     *
     * height: 100% and not auto, deliberately: the frame gives the height, the
     * picture does not. See the note on .item-hero.
     */
    object-fit: contain;

    /* Above the blurred copy below it. */
    position: relative;
    z-index: 1;
}

/*
 * The ground under a contained photograph: the same picture, filling the
 * frame, thrown well out of focus.
 *
 * A band of flat grey beside a picture reads as something that failed to load.
 * The picture's own colours do not — they read as the photograph having a
 * setting, which is what every phone gallery does with a portrait shot on a
 * landscape screen.
 *
 * scale, because a blur has nothing to sample past the edge and leaves the
 * outermost pixels pale; 1.15 pushes that fade outside the frame, which
 * overflow: hidden on .item-hero then clips away.
 *
 * It is decoration and takes no space: absolutely positioned, so it is not a
 * grid item and place-items on the frame never sees it.
 */
.item-hero .item-hero-wash {
    position: absolute;
    inset: 0;
    z-index: 0;
    width: 100%;
    height: 100%;
    /* cover, not the contain above — this one is meant to fill. */
    object-fit: cover;
    filter: blur(26px) saturate(1.25);
    transform: scale(1.15);
    /* The picture on top is the one being read; this only has to be there. */
    opacity: .55;
}

/*
 * Nothing at all where the browser will not blur it.
 *
 * Without the filter this is the cropped photograph the whole change was
 * about, sitting full-bleed behind a contained copy of itself — two versions
 * of one dish at two sizes, which is worse than a grey band.
 */
@supports not (filter: blur(1px)) {
    .item-hero .item-hero-wash { display: none; }
}

@media (prefers-reduced-motion: reduce) {
    .item-hero .item-hero-wash { transform: none; }
}

.item-hero .thumb-blank { width: 22%; height: 22%; }

/* The same two signals the card on the grid gives: greyed, and said in words. */
.item-hero.is-out img { filter: grayscale(1); opacity: .5; }

/* The ground goes grey with the dish it belongs to, or the frame stays in
   colour around a photograph that has been drained of it. */
.item-hero.is-out .item-hero-wash { filter: grayscale(1) blur(26px); opacity: .3; }

.item-soldout {
    position: absolute;
    right: 28px;
    bottom: 12px;
    background: rgba(20, 24, 28, .78);
    color: #fff;
    border-radius: 999px;
    padding: 6px 14px;
    font-size: .8rem;
    font-weight: 500;
    backdrop-filter: blur(4px);
}

.item-back {
    position: absolute;
    top: 12px;
    left: 28px;
    background: rgba(255, 255, 255, .92);
    border-color: transparent;
    box-shadow: var(--shadow-1);
}

.item-badge { top: auto; bottom: 12px; left: 28px; }

/* ============================================== the dish, over the menu */

/*
 * A <dialog>, so the browser owns the backdrop, Escape, the focus trap and
 * giving focus back to whatever opened it.
 *
 * A dialog is not in the normal flow, so it is placed rather than laid out:
 * centred on a computer, and against the bottom of a phone where the thumb is.
 */
.item-dialog {
    border: 0;
    padding: 0;
    background: transparent;
    max-width: 560px;
    width: calc(100% - 24px);
    max-height: 92dvh;
}

/*
 * A column, so the body can be told to scroll.
 *
 * [open] and not the bare selector: the browser's own rule is
 * `dialog:not([open]) { display: none }`, and `display: flex` here without the
 * guard would out-specify it and leave a closed dialog sitting in the page.
 */
.item-dialog[open] {
    display: flex;
    flex-direction: column;

    /*
     * Nothing escapes the sheet.
     *
     * A <dialog> is `height: fit-content` in the browser's own stylesheet, so a
     * flex child asked to fill it sizes to its CONTENT and then runs past
     * max-height — overflow being visible, it simply drew outside, and on a
     * laptop the Add to cart button was rendered below the bottom edge of the
     * sheet where nothing could reach it. This is the line that makes the cap
     * mean something; the body below does the scrolling.
     */
    overflow: hidden;
}

.item-dialog::backdrop { background: rgba(12, 16, 22, .55); backdrop-filter: blur(2px); }

.item-dialog-body {
    background: var(--bg);
    border-radius: var(--r-lg);
    /* The photograph runs to the edges of the box, so the box has to clip it
       back to its own corners. */
    overflow: hidden auto;
    /*
     * Whatever is left of the dialog, and scroll the rest.
     *
     * It used to repeat `92dvh` here, which is one limit written in two places
     * — and then `max-height: 100%`, which is worse: a percentage resolves
     * against the parent's height, the parent is `height: fit-content`, so it
     * computes to no limit at all. The sheet grew past the bottom of the phone
     * and took the Add to cart button with it.
     *
     * flex + min-height:0 is the pair that works. The dialog is capped at
     * 92dvh; this fills what is left of that and scrolls its own overflow.
     * min-height:0 is not optional — without it a flex item refuses to shrink
     * below its content, which is exactly the refusal that caused this.
     */
    flex: 1 1 auto;
    min-height: 0;

    /*
     * No padding under the action bar.
     *
     * There used to be 16px here, below a bar that sticks to bottom: 0 — so the
     * bar stopped 16px short of the sheet's edge and the dish options scrolled
     * through the strip underneath it. The bar carries its own bottom padding
     * (and the phone's home-indicator inset with it), which is the only way the
     * two can agree.
     */
    padding: 0 16px 0;
}

/* .item-page centres itself on a page; inside the modal the modal has already
   done that, and the extra max-width only made it narrower again. */
.item-dialog .item-page { max-width: none; }

/* The hero's negative margin escapes .wrap's 16px on a page. Here it has to
   escape the modal's padding instead — same number, different owner. */
.item-dialog .item-hero {
    margin-inline: -16px;
    margin-top: 0;

    /*
     * Square here too, but reached without aspect-ratio.
     *
     * aspect-ratio would take the height from the width, and the width inside
     * this sheet is not stable: the body is a scroll container, so the moment
     * its scrollbar appears the width drops 15px, the frame gets shorter, the
     * content fits, the scrollbar goes and the width comes back. The sheet
     * flickered for as long as it was open.
     *
     * Every term below is measured against the viewport or is a constant, so
     * none of them can move when that scrollbar does:
     *
     *   560px  the sheet's own max-width — square on a laptop
     *   100vw  the sheet at full width on a phone (vw counts the scrollbar,
     *          so it does not move either)
     *   62dvh  and the dish is not only a photograph: on a short window this
     *          wins, the frame stops being square, and the picture is
     *          contained inside it with the blurred copy behind.
     */
    aspect-ratio: auto;
    height: min(560px, 100vw, 62dvh);
}


/* Back to bottom:0 inside the modal. A <dialog> is painted in the browser's top
   layer, above every fixed element on the page including the footer band, so
   there is nothing down there for it to clear. */
.item-dialog .actionbar {
    position: sticky;
    bottom: 0;
    margin-inline: -16px;

    /*
     * Pulled out to the edges, then put back in.
     *
     * The negative margin is what lets the bar's own surface and hairline run
     * the full width of the sheet. Without the matching padding its CONTENTS go
     * out there with it, and the stepper and the button end up against the
     * rounded corners with nothing between them and the edge — which is what
     * the shop marked. The number is the same 16px, once out and once back.
     */
    padding-inline: 16px;
    padding-bottom: calc(18px + env(safe-area-inset-bottom, 0px));

    /*
     * Pinned to the foot of the sheet on every screen, laptop included.
     *
     * The sheet is often taller than the window — a dish with three option
     * groups always is — so the button that ends it has to be reachable without
     * scrolling to the end of a list. Solid, with a hairline above: the options
     * pass behind it, and a bar you can see through while it is being pressed
     * reads as a fault.
     *
     * The rounded corners are the sheet's own, kept so the bar does not square
     * off a box that is round everywhere else.
     */
    background: var(--bg);
    box-shadow: 0 -1px 0 var(--line);
    border-radius: 0 0 var(--r-lg) var(--r-lg);
}

@media (max-width: 560px) {
    /* A bottom sheet has square corners at the foot; so does its bar. */
    .item-dialog .actionbar { border-radius: 0; }
}

@media (max-width: 560px) {
    .item-dialog {
        width: 100%;
        max-width: none;

        /*
         * Against the bottom edge: on a phone the top of the screen is the
         * furthest thing from the thumb, and the button that ends this is at
         * the bottom of the card.
         *
         * **The margin is what does it, not align-self.** A modal dialog is
         * painted in the browser's top layer and positioned absolutely against
         * `inset: 0`, so align-self has nothing to align it inside — the UA
         * centres it with `margin: auto` and that margin is the only handle
         * there is. `auto auto 0` means "take up the slack above me", which is
         * the bottom sheet.
         *
         * It was `margin: 0 0 0 auto`, which set the TOP margin to zero and so
         * pinned the sheet to the top of the screen instead. Nobody saw it
         * while the card was short; the moment the action bar grew a second row
         * the Add to cart button was off the bottom of the phone, reachable
         * only by scrolling a sheet that looked like it had nothing left in it.
         */
        margin: auto auto 0;
    }

    .item-dialog-body { border-radius: var(--r-lg) var(--r-lg) 0 0; }
}

/* The page behind must not scroll under the modal — on a phone that is the
   difference between closing it and losing your place in the menu. */
/*
 * The page behind a modal must not scroll.
 *
 * scrollbar-gutter with it, and on this element specifically. The viewport's
 * scrollbar is governed by whichever of <html>/<body> first declares a
 * non-visible overflow — so the moment this class lands, `body` is the element
 * the viewport takes its scrolling from, and the gutter has to be declared
 * here or the page loses 15px of width the instant a dish is opened. Everything
 * measured against the width then moves, which is where the flicker came from.
 */
body.is-locked { overflow: hidden; scrollbar-gutter: stable; }

/* ============================================ when they are coming for it */

/*
 * On the cart page, above the food: this is where somebody stops choosing and
 * starts thinking about collecting. It used to be a pill in the header on every
 * page, which put it in front of people who were still reading the menu and
 * left the header carrying three things instead of two.
 */
/*
 * Where the food is collected from and when, at the head of the cart.
 *
 * A soft grey block rather than a white card: the cart's own white cards are
 * the FOOD, and this is the delivery note wrapped round them. Two rows, each
 * one label and one answer.
 */
.pickup-card {
    margin-bottom: 14px;
    padding: 4px 16px;
    background: var(--soft);
    border-radius: var(--r-lg);
}

/*
 * The row, not the summary.
 *
 * One of these two rows opens a panel and the other states the shop's address,
 * so only one of them is a <summary> — putting the layout on .fulfil-row is
 * what lets both look identical while behaving differently.
 */
.fulfil-row {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 0;
    cursor: pointer;
    list-style: none;
}

.fulfil-row + .fulfil-row,
.fulfil-row + .fulfil .fulfil-row,
.fulfil-row.is-fixed + .fulfil > .fulfil-row { border-top: 1px solid var(--line); }

.fulfil-row::-webkit-details-marker { display: none; }

/* White on the grey block, so the disc is a disc rather than a shape that
   happens to be the same colour as what is behind it. */
.fulfil-mark {
    display: grid;
    place-items: center;
    width: 32px;
    height: 32px;
    flex: none;
    border-radius: 999px;
    background: var(--bg);
    color: var(--brand);
}

.fulfil-mark .ico { width: 17px; height: 17px; }

/* The label, and then the answer against the right edge. Stacked, the two took
   two lines to say one thing. */
.fulfil-key { color: var(--ink-2); flex: none; }

.fulfil-value {
    flex: 1;
    min-width: 0;
    text-align: right;
    font-weight: 600;
    /* A long address gives way rather than pushing the pencil off the row. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The shop's address: a statement, not a choice. There is one shop, so a
   control offering to change it would have a single answer. */
.fulfil-row.is-fixed { cursor: default; }

/*
 * A pencil, not a "Change" pill.
 *
 * The pill was the widest thing on the row and it repeated what the row already
 * offered. A pencil at the end of a value is the mark every app uses for "this
 * one can be edited", and it leaves the width to the answer, which is the part
 * worth reading.
 */
.fulfil-change {
    display: grid;
    place-items: center;
    flex: none;
    width: 30px;
    height: 30px;
    border-radius: 999px;
    color: var(--ink-2);
}

.fulfil-change .ico { width: 16px; height: 16px; vertical-align: 0; }

.fulfil-row:hover .fulfil-change { background: var(--bg); color: var(--brand-ink); }

/* Open: the pencil turns solid, so the row says which state it is in. */
.fulfil[open] .fulfil-change { background: var(--bg); color: var(--brand); }

.fulfil-panel { padding: 4px 0 6px; border-top: 1px solid var(--line); }
.fulfil-panel h3 { margin: 12px 0 8px; font-size: .95rem; font-weight: 500; }

@media (min-width: 640px) {
    .item-hero { margin-inline: 0; border-radius: var(--r-lg); }
    .item-back, .item-badge { left: 12px; }
}

/*
 * Name on the left, price on the right, one row — as in the mockup.
 *
 * Sticky, so the dish being decided on stays on screen while the options
 * underneath it scroll — it used to scroll away with the photo, and a list
 * of choices with no name above them reads as a list of choices for
 * nothing in particular.
 *
 * Padding rather than margin for the space above: a sticky element's
 * margin sits outside its own background, so a margin-top here would have
 * let whatever just scrolled past show through a gap above the name.
 */
.item-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 16px;
    margin: 0 0 8px;
    padding: 22px 0 8px;
    position: sticky;
    /* Below the site's own sticky top bar on a standalone page; the modal
       has no top bar of its own, so it sticks flush to its lid instead. */
    top: var(--header-h);
    z-index: 5;
    background: var(--bg);
}

.item-dialog .item-head { top: 0; }

/*
 * Set heavy, both of them.
 *
 * The page is a decision about one dish, and these two are the decision: what
 * it is and what it costs. At 500 they read as the top of a paragraph, which is
 * how they were being scrolled past — the reference sets both at 700 and the
 * page has a subject again.
 */
.item-head h1 {
    margin: 0;
    font-size: 1.55rem;
    font-weight: 700;
    letter-spacing: -.02em;
}

.item-price {
    font-size: 1.5rem;
    font-weight: 700;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}

/* ========================================================= dish options */

.opt-group { margin-bottom: 22px; }

.opt-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 10px;
}

/* Heavy, like the dish name: these are the questions being asked, and the
   answers underneath are set light so the two are told apart at a glance. */
.opt-head h2 { margin: 0; font-size: 1.12rem; font-weight: 700; }

/* How many are allowed, under the question rather than in the badge — the
   badge says whether an answer is needed, this says the limit on it. */
.opt-max { margin: 2px 0 0; color: var(--ink-3); font-size: .85rem; }

.opt-rule { color: var(--ink-3); font-size: .95rem; white-space: nowrap; padding-top: 2px; }

/*
 * The badge is the same shape whether or not it is red.
 *
 * It used to be red from the moment the sheet opened, which the shop asked to
 * be rid of (slide 162): a dish with three required groups met the customer
 * with three red badges before they had done anything at all. It reads as
 * quietly as Optional now until Add to cart is pressed on an unanswered group,
 * and only the groups still unanswered then turn red — see .has-tried below
 * and showMissing() in js/item.js.
 *
 * Kept as a badge rather than plain text so it still reads as a status ("this
 * one is compulsory") and not as one more line of instructions to skim past.
 */
.opt-rule.is-required {
    padding: 2px 9px;
    border-radius: 999px;
    background: var(--soft);
    color: var(--ink-2);
    font-size: .78rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .03em;
}

/* Only the ones still unanswered after a press, and only after one. */
.item-page.has-tried .opt-group.is-missing .opt-rule.is-required {
    background: var(--bad-soft);
    color: var(--bad);
}

/*
 * One line for the whole dish, stuck to the bar it is stopping.
 *
 * Named groups — "Please complete selection for: Choice of meat, Choice of
 * Noodles" — because on a dish with three required groups a customer scanning
 * for the word "select" has to find the right one, and naming it is faster
 * than making them read all three rules again. It shortens as they answer.
 *
 * On the action bar rather than under each group: the bar is pinned to the
 * foot of the sheet, so the answer to a press cannot be scrolled away from the
 * press that caused it.
 */
.actionbar-error {
    margin: 0 0 8px;
    padding: 10px 12px;
    border-radius: var(--r);
    background: var(--bad-soft);
    color: var(--bad);
    font-size: .88rem;
    font-weight: 500;
    text-align: center;
}

/*
 * A question that has not been answered yet.
 *
 * Its own banner rather than a line of red text: it names the group ("Please
 * complete selection for: Protein"), because on a dish with three required
 * groups a customer scanning for the word "select" has to find the right one,
 * and naming it is faster than making them read all three rules again.
 *
 * It is there from the moment the dish opens and goes the moment the group is
 * answered — it is not a scolding for a press that failed, it is the part of
 * the order still outstanding, said where the answer is given.
 */
.opt-error {
    margin: 0 0 10px;
    padding: 10px 12px;
    border-radius: var(--r);
    background: var(--bad-soft);
    color: var(--bad);
    font-size: .88rem;
    font-weight: 500;
}

/* Only after Add to cart was pressed on an unanswered group — see
   showMissing() in js/item.js, which is the only thing that sets is-missing
   and does so only once the button has been tried. */
.opt-group.is-missing .opt-list {
    box-shadow: 0 0 0 1px var(--bad), var(--shadow-1);
}

.opt-list {
    border-radius: var(--r-lg);
    /* A hairline and a whisper of shadow, so the card lifts off the page
       instead of being drawn on it. */
    box-shadow: 0 0 0 1px var(--line), var(--shadow-1);
    overflow: hidden;
}

.opt-row {
    display: flex;
    align-items: center;
    gap: 14px;
    /* Taller rows: this is tapped with a thumb, over and over, by somebody
       deciding what to eat rather than filling in a form. */
    padding: 16px 18px;
    margin: 0;
    cursor: pointer;
    font-weight: 400;
    background: var(--bg);
    transition: background-color .12s;
}

/* The rule is inset to line up with the text rather than crossing the box. */
.opt-row + .opt-row { box-shadow: inset 0 1px 0 var(--line); }

/*
 * Drawn rather than left to the browser.
 *
 * accent-color paints the native control, which on a phone is a small, softly
 * drawn tick that all but disappears against a white row — and on iOS it is a
 * different shape again. The reference uses a firm outlined box you can see
 * from arm's length with the phone on the counter, and that is what this is.
 *
 * **Round for a radio, square for a checkbox, deliberately.** The shape is the
 * only thing on the row that says whether picking this un-picks the last one.
 * Making them both squares would look tidier and tell the customer nothing.
 */
.opt-row input {
    appearance: none;
    -webkit-appearance: none;
    width: 24px;
    height: 24px;
    min-height: 0;
    flex: none;
    margin: 0;
    border: 2px solid var(--line-strong);
    border-radius: 7px;
    background: var(--bg);
    display: grid;
    place-items: center;
    transition: border-color .12s, background-color .12s;
}

.opt-row input[type="radio"] { border-radius: 999px; }

.opt-row input:checked {
    border-color: var(--brand);
    background: var(--brand);
}

/*
 * The tick, and the dot, drawn with a pseudo-element.
 *
 * A mask rather than a background image, so the mark takes its colour from the
 * row — nothing here needs a second file fetched, and an SVG data URI in a
 * colour would have to be written twice for the light and dark cases.
 */
.opt-row input:checked::after {
    content: "";
    width: 13px;
    height: 13px;
    background: #fff;
    clip-path: polygon(14% 47%, 0 61%, 38% 100%, 100% 24%, 85% 10%, 38% 70%);
}

.opt-row input[type="radio"]:checked::after {
    width: 9px;
    height: 9px;
    border-radius: 999px;
    clip-path: none;
}

/* Keyboard focus has to stay visible now that the native ring is gone. */
.opt-row input:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: 2px;
}

.opt-name { flex: 1; min-width: 0; }

.opt-delta { color: var(--ink-2); font-variant-numeric: tabular-nums; white-space: nowrap; }

/*
 * A modifier the kitchen has run out of (slide 186).
 *
 * Still on the list, greyed and unpickable. Taking it off would leave somebody
 * who orders it every week hunting for something that looks as though it was
 * never there — and the price is replaced rather than joined by the words,
 * because what it costs stopped being the useful fact about it today.
 */
.opt-row.is-out { opacity: .55; cursor: not-allowed; }

.opt-row.is-out .opt-name { text-decoration: line-through; }

.opt-soldout {
    color: var(--bad);
    font-size: .78rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .03em;
    white-space: nowrap;
}

.opt-row:has(input:checked) { background: var(--brand-soft); }

/* Group already at its limit: the rest fade out instead of silently ignoring
   the tap. */
.opt-row:has(input:disabled) { opacity: .42; cursor: default; }

.item-desc { margin: 0 0 18px; color: var(--ink-2); }

.optional { color: var(--ink-3); font-weight: 400; font-size: .9rem; }

/* ====================================================== bottom bar */

/*
 * The page's main action bar (Add to cart, Checkout, Pay by card).
 *
 * sticky, not fixed: fixed anchors to the viewport, so whenever the viewport is
 * shorter than the page — a small window, a browser toolbar, the phone
 * emulation in devtools — the button is pushed off screen and the customer
 * thinks the page is broken. sticky stays in the flow: on a long page it clings
 * to the bottom while scrolling, on a short one it sits right after the
 * content. Either way it is visible.
 */
/*
 * The bar that carries Add to cart, Pay, and the rest. Sticky, so it rides the
 * bottom of the window while the page scrolls under it.
 *
 * It stops ABOVE the footer band rather than at the bottom of the window: the
 * band is fixed there now, and a sticky bar at bottom:0 sits behind it — which
 * is how the Add to cart button on the dish page went missing the moment the
 * footer was pinned.
 */
.actionbar {
    position: sticky;
    bottom: var(--footer-band);
    z-index: 40;
    margin-top: 20px;
    padding-block: 10px 12px;
    background: linear-gradient(to top, var(--bg) 62%, rgba(255, 255, 255, 0));
}

.actionbar[hidden] { display: none; }

.actionbar-inner {
    display: flex;
    align-items: center;
    gap: 12px;
}

/*
 * Inside the bar the total has no rule above it and no margin: the bar's own
 * hairline is the separation, and a line as well is two.
 *
 * `.cart-total.actionbar-total`, not `.actionbar-total` alone. Written with one
 * class this rule had exactly the same weight as the `.cart-total` it was
 * meant to cancel, and that one is 500 lines further down the file — so order
 * decided it and the reset lost every time. What reached the checkout was the
 * bar's hairline, then 14px of nothing, then a second shorter rule above the
 * Total: three pieces of separation where the design asked for one.
 */
.cart-total.actionbar-total {
    margin-top: 0;
    padding-top: 0;
    border-top: 0;
}

/*
 * The menu page's cart bar is the exception: it has to float above the content
 * the whole time the customer is scrolling and picking, so it does anchor to
 * the viewport.
 *
 * It sits ON TOP of the footer band rather than at the very bottom, because the
 * footer is fixed there now and the two would land on each other. The cart is
 * the thing being reached for; a credit line is not, so the credit line is the
 * one that goes underneath.
 *
 * z-index above the footer's 45, and its own safe-area padding is dropped —
 * the band below is already holding that room open.
 */
.actionbar-mobile {
    position: fixed;
    left: 0;
    right: 0;
    bottom: var(--footer-band);
    z-index: 50;
    margin-top: 0;
    padding-block: 22px 12px;
    padding-inline: 16px;
    pointer-events: none;

    /*
     * A fade under the pill, so the menu ends instead of being sliced.
     *
     * The bar floats over the dishes the whole time somebody is scrolling, and
     * with nothing behind it a card was cut clean in half at the pill's edge —
     * a price with its top showing reads as a page that has broken rather than
     * as content continuing underneath. The page already reserves room at the
     * very bottom so the last card clears the bar entirely; this is only about
     * the hundreds of rows that pass behind it on the way.
     */
    background: linear-gradient(to top, var(--bg) 58%, rgba(255, 255, 255, 0));
}

.actionbar-mobile .actionbar-inner {
    pointer-events: auto;
    max-width: 1208px;
    margin-inline: auto;
}

.actionbar .stepper-lg {
    background: var(--bg);
    border-radius: 999px;
    box-shadow: 0 0 0 1px var(--line);
    flex: none;
}

/*
 * Solid, not the usual fade.
 *
 * The other bars carry one control and a gradient up to transparent lets the
 * page show through above it without anything looking wrong. This bar is doing
 * more — a stepper AND a button, or a total AND a button — so it has to read as
 * a surface being pressed, not a wash the content drifts under.
 *
 * Shared by both shapes below; [.actionbar-row] and [.actionbar-column] only
 * say which way the pieces INSIDE it line up.
 */
.actionbar-stacked {
    background: var(--bg);
    box-shadow: 0 -1px 0 var(--line);
    padding-block: 14px 14px;
}

/*
 * The dish page: how many, beside Add to cart, on one row.
 *
 * It was stacked — the stepper above a full-width button — on the reasoning
 * that side by side the button had to ellipsise "Add to cart" on a narrow
 * phone. The shop asked for the compact row back regardless, so the button
 * gives up being full width and takes whatever the stepper leaves it instead.
 */
.actionbar-row .actionbar-inner {
    flex-direction: row;
    align-items: center;
    gap: 12px;
}

.actionbar-row .btn { flex: 1 1 auto; min-width: 0; }

/*
 * The checkout's bar: the Total, then the button under it.
 *
 * It was a row with the button alone, and the total lived in a card off to the
 * side — which on a phone put the button FIRST and the figure below it. Press
 * this, and then find out how much.
 */
.actionbar-column .actionbar-inner {
    flex-direction: column;
    align-items: stretch;
    gap: 12px;
}

.actionbar-stacked .stepper-lg {
    align-self: center;
    /* No border here: the bar it sits in is already a surface, and a pill
       outline around three characters reads as a control that has been disabled
       rather than one waiting to be used. */
    box-shadow: none;
    background: transparent;
}

.actionbar-stacked .stepper-lg button {
    border-color: var(--line-strong);
}

/* The tall button at the foot of the cart, whichever of the two shapes it is
   wearing. It used to name .btn-split, so changing the cart's Checkout to the
   centred shape silently took eight pixels off its height. */
.actionbar-stacked .btn {
    padding-inline: 26px;
    min-height: 60px;
    font-size: 1.02rem;
}

/*
 * On a computer the bar stops growing and sits in the middle.
 *
 * Full width is right on a phone, where the screen IS the button's width. On a
 * 1400px monitor it drew a blue band the whole way across the page with two
 * words at one end and a figure at the other, and the eye had to travel the
 * width of the screen to read one button. Capped and centred, it reads as a
 * button again.
 */
@media (min-width: 640px) {
    .actionbar-column .actionbar-inner {
        max-width: 520px;
        margin-inline: auto;
    }

    .actionbar-stacked .btn-split {
        /* Label and amount at the two ends of a 520px button rather than a
           whole monitor. */
        padding-inline: 22px;
    }
}

/* ============================================================== buttons */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    min-height: 52px;
    border: 1px solid transparent;
    border-radius: 999px;
    padding: 0 22px;
    font: inherit;
    font-size: 1rem;
    font-weight: 500;
    text-decoration: none;
    cursor: pointer;
    /* A button label must not wrap: "Add to cart" broken over two lines makes
       the button half again as tall and throws the whole bottom bar out. */
    white-space: nowrap;
    -webkit-tap-highlight-color: transparent;
    transition: background-color .15s, border-color .15s, transform .06s;
}

.btn:active { transform: translateY(1px); }

.btn-primary { background: var(--brand); color: #fff; }
.btn-primary:hover { background: var(--brand-ink); }

.btn-ghost { background: var(--bg); border-color: var(--line-strong); color: var(--ink); }
.btn-ghost:hover { border-color: var(--brand); color: var(--brand-ink); }

.btn-soft { background: var(--soft); color: var(--ink); }

.btn-block { width: 100%; }

.btn-sm { min-height: 38px; padding: 0 14px; font-size: .88rem; }

.btn[disabled] { opacity: .45; cursor: not-allowed; }

/*
 * Pressed, and now waiting on the server.
 *
 * Dimmed less than a plain disabled button: this one is working, not refusing,
 * and the difference has to be visible or the customer reads it as "it didn't
 * take" and starts looking for another way to press it.
 */
.btn.is-busy { opacity: .7; cursor: progress; pointer-events: none; }

/*
 * The main action button, in two shapes.
 *
 * [btn-split] pushes the label to the left edge and the amount to the right.
 * It is for a label that DESCRIBES what is in the cart — "View cart · 12
 * items" — where the two halves are two different facts and want the width
 * between them.
 *
 * [btn-total] keeps the pair together in the middle, which is what the shop's
 * reference has and what it asked for: "Checkout $34.76" reads as one phrase,
 * and a phrase split across half a metre of pink reads as two buttons.
 *
 * Both protect the money the same way — see the note above. It is the most
 * important thing on the button and must never be the part that gets clipped.
 */
.btn-split { justify-content: space-between; }

.btn-link {
    background: none;
    border: 0;
    padding: 6px 0;
    color: var(--ink-2);
    font: inherit;
    font-size: .88rem;
    text-decoration: underline;
    cursor: pointer;
}

.btn-link:hover { color: var(--brand-ink); }

/* ================================================================ pages */

/* See the note on .searchbar — <main> carries both .wrap and .page.
   The bottom is the room between the content and the footer band; it used to be
   28px here plus 2rem on the body, and the body's half has moved here. */
.page { padding-block: 18px 60px; }

.page-title { margin: 0 0 4px; font-size: 1.45rem; font-weight: 500; }

/*
 * A way back on the left, the title in the middle.
 *
 * Three columns and not flex with a spacer: the third column is the same width
 * as the button, so the title is centred against the PAGE rather than against
 * whatever is left after the button — which is what makes it sit a little off
 * to the right and look like a mistake nobody can name.
 */
.page-head {
    display: grid;
    grid-template-columns: 44px 1fr 44px;
    align-items: center;
    gap: 8px;
    margin-bottom: 14px;
}

.page-head .page-title {
    margin: 0;
    text-align: center;
    font-size: 1.25rem;
    font-weight: 700;
}

/*
 * A page that is one column of its own, held to a readable width and centred.
 *
 * The checkout steps. With no side column they would otherwise run the full
 * 1240px of .wrap, which puts a first-name box a foot wide in front of
 * somebody — and the title, centred on the page, would be centred on nothing
 * in particular. Wrapping the head and the form together is what keeps the two
 * lined up with each other whatever the window does.
 */
.lane {
    max-width: 760px;
    margin-inline: auto;
}

/*
 * A second copy of the same back button, fixed to the true edge of a
 * desktop window — see the markup note beside it in checkout/form and
 * checkout/confirm.
 *
 * The button INSIDE .page-head is left completely alone: taking it out
 * of the grid — with position: fixed, or removing it from the DOM, or
 * display: none — shrank the grid itself and wrapped the title onto two
 * lines, for reasons that were not worth chasing down when leaving the
 * original exactly where it has always been sidesteps the question
 * entirely. It still holds the title centred over the form below it,
 * same as ever; visibility: hidden takes it off the screen without
 * taking it out of the grid that depends on it being there.
 */
.page-head-edge-btn { display: none; }

@media (min-width: 900px) {
    .page-head.lane .round-btn { visibility: hidden; }

    .page-head-edge-btn {
        display: grid;
        position: fixed;
        /* The same left edge the logo sits on: .wrap is centred with a
           1240px max-width and 16px of padding inside it, so this button
           lines up under the logo instead of drifting off to the bare
           window edge on a wide monitor. */
        left: max(16px, calc((100vw - 1240px) / 2 + 16px));
        top: calc(var(--header-h) + 18px);
        visibility: visible;
    }
}

/*
 * The other shape: title hard against the left, one round button on the right.
 *
 * For a page somebody is looking INTO rather than travelling through — the
 * cart. There is no third column to balance against here, so two columns, and
 * the title takes whatever the button does not.
 */
.page-head-start { grid-template-columns: 1fr auto; }

.page-head-start .page-title { text-align: left; }

/* "1 item", beside the title and deliberately quieter than it: it is a fact
   about the title, not a second heading. */
.page-title-note {
    margin-left: 8px;
    color: var(--ink-2);
    font-size: .92rem;
    font-weight: 400;
    white-space: nowrap;
}

/* =========================================================== collection times */

/*
 * Every time the kitchen can meet, three to a row.
 *
 * A grid and not a dropdown because the question is a comparison — "can I be
 * there by quarter past, or should I say half past" — and a dropdown shows one
 * answer at a time with the rest behind a tap.
 *
 * It scrolls rather than growing: four hours of quarter-hours is seventeen
 * slots, and a checkout that has to be scrolled past a wall of times to reach
 * the name field is worse than a short list that is scrolled on purpose.
 */
.slots {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 8px;
    max-height: 232px;
    overflow-y: auto;
    padding: 2px;
}

@media (max-width: 380px) {
    .slots { grid-template-columns: repeat(2, 1fr); }
}

.slot { cursor: pointer; }

/* Moved off screen rather than hidden: display:none drops it out of the tab
   order, and then the list cannot be used from a keyboard at all. */
.slot input {
    position: absolute;
    opacity: 0;
    width: 1px;
    height: 1px;
    min-height: 0;
}

.slot span {
    display: block;
    padding: 11px 6px;
    border: 1px solid var(--line-strong);
    border-radius: var(--r);
    text-align: center;
    font-size: .92rem;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    transition: border-color .12s, background-color .12s;
}

.slot input:checked + span {
    border-color: var(--brand);
    background: var(--brand-soft);
    color: var(--brand-ink);
    font-weight: 600;
}

.slot input:focus-visible + span { outline: 2px solid var(--brand); outline-offset: 2px; }

/* ============================================================== the notes */

/*
 * Two notes, and they are not the same note.
 *
 * One belongs to a dish — "no onion on the second banh mi" — and prints on that
 * line. One belongs to the order — "please ring when you arrive" — and prints
 * once at the foot of the docket. They were one box for a while and the kitchen
 * read a line about the car park under a bowl of pho.
 */
.note-label {
    display: block;
    margin-bottom: 4px;
    color: var(--ink-2);
    font-size: .82rem;
}

.order-note {
    margin-bottom: 14px;
    padding: 14px 16px 16px;
    background: var(--soft);
    border-radius: var(--r-lg);
}

.order-note label {
    display: block;
    margin-bottom: 8px;
    font-weight: 600;
}

.order-note input { width: 100%; }

/*
 * A tap is in flight.
 *
 * Dimmed a little and not tappable, rather than each control disabling itself:
 * the whole block is about to be replaced, so anything pressed in the meantime
 * would be answered by a cart that no longer exists — and the second press
 * would arrive out of order with the first.
 *
 * Nothing moves and nothing is hidden. On a fast connection this lasts two
 * frames and should not be noticeable; on a slow one it says the tap was heard.
 */
.is-working { opacity: .6; pointer-events: none; }

@media (prefers-reduced-motion: no-preference) {
    [data-cart-body] { transition: opacity .12s; }
}

.page-lead { margin: 0 0 16px; color: var(--ink-2); font-size: .92rem; }


.card {
    background: var(--bg);
    border-radius: var(--r-lg);
    box-shadow: 0 0 0 1px var(--line);
    padding: 16px;
}

/*
 * ~ rather than +: a hidden field sitting between two cards — the
 * checkout confirm page has one, the payment method when there is only
 * one to choose — is still an element, so + (which demands the two
 * cards be the ONLY thing next to each other) skipped straight past it
 * and left them touching with no gap at all, looking like one card with
 * an odd seam rather than two. ~ asks only that a .card came before this
 * one somewhere among its siblings, which an invisible input does not
 * change.
 */
.card ~ .card { margin-top: 20px; }

.card > h2 { margin: 0 0 12px; font-size: 1.05rem; font-weight: 500; }

/*
 * minmax(0, 1fr), not 1fr.
 *
 * A grid column is `auto` by default, and an auto column refuses to go narrower
 * than the widest thing inside it. One line of text that must not wrap — a shop
 * address on the collection row — and the whole checkout grows wider than the
 * phone, taking the header and every field with it. Nothing on the page looks
 * broken; it is simply all 90px too far to the right.
 */
.columns { display: grid; grid-template-columns: minmax(0, 1fr); gap: 14px; }

/*
 * The strip of featured dishes above the menu.
 *
 * It used to be its own shape — a row that scrolled sideways, so six
 * recommendations could not push the menu itself below the fold on a phone.
 * That made it the one part of the page the View switch did not reach, and a
 * switch that changes everything except the first thing you see is a switch
 * that looks broken.
 *
 * So it lays out like any other list of dishes now, and the six-item cap is
 * what keeps it from swallowing the screen: six rows in the dense list, three
 * in cards, one in tiles.
 */
.dishes-featured {
    /* Room for the cards' shadow, which the section would otherwise clip flat
       against its edges. */
    padding: 4px;
    margin-inline: -4px;
}

/* ==================================================== ticket-shaped total */

.ticket {
    background: var(--soft);
    border-radius: var(--r-lg);
    padding: 16px 18px;
}

.ticket .totals li { padding: 5px 0; }

/*
 * A dashed line with a round notch at each end, separating the detail from the
 * total. The notches are "punched" with circles painted in the page background
 * colour, so this block only looks right on white.
 */
.ticket-split {
    position: relative;
    height: 0;
    margin: 12px -18px;
    border-top: 2px dashed var(--line-strong);
}

.ticket-split::before,
.ticket-split::after {
    content: "";
    position: absolute;
    top: -11px;
    width: 20px;
    height: 20px;
    border-radius: 999px;
    background: var(--bg);
}

.ticket-split::before { left: -10px; }
.ticket-split::after { right: -10px; }

.totals { list-style: none; margin: 0; padding: 0; }

.totals li {
    display: flex;
    justify-content: space-between;
    gap: 16px;
    padding: 6px 0;
    font-variant-numeric: tabular-nums;
}

.totals li.muted { color: var(--ink-2); font-size: .85rem; }

.totals li.free { color: var(--ok); }

.totals li.grand { font-size: 1.15rem; font-weight: 500; padding-top: 4px; }

/*
 * The one number in the cart, and it sits on the button that acts on it.
 *
 * A rule above and nothing else: it is not a card, because a card would float
 * it away from the Checkout button underneath, and the two are one thing —
 * "this much, pay it".
 */
.cart-total {
    margin-top: 4px;
    padding-top: 14px;
    border-top: 1px solid var(--line);
}

.cart-total-line {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 16px;
    padding: 4px 0;
    font-variant-numeric: tabular-nums;
}

.cart-total-line.grand {
    font-size: 1.3rem;
    font-weight: 700;
}

/* ============================================================ cart lines */

.lines { display: grid; }

.line {
    display: grid;
    grid-template-columns: 64px 1fr;
    gap: 4px 12px;
    padding: 14px 0;
    border-bottom: 1px solid var(--line);
}

.line:last-child { border-bottom: 0; }

.line.no-thumb { grid-template-columns: 1fr; }

.line-thumb {
    grid-row: span 2;
    width: 64px;
    height: 64px;
    border-radius: var(--r);
    overflow: hidden;
    background: var(--soft);
    display: grid;
    place-items: center;
}

.line-thumb img { width: 100%; height: 100%; object-fit: cover; }

.line-thumb .thumb-blank { width: 40%; height: 40%; }

.line-name { font-weight: 500; }

/* Beside the dish name, not down in the tool row: it is a property of WHICH
   dish this is, not an action alongside the quantity. */
.line-edit {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    margin-left: 8px;
    padding: 2px 8px 2px 6px;
    border-radius: 999px;
    background: var(--soft);
    color: var(--ink-2);
    font-size: .78rem;
    font-weight: 500;
    text-decoration: none;
    vertical-align: 1px;
}

.line-edit .ico { width: 12px; height: 12px; vertical-align: -1px; }

.line-edit:hover { background: var(--line); color: var(--ink); }

.line-meta { color: var(--ink-2); font-size: .84rem; }

.line-note { color: var(--brand-ink); font-size: .84rem; }

/* One row per option group the customer answered — the label ("Choice of
   filling") on the left, the value ("Prawn +$1.50") on the right, the way a
   receipt lines up a question and its answer. */
/*
 * One answered question on a cart line: the question, then every answer to it
 * on a line of its own with that answer's own price on the right.
 *
 * The question and the answers were a left-and-right pair on one row, which put
 * a long answer hard against the far edge of the card with a gap between it and
 * the question it was answering. Worse, the answers ran together as one string
 * with the group's total beside them — "Add egg, Change to fried rice,
 * Vegetarian/Vegan +$7.50" — where nothing said which of the three cost what,
 * or that one of them was free. A price sits against the thing it is the price
 * of.
 */
.line-opt {
    display: flex;
    flex-direction: column;
    font-size: .84rem;
    color: var(--ink-2);
}

.line-opt-row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
}

.line-opt-key { color: var(--ink-2); }

.line-opt-value { color: var(--ink); min-width: 0; }

/* What this group added. Quiet: the figure that matters on this row is the
   line's total below, and this is the working behind it. */
.line-opt-extra {
    flex: none;
    color: var(--ink-2);
    font-variant-numeric: tabular-nums;
}

.line-tools {
    grid-column: 2 / -1;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    margin-top: 4px;
}

.line.no-thumb .line-tools { grid-column: 1 / -1; }

/* The line's price, sharing the stepper's row rather than a column of its
   own — tabular so the figure does not jump about as the stepper is used. */
.line-price {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.line-total { font-weight: 500; }

.line-each { color: var(--ink-2); font-size: .78rem; }

.line-tools .stepper {
    background: var(--bg);
    border-radius: 999px;
    box-shadow: 0 0 0 1px var(--line-strong);
    flex: none;
}

.note-input { flex: 1 1 200px; min-width: 0; }

/* ============================================================= messages */

.flash, .alert, .hint-box {
    border-radius: var(--r);
    padding: 12px 14px;
    margin: 0 0 14px;
    font-size: .9rem;
}

.flash { background: var(--ok-soft); color: var(--ok); }
.alert { background: var(--bad-soft); color: var(--bad); }
.hint-box { background: var(--brand-soft); color: var(--brand-ink); }

.alert ul { margin: 6px 0 0; padding-left: 18px; }

/* A link inside a banner takes the banner's colour, underlined: on a red panel
   the usual blue reads as a mistake, and a link nobody can see is a banner that
   still leaves somebody hunting through the list. */
.alert a {
    color: inherit;
    font-weight: 600;
    text-decoration: underline;
    text-underline-offset: 3px;
}

/* ================================================================ forms */

.field { margin-bottom: 14px; }

.field > label { display: block; font-weight: 500; font-size: .9rem; margin-bottom: 6px; }

.field .hint { font-weight: 400; color: var(--ink-3); font-size: .82rem; }

/*
 * A pale grey ground and the faintest of edges.
 *
 * The ground alone was the original idea, and on a page with one or two fields
 * it is right — a full border around every box makes a form look like a tax
 * return. On the checkout, where four fields stack, edgeless grey blocks run
 * together into a single grey mass with no shape to it. One hairline in --line
 * is barely visible on its own and is enough to give each field a beginning and
 * an end.
 */
/* date and time are in the list too: left out, they kept the browser's own
   box, which is a different height from everything beside them — a row of
   a date and three text fields did not line up on any of its edges. */
input[type=text], input[type=tel], input[type=email], input[type=number],
input[type=password], input[type=search], input[type=file],
input[type=date], input[type=time], select, textarea {
    width: 100%;
    min-height: 48px;
    border: 1px solid var(--line);
    border-radius: var(--r);
    padding: 12px 14px;
    /* Safari's 16px threshold again — see the note on the search field. */
    font: inherit;
    font-size: 16px;
    background: var(--soft);
    color: var(--ink);
}

input:focus, select:focus, textarea:focus {
    outline: none;
    background: var(--bg);
    border-color: var(--brand);
    box-shadow: 0 0 0 3px var(--brand-soft);
}

/* Room for the sentence people actually write — "no coriander" — not for an
   essay. It still drags taller for anyone who wants more. */
textarea { resize: vertical; min-height: 76px; }

/* A hint under a field rather than inside its label: the label names the thing,
   this explains it, and the two should not be read as one line. */
.field > .hint { display: block; margin-top: 6px; }

.error { color: var(--bad); font-size: .84rem; margin-top: 5px; }

/*
 * The photo field — a square thumbnail and, beside it, the file picker,
 * the upload status and the way to remove it.
 *
 * This carried no sizing at all before: the placeholder icon (a bare SVG,
 * nothing telling it how big to be) rendered at whatever size that left
 * it, which on most screens was large enough to read as a spinner stuck
 * mid-upload rather than as "no photo chosen yet" — a manager watching it
 * during a real upload had no way to tell the two apart.
 */
.image-now {
    display: flex;
    gap: 14px;
    align-items: flex-start;
}

.image-thumb {
    display: grid;
    place-items: center;
    width: 84px;
    height: 84px;
    flex: none;
    border-radius: var(--r);
    background: var(--soft);
    overflow: hidden;
}

.image-thumb img { width: 100%; height: 100%; object-fit: cover; }
.image-thumb .thumb-blank { width: 30px; height: 30px; color: var(--ink-3); }

.image-actions { flex: 1; min-width: 0; padding-top: 2px; }
.image-actions .hint { display: block; margin-top: 8px; }
.image-actions .btn-link { margin-top: 4px; }

/*
 * The photo beside the item's own name, the way the shop's own reference
 * pairs them — a picture is chosen at the same moment the dish it belongs
 * to is being named, not somewhere further down the form.
 */
.form-photo-row {
    display: flex;
    gap: 16px;
    align-items: flex-start;
    margin-bottom: 14px;
}

.form-photo-row .image-field { flex: none; width: 128px; margin-bottom: 0; }
.form-photo-row .image-field > label { font-size: .78rem; margin-bottom: 4px; }
.form-photo-row .image-field > label .hint { display: block; }
.form-photo-row .image-now { flex-direction: column; }
.form-photo-row .image-thumb { width: 128px; height: 128px; }
.form-photo-row .image-actions { padding-top: 0; }
.form-photo-row .image-actions .btn { width: 128px; }
.form-photo-row .image-fields { flex: 1; min-width: 0; }

/*
 * An answer already given, with a Change beside it.
 *
 * The collection time used to be two large cards and a dropdown, standing open
 * on a question nearly every customer answers the same way. One line, and the
 * cards behind it, is the shape every ordering site of this kind has settled on:
 * the default is stated so it can be checked at a glance, and changing it is one
 * tap for the few who want to.
 */
.pick {
    display: flex;
    align-items: center;
    /* Tight, because on a 390px phone the disc, the sentence and the Change are
       all competing for the same line — and "Ready in about 15 minutes" breaking
       across two lines is the first thing to go wrong. */
    gap: 10px;
    width: 100%;
    padding: 10px 12px;
    border: 1px solid var(--line-strong);
    border-radius: var(--r);
    background: var(--bg);
    color: inherit;
    font: inherit;
    text-align: left;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}

.pick:hover { border-color: var(--brand); }
.pick:active { background: var(--soft); }

.pick-mark {
    display: grid;
    place-items: center;
    width: 34px;
    height: 34px;
    flex: none;
    border-radius: 999px;
    background: var(--brand-soft);
    color: var(--brand);
}

.pick-mark .ico { width: 18px; height: 18px; vertical-align: 0; }

.pick-text { min-width: 0; flex: 1; }
.pick-text b { display: block; font-weight: 500; font-size: .93rem; }

/* One line, clipped: the shop's address is long and this is a summary, not the
   footer. */
.pick-text small {
    display: block;
    color: var(--ink-2);
    font-size: .8rem;
    margin-top: 1px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Room for the address only where there is room for it. */
.pick-address { display: none; }

@media (min-width: 560px) {
    .pick-address { display: inline; }
}

/* The same pencil the cart's pickup rows use, for the same panel. */
.pick-change {
    display: grid;
    place-items: center;
    flex: none;
    width: 30px;
    height: 30px;
    border-radius: 999px;
    color: var(--ink-2);
}

.pick-change .ico { width: 16px; height: 16px; vertical-align: 0; }

.pick:hover .pick-change { background: var(--soft); color: var(--brand-ink); }

.choices { display: grid; gap: 8px; }

.choice {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    border: 1px solid var(--line-strong);
    border-radius: var(--r);
    padding: 13px 14px;
    background: var(--bg);
    cursor: pointer;
    margin: 0;
    font-weight: 500;
}

.choice input { margin: 2px 0 0; flex: none; accent-color: var(--brand); }

.choice:has(input:checked) {
    border-color: var(--brand);
    background: var(--brand-soft);
    box-shadow: inset 0 0 0 1px var(--brand);
}

.choice-text { display: block; min-width: 0; }

.choice-text small {
    display: block;
    font-weight: 400;
    color: var(--ink-2);
    font-size: .78rem;
    margin-top: 2px;
}

[data-when] { display: none; }
[data-when].is-on { display: block; }

.inline-form {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: 10px;
}

.inline-form .field { margin: 0; flex: 1 1 160px; }
.inline-form .field.narrow { flex: 0 0 100px; }
.inline-form .field.wide { flex: 2 1 260px; }

.check {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: .88rem;
    white-space: nowrap;
    padding-bottom: 12px;
}

.check input { width: 18px; height: 18px; min-height: 0; accent-color: var(--brand); }

/* =============================================================== rating */

.rate-page { max-width: 460px; margin-inline: auto; padding-top: 8px; text-align: center; }

.rate-page .field { text-align: left; margin-top: 22px; }

/*
 * Five stars you TAP.
 *
 * Radios drawn as stars, laid out in REVERSE so the sibling selector can light
 * the one under the finger and everything before it — a star picker means "four
 * or more", not "the fourth star", and `~` only reaches forwards. Reversing the
 * row is what turns "forwards" into "to the left".
 *
 * No JavaScript. A rating page that needs a script is a rating page that fails
 * silently in whichever in-app browser the customer opened their email in.
 */
.stars {
    display: flex;
    flex-direction: row-reverse;
    justify-content: center;
    gap: 4px;
    border: 0;
    margin: 22px 0 0;
    padding: 0;
}

.stars input {
    /* Off screen rather than display:none — a hidden radio cannot be reached by
       the keyboard, and this is the only control on the page. */
    position: absolute;
    opacity: 0;
    width: 1px;
    height: 1px;
}

.stars label {
    font-size: 2.6rem;
    line-height: 1;
    color: var(--line-strong);
    cursor: pointer;
    padding: 2px;
    transition: color .12s, transform .08s;
}

.stars label:active { transform: scale(.92); }

.stars input:checked ~ label,
.stars label:hover,
.stars label:hover ~ label { color: #f5b301; }

/* The keyboard needs to see where it is, and the star it lands on is not the
   one the mouse would light. */
.stars input:focus-visible + label { outline: 2px solid var(--brand); border-radius: 6px; }

.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/* What they said, read back to them so they know it arrived. */
.rate-said { margin: 22px 0 26px; }

.stars-shown { font-size: 2rem; letter-spacing: 2px; color: var(--line-strong); }

.stars-shown .is-on { color: #f5b301; }

.rate-said blockquote {
    margin: 14px 0 0;
    padding: 14px 16px;
    border-radius: var(--r);
    background: var(--soft);
    color: var(--ink);
    text-align: left;
    font-style: italic;
}

/* The shop's side: the average, and the sentences under it. */
.rating-summary {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-bottom: 18px;
}

.rating-summary b { font-size: 2.4rem; font-weight: 700; letter-spacing: -.02em; }

.stars-shown.is-small { font-size: 1rem; letter-spacing: 1px; }

/* The Sales page's own small "what customers said" excerpt — the Reviews
   page below has its own, richer row style, see .review-row. */
.review { padding: 12px 0; border-top: 1px solid var(--line); }

.review-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }

.review p { margin: 6px 0 0; }

/* One row per review, the shop's own reference: a coloured avatar, the
   customer's name and how many orders they have on file, the stars, then
   what they wrote — one line, the way the reference's own row reads left to
   right rather than stacking each fact under the last. */
.review-row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px 16px;
    padding: 14px 0;
    border-top: 1px solid var(--line);
}

.review-row:first-child { border-top: none; padding-top: 0; }

.review-avatar {
    display: grid;
    place-items: center;
    flex: none;
    width: 38px;
    height: 38px;
    border-radius: 50%;
    color: #fff;
    font-size: .95rem;
    font-weight: 600;
    text-transform: uppercase;
}

.review-row-name {
    display: grid;
    gap: 1px;
    flex: 0 0 150px;
    min-width: 0;
}

.review-row-name b, .review-row-name span {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.review-row .stars-shown.is-small { flex: 0 0 auto; }

.review-row-text {
    flex: 1 1 240px;
    min-width: 0;
    margin: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.review-row-time { flex: none; font-size: .76rem; white-space: nowrap; }

/* The two dropdowns above the reviews — the same pill the catalogue's own
   search box is, so a row of filters reads as one row of controls. */
.review-filters { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 14px; }

.review-filters select {
    width: auto;
    min-height: 0;
    padding: 8px 34px 8px 14px;
    border: 0;
    border-radius: 999px;
    background-color: var(--soft);
    font-size: .88rem;
}

/* The Reviews page's own star breakdown — 5 down to 1, a horizontal bar
   each, read top to bottom the way the stars themselves count down. */
.star-breakdown { display: flex; flex-direction: column; gap: 6px; }

.star-row {
    display: grid;
    grid-template-columns: 36px 1fr 28px;
    align-items: center;
    gap: 10px;
}

.star-row-label { color: var(--ink-2); font-size: .84rem; }

.star-row-track {
    height: 8px;
    border-radius: 999px;
    background: var(--soft);
    overflow: hidden;
}

.star-row-fill { display: block; height: 100%; border-radius: 999px; background: #f5b301; }

.star-row-count { text-align: right; color: var(--ink-2); font-size: .84rem; font-variant-numeric: tabular-nums; }

/* =============================================================== result */

.result { text-align: center; }

/*
 * The mark at the top of the checkout result: a drawn icon in a tinted disc.
 *
 * It used to be an emoji — ✅ — and looked like one: drawn by the operating
 * system, a different picture on every machine, in a green belonging to
 * nobody, sitting on the page like a sticker somebody had put there. Every
 * other icon on the site is a line drawing that takes the colour around it.
 */
.result-badge {
    width: 60px;
    height: 60px;
    margin: 0 auto 12px;
    display: grid;
    place-items: center;
    border-radius: 999px;
    background: var(--ok-soft);
    color: var(--ok);
}

.result-badge .ico { width: 28px; height: 28px; vertical-align: 0; }

.result-badge.is-bad { background: var(--bad-soft); color: var(--bad); }

/* Cancelled is not a failure — nothing was charged and nothing went wrong. Red
   would tell the customer off for changing their mind. */
.result-badge.is-quiet { background: var(--soft); color: var(--ink-2); }

/* Three rem is already the loudest thing on the page; it does not need weight
   on top of size to be found. */
/*
 * The order number, which is thirteen characters — "SG-" and ten more.
 *
 * It was a flat 3rem, and thirteen characters at 3rem is wider than a
 * phone: it broke at the hyphen and the rest ran off the side of the card.
 * Sized against the viewport instead, so it stays as large as it was on a
 * desktop and shrinks to fit on a phone; overflow-wrap is the guarantee
 * that it can never leave the card whatever the screen.
 */
.big-number {
    font-size: clamp(1.75rem, 10vw, 3rem);
    font-weight: 400;
    color: var(--brand-ink);
    line-height: 1.05;
    margin: 6px 0;
    overflow-wrap: anywhere;
}

.mono, .receipt-preview {
    font-family: "Cascadia Mono", ui-monospace, Consolas, "Courier New", monospace;
    font-size: .82rem;
}

.receipt-preview {
    white-space: pre;
    overflow-x: auto;
    background: var(--soft);
    border-radius: var(--r);
    padding: 14px;
    line-height: 1.45;
}

.divider { border: 0; border-top: 1px solid var(--line); margin: 18px 0; }

/* ========================================================= print options */

/*
 * The switches on the left, a real docket on the right.
 *
 * minmax(0, …) on both columns: the preview is a <pre> that never wraps, and an
 * `auto`/`1fr` track refuses to be narrower than its widest line — one 48
 * column docket would otherwise push the whole page sideways.
 */
.print-options {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 420px);
    gap: 20px;
    align-items: start;
}

/*
 * Small enough that all 48 columns of the docket fit without scrolling.
 *
 * A preview whose right-hand column — the prices — is off the edge until you
 * drag it sideways is not a preview: the whole reason it is here is to show
 * whether the amounts still line up after a switch was changed.
 */
.print-options .receipt-preview { font-size: .7rem; padding: 12px; }

/*
 * What the printer would do to each line.
 *
 * Character size, bold and white-on-black are control bytes, so a plain-text
 * preview cannot show them — "Big" and "Normal" came out identical on screen
 * while the paper was right, which is the worst way for a preview to be wrong.
 * Escpos marks each line with these classes instead.
 */
.dk { display: block; white-space: pre; }

/* Twice as tall and the same width. scaleY rather than a bigger font, because
   a bigger font would widen it too and 48 columns would stop being 48. */
.dk-tall {
    transform: scaleY(2);
    transform-origin: left bottom;
    margin-top: 1em;
}

/* And twice as wide with it. */
.dk-wide.dk-tall { transform: scale(2, 2); }

/* Wide but not tall is not something the docket asks for; if it ever does,
   this keeps it honest rather than silently looking normal. */
.dk-wide:not(.dk-tall) { transform: scaleX(2); transform-origin: left; }

.dk-bold { font-weight: 700; }

/* The panel at the head of the docket, actually black. Padded across the full
   line so it reads as a block and not as a highlighted word. */
.dk-inverse { background: #14181c; color: #fff; }

/*
 * Inside the black panel, size is font-size rather than a transform.
 *
 * A transform scales the drawn box but leaves the line box the old height, so
 * the taller lines need a margin to stop them overlapping — and a transparent
 * margin between two black rows cuts the panel into stripes. Sizing the font
 * grows the background with it, so the four rows stay one solid block. The
 * width is then slightly off inside the panel, which nobody can see, because
 * the panel is full width either way.
 */
.dk-inverse.dk-tall,
.dk-inverse.dk-wide {
    transform: none;
    margin: 0;
    font-size: 1.5em;
    line-height: 1.15;
}

.dk-inverse.dk-wide.dk-tall { font-size: 1.9em; }

@media (max-width: 1000px) {
    .print-options { grid-template-columns: minmax(0, 1fr); }
    .print-options .preview-pane { position: static; }
}

/* Still beside the switch being changed forty rows down the form. */
.print-options .preview-pane { position: sticky; top: 16px; }

/*
 * One switch: the label and its sentence stacked beside the box, rather than
 * on one nowrap line. The sentence is the half that says what the switch does,
 * so it cannot be the half that gets cut off.
 */
.opt {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    white-space: normal;
    padding: 10px 0;
    border-top: 1px solid var(--line);
    font-size: .9rem;
}

.opt:first-of-type { border-top: 0; }

.opt input[type="checkbox"] { margin-top: 2px; flex: none; }

.opt > span { display: grid; gap: 2px; }
.opt small { color: var(--ink-2); font-size: .84rem; line-height: 1.45; }

.field.opt { display: block; }

/* Three choices that fit on one line are quicker to compare than three that
   have to be opened one at a time. */
.segmented {
    display: inline-flex;
    flex-wrap: wrap;
    gap: 4px;
    padding: 4px;
    background: var(--soft);
    border-radius: 999px;
}

.segmented label { cursor: pointer; }

/* The radio itself is taken off the screen rather than hidden: display:none
   drops it out of the tab order, and then the group cannot be used from a
   keyboard at all. */
.segmented input {
    position: absolute;
    opacity: 0;
    width: 1px;
    height: 1px;
    min-height: 0;
}

.segmented span {
    display: block;
    padding: 6px 14px;
    border-radius: 999px;
    font-size: .86rem;
    white-space: nowrap;
}

.segmented input:checked + span { background: #fff; box-shadow: 0 1px 3px rgba(0, 0, 0, .12); }
.segmented input:focus-visible + span { outline: 2px solid var(--brand); outline-offset: 2px; }

/*
 * The picture as well as the word.
 *
 * "Tiles" asks the manager to hold an idea of what tiles look like; the icon
 * beside it is the thing itself, and it is the same icon the customer taps
 * above the menu — so what is chosen here is recognisable on the shop's own
 * site rather than being a word that has to be translated twice.
 */
.segmented-icons span {
    display: flex;
    align-items: center;
    gap: 7px;
    padding: 7px 15px;
}

.segmented-icons .ico {
    width: 17px;
    height: 17px;
    vertical-align: 0;
    color: var(--ink-2);
}

.segmented-icons input:checked + span .ico { color: var(--brand); }

/* ============================================================= switches */

/*
 * A setting the shop turns on and leaves on.
 *
 * A tick box is for filling in a form; a switch is for a state the shop lives
 * with, and the two should not look the same. This is also the shape the
 * manager already knows from the POS back office they came from, which is worth
 * more than any argument about which control is better.
 *
 * The label is the whole row, so the target is the sentence rather than a 44px
 * square at the end of it.
 */
.switches { display: grid; }

.switch {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 18px;
    padding: 14px 0;
    cursor: pointer;
}

.switch + .switch { border-top: 1px solid var(--line); }

.switch-text { font-weight: 500; }

/*
 * A line under the label, for a switch whose name cannot carry its meaning.
 *
 * Most of these say what they do in the saying — "Ask the customer for a phone
 * number" needs nothing. "Skip acceptance" does: the shop read it, turned it
 * on, and reported it broken, having taken it for the Pause button. A control
 * nobody can tell the purpose of is a control that gets reported as faulty
 * whatever it does.
 *
 * min-width:0 on the column so a long note wraps instead of pushing the track
 * off the right of the card.
 */
.switch-label { display: flex; flex-direction: column; gap: 3px; min-width: 0; }

.switch-note { font-weight: 400; font-size: .82rem; color: var(--ink-2); }

/*
 * The input is moved off the screen rather than hidden.
 *
 * display:none would drop it out of the tab order, and then the whole page
 * could not be worked from a keyboard — which is how somebody setting up a shop
 * on a laptop with a broken trackpad gets stuck.
 */
.switch input {
    position: absolute;
    opacity: 0;
    width: 1px;
    height: 1px;
    min-height: 0;
}

.switch-track {
    position: relative;
    flex: none;
    width: 50px;
    height: 30px;
    border-radius: 999px;
    background: var(--line-strong);
    transition: background-color .16s;
}

.switch-track::after {
    content: "";
    position: absolute;
    top: 3px;
    left: 3px;
    width: 24px;
    height: 24px;
    border-radius: 999px;
    background: #fff;
    box-shadow: 0 1px 3px rgba(20, 24, 28, .3);
    transition: transform .16s;
}

.switch input:checked + .switch-track { background: var(--brand); }
.switch input:checked + .switch-track::after { transform: translateX(20px); }

.switch input:focus-visible + .switch-track {
    outline: 2px solid var(--brand);
    outline-offset: 3px;
}

/* Somebody who has asked their machine for less movement has asked for it
   here too: the switch still changes, it simply does not slide. */
@media (prefers-reduced-motion: reduce) {
    .switch-track, .switch-track::after { transition: none; }
}

/*
 * The same switch inside a table cell, and a BUTTON rather than a tick box.
 *
 * In a settings list the switch is a field on a form somebody is filling in.
 * In a row it is an action — one press, saved, done — so it posts on its own
 * and its state comes from .is-on rather than from a checked input. Smaller
 * too: a 50px track in a table row makes the row taller than its text.
 */
.switch-inline {
    justify-content: flex-start;
    gap: 8px;
    padding: 0;
    background: none;
    border: 0;
    font: inherit;
    color: var(--ink-2);
}

.switch-inline .switch-track { width: 40px; height: 24px; }

.switch-inline .switch-track::after { width: 18px; height: 18px; }

.switch-inline.is-on .switch-track { background: var(--brand); }

.switch-inline.is-on .switch-track::after { transform: translateX(16px); }

.switch-label { font-weight: 600; font-size: .85rem; }

.switch-inline.is-on .switch-label { color: var(--brand-ink); }

.switch-inline:focus-visible .switch-track {
    outline: 2px solid var(--brand);
    outline-offset: 3px;
}

.tabs-mini { display: flex; gap: 4px; margin-bottom: 10px; }

.tabs-mini a {
    padding: 6px 12px;
    border-radius: 999px;
    font-size: .86rem;
    text-decoration: none;
    color: var(--ink-2);
    background: var(--soft);
}

.tabs-mini a.is-on { background: var(--brand); color: #fff; }

/*
 * The logo as it stands, on the chequerboard.
 *
 * The board matters: a logo is usually black on transparent, and on a white
 * card it looks finished — right up until the printer flattens the transparency
 * onto white too and the shop wonders why the edges changed.
 */
.logo-now {
    display: flex;
    align-items: center;
    gap: 12px;
    margin: 6px 0 10px;
}

.logo-now img {
    max-width: 180px;
    max-height: 80px;
    padding: 6px;
    border: 1px solid var(--line);
    border-radius: 8px;
    background:
        linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%) 0 0 / 12px 12px,
        linear-gradient(45deg, #eee 25%, #fff 25%, #fff 75%, #eee 75%) 6px 6px / 12px 12px;
}

/* ==================================================== the catalogue screens */

/*
 * A searchable list on the left, the selected row's form on the right.
 *
 * Two panes rather than a list page and a form page: editing a menu is twenty
 * small changes to things that sit near each other, and a full page load
 * between each one turns twenty edits into forty loads and a lost place in the
 * list.
 */
.catalogue {
    display: grid;
    /*
     * A share of the row, not a fixed number of pixels — a flat 420px cap
     * meant the list stayed exactly that width on any screen and every
     * pixel .admin-body gained on a wide monitor went to the form beside
     * it alone, which is the opposite of what widening the page was for.
     * 38% keeps both panes growing together.
     */
    grid-template-columns: minmax(320px, 38%) minmax(0, 1fr);
    gap: 18px;
    align-items: start;
}

/* The two panes are siblings, not a stack — .card + .card's own top margin
   is for cards one under another and would only nudge this one out of line
   with the row beside it. */
.catalogue > .card + .card { margin-top: 0; }

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

/* Search on the left, "+ New" on the right, sharing one row right above the
   table — not up in the page header, where it used to sit apart from the
   list it belongs to. */
.cat-toolbar {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-bottom: 14px;
}

.cat-toolbar .cat-search { flex: 1; margin-bottom: 0; min-width: 0; }
.cat-toolbar .btn { flex: none; }

/* Narrowing to one category, beside the search box — the same pill the
   search box is, so the two read as one row of controls. */
.cat-filter {
    flex: none;
    width: auto;
    min-height: 0;
    padding: 9px 34px 9px 14px;
    border: 0;
    border-radius: 999px;
    background-color: var(--soft);
}

/* The table fills what is left of the screen and scrolls inside itself,
   rather than running off the bottom of the page.

   The subtraction is everything above and below it that is not the rows:
   the header bar, the page's own top and bottom padding, the card padding
   and the search/＋New row inside it. min-height keeps it usable on a short
   window, where that sum would otherwise leave almost nothing. */
.cat-table-scroll {
    max-height: calc(100vh - var(--header-h) - 190px);
    min-height: 260px;
    overflow-y: auto;
}
.cat-table-scroll thead th { position: sticky; top: 0; background: var(--bg); z-index: 1; }

.cat-search {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 0 12px;
    margin-bottom: 14px;
    background: var(--soft);
    border-radius: 999px;
}

.cat-search .ico { width: 17px; height: 17px; color: var(--ink-3); flex: none; }

.cat-search input {
    border: 0;
    background: transparent;
    padding: 9px 0;
    min-height: 0;
}

/* The list, a table row per entry — replaces what used to be a flex list of
   links beside a permanently open form. See catalogue/layout.blade.php for
   why: a table reads closer to the shop's own reference and holds more at a
   glance, now that the form only needs the screen while the dialog is open. */
.cat-table td { vertical-align: middle; }

/*
 * .admin-table's own min-width is 760px, sized for a table running the
 * full width of the page — see the note there. In the narrow list pane
 * beside the form that width is not available, and without this override
 * the table stayed 760px wide anyway: the Price and status columns did
 * not disappear, they were simply painted over by the detail pane's own
 * card beside them, which is a harder fault to notice than a table that
 * visibly does not fit.
 */
.cat-table { min-width: 0; }

.cat-row-link {
    color: inherit;
    text-decoration: none;
    font-weight: 500;
    /* A long dish name gives way rather than wrapping onto a second line
       and pushing the price below the fold of the row. */
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.cat-row-link:hover { text-decoration: underline; }

/*
 * A heading for the category (Items) or modifier group (Modifiers) the
 * rows under it belong to — see the note on this class in items.blade.php
 * and modifiers.blade.php for why it replaced a column repeating the same
 * word down every row. sectionOf() in this file's own script reads this
 * same class to keep a drag inside one heading's rows, the way the old
 * preview mockups (public/_preview/*-clean.html) used it before the list
 * became a table.
 */
.cat-section td {
    padding: 14px 8px 6px;
    border-bottom: 0;
    font-size: .72rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--ink-3);
}

.cat-section:first-child td { padding-top: 4px; }

.cat-row.is-dragging td { background: var(--bg); box-shadow: var(--shadow-2); }

.cat-grip-cell, .cat-icon-cell { width: 1%; white-space: nowrap; }

/*
 * The drag handle.
 *
 * Only the grip starts a drag, never the row: the name is a link to its own
 * dialog, and a whole-row handle would mean every tap began a drag and the
 * row became hard to open.
 *
 * touch-action: none is what lets a finger drag it at all — without it the
 * browser claims the gesture to scroll the list and the row never moves.
 */
.cat-grip {
    display: grid;
    place-items: center;
    width: 18px;
    color: var(--ink-3);
    font-size: .8rem;
    letter-spacing: -2px;
    cursor: grab;
    touch-action: none;
    user-select: none;
}

.cat-grip:active { cursor: grabbing; }

/* The "your drag is not saved yet" bar — plain until a row actually moves. */
.cat-sort-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    margin: 12px 0 0;
    padding: 10px 12px;
    border-radius: var(--r);
    background: #fff4e0;
    color: var(--ink);
    font-size: .82rem;
}

/* Two letters where there is no photograph — enough to tell one row from the
   next while scrolling, without a grey box that says nothing. */
.cat-initials {
    display: grid;
    place-items: center;
    width: 34px;
    height: 34px;
    border-radius: 9px;
    background: var(--soft);
    color: var(--ink-2);
    font-size: .78rem;
    font-weight: 500;
    text-transform: capitalize;
}

.cat-empty { color: var(--ink-2); font-size: .88rem; text-align: center; padding: 20px 0; }

/* The row whose form is open in the pane beside it — the only way to tell
   which one, now that the form no longer covers the table to say so. */
.cat-row.is-selected td { background: var(--brand-soft); }

/* The pane itself is a .card like the list beside it; this is only what
   makes it different — the heading atop the form, standing in for the
   dialog's own head. */
.cat-detail-head { margin-bottom: 14px; }
.cat-detail-head h2 { margin: 0; font-size: 1.1rem; }

/*
 * The add/edit dialog — a native <dialog>, opened straight from the HTML
 * (see the `open` attribute in layout.blade.php) rather than with JS, so a
 * link into it works with no script at all and the browser owns the
 * backdrop, Escape and focus trap for free.
 */
.admin-dialog {
    width: min(640px, calc(100vw - 32px));
    max-height: calc(100vh - 64px);
    margin: auto;
    padding: 0;
    border: 0;
    border-radius: 16px;
    box-shadow: var(--shadow-2);
    background: var(--bg);
    color: inherit;
}

.admin-dialog::backdrop { background: rgba(20, 18, 15, .42); }

.admin-dialog-body { padding: 22px 24px 26px; overflow-y: auto; }

.admin-dialog-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 16px;
}

.admin-dialog-head h2 { margin: 0; font-size: 1.1rem; }

/*
 * An order is wider than a form.
 *
 * The 640px the forms use is right for a column of fields and too narrow for
 * a receipt beside a list of print jobs — which is the shape of an order, and
 * the reason it had a page to itself until the shop asked for a dialog.
 */
.admin-dialog-wide { width: min(1040px, calc(100vw - 32px)); }

/* Reprint, resend and the close button, on one line in the dialog's head. The
   same buttons sit in the admin header when the order is opened on its own
   page — see staff/orders/_actions. */
.admin-dialog-actions {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
}

/*
 * The order dialog holds cards, and a card inside a dialog does not need the
 * page's own margins around it — the dialog is already a card.
 */
.admin-dialog .columns { gap: 14px; }

/* A long list of things to tick, in its own scrolling box so it cannot push
   the Save button off the screen. */
.pick-list {
    display: grid;
    gap: 2px;
    max-height: 220px;
    overflow-y: auto;
    padding: 10px 12px;
    border: 1px solid var(--line);
    border-radius: var(--r);
}

.pick-list .check { padding: 3px 0; white-space: normal; }

/* ================================================ reports */

/* Nothing run yet — the shop's own reference says what the screen is for
   rather than drawing a table with no rows in it. */
.report-empty { text-align: center; padding: 44px 16px 48px; }

.report-empty h2 { margin: 0 0 6px; }

.report-empty p { margin: 0 0 20px; color: var(--ink-2); }

.report-empty-mark { width: 56px; height: 56px; color: var(--line-strong); margin-bottom: 14px; }

/* One box holding all three groups, each folding open on its own — the
   shape of the reference's own type picker. */
.report-types {
    border: 1px solid var(--line);
    border-radius: var(--r);
    padding: 6px 16px;
}

/*
 * A heading, not a fold.
 *
 * Each of these groups holds exactly one report, so the disclosure that was
 * here meant clicking a heading to reveal a single tick box — three clicks to
 * read a list four lines long. The names stay because they say what kind of
 * answer each report gives.
 */
.report-group-head {
    margin: 0;
    padding: 14px 0 4px;
    color: var(--ink-2);
    font-weight: 600;
    font-size: .82rem;
    letter-spacing: .02em;
    text-transform: uppercase;
}

.report-group + .report-group { border-top: 1px solid var(--line); }

.report-group-body { padding: 0 0 12px; }

.report-type {
    padding: 8px 10px;
    margin: 0;
    border-radius: 8px;
    white-space: normal;
}

/* Ticked rows are marked, the way the reference fills them in — so what
   the report will hold is readable without checking every box in turn. */
.report-type:has(input:checked) { background: var(--brand-soft); color: var(--brand-ink); }

.report-actions { justify-content: flex-end; }

/* ================================================ taking orders, or not */

.intake form {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
}

.intake .spacer { flex: 1; }
.intake b { display: block; }

/* Amber, not red: the shop is not broken, it is busy — and somebody chose
   this. Red would send whoever glanced at it looking for a fault. */
.intake.is-paused { border-color: #f0c36d; background: #fffaf0; }

.intake-for { display: flex; align-items: center; gap: 6px; font-size: .86rem; color: var(--ink-2); }
.intake-for select { min-height: 34px; padding: 4px 8px; width: auto; }

/*
 * How far an order has got, and the control that moves it — one thing, not a
 * label beside a button.
 *
 * The select IS the state: a row of identical "Mark ready" buttons would say
 * nothing about which orders were done, and which are done is the only thing
 * anyone scans this column for. So it is coloured by what it currently says.
 */
.stage {
    display: inline-flex;
    align-items: stretch;
    border: 1px solid var(--line-strong);
    border-radius: 999px;
    overflow: hidden;
    background: #fff;
}

.stage select {
    border: 0;
    background: transparent;
    min-height: 0;
    width: auto;
    padding: 5px 4px 5px 11px;
    font-size: .8rem;
    color: inherit;
    cursor: pointer;
}

.stage button {
    display: grid;
    place-items: center;
    width: 26px;
    border: 0;
    border-left: 1px solid var(--line);
    background: transparent;
    color: inherit;
    cursor: pointer;
}

.stage button .ico { width: 14px; height: 14px; }

/* Green once the bag exists, grey while it is still an intention. */
.stage:has(select[data-stage="ready"]),
.stage:has(select[data-stage="picked_up"]) {
    border-color: #a8d5b0;
    background: #eef8f0;
    color: #1f6b32;
}

.stage:has(select[data-stage="making"]) {
    border-color: #b9cfe8;
    background: #eef4fb;
    color: #14548f;
}

/* Picked up is finished business: it steps back out of the way rather than
   competing with the orders still being worked on. */
.stage:has(select[data-stage="picked_up"]) { opacity: .62; }

/* On the order's own page there is room for all four at once, and four things
   you can see beat a list you have to open. */
.stage-buttons { display: flex; flex-wrap: wrap; gap: 6px; }
.stage-buttons form { display: contents; }

.btn-link.danger { color: var(--bad, #b3261e); }

/*
 * The shop has stopped taking orders — said to the customer, on every page.
 *
 * Above the content, not inside it: somebody is going to fill a cart, and they
 * have to meet this before the checkout button refuses them and not after.
 */
.paused-note {
    margin: 0 0 16px;
    padding: 14px 16px;
    border: 1px solid #f0c36d;
    background: #fffaf0;
    border-radius: var(--r);
    font-size: .94rem;
}

.paused-note b { display: block; margin-bottom: 3px; }

/* ========================================================= staff tables */

.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

.table { width: 100%; min-width: 720px; border-collapse: collapse; font-size: .9rem; }

.table th, .table td {
    text-align: left;
    padding: 10px 8px;
    border-bottom: 1px solid var(--line);
    vertical-align: top;
}

/* 500, the same as .admin-table th: two tables of the same kind should not have
   column headers of different weights. The uppercase and the letter-spacing are
   what mark these as headers; the weight is not needed for it. */
.table th {
    font-size: .74rem;
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: .07em;
    color: var(--ink-3);
    white-space: nowrap;
}

.tag {
    display: inline-block;
    border-radius: 999px;
    padding: 2px 9px;
    font-size: .72rem;
    font-weight: 500;
    letter-spacing: .03em;
    text-transform: uppercase;
    white-space: nowrap;
}

.tag.paid { color: var(--ok); background: var(--ok-soft); }
.tag.pending { color: #8a6d1f; background: #fdf7e6; }
.tag.printed { color: var(--ink-2); background: var(--soft); }
.tag.bad { color: var(--bad); background: var(--bad-soft); }
.tag.type { color: var(--brand-ink); background: var(--brand-soft); }

.filters {
    display: flex;
    gap: 6px;
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    margin-bottom: 14px;
    padding-bottom: 2px;
}

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

/* Splits the date range from the star filter sitting beside it — a gap
   wider than the 6px between same-group buttons, without a visible rule. */
.filters-sep { flex: none; width: 8px; }

/*
 * The orders filter row.
 *
 * Where Active / Scheduled / History used to be. Those answered "show me the
 * pile"; the question this screen is opened with is nearly always about one
 * order, which a tab bar answers with "not in this one, try the other two".
 *
 * It wraps rather than scrolls: a filter somebody cannot see is a filter left
 * set by accident, and this row explains why the list looks the way it does.
 */
.order-filters {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    margin-bottom: 14px;
}

/* The search takes the room the rest do not want, down to a width it is still
   possible to read a reference in. */
.order-filters .cat-search {
    flex: 1 1 240px;
    min-width: 200px;
}

.order-filters select,
.order-filters input[type=date] {
    width: auto;
    min-height: 0;
    padding: 8px 10px;
    font-size: .9rem;
}

.order-filters-dash { color: var(--ink-3); }

.row-between { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }

.row-between .spacer { flex: 1; }

/*
 * Admin pages: the list is a table to read, and the form lives on its own page.
 *
 * An earlier version put an edit form inside every row, so opening the page
 * meant meeting a wall of input boxes with no way to see which dishes were on
 * sale and which had sold out.
 */
/*
 * min-width so the table scrolls sideways inside .table-scroll instead of
 * squeezing itself. Without it, every cell on a phone narrows to two words a
 * line and "Pho bo tai" stacks into four rows — unreadable.
 */
.admin-table { width: 100%; min-width: 760px; border-collapse: collapse; font-size: .92rem; }

.admin-table th {
    text-align: left;
    padding: 8px;
    border-bottom: 1px solid var(--line);
    font-size: .72rem;
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: .07em;
    color: var(--ink-3);
    white-space: nowrap;
}

.admin-table td {
    padding: 10px 8px;
    border-bottom: 1px solid var(--line);
    vertical-align: middle;
}

.admin-table tbody tr:last-child td { border-bottom: 0; }

.admin-table tbody tr:hover td { background: #fbfbfc; }

/*
|------------------------------------------------------------------------------
| The back office on a phone
|------------------------------------------------------------------------------
|
| Slide 180: "cái manager này bạn cho nó responsive trên phone lun hen".
|
| The frame already answered most of it — the sidebar slides away under 960px
| and the forms restack — but a table 760px wide inside a horizontal scroller
| is not a phone screen, it is a desktop one being dragged past a window. The
| owner checking the evening's orders on the way home was reading six columns
| through a letterbox.
|
| So a table marked `stacks` becomes one card per row below 560px: the header
| goes, and each cell carries its own column name from data-label. Opt-in with
| the class rather than applied to every .admin-table, because a row with no
| data-label on its cells would stack into a column of unlabelled values —
| worse than the scroller it replaced.
*/
@media (max-width: 560px) {
    .admin-table.stacks { min-width: 0; }

    /* The header row says nothing once each cell carries its own label, and
       it must go rather than be hidden with display:none — a screen reader
       still reads a table's headers, and repeating them beside every value
       would double the page. */
    .admin-table.stacks thead { display: none; }

    .admin-table.stacks,
    .admin-table.stacks tbody,
    .admin-table.stacks tr,
    .admin-table.stacks td { display: block; width: 100%; }

    .admin-table.stacks tr {
        margin-bottom: 10px;
        padding: 4px 2px;
        border: 1px solid var(--line);
        border-radius: var(--r);
    }

    .admin-table.stacks tbody tr:last-child { margin-bottom: 0; }

    .admin-table.stacks td {
        display: flex;
        align-items: baseline;
        justify-content: space-between;
        gap: 14px;
        padding: 7px 10px;
        border-bottom: 0;
        text-align: left;
    }

    /* The column name, in front of the value it belongs to. */
    .admin-table.stacks td[data-label]::before {
        content: attr(data-label);
        flex: 0 0 auto;
        font-size: .7rem;
        font-weight: 500;
        text-transform: uppercase;
        letter-spacing: .06em;
        color: var(--ink-3);
    }

    /* A cell with nothing in it is a line saying "Table —" on every pickup
       order. Empty ones keep their label off the card entirely. */
    .admin-table.stacks td:empty { display: none; }

    .admin-table.stacks tbody tr:hover td { background: transparent; }
}

.admin-table .num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }

.admin-table .actions { text-align: right; white-space: nowrap; }

/*
 * The row of buttons at the end of each line.
 *
 * The "Delete" button has to sit inside a <form> because it sends a POST. That
 * <form> is a box of its own, a different height from the plain button beside
 * it, which left the row of buttons uneven — display:contents removes it from
 * the layout so the button inside becomes a direct child of the flex row.
 */
.row-actions {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 8px;
}

.row-actions form,
.admin-top-actions form,
.section-head form,
.cell-form form,
.choice-row form,
.form-actions form { display: contents; }

.admin-thumb {
    width: 44px;
    height: 44px;
    border-radius: 10px;
    object-fit: cover;
    background: var(--soft);
    display: grid;
    place-items: center;
}

.admin-thumb .thumb-blank { width: 45%; height: 45%; }

.cell-title { font-weight: 500; }

.cell-sub { color: var(--ink-2); font-size: .82rem; }

.section-head {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    margin: 26px 0 10px;
}

.section-head h2 { margin: 0; font-size: 1.1rem; font-weight: 500; }

.section-head .spacer { flex: 1; }

/* A form page: narrow, one column, read top to bottom. */
.form-page { max-width: 620px; margin-inline: auto; }

.form-row { display: grid; gap: 14px; grid-template-columns: 1fr 1fr; }

@media (max-width: 560px) { .form-row { grid-template-columns: 1fr; } }

.form-actions {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
    margin-top: 20px;
}

.form-actions .spacer { flex: 1; }

/*
 * The table of choices inside an option group.
 *
 * A grid rather than a <table>: every row is a <form>, and a form cannot be a
 * child of <tr>. The header row and the data rows share one column declaration,
 * so they stay lined up even if the number of columns changes later.
 *
 * display:contents swallows the form (declared above), so the fields inside it
 * become cells of the grid. The hidden input from @csrf does not count, since
 * browsers already give it display:none.
 */
.choice-grid { display: grid; gap: 2px; }

/*
 * The last two columns need fixed widths, never auto: the header row has no
 * buttons, so its auto column collapses to 0, the 1fr column widens to make up
 * for it, and every heading drifts right of the field below it.
 */
.choice-head,
.choice-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 130px 92px 104px 84px 60px;
    align-items: center;
    gap: 10px;
}

.choice-head {
    padding: 6px 2px;
    border-bottom: 1px solid var(--line);
    font-size: .72rem;
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: .07em;
    color: var(--ink-3);
}

.choice-row { padding: 5px 0; }

.cell-form input[type=text], .cell-form input[type=number],
.choice-row input[type=text], .choice-row input[type=number] {
    min-height: 40px;
    padding: 8px 10px;
    width: 100%;
}

.choice-row .check { padding: 0; }

/*
 * The kitchen's settings for one choice, on a line under the row.
 *
 * Full width and quieter than what is above it: almost every choice leaves all
 * three empty, and squeezing them into the row would leave the name — the one
 * field that is always filled in — two words wide.
 */
/*
 * grid-row: 2 as well as the full-width column span.
 *
 * Without the explicit row this block is auto-placed, which pushes the Delete
 * link that follows it in the markup onto a row of its own — leaving "Delete"
 * floating in the middle of the card, further from the choice it deletes than
 * from the next one down.
 */
.choice-extra {
    grid-column: 1 / -1;
    grid-row: 2;
    display: grid;
    grid-template-columns: minmax(0, 1fr) 130px 170px;
    gap: 10px;
    padding: 2px 0 10px;
    border-bottom: 1px solid var(--line);
}

.choice-extra label { display: grid; gap: 3px; }

.choice-extra span {
    font-size: .7rem;
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--ink-3);
}

.choice-extra input, .choice-extra select { min-height: 34px; padding: 5px 9px; font-size: .84rem; }

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

/* The "add a choice" row at the end, on the same grid so it lines up too. */
.choice-add { margin-top: 10px; padding-top: 12px; border-top: 1px solid var(--line); }

@media (max-width: 900px) {
    .choice-head { display: none; }

    .choice-row {
        display: flex;
        flex-wrap: wrap;
        gap: 8px;
    }

    .choice-row .w-name { flex: 1 1 160px; }
    .choice-row .w-num { width: 96px; }
}

/*
|==============================================================================
| The admin frame
|==============================================================================
|
| A fixed sidebar on the left and scrolling content on the right — the familiar
| CMS layout, chosen because the manager jumps between sections constantly and
| needs to see all of them at once.
|
| The sidebar is dark: one glance says this is the admin, not the shop front, so
| nobody edits the menu thinking they are browsing it.
|
| Under 960px the sidebar slides off screen and opens from the hamburger: the
| tablet at the counter still has to show the orders.
*/

/*
| The back office is PERDIGI, and the shop front is Saigon.
|
| The customer's pages carry the restaurant's own navy and gold, off the sign
| above the door. This is not those pages. It is the product the restaurant
| bought — the thing the ordering system is — and it carries Perdigi's mark,
| Perdigi's name and, from here, Perdigi's colour.
|
| That is not decoration. One shop's staff will use this beside another shop's
| one day, and a back office painted in whichever restaurant it happens to be
| pointed at is a back office nobody can tell apart. It also means the two sets
| of pages cannot be confused at a glance, which is the older reason the sidebar
| went dark in the first place: nobody edits the menu thinking they are
| browsing it.
|
| So the brand tokens are RE-DECLARED here rather than changed at :root. Every
| rule in this file that says var(--brand) — buttons, links, focus rings, the
| active row — follows without being touched, and nothing the customer sees
| moves at all.
|
| Black and white, and the orange has gone with the shop's second look at it.
| Perdigi's mark at the top of the bar is what says whose tool this is; it does
| not need the page painted to match, and one accent used on buttons, links,
| focus rings and selected rows at once is an accent that emphasises nothing.
| See the tokens below.
*/
body.admin {
    --side-w: 264px;

    /*
     * The column is WHITE, and it was near-black an hour ago.
     *
     * The shop sent the reference it wants this page to look like — Uber Eats
     * Manager — and every screen of it has a white column with black text. That
     * is not a taste: a dark column against a white page is two grounds
     * competing, and the thing the eye should land on is the page.
     *
     * What tells you this is the back office is the mark at the top of the
     * column and the shop's name under it, which is a better job than a slab of
     * colour was doing.
     */
    --side-bg: #ffffff;
    --side-ink: #4a4a4a;
    --side-hover: #f4f4f5;

    /*
     * Black, white and grey. The orange has gone.
     *
     * The shop asked for the back office to look like the reference it was
     * drawn from, and that reference has no accent colour at all: buttons are
     * black, the selected row is grey, and the only colour on the page is the
     * green and red on a figure that has moved. It is not decoration removed
     * for its own sake — a page where three different things are orange is a
     * page where none of them is emphasis, and the tables here are dense.
     *
     * Colour is left to the two places it MEANS something: --ok and --bad on a
     * change, and the green dot on orders waiting. Everything else earns
     * attention by weight and position.
     *
     * The customer's pages keep the shop's blue and gold: those are the shop's
     * own colours in front of its own customers, and this is a tool.
     */
    --brand: #000000;
    /* Pressed and hovered. Lifted off black rather than deepened — there is
       nowhere darker to go. */
    --brand-ink: #3a3a3a;
    /* The wash behind a selected row or a quiet panel. */
    --brand-soft: #f2f2f3;

    /*
     * White, not near-white.
     *
     * The page ground was #f6f6f7 with white cards on it — two greys a shade
     * apart, which is what makes a screen look faintly grubby without anybody
     * being able to say why. One ground, and cards separated by a hairline.
     */
    background: #ffffff;
    padding-bottom: 0;
}

/* Below the full-width header, which takes its own --header-h out of the
   viewport already — without the subtraction here the page ran a header's
   worth of empty scroll past the bottom of short content. */
.admin-shell { display: flex; min-height: calc(100vh - var(--header-h)); }

.side {
    position: fixed;
    /* Below the full-width header, not beside it. */
    top: var(--header-h);
    bottom: 0;
    left: 0;
    z-index: 60;
    width: var(--side-w);
    display: flex;
    flex-direction: column;
    background: var(--side-bg);
    color: var(--ink);
    /* A hairline instead of a colour change: the column and the page are the
       same white, and this is what separates them. */
    border-right: 1px solid var(--line);
    transform: translateX(-100%);
    transition: transform .22s ease;
}

.side.is-open { transform: none; }

.side-backdrop {
    position: fixed;
    inset: 0;
    z-index: 55;
    background: rgba(20, 24, 28, .45);
}

/*
 * On a phone the drawer owns the whole screen, header included.
 *
 * It used to start at `top: var(--header-h)` and sit at z-index 60, under the
 * header's 70. That works where the header is one row of the height the token
 * claims. It is not: .admin-topbar wraps, and on a narrow screen the greeting,
 * the page title and the row of actions come to twice --header-h or more — so
 * the header was painted over the top of the drawer and the first few items,
 * Home among them, could not be seen or reached. The list scrolls, but its top
 * was behind something opaque.
 *
 * Measuring the header and feeding it back into --header-h is the other way to
 * do this, and it is the way that flickers: the drawer's height would depend on
 * a measurement that the drawer can change. Covering the header instead has no
 * feedback in it at all, and on a screen this size a menu that takes the screen
 * is what a menu is.
 */
@media (max-width: 959.98px) {
    .side {
        top: 0;
        /* Above .admin-topbar's 70. */
        z-index: 80;
        width: min(var(--side-w), 88vw);
        /* Its own bottom padding, so the last item clears a phone's home bar. */
        padding-bottom: env(safe-area-inset-bottom, 0px);
    }

    .side-backdrop { z-index: 75; }
}

/* Kept for the customer's pages, which do have a square badge. */
.side-mark {
    width: 36px;
    height: 36px;
    flex: none;
    border-radius: 11px;
    object-fit: cover;
}

/* The list of sections takes all the free space, pushing the account block to
   the bottom. */
.side-nav {
    flex: 1;
    overflow-y: auto;
    padding-bottom: 12px;
}

.side-label {
    margin: 18px 0 4px;
    padding-inline: 22px;
    color: var(--ink-3);
    font-size: .7rem;
    font-weight: 600;
    letter-spacing: .09em;
    text-transform: uppercase;
}

/*
 * Bigger rows than they were, because the reference's are and it is right: this
 * column is read across a desk, and a 14px row with a 19px icon is a list you
 * have to lean towards.
 */
.side-nav a {
    display: flex;
    align-items: center;
    gap: 13px;
    /* The left margin is 6px smaller than the right, and the difference is
       where the marker on the active row goes. */
    margin: 1px 10px 1px 16px;
    padding: 11px 12px;
    border-radius: 10px;
    color: var(--ink);
    text-decoration: none;
    font-size: 1rem;
    font-weight: 500;
}

.side-nav a:hover { background: var(--side-hover); }

/*
 * The open section: a grey pill, and a bar down the left edge.
 *
 * Straight off the reference, and the bar is the part that matters — a grey
 * pill on a white column is a whisper, and on a page of twelve entries you want
 * to find your place without reading any of them. The bar is Perdigi's orange
 * rather than the reference's black, which is the whole of what this page
 * borrows from the brand.
 */
.side-nav a.is-on {
    position: relative;
    background: #f1f1f2;
    color: var(--ink);
    font-weight: 600;
}

.side-nav a.is-on::before {
    content: "";
    position: absolute;
    left: -10px;
    top: 8px;
    bottom: 8px;
    width: 3px;
    border-radius: 999px;
    background: var(--brand);
}

.side-nav .ico { width: 21px; height: 21px; vertical-align: 0; }

.side-badge {
    margin-left: auto;
    background: var(--bad);
    color: #fff;
    border-radius: 999px;
    padding: 1px 7px;
    font-size: .72rem;
    font-weight: 500;
}

/*
 * A collapsible group — Store, Menu, Settings — <details> rather than
 * JavaScript. The summary row wears the same look as a plain link in
 * .side-nav a; the links underneath are the same shape, smaller and
 * indented, with their own small-caps sub-heading where one applies
 * (Menu's own "Catalogue").
 */
.side-group summary { list-style: none; cursor: pointer; }
.side-group summary::-webkit-details-marker { display: none; }

.side-group-head {
    display: flex;
    align-items: center;
    gap: 13px;
    margin: 1px 10px 1px 16px;
    padding: 11px 12px;
    border-radius: 10px;
    color: var(--ink);
    font-size: 1rem;
    font-weight: 500;
}

.side-group-head:hover { background: var(--side-hover); }

.side-group-head.is-on { background: #f1f1f2; color: var(--ink); font-weight: 600; }

.side-group-head .ico { width: 21px; height: 21px; vertical-align: 0; }

.side-group-label { flex: 1; }

.side-group-arrow { width: 16px; height: 16px; color: var(--ink-3); transition: transform .15s; }

.side-group[open] .side-group-arrow { transform: rotate(180deg); }

.side-group-body a {
    display: block;
    margin: 1px 10px 1px 34px;
    padding: 9px 12px;
    border-radius: 10px;
    color: var(--ink-2);
    text-decoration: none;
    font-size: .93rem;
    font-weight: 500;
}

.side-group-body a:hover { background: var(--side-hover); }

.side-group-body a.is-on { background: #f1f1f2; color: var(--ink); font-weight: 600; }

.side-sub-label { margin: 6px 0 2px; padding-inline: 34px; }

.side-foot { border-top: 1px solid var(--line); padding: 10px; }

.side-out {
    display: flex;
    align-items: center;
    gap: 13px;
    padding: 10px 12px;
    border-radius: 10px;
    color: var(--side-ink);
    text-decoration: none;
    font-size: .94rem;
}

.side-out:hover { background: var(--side-hover); color: var(--ink); }
.side-out .ico { width: 19px; height: 19px; vertical-align: 0; }

.side-user {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 12px 4px;
}

/* The account block links to the change-password page, but it must not look
   like a link in the middle of the sidebar — no underline, no colour change. */
.side-user-text {
    flex: 1;
    min-width: 0;
    line-height: 1.3;
    padding: 6px 8px;
    margin: -6px -8px;
    border-radius: 9px;
    color: var(--ink);
    text-decoration: none;
}

.side-user-text:hover { background: var(--side-hover); }
.side-user-text b { display: block; font-size: .88rem; }

.side-user-text small {
    display: block;
    color: var(--side-ink);
    font-size: .76rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* .side-signout was here — signing out is .topbar-signout now, top right of
   the header. Nothing in the sidebar posts any more, only the name that opens
   the account page. */

/*
 * The full-width header — brand, greeting, page title, and the page's own
 * action button, all in one bar above the sidebar rather than the sidebar
 * running the full height beside it. The shop's own reference draws it this
 * way. Sticky rather than static for the same reason the old in-column bar
 * was: half these pages are long tables, and the button that adds a row
 * should not need a scroll back to the top to reach.
 */
.admin-topbar {
    position: sticky;
    top: 0;
    z-index: 70;
    display: flex;
    align-items: center;
    gap: 16px;
    flex-wrap: wrap;
    min-height: var(--header-h);
    padding: 12px 20px;
    /*
     * Solid white, and NO backdrop-filter.
     *
     * It was 92% white with an 8px blur behind it. A blur on a sticky element
     * is recomputed against whatever is scrolling underneath it, every frame,
     * for the whole height of the bar — on the shop's own machines that is the
     * juddering somebody described as the back office "not feeling smooth",
     * and it bought nothing: the page under it is white too, so there was
     * never anything to see through the glass.
     *
     * The hairline is what separates the bar from the page. See the note on
     * body.admin's background.
     */
    background: #fff;
    border-bottom: 1px solid var(--line);
}

/* The mark, and the word for which of Perdigi's products this is, stacked
   as one lockup — a column, so "Manager" sits under the logo rather than
   beside it. */
.topbar-brand {
    flex: none;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 1px;
    text-decoration: none;
}

.topbar-tagline {
    color: var(--ink-2);
    font-size: .74rem;
    font-weight: 600;
    letter-spacing: .08em;
    text-transform: uppercase;
    /* Set against the logo's own left edge, which the image carries a
       sliver of transparent padding on. */
    padding-left: 2px;
}

@media (min-width: 960px) {
    /* As wide as the sidebar below it, so the greeting-and-title block that
       follows starts flush with the content column rather than crowding
       the logo — the shop asked for this alignment explicitly. */
    .topbar-brand { width: var(--side-w); }
}

.topbar-word { width: 96px; height: auto; }

.topbar-greeting { min-width: 0; flex: 1 1 240px; }

.topbar-greeting > small {
    display: block;
    color: var(--ink-2);
    font-size: .8rem;
}

.topbar-greeting h1 {
    margin: 0;
    font-size: 1.5rem;
    font-weight: 700;
    letter-spacing: -.02em;
    line-height: 1.25;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.admin-top-actions {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    margin-left: auto;
}

/*
 * Who is signed in and the way out, hard right.
 *
 * Last in the bar, after the page's own action buttons: those change from page
 * to page and this does not, and a fixture that moves as the page beside it
 * changes is a fixture nobody can aim at. No margin-left of its own — the
 * greeting already grows into the gap and pushes both to the right, and a
 * second auto margin here would split that gap between them instead.
 */
.topbar-account {
    order: 99;
    display: flex;
    align-items: center;
    gap: 16px;
}

.topbar-user {
    color: var(--ink-2);
    font-size: .88rem;
    text-decoration: none;
    white-space: nowrap;
}

.topbar-user:hover { color: var(--ink); }

/*
 * Plain words, not a button.
 *
 * Signing out is not something the page is asking anybody to do — a filled
 * button up here would compete with the one that adds an item, which IS what
 * the page wants. The reference draws it as text and so does this.
 */
.topbar-signout {
    padding: 6px 2px;
    border: 0;
    background: none;
    color: var(--ink);
    font: inherit;
    font-size: .88rem;
    white-space: nowrap;
    cursor: pointer;
}

.topbar-signout:hover { text-decoration: underline; }

/* A numbered set of instructions inside an admin card. */
.steps {
    margin: 0;
    padding-left: 22px;
    color: var(--ink-2);
    font-size: .86rem;
    line-height: 1.65;
}

.steps li + li {
    margin-top: 7px;
}

/*
 * A field and its button side by side in the page header — "send a test email
 * to ___". The address belongs beside the button rather than down in the form,
 * because it is not a setting: it is one of the arguments to the action, and it
 * is different every time somebody checks.
 */
.inline-form {
    display: flex;
    align-items: center;
    gap: 6px;
}

.inline-form input {
    width: 220px;
    max-width: 42vw;
    padding: 7px 10px;
    border: 1px solid var(--line-strong);
    border-radius: 9px;
    font: inherit;
    font-size: .85rem;
}

.crumb {
    display: flex;
    align-items: center;
    gap: 3px;
    margin-bottom: 1px;
    color: var(--ink-3);
    font-size: .78rem;
}

.crumb a { color: var(--ink-2); text-decoration: none; }
.crumb a:hover { color: var(--brand-ink); }
.crumb .ico { width: 13px; height: 13px; vertical-align: 0; }

.icon-btn {
    display: grid;
    place-items: center;
    width: 40px;
    height: 40px;
    flex: none;
    border: 1px solid var(--line-strong);
    border-radius: 11px;
    background: var(--bg);
    color: var(--ink);
    cursor: pointer;
}

.icon-btn .ico { width: 20px; height: 20px; vertical-align: 0; }

.admin-body {
    flex: 1;
    min-width: 0;
    width: 100%;
    max-width: 1600px;
    padding: 20px 20px 44px;
}

/* A white card on a grey ground needs a little shadow to lift off it. */
body.admin .card { box-shadow: 0 0 0 1px var(--line), 0 1px 2px rgba(20, 24, 28, .04); }

/*
 * The board watches for new orders, and this says so.
 *
 * A dot rather than a sentence: "up to date" written out every ten seconds is
 * noise nobody reads, whereas a dot that has stopped pulsing is the only thing
 * that tells the counter the link has gone and the screen is now a photograph.
 */
.live {
    display: flex;
    align-items: center;
    gap: 7px;
    margin: 0 0 12px;
    color: var(--ink-3);
    font-size: .78rem;
}

.live-dot {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--ok);
    animation: live-pulse 2.4s ease-in-out infinite;
}

.live.is-down { color: var(--bad); }

.live.is-down .live-dot {
    background: var(--bad);
    animation: none;
}

@keyframes live-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: .25; }
}

/*
 * A row that has just arrived, marked for a few seconds.
 *
 * The whole point of a board that refreshes itself is that nobody is watching
 * it closely — so an order that appeared while somebody was looking at the
 * kitchen has to still be identifiable when they look back.
 */
.admin-table tbody tr.is-new td {
    animation: row-arrived 6s ease-out;
}

@keyframes row-arrived {
    0%, 55% { background: #fff6d8; }
    100% { background: transparent; }
}

/* Somebody who has asked for less movement gets the colour without the pulse,
   and a row that simply stays marked. */
@media (prefers-reduced-motion: reduce) {
    .live-dot { animation: none; }
    .admin-table tbody tr.is-new td { animation: none; background: #fff6d8; }
}

/* The sign-in page: no sidebar yet, just one card in the middle. */
.admin-auth {
    min-height: 100vh;
    display: grid;
    place-items: center;
    padding: 24px 16px;
}

.admin-auth-box { width: 100%; max-width: 380px; }

.admin-auth-brand {
    display: grid;
    justify-items: center;
    gap: 2px;
    margin-bottom: 20px;
    text-align: center;
}

.admin-auth-brand b { font-size: 1.15rem; margin-top: 8px; }

.auth-word { width: 168px; height: auto; }

.admin-auth-brand small {
    color: var(--ink-3);
    font-size: .76rem;
    letter-spacing: .04em;
    text-transform: uppercase;
}

@media (min-width: 960px) {
    .side { transform: none; }

    /* On a computer the sidebar is always there, so the hamburger and the
       backdrop mean nothing. */
    .side-toggle,
    .side-backdrop { display: none !important; }

    .admin-topbar { padding-inline: 26px; }
    .admin-body { margin-left: var(--side-w); padding: 24px 26px 52px; }
}

/* ================================================================ hours */

/*
 * The week: a day, a start, a word, an end — seven times over.
 *
 * A grid rather than seven rows of their own, so the four columns line up down
 * the page. Reading "is Thursday the odd one out" off a list means comparing
 * the same position on seven lines, and that only works if they are in the same
 * position.
 */
/* The plain rows the picker writes into, and the whole page with the script
   off: a tick per day, and a pair of times per span it trades. */
.hours-grid { display: grid; gap: 10px; }

.hours-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }

/* Fixed, so the seven days' times start at the same place. */
.hours-day { margin: 0; flex: none; width: 150px; }

.hours-row-times { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }

.hours-pair { display: flex; align-items: center; gap: 8px; }

.hours-dash { color: var(--ink-2); font-size: .9rem; }

.hours-grid input[type="time"] { width: auto; min-width: 120px; }

@media (max-width: 560px) {
    /* The day's name gets its own line; the two times keep theirs, because a
       start and an end stacked is not a range any more. */
    .hours-day { width: 100%; }
}

/* ------------------------------------------------- the week, picked by hand */

.hours-picker { max-width: 560px; }

/* Seven days as one unbroken strip, dragged across to pick a run of them —
   the reference draws it with no seams, and a gap between the days made it
   read as seven buttons that happened to be adjacent. */
.hours-days {
    display: flex;
    gap: 0;
    border-radius: 8px;
    overflow: hidden;
}

.hours-chip {
    flex: 1;
    padding: 12px 4px;
    border: 0;
    background: var(--ink);
    color: #fff;
    font: inherit;
    font-weight: 500;
    font-size: .9rem;
    cursor: pointer;
    touch-action: none;
    user-select: none;
}

/* Shut reads as the absence of the day, the way the reference greys it. */
.hours-chip.is-shut { background: var(--soft); color: var(--ink-3); }

/*
 * The day being edited is inverted, not underlined.
 *
 * It was `inset 0 -3px 0 var(--brand)` — a navy underline inside a chip painted
 * --ink. #002f90 on #14181c is about 1.3:1, which is to say invisible: the
 * strip read as seven identical black boxes, and the only thing saying which
 * day the rows below belonged to was a line of small grey type under them.
 * Slide 168 ("nên highlight selected date") is that. Editing Tuesday's hours
 * while reading a strip that starts at Monday is how a shop ends up shut on the
 * wrong day.
 *
 * White ground with dark type is the one pairing that cannot be missed against
 * black, and it is how a selected tab reads everywhere else.
 */
.hours-chip.is-picked {
    background: var(--bg);
    color: var(--ink);
    font-weight: 700;
    box-shadow: inset 0 -3px 0 var(--brand);
}

/* A day that is shut and also the one being edited: still inverted, so the
   selection never disappears, but kept quieter than an open one. */
.hours-chip.is-shut.is-picked { background: var(--bg); color: var(--ink-2); }

.hours-hint { margin: 8px 0 12px; color: var(--ink-2); font-size: .8rem; }

/* Midnight to midnight, with the open window drawn on it. */
.hours-bar {
    position: relative;
    height: 24px;
    border-radius: 6px;
    background: var(--soft);
    overflow: hidden;
}

/* Over the hour marks, so a window that is open reads as one solid block
   rather than a block someone has ruled lines across. */
.hours-bar-fill { position: absolute; top: 0; bottom: 0; z-index: 1; background: var(--ok); }

/*
 * One mark an hour, so a green block can be counted rather than guessed at.
 *
 * The plain hours are a hairline in the track's own grey, faint on purpose —
 * twenty-three strong lines would be a fence, and what is being read here is
 * the green, not the ruler. Every third one is darker and full height, which is
 * where the figures underneath are, and what the eye counts from.
 */
.hours-tick {
    position: absolute;
    top: 6px;
    bottom: 6px;
    width: 1px;
    background: var(--line-strong);
}

.hours-tick.is-major {
    top: 0;
    bottom: 0;
    background: var(--ink-3);
}

.hours-scale {
    display: flex;
    justify-content: space-between;
    margin-top: 4px;
    color: var(--ink-3);
    font-size: .72rem;
}

/* Small enough to sit in a table cell without setting the row's height. */
.admin-table select {
    width: auto;
    min-width: 118px;
    min-height: 0;
    padding: 6px 30px 6px 10px;
    font-size: .86rem;
}

/* ----------------------------------------------- a new sign-in to hand over */

/* Marked out, because it is on screen once and then never again. */
.password-card { border: 2px solid var(--brand); }

.password-pair {
    display: grid;
    grid-template-columns: auto 1fr;
    align-items: center;
    gap: 8px 16px;
    margin: 0 0 14px;
}

.password-pair dt { color: var(--ink-2); font-size: .84rem; }

.password-pair dd { margin: 0; }

.password-pair code {
    display: inline-block;
    padding: 6px 10px;
    border-radius: 8px;
    background: var(--soft);
    font-size: 1.05rem;
    user-select: all;
}

/* --------------------------------------------------- busier prep times */

/* A row per exception: how many minutes, which day, and the hours of it. */
.prep-rule {
    display: grid;
    grid-template-columns: 1fr 1fr 1fr 1fr auto;
    align-items: end;
    gap: 12px;
    margin-bottom: 12px;
    padding: 14px 16px;
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
}

.prep-rule .field { margin: 0; min-width: 0; }

.prep-drop { margin-bottom: 15px; }

@media (max-width: 900px) {
    .prep-rule { grid-template-columns: 1fr 1fr; }
    .prep-drop { margin-bottom: 0; }
}

/* ------------------------------------------------------- holiday hours */

/* A row per dated override, in a box of its own the way the reference
   draws it: the date, what it is, whether the shop shuts at all, and the
   hours it keeps instead.

   A grid rather than a flex row, so the four boxes are the same width and
   every label starts on the same line — laid out by flex they sized
   themselves to their contents and the labels stepped up and down the row. */
.holiday-row {
    display: grid;
    grid-template-columns: 1fr 1fr auto 1fr 1fr auto;
    align-items: end;
    gap: 12px;
    margin-bottom: 12px;
    padding: 14px 16px;
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
}

.holiday-row .field { margin: 0; min-width: 0; }

/* Sits on the line of the boxes it governs, not above them. */
.holiday-shut { margin: 0 0 13px; white-space: nowrap; }

.holiday-drop { margin-bottom: 15px; }

@media (max-width: 900px) {
    /* Two boxes to a line rather than six squeezed onto one. */
    .holiday-row { grid-template-columns: 1fr 1fr; }
    .holiday-shut, .holiday-drop { margin-bottom: 0; }
}

/* One row per span the day trades — two of them where the kitchen shuts
   for the afternoon. */
.hours-spans { margin-top: 14px; }

.hours-span {
    display: flex;
    align-items: flex-end;
    gap: 12px;
    margin-bottom: 10px;
}

.hours-span .field { flex: 1; margin: 0; }

.hours-drop { flex: none; margin-bottom: 4px; color: var(--ink-3); }

.hours-switches { display: flex; align-items: center; gap: 20px; flex-wrap: wrap; margin-top: 4px; }

/* The whole week in words, under the picker that only ever shows a few days
   of it — so nothing being edited can hide what is saved. */
.hours-summary {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
    gap: 2px 16px;
    margin-top: 18px;
    padding-top: 14px;
    border-top: 1px solid var(--line);
    font-size: .84rem;
}

.hours-summary-row.is-shut { color: var(--ink-3); }

/* =========================================================== order dots */

/*
 * One dot per stage, in the first column of the board.
 *
 * The reference draws it and it is right: colour is read faster than a word,
 * and the question asked of twenty rows is "which of these is still work",
 * which a column of dots answers at a glance and a column of words does not.
 *
 * The same three colours the tablet's board uses — amber being cooked, green on
 * the shelf, grey gone — so the two screens in one shop agree about what green
 * means.
 */
.cell-no { white-space: nowrap; }

.dot {
    display: inline-block;
    width: 10px;
    height: 10px;
    margin-right: 8px;
    border-radius: 999px;
    vertical-align: 1px;
    background: var(--ink-3);
}

.dot-new { background: #b26a00; }
.dot-accepted { background: #b26a00; }
.dot-ready { background: var(--ok); }
.dot-collected { background: var(--ink-3); }

/* ============================================================ shop info */

/*
 * A fact, with a mark that says whether it is there.
 *
 * The reference draws a green tick beside each one. The tick is worth more here
 * than it is there: these facts are typed on five different settings pages, and
 * nothing until now said "you never filled that one in" — so what this really
 * is, is a checklist wearing a profile's clothes.
 */
.facts { display: grid; gap: 2px; }

.fact {
    display: grid;
    grid-template-columns: 26px 130px 1fr;
    align-items: baseline;
    gap: 10px;
    padding: 9px 0;
    border-bottom: 1px solid var(--line);
}

.fact:last-child { border-bottom: 0; }

.fact-mark {
    display: grid;
    place-items: center;
    width: 22px;
    height: 22px;
    border-radius: 999px;
    align-self: center;
}

.fact-mark .ico { width: 14px; height: 14px; vertical-align: 0; }

.fact-mark.is-on { background: var(--ok-soft); color: var(--ok); }

/* Amber, not red. A missing phone number is a job, not a fault — and a page of
   red marks reads as a broken system rather than as a list of things to do. */
.fact-mark.is-off { background: #fdf3e3; color: #9a6700; }

.fact-mark.is-note { background: var(--soft); color: var(--ink-3); }

.fact-key { color: var(--ink-2); font-size: .9rem; }

.fact-value { font-weight: 500; min-width: 0; word-break: break-word; }

.fact-head {
    margin: 20px 0 6px;
    font-size: .78rem;
    font-weight: 700;
    letter-spacing: .07em;
    text-transform: uppercase;
    color: var(--ink-3);
}

@media (max-width: 560px) {
    /* The label goes above the value: at 340px a 130px column leaves an address
       three words wide. */
    .fact { grid-template-columns: 26px 1fr; }
    .fact-value { grid-column: 2; }
}

/* ================================================================ sales */

.sales-head { margin: 4px 0 2px; font-size: 1.5rem; font-weight: 700; letter-spacing: -.01em; }

.sales-dates { margin: 0 0 16px; color: var(--ink-2); font-size: .9rem; }

/*
 * Three figures in bordered boxes, as the reference draws them: a quiet label
 * above, the number large, and the change against the period before it on the
 * same line as the number.
 *
 * Beside rather than under, which is the detail that makes the row read as
 * three facts instead of six.
 */
.metric-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 12px;
    margin-bottom: 20px;
}

.metric {
    padding: 14px 16px;
    border: 1px solid var(--line-strong);
    border-radius: var(--r);
    background: var(--bg);
}

.metric-label { display: block; color: var(--ink-2); font-size: .8rem; }

.metric-value {
    display: flex;
    align-items: baseline;
    gap: 8px;
    margin-top: 2px;
    font-size: 1.75rem;
    font-weight: 700;
    letter-spacing: -.02em;
    font-variant-numeric: tabular-nums;
}

/*
 * Up or down. Green and red, and nothing else on this page takes a colour — so
 * the eye goes to the one thing that is a judgement rather than a fact.
 */
.delta {
    font-size: .82rem;
    font-style: normal;
    font-weight: 600;
    letter-spacing: 0;
    color: var(--ink-3);
}

.delta.is-up { color: var(--ok); }
.delta.is-down { color: var(--bad); }

/*
 * A column chart: a scale down the left, bars growing from a baseline.
 *
 * A month of days reads left to right the way a month is read, and the tall bar
 * is the busy day without anybody having to compare lengths of horizontal lines.
 */
.chart-head {
    display: flex;
    align-items: baseline;
    gap: 10px;
    margin-bottom: 14px;
}

.chart-head h2 { margin: 0; }

.chart-key { color: var(--ink-2); font-size: .85rem; }

.chart {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 10px;
    height: 220px;
}

/* The three gridline figures, spread against the same height as the bars. The
   last one sits on the baseline, which is why the labels row is subtracted. */
.chart-scale {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    align-items: flex-end;
    padding-bottom: 22px;
    color: var(--ink-3);
    font-size: .72rem;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.chart-cols {
    display: flex;
    align-items: flex-end;
    gap: 3px;
    padding-bottom: 22px;
    /* A line at the foot and two behind the bars, so a height can be read
       against something rather than guessed at. */
    background:
        linear-gradient(var(--line), var(--line)) 0 calc(100% - 22px) / 100% 1px no-repeat,
        linear-gradient(var(--line), var(--line)) 0 calc(50% - 11px) / 100% 1px no-repeat,
        linear-gradient(var(--line), var(--line)) 0 0 / 100% 1px no-repeat;
    /* Thirty-one days will not fit on a phone, and squeezing them to hairlines
       says less than scrolling does. */
    overflow-x: auto;
    scrollbar-width: thin;
}

.chart-col {
    position: relative;
    flex: 1 0 18px;
    min-width: 18px;
    /*
     * A column is a column, never a wall.
     *
     * With one day in the range — "Today" — flex-grow handed the single column
     * the whole card and it drew a solid black rectangle four hundred pixels
     * wide, which reads as a broken page rather than as a day's takings. The
     * cap costs nothing at thirty columns, where none of them reaches it.
     */
    max-width: 64px;
    height: 100%;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
}

/*
 * The chart is the one coloured thing in the admin, and deliberately so.
 *
 * The rest of this frame is monochrome on purpose — see the note on --brand
 * below the sidebar rules — and that left the chart drawn in flat black, which
 * the shop read as dull and asked to be rid of (slide 173).
 *
 * One hue, and no legend: a single series of magnitudes. A rainbow across the
 * columns would say the days differ in kind, which they do not, and the heading
 * already names what is being measured.
 *
 * Validated against this surface for colour-vision deficiency and contrast
 * before being written down.
 */
:root {
    --chart-sales: #2a78d6;
    --chart-dishes: #eb6834;
}

/*
 * What sold most: a bar per dish, lying down.
 *
 * Horizontal because the labels are dish names. Stood upright they would be
 * set sideways or cut to four characters, and "Banh Mi Grilled Chicken" is
 * the whole point of the row.
 *
 * Its own hue, because it is its own question -- the bars by day say how much
 * came in, these say what went out. Within the chart it is one colour: these
 * are plates, all of them, and a rainbow would say the dishes differ in kind.
 */
.hbars { display: grid; gap: 10px; }

.hbar {
    display: grid;
    /* Name, bar, count. minmax(0,1fr) on the track so a long name cannot push
       the count off the right of the card. */
    grid-template-columns: minmax(0, 11rem) minmax(0, 1fr) 2.5rem;
    align-items: center;
    gap: 12px;
}

.hbar-name {
    font-size: .88rem;
    /* One line. A dish with a long name must not make its row taller than the
       others, and the whole name is in the row's title. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.hbar-track {
    display: block;
    height: 18px;
    border-radius: 4px;
    background: var(--soft);
}

.hbar-fill {
    display: block;
    height: 100%;
    border-radius: 4px;
    background: var(--chart-dishes);
}

/* The number at the end of its own bar: a direct label, so nothing here is
   read off an axis that is not drawn. */
.hbar-value {
    text-align: right;
    font-size: .88rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

/* On a phone the name takes a line of its own and the bar the next, rather
   than three columns squeezed into 320px. */
@media (max-width: 560px) {
    .hbar {
        grid-template-columns: minmax(0, 1fr) 2.5rem;
    }

    .hbar-name { grid-column: 1 / -1; }
}

.chart-bar {
    display: block;
    width: 100%;
    border-radius: 3px 3px 0 0;
    background: var(--chart-sales);
}

/* A hairline for a day that sold nothing, so the column still reads as a day
   rather than as a missing one. */
.chart-bar[style*="height: 1%"] { background: var(--line-strong); }

.chart-label {
    position: absolute;
    bottom: -20px;
    left: 0;
    right: 0;
    text-align: center;
    color: var(--ink-3);
    font-size: .66rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/*
 * The row of sections across the top of a page that has several — the four
 * catalogue screens.
 *
 * Underlined rather than pills: .tabs on the customer's menu is already a row
 * of pills, and the two rows mean different things. Those filter one list; these
 * move between pages, which is what the reference draws as an underline under
 * the one you are on.
 */
.page-tabs {
    display: flex;
    gap: 4px;
    margin: -4px 0 18px;
    border-bottom: 1px solid var(--line);
    overflow-x: auto;
    scrollbar-width: none;
}

.page-tabs::-webkit-scrollbar { display: none; }

.page-tabs a {
    position: relative;
    padding: 10px 14px;
    white-space: nowrap;
    color: var(--ink-2);
    font-size: .94rem;
    font-weight: 500;
    text-decoration: none;
}

.page-tabs a:hover { color: var(--ink); }

.page-tabs a.is-on { color: var(--ink); font-weight: 600; }

/*
 * The underline sits ON the row's own border rather than under it, so the tab
 * and the rule read as one line with a piece of it thickened — which is the
 * whole trick of this control.
 */
.page-tabs a.is-on::after {
    content: "";
    position: absolute;
    left: 10px;
    right: 10px;
    bottom: -1px;
    height: 2px;
    border-radius: 2px 2px 0 0;
    background: var(--ink);
}

/*
 * The hairline that says a screen is on its way.
 *
 * Without a page reload there is no spinner in the browser's own tab, and a
 * button that appears to do nothing for a third of a second gets pressed twice.
 * A line at the very top and nothing else: this is feedback, not a feature, and
 * a full-screen overlay for a 300ms fetch is worse than no feedback at all.
 *
 * It grows quickly to four fifths and then crawls, which is the honest shape of
 * a wait whose length is unknown — the last fifth arrives when the page does.
 */
.admin-progress {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 120;
    height: 2px;
    width: 0;
    background: var(--brand);
    opacity: 0;
    pointer-events: none;
    transition: width .2s ease, opacity .2s ease;
}

.admin-progress.is-on {
    width: 80%;
    opacity: 1;
    transition: width 6s cubic-bezier(.1, .8, .2, 1), opacity .1s;
}

/* A button that has been pressed and is waiting for the answer. Dimmed rather
   than greyed: it is still the button, it is simply busy. */
.btn:disabled,
.btn-link:disabled,
.switch:disabled { opacity: .6; cursor: default; }

@media (prefers-reduced-motion: reduce) {
    .admin-progress { transition: opacity .1s; }
    .admin-progress.is-on { width: 80%; transition: opacity .1s; }
}

/*
 * A table whose cells are the form — the printers list.
 *
 * The boxes have to sit DOWN into the row rather than each setting its own
 * height: at the admin's default 48px a row of six of them is an inch and a
 * half tall, and four printers fill a laptop screen.
 */
.printers-table input,
.printers-table select {
    width: 100%;
    min-width: 90px;
    min-height: 0;
    padding: 7px 10px;
    font-size: .92rem;
}

/* The address and its port, one under the other in a single cell: they are one
   fact about the machine, and 9100 is the port on every thermal printer ever
   sold — it is here to be seen, not typed. */
.printers-address {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 150px;
}

.printers-address input:last-child {
    max-width: 88px;
    color: var(--ink-2);
}

.printers-table td.num input { max-width: 72px; text-align: right; }

/* Delete is the one destructive thing on the page and says so — the only
   colour in a back office that is otherwise black and white. */
.btn-link.is-bad { color: var(--bad); }

.empty-search {
    margin: 14px 2px 2px;
    color: var(--ink-2);
    font-size: .9rem;
}

/*
 * The rules that govern a screen, said above the table they govern.
 *
 * Quiet and small: they are read once by somebody setting the shop up and
 * skipped by everybody else every day after that, so they must not look like a
 * warning.
 */
.hint-list {
    margin: -4px 0 16px;
    padding-left: 18px;
    color: var(--ink-2);
    font-size: .86rem;
    line-height: 1.6;
}

/* ============================================================= reports */

/*
 * The report workbench: the panel on the left, the report on the right.
 *
 * Asking for a range is not a one-off errand. "How did last week go" is
 * followed by "and the week before that", and a form on a page of its own turns
 * that into a trip through a menu every single time.
 */
.report-screen {
    display: grid;
    grid-template-columns: minmax(230px, 280px) minmax(0, 1fr);
    gap: 16px;
    align-items: start;
}

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

.report-panel {
    position: sticky;
    /* Under the header, so scrolling a long report leaves the dates in reach. */
    top: calc(var(--header-h) + 12px);
    padding: 14px;
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    background: var(--bg);
}

@media (max-width: 980px) {
    /* The header wraps to several rows on phones/tablets. A sticky filter
       panel then sits over the report instead of above it. */
    .report-panel { position: static; }
}

/* Daily / Monthly: two ways of asking the same question. */
.report-modes {
    display: flex;
    gap: 4px;
    padding: 3px;
    margin-bottom: 14px;
    border-radius: 999px;
    background: var(--soft);
}

.report-mode {
    flex: 1;
    padding: 7px 10px;
    border: 0;
    border-radius: 999px;
    background: none;
    color: var(--ink-2);
    font: inherit;
    font-size: .88rem;
    font-weight: 600;
    cursor: pointer;
}

.report-mode.is-on { background: var(--bg); color: var(--ink); box-shadow: var(--shadow-1); }

.report-field { margin-bottom: 14px; }

.report-field > label {
    display: block;
    margin-bottom: 5px;
    color: var(--ink-2);
    font-size: .82rem;
    font-weight: 600;
}

/*
 * A date with an arrow either side.
 *
 * The commonest thing anybody does to a date on this screen is move it by one,
 * and doing that through a date picker is three taps.
 */
.report-stepper {
    display: flex;
    align-items: stretch;
    gap: 6px;
}

.report-stepper input {
    flex: 1;
    min-width: 0;
    min-height: 0;
    padding: 8px 10px;
    text-align: center;
    font-size: .92rem;
}

.report-stepper button {
    flex: none;
    width: 34px;
    border: 1px solid var(--line-strong);
    border-radius: var(--r);
    background: var(--bg);
    color: var(--ink);
    font-size: 1.1rem;
    line-height: 1;
    cursor: pointer;
}

.report-stepper button:hover { background: var(--soft); }

.report-quick {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
}

.report-quick button {
    padding: 6px 10px;
    border: 0;
    border-radius: 999px;
    background: var(--soft);
    color: var(--ink);
    font: inherit;
    font-size: .82rem;
    cursor: pointer;
}

.report-quick button:hover { background: var(--line); }

.report-hint {
    margin: 0 0 10px;
    color: var(--ink-2);
    font-size: .8rem;
    line-height: 1.5;
}

/* At the foot of the panel, where the thumb is — as the shop's POS draws it. */
.report-generate { width: 100%; margin-top: 4px; }


/*
 * Step one: which of the eight.
 *
 * One name to a line, straight down, the way the shop's POS lists them — a
 * column of names is read in one movement of the eye, where a grid of boxes is
 * read in eight. Hairlines between them rather than a border round each: what
 * is wanted here is the list, not eight things that each look clickable.
 */
.report-choices {
    max-width: 520px;
    border: 1px solid var(--line-strong);
    border-radius: var(--r-lg);
    overflow: hidden;
}

.report-choice {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 14px 16px;
    border-top: 1px solid var(--line);
    background: var(--bg);
    color: var(--ink);
    font-weight: 500;
    text-decoration: none;
}

.report-choice:first-child { border-top: 0; }

.report-choice:hover { background: var(--bg-2); }

.report-choice svg { flex: none; color: var(--ink-3, var(--ink-2)); }

/* Which report the panel is about, with the way back to change it. */
.report-chosen {
    margin: 0 0 12px;
    padding-bottom: 10px;
    border-bottom: 1px solid var(--line);
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 8px;
}

.report-chosen a { font-size: .82rem; }

.report-section + .report-section { margin-top: 14px; }

.report-range { margin: 0 0 14px; font-weight: 600; }

.report-range .cell-sub { font-weight: 400; }

/* What the report contains, on the form: a name and a line about it. */
.report-line { margin: 0 0 8px; }

.report-line small {
    display: block;
    color: var(--ink-2);
    font-size: .84rem;
}

.hint-list-lead { margin: -6px 0 14px; color: var(--ink-2); font-size: .9rem; }

/* "Email it" opens a row of fields under the buttons rather than a dialog: it
   is one address and one format, which is less than a dialog is for. */
.report-email > summary { list-style: none; cursor: pointer; }

.report-email > summary::-webkit-details-marker { display: none; }

.report-email form {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    margin-top: 10px;
    padding: 10px;
    border: 1px solid var(--line);
    border-radius: var(--r);
    background: var(--soft);
}

.report-email label { font-size: .85rem; color: var(--ink-2); }

.report-email input,
.report-email select {
    width: auto;
    min-height: 0;
    padding: 8px 10px;
    font-size: .9rem;
}

.report-title { margin: 0 0 2px; font-size: 1.05rem; }

.report-meta {
    margin: 0 0 12px;
    color: var(--ink-2);
    font-size: .82rem;
}

.report-meta span { margin-right: 14px; }

.report-note {
    margin: 0 0 12px;
    padding: 8px 10px;
    border-radius: var(--r);
    background: var(--soft);
    color: var(--ink-2);
    font-size: .84rem;
}

/* A table of figures reads tighter than a table of rows to act on. */
.report-table { font-size: .86rem; }

.report-table td { padding: 6px 10px; }

/* A day or a category, across the width of the table. */
tr.report-group td {
    padding-top: 14px;
    font-weight: 700;
    background: var(--bg);
}

tr.report-subtotal td { color: var(--ink-2); font-style: italic; }

tr.report-total td {
    border-top: 1px solid var(--line-strong);
    font-weight: 700;
}

.report-empty { text-align: center; padding: 40px 20px; color: var(--ink-2); }

.report-empty h2 { margin: 0 0 6px; }

/* ============================================================== prose */

/*
 * A page that is a document: the terms, the refund policy, the privacy policy.
 *
 * Nothing on this site is otherwise a wall of text, so the rules live here
 * rather than on the elements themselves — a dish description is not a
 * paragraph in a contract and should not inherit a contract's measure.
 *
 * The measure is wide but not unlimited. 68ch is the classic figure for a book
 * and it looked mean here — a column of text down the middle of a 1700px
 * window with a hand's width of white either side, on a page that is plainly a
 * document rather than a page of a novel. 58rem is about a hundred characters:
 * wider than comfortable reading, narrow enough that the eye still finds the
 * start of the next line.
 */
.prose {
    max-width: 58rem;
    margin-inline: auto;
    padding-block: 8px 24px;
    line-height: 1.7;
}

.prose h1 {
    margin: 0 0 6px;
    font-size: 1.7rem;
    letter-spacing: -.01em;
}

.prose h2 {
    margin: 32px 0 8px;
    font-size: 1.12rem;
    font-weight: 600;
}

.prose h3 {
    margin: 22px 0 6px;
    font-size: 1rem;
    font-weight: 600;
}

.prose p, .prose ul, .prose ol { margin: 0 0 14px; }

.prose ul, .prose ol { padding-left: 22px; }

.prose li { margin-bottom: 6px; }

.prose strong { font-weight: 600; }

/* The rule between the shop's address block and the document proper. */
.prose hr {
    margin: 22px 0;
    border: 0;
    border-top: 1px solid var(--line);
}

.prose a { color: var(--brand-ink); }

/* The table of retention periods in the privacy policy. */
.prose table {
    width: 100%;
    margin: 0 0 16px;
    border-collapse: collapse;
    font-size: .94rem;
}

.prose th, .prose td {
    padding: 8px 10px;
    border-bottom: 1px solid var(--line);
    text-align: left;
    vertical-align: top;
}

/* ================================================================ home */

/*
 * Which stretch of time the page is about.
 *
 * A grey pill with a caret, the shape the reference uses. It was a row of five
 * pills for a day — which said all five answers at once, which is more than a
 * page needs to say about a setting somebody changes now and then.
 */
.range-form {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 10px;
}

/* The browser draws its own caret — no appearance:none here, so the control
   stays the one the operating system knows how to open. */
.range-form select {
    width: auto;
    min-height: 0;
    padding: 8px 10px 8px 14px;
    border: 0;
    border-radius: 999px;
    background: var(--soft);
    color: var(--ink);
    font-size: .86rem;
    font-weight: 500;
}

.range-form select:hover { background: var(--line); }

.range-dates {
    margin: 0 0 16px;
    color: var(--ink-2);
    font-size: .85rem;
}

/* What the percentages are measured against, said once under the row rather
   than three times inside it. */
.metric-note {
    margin: -12px 0 20px;
    color: var(--ink-3);
    font-size: .8rem;
}

/*
 * The one line on this page somebody can act on: orders on the pass right now.
 *
 * A whole card and a link, above the week's figures — a number that can be
 * acted on outranks a number that can only be read, and on a busy afternoon
 * this is the only thing here that matters.
 */
.live-strip {
    display: flex;
    align-items: center;
    gap: 14px;
    margin-bottom: 20px;
    padding: 14px 18px;
    color: inherit;
    text-decoration: none;
}

.live-strip:hover { border-color: var(--brand); }

/* Alive, not decorative: it says these orders are happening while you read. */
.live-dot {
    flex: none;
    width: 10px;
    height: 10px;
    border-radius: 999px;
    background: var(--ok);
    box-shadow: 0 0 0 4px color-mix(in srgb, var(--ok) 18%, transparent);
}

.live-text { display: grid; gap: 2px; }

.live-sub { color: var(--ink-2); font-size: .85rem; }

.live-go {
    margin-left: auto;
    flex: none;
    color: var(--brand-ink);
    font-size: .88rem;
    font-weight: 600;
}

/* .home-grid, .home-table and .empty-note were here — the chart shared the row
   with a best-sellers table until the shop asked for the width back. The chart
   is one card across the page now and needs no rules of its own. */

/* The same figures as a table, for reading rather than looking — and because a
   chart cannot be copied into a message to an accountant. */
.chart-table { margin-top: 26px; }

.chart-table summary {
    cursor: pointer;
    color: var(--ink-2);
    font-size: .88rem;
    padding: 4px 0;
}

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

/* =============================================================== footer */

/*
 * One line on a band of the shop's own colour.
 *
 * A band and not a hairline rule, because this is now the only thing down here:
 * a single grey sentence under a white page reads as something left over, while
 * a coloured bar reads as the end of the page. It is also the second and last
 * place the brand colour appears, which is what stops the footer looking like
 * part of somebody else's site.
 */
/*
 * Pinned to the bottom of the window and kept there, scrolled or not. See the
 * note by --footer-h at the top of this file for what it costs and how to undo
 * it.
 *
 * z-index 45 puts it over the page and UNDER the cart bar at 40 — no: over it
 * would be wrong, so the cart bar is lifted to 50 below and sits on top of the
 * band's edge rather than behind it. The header is 30 and scrolls under nothing.
 *
 * The safe-area padding is the iPhone's home indicator: without it the credit
 * line sits under the little black bar on every recent iPhone.
 */
.site-footer {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 45;
    /*
     * min-height, not height. It was a fixed height, so on a narrow phone the
     * links wrapped to a second line and the credit under them was pushed
     * straight off the bottom of the screen — the band could not grow, so the
     * content left it. Now the band grows and footer.js tells everything else
     * how tall it ended up.
     */
    min-height: calc(var(--footer-h) + env(safe-area-inset-bottom, 0px));
    padding-block: 6px;
    padding-bottom: calc(6px + env(safe-area-inset-bottom, 0px));
    display: grid;
    place-items: center;
    /*
     * The shop's blue, as it always was.
     *
     * It was briefly white, on a reading of slide 163 that turned out to be
     * wrong: what the shop wanted off this band was the three policy links, not
     * the colour. They are up in the page now (see .page-legal) and the band
     * carries the credit alone.
     */
    background: var(--brand);
    color: #fff;
    font-size: .92rem;
    text-align: center;
}

.site-footer .sep { opacity: .45; margin: 0 3px; }

/*
 * One centred line: the credit, and nothing beside it.
 *
 * It was three columns while the policy links lived down here, and the empty
 * first column was what kept the credit in the middle of the BAND rather than
 * in the middle of whatever the links left over. With the links gone there is
 * one thing to centre and no column arithmetic to do it.
 */
.site-footer .wrap {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    justify-items: center;
}

/*
 * Terms, refunds, privacy — at the foot of the page, not in the band.
 *
 * Small, grey and centred: this is the paperwork, and it should read as the
 * small print rather than as somewhere to go. Underlined all the same, because
 * three grey words on white with no underline are three words nobody knows are
 * links, and a policy nobody can find is a policy that is not published.
 */
.page-legal {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 16px;
    font-size: .78rem;

    /*
     * Pushed to the foot of the page rather than following the content.
     *
     * auto takes every pixel the content did not, so on a long menu this sits
     * under the last dish and on an empty cart it sits just above the band
     * instead of floating in the middle of the window. The 28px is the least
     * it keeps from whatever is above it when there is nothing spare.
     */
    margin-top: auto;
    padding-top: 28px;
}

.page-legal a {
    color: var(--ink-3);
    font-weight: 400;
    text-decoration: underline;
    text-underline-offset: 3px;
    text-decoration-thickness: 1px;
    text-decoration-color: var(--line-strong);
}

.page-legal a:hover,
.page-legal a:focus-visible {
    color: var(--ink);
    text-decoration-color: currentColor;
}

/*
 * The one link left in the band is the maker's name in the credit.
 *
 * Underlined from the start rather than on hover: on a single line of footer
 * there is nothing else for the eye to compare it against, so a link that only
 * announces itself under the pointer is a link nobody on a phone ever finds.
 */
.site-footer a {
    color: #fff;
    font-weight: 500;
    text-decoration: underline;
    text-underline-offset: 3px;
    text-decoration-thickness: 1px;
    text-decoration-color: rgba(255, 255, 255, .45);
}

.site-footer a:hover,
.site-footer a:focus-visible { text-decoration-color: #fff; }

/* "Showing 1–10 of 312 orders" — above the pager, quiet. On a page of ten out
   of everything the shop has taken, the count is the only thing that says
   whether a filter did anything. */
.table-count {
    margin: 14px 2px 0;
    color: var(--ink-2);
    font-size: .85rem;
}

.pagination {
    display: flex;
    gap: 4px;
    flex-wrap: wrap;
    list-style: none;
    padding: 0;
    margin: 16px 0 0;
    font-size: .88rem;
}

.pagination a, .pagination span {
    display: block;
    min-width: 38px;
    text-align: center;
    padding: 7px 10px;
    border: 1px solid var(--line-strong);
    border-radius: 10px;
    text-decoration: none;
}

.pagination .active span { background: var(--brand); color: #fff; border-color: var(--brand); }
.pagination .disabled span { color: var(--ink-3); }

/* ========================================================= pairing code */

/*
 * The six digits that set a tablet up.
 *
 * Read off this screen and typed into a machine held in the other hand, so it
 * is set large, spaced into two groups of three, and in a face where 0 and O
 * cannot be confused. Nothing else on the admin is this size — that is the
 * point, it has to be findable across a room.
 */
.pair-card {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    flex-wrap: wrap;
    border-color: var(--brand);
    box-shadow: inset 0 0 0 1px var(--brand);
}

.pair-code {
    font-family: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace;
    font-size: 2.4rem;
    font-weight: 500;
    letter-spacing: .08em;
    line-height: 1.2;
    margin: 2px 0 4px;
    color: var(--brand);
}

.pair-form {
    display: flex;
    align-items: flex-end;
    gap: 12px;
    flex-wrap: wrap;
}

.pair-form .field { flex: 1 1 220px; }

/* ================================================================= toast */

.toast {
    position: fixed;
    left: 16px;
    right: 16px;
    /*
     * Under the header, not above the cart bar.
     *
     * It used to sit at the bottom, clear of the footer band and the cart bar —
     * which meant three pills stacked in the same corner on the menu page, and
     * the message landing wherever the other two left room. The top of the
     * screen is empty, it is where the shop's reference puts it, and nothing
     * the thumb is doing is up there.
     */
    top: calc(var(--header-h) + 8px);
    z-index: 55;
    display: flex;
    align-items: center;
    gap: 11px;
    width: fit-content;
    max-width: min(460px, calc(100% - 32px));
    margin-inline: auto;
    background: rgba(16, 20, 26, .95);
    color: #fff;
    border-radius: 999px;
    padding: 9px 20px 9px 9px;
    font-size: .88rem;
    font-weight: 500;
    box-shadow: var(--shadow-2);
    animation: toast-in .22s cubic-bezier(.2, .9, .3, 1.3);
}

/* Two seconds is not long enough to hunt for, so the way out is beside the
   words rather than somewhere the eye has to travel. */
.toast-close {
    display: grid;
    place-items: center;
    width: 26px;
    height: 26px;
    flex: none;
    margin-left: 2px;
    padding: 0;
    border: 0;
    border-radius: 999px;
    background: rgba(255, 255, 255, .14);
    color: inherit;
    cursor: pointer;
}

.toast-close:hover { background: rgba(255, 255, 255, .26); }
.toast-close .ico { width: 15px; height: 15px; vertical-align: 0; }

/*
 * The tick rides in its own green disc rather than sitting in the text.
 *
 * A message that reads "Added Phở bò tái" is a sentence; a message with a mark
 * beside it is an answer, and it is read without being read — which is the whole
 * job of something on screen for two seconds.
 */
.toast-mark {
    display: grid;
    place-items: center;
    width: 28px;
    height: 28px;
    flex: none;
    border-radius: 999px;
    background: #2fa36b;
    color: #fff;
}

.toast-mark .ico { width: 16px; height: 16px; vertical-align: 0; }

/* One mark or the other. Both are in the page; the stylesheet chooses. */
.toast .toast-no,
.toast.is-error .toast-yes { display: none; }
.toast.is-error .toast-no { display: block; }

.toast-text { min-width: 0; }

.toast.is-error { background: var(--bad); }
.toast.is-error .toast-mark { background: rgba(255, 255, 255, .18); }

/* Down from the header, since that is where it now lives. */
@keyframes toast-in {
    from { opacity: 0; transform: translateY(-10px) scale(.96); }
    to { opacity: 1; transform: none; }
}

/* ================================================ into the cart it goes */

/*
 * The copy of the dish photograph that flies to the cart button.
 *
 * Drawn on top of everything and taken out again the moment it lands. It must
 * never catch a tap on the way past — somebody adding a second dish while the
 * first is still in the air would hit the picture instead of the button.
 */
.fly {
    position: fixed;
    z-index: 60;
    margin: 0;
    /* The same grid the card's own thumbnail uses, so a dish with no photograph
       flies as its faint plate drawing rather than as a stray icon. */
    display: grid;
    place-items: center;
    background: var(--soft);
    border-radius: 14px;
    overflow: hidden;
    pointer-events: none;
    box-shadow: var(--shadow-2);
    will-change: transform, opacity;
}

.fly img { width: 100%; height: 100%; object-fit: cover; }

/* Anything drawn over the photograph — the "Featured" flag — goes with it and
   would only arrive as a scrap of text sailing across the screen. */
.fly .badge { display: none; }

/* The count takes the catch. */
.round-btn .count.is-bumped { animation: count-bump .42s cubic-bezier(.2, .9, .3, 1.4); }

@keyframes count-bump {
    0% { transform: scale(1); }
    40% { transform: scale(1.45); }
    100% { transform: scale(1); }
}

[hidden] { display: none !important; }

/*
|==============================================================================
| Wide screens — the same shape, more of it
|==============================================================================
|
| One column at every width: the categories across the top, then the food.
|
| A computer used to get a different layout altogether — categories standing
| down the left, the cart pinned to the right, and the dishes crammed into the
| strip between them. Two columns of food on a screen wide enough for five, so
| the photographs came out smaller on the big screen than on the phone. The two
| side columns have gone and the grid takes the page.
|
| The cart went with them, and is not missed: the header button carries the
| count, and the bar along the bottom carries the total. Both were already
| there for phones, and both are still true on a computer.
*/

@media (min-width: 768px) {
    .dishes { grid-template-columns: repeat(3, minmax(0, 1fr)); }

    /*
     * auto-fit, not a fixed three.
     *
     * Three columns was written when the choices were Pickup, Delivery and Dine
     * in. Dine in has gone and the payment question only ever has two answers,
     * so a fixed three left both cards squeezed into two thirds of the row with
     * an empty column beside them — which reads as a missing option rather than
     * a layout. auto-fit lets two answers take half each and three take a third.
     */
    .choices { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }

    /*
     * Two columns only where there is a second column to put in them.
     *
     * The cart and the staff order page have a .col-side; the two checkout
     * steps do not — they are one form and a button. Split unconditionally,
     * those two pages drew their form into 63% of the page with a column of
     * nothing beside it, while .page-head above went on centring its title
     * against the FULL width. The title therefore sat well to the right of the
     * form it belonged to, which is exactly the "off to the right and looks
     * like a mistake nobody can name" the .page-head rule was written to avoid.
     *
     * :has() decides it from the markup rather than from a class somebody has
     * to remember to add. A browser too old for :has() keeps one column, which
     * is the safe half of the choice.
     */
    .columns:has(> .col-side) { grid-template-columns: minmax(0, 1.7fr) minmax(320px, 1fr); align-items: start; }

    .columns > .col-side { position: sticky; top: calc(var(--header-h) + 16px); }

    /*
     * The header on a computer: name, then the search box filling the middle,
     * then the collection chip and the cart.
     *
     * The search box comes out from behind the magnifier and simply sits there
     * — a menu of seventy dishes is searched far more often than it is
     * scrolled, and hiding the box behind a button on a screen with room for it
     * is a tap charged for nothing. The magnifier goes with it.
     */
    /*
     * It fills whatever the logo and the cart leave, rather than stopping at
     * 620px with a gap after it — that is the shape the shop pointed at, and a
     * box that reaches across the bar reads as the main thing on it, which for
     * a menu of seventy dishes it is.
     */
    .searchbar {
        position: static;
        flex: 1;
        padding: 0;
        background: none;
        backdrop-filter: none;
    }

    .searchbar input[type=search] { height: 44px; min-height: 44px; }
    .searchbar .search-icon, .searchbar .search-clear { top: 22px; }

    .topbar .spacer { display: none; }
    .is-phone-only { display: none; }

    /*
     * Where the pickup block sits once there is room: after the search box.
     *
     * On a phone it is beside the badge because the search hides behind a
     * magnifier and the bar has nothing else in it. On a computer the search box
     * is the widest thing on the header and the main thing on the page — a menu
     * of seventy dishes is searched far more often than it is scrolled — so it
     * takes the middle, and the two facts about collecting sit beside the cart,
     * which is where the rest of "how am I getting this" lives.
     *
     * Done with `order` rather than by moving the markup: on a phone the reading
     * order is logo, address, search, cart, and that is the order somebody using
     * a screen reader should meet them in whatever the screen is doing.
     */
    .place-wrap { order: 1; }
    .topbar .wrap > .round-btn:last-child { order: 2; }

    /* It no longer has to give way to anything, so the address can be whole. */
    .place-text b { max-width: 260px; }

    /* Room for words beside the clock, which is the whole point of the chip:
       the answer is readable without opening anything. */
    .chip-text, .chip-arrow { display: block; }
    .chip { padding: 0 12px 0 9px; }

    /* Wide enough for the panel to hang off the chip and still fit, which reads
       as belonging to the button that opened it. */
    .chip-panel {
        position: absolute;
        top: calc(100% + 8px);
        left: auto;
        right: 0;
        width: 340px;
    }
}

@media (min-width: 1080px) {
    .dishes { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

@media (min-width: 1360px) {
    .dishes { grid-template-columns: repeat(5, minmax(0, 1fr)); }
}

@media (prefers-reduced-motion: reduce) {
    html { scroll-behavior: auto; }
    * { transition: none !important; }

    /* The flight is already skipped in the script; these are the two that are
       drawn by the stylesheet alone and would otherwise still move. */
    .toast,
    .round-btn .count.is-bumped { animation: none !important; }
}

/*
 * Dine in, or take away — the one dialog on the site with no way out but
 * answering it, so it is drawn plainly: two big cards, nothing to dismiss.
 */
.type-dialog {
    border: 0;
    padding: 0;
    background: transparent;
    max-width: 420px;
    width: calc(100% - 24px);
    border-radius: var(--r-lg);
}

.type-dialog::backdrop { background: rgba(12, 16, 22, .55); backdrop-filter: blur(2px); }

.type-dialog-body {
    background: var(--bg);
    border-radius: var(--r-lg);
    padding: 24px 20px;
    text-align: center;
}

.type-dialog-body h2 { margin: 0 0 18px; }

.type-options {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 10px;
}

.type-option {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    border: 1px solid var(--line-strong);
    border-radius: var(--r);
    padding: 20px 10px;
    background: var(--bg);
    cursor: pointer;
    font: inherit;
    color: inherit;
}

.type-option .ico { width: 30px; height: 30px; color: var(--brand); }
.type-option small { color: var(--ink-2); font-size: .78rem; }

.type-option:hover,
.type-option:focus-visible,
.type-option.is-current { border-color: var(--brand); background: var(--brand-soft); }

/* The answer already given, marked whether or not a hand is anywhere near
   it — the modal is reopened from the "Change" row with nothing pointing
   at either card otherwise, and a ring drawn only on whichever button
   happened to take focus first read as that one being the current answer
   when it was not. */
.type-option.is-current { box-shadow: inset 0 0 0 1px var(--brand); }
.type-option.is-current small { color: var(--brand-ink); }

@media (max-width: 380px) {
    .type-options { grid-template-columns: 1fr; }
}
