/* Little Kitchen — owner app, storefront and marketing page.
   Structure, type scale and component language are still the ones lifted from
   reference/after-hours.html; see reference/DESIGN.md. What changed for Little
   Kitchen is the VALUES in the token block below and nothing else: the chrome
   ramp moved off After Hours' 2am purple onto warm cocoa and clay, because the
   product is somebody's mother cooking at home, not a truck at closing time.

   Every colour resolves through a custom property. --accent is the only one a
   seller controls; it is set at runtime from shops.accent. The chrome ramp is
   app chrome and is never derived from the accent. */

/* ------------------------------------------------------------------ fonts */
/* Self-hosted. No third-party request at runtime. See fonts/OFL.txt. */

@font-face {
  font-family: 'Anton';
  font-style: normal; font-weight: 400; font-display: swap;
  src: url('/fonts/anton-400-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Anton';
  font-style: normal; font-weight: 400; font-display: swap;
  src: url('/fonts/anton-400-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'DM Mono';
  font-style: normal; font-weight: 400; font-display: swap;
  src: url('/fonts/dmmono-400-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'DM Mono';
  font-style: normal; font-weight: 400; font-display: swap;
  src: url('/fonts/dmmono-400-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'DM Mono';
  font-style: normal; font-weight: 500; font-display: swap;
  src: url('/fonts/dmmono-500-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'DM Mono';
  font-style: normal; font-weight: 500; font-display: swap;
  src: url('/fonts/dmmono-500-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'Inter';
  font-style: normal; font-weight: 400 700; font-display: swap;
  src: url('/fonts/inter-400-700-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: normal; font-weight: 400 700; font-display: swap;
  src: url('/fonts/inter-400-700-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ----------------------------------------------------------------- tokens */

:root {
  /* -- the one seller-controlled value. JS overwrites all three at runtime. -
     Brick: a fired clay red rather than a street sign. It is only the default;
     the seller picks, and the value round-trips through shops.accent.

     THESE THREE ARE NOT INDEPENDENT. --accent-ink and --accent-text are what
     applyAccent() computes from --accent, written out statically so the first
     paint is right before any JS runs. Change --accent here and these two must
     be recomputed with it, or the CTA paints unreadable until the script lands.

     At the previous default the pairing was #24120a ink at 5.68:1. Brick is
     dark enough that the algorithm flips the ink to cream instead - dark ink
     on brick is 2.95:1, which fails AA outright. */
  --accent: #a8402f;
  --accent-ink: #f7efe3;
  /* The accent as text on the dark surfaces. Identical to --accent for any
     accent that already reads there; lightened by JS for ones that do not, and
     checked against --surface, --panel and the 18% accent wash below, which is
     the lightest ground accent text ever sits on. */
  --accent-text: #d19a91;

  /* Everything accent-derived goes through color-mix, so a red, green or blue
     accent produces its own glow and washes without touching the palette. */
  --accent-glow: color-mix(in srgb, var(--accent) 30%, transparent);
  --accent-glow-soft: color-mix(in srgb, var(--accent) 18%, transparent);
  --accent-line: color-mix(in srgb, var(--accent) 45%, transparent);
  --accent-ring: color-mix(in srgb, var(--accent) 22%, transparent);

  /* -- chrome ramp: app chrome, never accent-derived ---------------------- */
  /* Cocoa and toasted clay, in the same five steps and the same spacing the
     purple ramp had, so every surface keeps the separation it was drawn with.
     --heroglow is the warm light over a stove rather than a nightclub bloom. */
  --backdrop: #0f0a07;
  --ink: #1b1310;
  --surface: #2a1e18;
  --panel: #35261e;
  --heroglow: #4d3524;
  --disc1: #503726;
  --disc2: #2f211a;

  --line: rgba(247, 239, 227, 0.10);
  --line2: rgba(247, 239, 227, 0.16);

  /* -- text -------------------------------------------------------------- */
  /* --mauve and --mauve2 keep their names from the purple ramp they were
     mixed for. They hold warm stone now; the names are the only thing here
     that did not move, and renaming them would touch every component. */
  --cream: #f7efe3;
  --mauve: #cdb9a4;
  --mauve2: #a08d7c;

  /* -- status ------------------------------------------------------------ */
  /* Off the kitchen shelf rather than off a neon sign: tomato, garden herb,
     basil. Each one still clears AA on --surface, --panel and --ink. */
  --coral: #f0604e;
  --lime: #b6d271;
  --fresh: #63c98a;

  /* -- receipt stock ----------------------------------------------------- */
  --paper: #f4e9d6;
  --paperink: #2b1f18;
  --paperline: #cdbfa6;

  /* -- type -------------------------------------------------------------- */
  --font-display: 'Anton', 'Arial Narrow', Impact, sans-serif;
  --font-mono: 'DM Mono', ui-monospace, 'SFMono-Regular', Menlo, monospace;
  --font-body: 'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif;

  --fs-hero: 40px;
  --fs-title: 26px;
  --fs-head: 22px;
  --fs-item: 17px;
  --fs-body: 15px;
  --fs-sm: 13px;
  --fs-desc: 12.5px;
  --fs-label: 11px;
  --fs-tag: 10px;

  /* THE ONE SIZE IN HERE THAT IS NOT A DESIGN DECISION.

     iOS zooms the page when a form control under 16px takes focus, and in
     an installed web app it does not zoom back out. On a 375pt phone that
     is 375 x 16/15 = 400pt of layout in a 375pt window: the cart column
     runs off the right edge and the page drags sideways, which is exactly
     the report. The base input rule has carried 16px and a comment about
     this for a long time; what went wrong is that individual controls
     overrode it with a smaller token and the reason did not travel with
     the number.

     So the floor has a name, and every control that sets its own size sets
     this one. Declared on :root only - the .tool ramp below re-scales the
     type for the seller app and must NOT re-scale this, because the phone
     does not care which side of the product a field is on.

     It is not fixed in the viewport meta: user-scalable=no would stop the
     zoom and take pinch-to-zoom away from everybody who needs it. */
  --fs-field: 16px;

  --track-label: 0.14em;
  --track-cap: 0.1em;
  --track-receipt: 0.06em;

  /* -- radii ------------------------------------------------------------- */
  --r-pill: 999px;

  /* ONE DEMO FRAME SIZE ACROSS THE MARKETING SITE. The storefront demo grows
     with its content and uses this as its floor; Back of House is a column
     with a scrolling middle and cannot grow, so it takes it as its height.
     Same number either way, named once so they cannot drift apart. */
  --demo-frame-h: 520px;
  --r-sheet: 26px;
  --r-card: 18px;
  --r-tile: 16px;
  --r-control: 14px;
  --r-input: 12px;

  /* -- elevation --------------------------------------------------------- */
  --shadow-frame: 0 30px 80px rgba(0, 0, 0, 0.6);
  --shadow-float: 0 18px 40px rgba(0, 0, 0, 0.45);
  --shadow-cta: 0 10px 28px var(--accent-glow);
  --drop-ticket: drop-shadow(0 8px 20px rgba(0, 0, 0, 0.22));

  /* -- metrics ----------------------------------------------------------- */
  --tap: 52px;
  --pad: 18px;
  --ease: cubic-bezier(0.2, 0.8, 0.2, 1);
  /* CLAMPED, AND THE CLAMP IS THE FIX.
     34px is the home indicator inset in portrait on every iPhone that has one,
     X through the current ones, and this app is orientation: portrait in both
     manifests. So 34 is not a guess at a nice number, it is the largest inset
     that can exist here. Anything larger is not a safe area, it is the web view
     reporting a frame it is not in yet - which is what a customer sees as a
     bottom bar that launches with a deep empty band under it and settles a
     moment later.
     In steady state this is a no-op: every real value is at or under the clamp,
     so nothing on any device loses a pixel of the room it needs. It only ever
     refuses a value that could not have been true. One token, so the tab bar,
     the storefront's cart bar and every padding-bottom that reserves room for
     them are all fixed by the same line rather than three of them separately. */
  --safe-b: min(env(safe-area-inset-bottom, 0px), 34px);

  /* --bar-bg IS GONE, AND IT HAD TO GO RATHER THAN BE RETUNED.

     It was `color-mix(in srgb, var(--surface) 92%, transparent)`, declared
     here, and that is a custom property whose value references another custom
     property. Such a value is resolved ON THE ELEMENT THAT DECLARES IT - here
     the root - so `var(--surface)` was substituted against the ROOT ramp and
     the computed value inherited into .tool already frozen as the warm cocoa
     #2a1e18. The owner app re-declares --surface as #17171a and it made no
     difference: the bar carried the storefront's brown into a screen whose
     ground is #0e0e10 and whose cards are #17171a, which is where its third
     colour came from. It was never a chosen colour.

     The rule this leaves behind: a token defined up here must not be built
     out of another token that .tool re-declares. Both bottom bars now name
     the colour they want at the point of use, where the cascade can see which
     ramp they are standing in. */
  --safe-t: env(safe-area-inset-top, 0px);

  /* -- the compact control ------------------------------------------------
     THE ONE CONTROL EVERYTHING ELSE COPIES.

     The "I'm a customer / I'm a business" switch is the right size, weight and
     radius, so it stopped being a special case and became these four numbers.
     Every chip, filter pill, segmented tab, stepper button and small toggle in
     the product is built from them, on both sides: same sizes, same weights,
     same radii, warm palette on the brand side and neutral inside the tool.

     Small to look at. The 52px target is kept by extending it with a
     pseudo-element, never by inflating the visual, so a control can be 30px
     tall and still be honest under a thumb. */
  --ctl-h: 30px;         /* visible height */
  --ctl-pad: 12px;       /* horizontal padding */
  --ctl-fs: 11.5px;      /* type size */
  --ctl-fw: 600;         /* type weight */
  --ctl-r: 7px;          /* the corner on a segment */
  --ctl-r-out: 9px;      /* the corner on the tray one sits in */
  --ctl-gap: 6px;

  /* THE ONE VERTICAL RHYTHM. Every stack in the product is spaced by this and
     nothing else, so a screen cannot end up with three different gaps in it.

     It is not an arbitrary number. It is exactly the room a 30px control needs
     above and below itself to be a 52px target, which means a chip rail can sit
     in the stack with its target reaching precisely to its neighbours' edges -
     no overlap, no extra air, and the same gap whether the thing above is a
     card, a heading or another rail. */
  --stack: calc((var(--tap) - var(--ctl-h)) / 2);

  /* -- the bottom bar -----------------------------------------------------
     Full width, pinned, on the safe area. Its height is a token because every
     scroll container has to end exactly this far above the bottom, and a
     number that lives in two places is a number that drifts. */
  --barh: 53px;          /* the tab bar: 52px of button plus its hairline */
  --cartbarh: 76px;      /* the storefront's cart bar, which holds a button */
  --barclear: 14px;      /* the gap between the last content and the bar */
}

/* ------------------------------------------------------------------ base */

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

html, body {
  margin: 0;
  min-height: 100%;
  background: var(--backdrop);
  color: var(--cream);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;

  /* BOTH AXES, DELIBERATELY - AND THIS IS NOT WHAT STOPS THE BACK SWIPE.
     It was tried as the fix for the sideways swipe on iOS and it did not work,
     on a real device, after a deploy. iOS Safari's interactive back gesture is
     driven by session history, not by scroll, and no CSS property reaches it.
     What stopped it was store.js no longer pushing a history entry, so there is
     nothing behind the page to go back to; the reasoning is written out above
     pushView there. Do not come back here looking for the lever.
     What this does do is honest and worth keeping: it contains scroll chaining
     on both axes rather than declaring y and leaving x at `auto`, which is how
     it read before. Nothing at this level scrolls sideways anyway - the
     document is exactly as wide as the viewport - so `none` costs nothing and
     stops the next reader having to notice which axis was missing. */
  overscroll-behavior: none;
}

/* WHY EVERY BUTTON RESETS ITS PADDING.
 *
 * This is the whole of the off-centre X, and it was not the icon.
 *
 * WebKit's user-agent stylesheet gives a <button> `padding: 2px 6px 3px` -
 * one pixel more underneath than on top. Blink gives it `1px 6px`, which is
 * symmetric. Every icon button in this product sets an explicit width and
 * height, and `box-sizing: border-box` is global, so that UA padding is taken
 * out of the INSIDE of the box: a 34px button becomes a 34x29 content box
 * sitting 2px from the top and 3px from the bottom. `place-items: center`
 * then centres the glyph in the content box, which is a pixel above the
 * centre of the circle you can see.
 *
 * So it looked right on a desktop in Chrome and wrong on every phone, which
 * is exactly the report. One pixel on a 34px circle with a symmetric glyph in
 * it is visible, and it was visible on every icon button in the app at once:
 * the sheet close, the row edit and delete, the Back of House back arrow, the
 * preview back arrow, the steppers, the search clear.
 *
 * Nudging the glyph would have hidden it in WebKit and broken it in Blink.
 * Resetting the padding makes the content box the border box in both, so a
 * centred glyph is centred. Every button below that wants padding states it.
 */
button { font-family: inherit; padding: 0; }
p { margin: 0; }
[hidden] { display: none !important; }

/* The app frame: a centred 430px column on the purple backdrop. */
#app {
  width: 100%;
  max-width: 430px;
  min-height: 100dvh;
  margin: 0 auto;
  position: relative;
  background: var(--ink);
  box-shadow: var(--shadow-frame);
  padding-top: var(--safe-t);
  /* THE FRAME CONTAINS ITS CHILDREN'S MARGINS.
     Without this the side switch's 12px top margin collapses straight out
     through #app and body to the root, pushing the whole document down by 12px
     and making every screen 12px taller than the viewport: measured at 856
     against 844 on the sign in screen, which is why it scrolled with nothing
     on it. flow-root is a block formatting context and nothing else, so no
     layout inside the frame moves. */
  display: flow-root;
}

/* THE STATUS BAR STRIP.
 *
 * These pages declare `apple-mobile-web-app-status-bar-style: black-translucent`,
 * which is what lets the app fill the screen - and it means the web view runs
 * UNDER the clock and the battery, and the page is responsible for the inset.
 *
 * #app's padding-top handles that for anything in normal flow. It does not
 * handle the topbar, because `position: sticky; top: 0` sticks to the
 * SCROLLPORT, not to the padded content box: the moment the page scrolled, the
 * header parked itself at viewport y=0 and the kitchen name was printed across
 * the clock. Measured with the inset forced to a real notch's 59px: the header
 * box at 0 and its content starting at 14.
 *
 * So the sticky offset moves down by the inset, and this band fills what that
 * leaves - an opaque strip the page scrolls underneath, the way a native app
 * does it. Above the topbar and the tab bar, below sheets, Back of House and
 * the preview, all three of which are full-screen and carry their own inset.
 *
 * On a device with no inset the strip is 0px tall and nothing is drawn. */
#app::before {
  content: '';
  position: fixed;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  width: 100%;
  max-width: 430px;
  height: var(--safe-t);
  background: var(--ink);
  z-index: 50;
  pointer-events: none;
}

/* THE PAGE NEVER SCROLLS SIDEWAYS.

   A CLAMP, NOT A DIAGNOSIS. Installed to the home screen on a 375pt phone,
   the cart column renders about 400pt wide and the receipt and Checkout
   button run off the right edge. Three passes could not reproduce it -
   headless at seven widths, four device profiles, and real iOS 18.7 in the
   Simulator, all reporting the document exactly as wide as the viewport -
   so what is written here is not "we found it", it is "this cannot happen
   again whatever it was". Nothing in this product is ever meant to be read
   by dragging it left.

   CLIP, NOT HIDDEN, AND THAT IS THE WHOLE REASON IT IS SAFE HERE.

   CSS says a computed `visible` on one axis becomes `auto` when the other
   axis is anything but `visible` OR `clip`. So `overflow-x: hidden` on the
   page would quietly turn the whole document into a VERTICAL scroll
   container - the exact trap already documented on .sheet, where declaring
   one axis made the other a draggable one by accident. On iOS that
   container is what breaks `position: sticky`: the topbar would stop
   sticking to the viewport and the fixed tab bar would go with it.

   `clip` creates no scroll container at all. The vertical axis stays
   `visible`, the page scrolls the way it always has, and nothing sticky or
   fixed moves.

   WHAT IT DOES AND DOES NOT REACH. The root's overflow propagates to the
   viewport, which is what actually stops the drag. It clips the page's own
   overflow; `position: fixed` escapes an ancestor's clip unless that
   ancestor is its containing block, and neither html nor body is one for
   #app::before, .tabbar or .cartbar - all three of which centre themselves
   correctly anyway.

   NOT ON #app. The frame paints --shadow-frame outside its own box, and a
   clip there would cut the shadow off on a wide screen.

   Safari has had overflow-x: clip since 16. On anything older the
   declaration is dropped and nothing breaks - which is why the clamp below
   is the real fix and this is the net under it. */
html, body { overflow-x: clip; }

/* AND NOTHING IN THE COLUMN IS WIDER THAN THE COLUMN.

   The clip above stops the page being dragged; this stops there being
   anything to drag. Both matter: clipping alone would hide a too-wide
   receipt's right edge rather than fit it, and the seller's total is on
   that edge.

   max-width pins the box to the column. min-width: 0 is the half that does
   the work in a flex context - .tab is a column flex container, so its
   children's automatic minimum size is their content, and a child that
   cannot shrink is a child that pushes the column out. That is precisely
   what happened inside .sheet once, and the cart is the same shape.

   Stated on the components rather than on a view, because the same receipt
   is drawn on the cart, the checkout, the pay screen and the order status
   page, and a guard that only covered one of them would be a guard that
   moved the bug. */
.ticket,
.btn.block,
.tab > * { max-width: 100%; min-width: 0; }

/* -------------------------------------------------------------- type roles */

.display {
  font-family: var(--font-display);
  font-weight: 400;
  letter-spacing: 0.01em;
  text-transform: uppercase;
  line-height: 1.05;
  margin: 0;
}

.mono { font-family: var(--font-mono); }

/* The most repeated pattern in the reference — every section and field label. */
.label {
  font-family: var(--font-mono);
  font-size: var(--fs-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--mauve2);
  line-height: 1;
}

.label em { font-style: normal; letter-spacing: 0.04em; opacity: 0.65; }

.cap {
  font-family: var(--font-mono);
  font-size: var(--fs-tag);
  letter-spacing: var(--track-cap);
  color: var(--mauve2);
}

.muted { color: var(--mauve); font-size: var(--fs-sm); }

.scroll::-webkit-scrollbar { display: none; }
.scroll { -ms-overflow-style: none; scrollbar-width: none; }

/* ---------------------------------------------------------------- layout */

/* The centred views (sign in, setup) sit under the side switch now, so their
   full-height box has to come down by exactly the switch's height or the
   content they centre is pushed below the middle of the screen. */
.view { min-height: 100dvh; }

#sideswitch ~ .view { min-height: calc(100dvh - 58px); }

.view.center {
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 28px var(--pad) calc(28px + var(--safe-b));
  /* The purple bloom, exactly as the reference hero: heroglow out of the top
     edge into ink by 62%. */
  background: radial-gradient(120% 80% at 50% -10%, var(--heroglow) 0%, var(--ink) 62%);
}

.stack { display: flex; flex-direction: column; gap: 14px; width: 100%; }
.row { display: flex; gap: 12px; }
.grow { flex: 1 1 auto; min-width: 0; }
.narrow { flex: 0 0 92px; }

.brand { text-align: center; display: flex; flex-direction: column; align-items: center; gap: 8px; }

.brand-mark {
  width: 64px; height: 64px;
  display: grid; place-items: center;
}

.brand-mark img { width: 100%; height: 100%; object-fit: contain; display: block; }
.brand h1 { font-size: var(--fs-hero); }

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  padding: var(--pad);
}

/* ---------------------------------------------------------------- topbar */

/* top is the safe-area inset, not 0: a sticky element parks against the
   scrollport, so at 0 this slides under the status bar. #app::before fills the
   strip it leaves. */
.topbar {
  position: sticky; top: var(--safe-t); z-index: 20;
  display: flex; align-items: center; gap: 12px;
  padding: 14px var(--pad);
  background: var(--ink);
  border-bottom: 1px solid var(--line);
}

.topbar-id { flex: 1; min-width: 0; }
/* h1 AND h2, because this slot holds both. Browse puts its h1 here - it is
   that page's only heading of any level - while the storefront's cart,
   checkout and payment views put an h2 here, under the kitchen name that is
   already the h1 further up their document. Same bar, same type, two levels,
   and the difference is the outline rather than the look. */
.topbar-id h1,
.topbar-id h2 { font-size: var(--fs-head); }
.topbar-id h1,
.topbar-id h2,
.topbar-id .cap { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.topbar-id .cap { margin-top: 3px; }

/* Open/closed pill reads status, so it uses status colours, not the accent.
   Sized off the switch like everything else; the target is extended below. */
.pill {
  position: relative;
  flex: none;
  display: inline-flex; align-items: center; gap: 6px;
  min-height: var(--ctl-h); padding: 0 var(--ctl-pad);
  border-radius: var(--ctl-r);
  border: 1px solid var(--line);
  background: var(--panel);
  color: var(--mauve);
  font-family: var(--font-mono);
  font-size: var(--ctl-fs);
  letter-spacing: var(--track-cap);
  text-transform: uppercase;
  cursor: pointer;
}

.pill .dot { width: 7px; height: 7px; border-radius: var(--r-pill); background: var(--mauve2); }

.pill[aria-pressed="true"] {
  color: var(--fresh);
  border-color: color-mix(in srgb, var(--fresh) 45%, transparent);
  background: color-mix(in srgb, var(--fresh) 12%, var(--panel));
}

.pill[aria-pressed="true"] .dot { background: var(--fresh); box-shadow: 0 0 8px var(--fresh); }

/* ------------------------------------------------------------------ tabs */

/* Every scroll container ends exactly one bar-height plus the safe area above
   the bottom, so nothing is ever stranded under the bar. One token, so the
   two numbers cannot drift apart. */
.tab {
  padding: var(--pad);
  padding-bottom: calc(var(--barh) + var(--safe-b) + var(--barclear));
  display: flex; flex-direction: column; gap: var(--stack);
}

.listhead { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.listhead h3 { font-size: var(--fs-head); }

/* THE CHECKLIST HEADING, WHICH IS ALSO ITS SWITCH.

   A button that has to look exactly like the .listhead it replaced: no chrome,
   no background, the same baseline row. Only the chevron says it does
   anything, and the whole row is the target so it is thumbable rather than a
   14px icon hunt. */
.setup-head {
  appearance: none;
  -webkit-appearance: none;
  background: none;
  border: 0;
  padding: 0;
  width: 100%;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  /* Baseline alignment is right for the two words; the chevron is a box and
     wants centring, so it gets its own rule below. */
  align-items: baseline;
}

.setup-chev {
  align-self: center;
  flex: 0 0 auto;
  margin-left: -4px;
  transition: transform 0.18s ease;
}

/* Right when folded, down when open. The -90deg base lives on .row-chev and
   the open state uses the same aria-expanded hook .row-solo already uses, so
   there is one convention for this in the stylesheet rather than two. */
.setup-head[aria-expanded="true"] .setup-chev { transform: rotate(0deg); }

/* Folded, the bar carries the whole message, so it keeps its spacing rather
   than collapsing against the heading. */
.setup-card.folded .onboard-bar { margin-bottom: 2px; }

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

/* A BAR, THE WAY iOS DRAWS ONE.

   It used to float: an inset pill with rounded corners, ten pixels off the
   bottom, sitting over the content. That is what a floating action cluster
   looks like, not what a tab bar looks like, and it had the failure a floating
   bar always has - content is visible in the gutters either side of it and
   through the gap under it, so a chip rail scrolling past looked like it was
   sliding out from behind a broken edge.

   So: full width of the app column, pinned to the bottom, sitting ON the safe
   area rather than above it, translucent with a blur, one hairline along the
   top, and content scrolling underneath it. Every scroll container in the
   product ends --barh + the safe area above the bottom, so nothing is ever
   stranded beneath it.

   Every button is a real 52px target; the bar is 53px tall plus the bottom
   safe-area inset, which is PADDING INSIDE THE BAR and not space below it:
   the border box reaches bottom: 0 of the viewport, the buttons are pushed up
   off the home indicator by the padding, and the background paints through it.

   THE MATERIAL IS THE PAGE GROUND, AND THE HAIRLINE IS THE BAR.

   HISTORY, BECAUSE THIS HAS BEEN BOTH WAYS AND THE EARLIER NOTE IS WORTH
   KEEPING. It was once `color-mix(in srgb, var(--ink) 92%, transparent)`,
   which composited over #app - also var(--ink) - to exactly the page ground
   on every channel. The bar was reported as "stopping short of the bottom":
   with no colour of its own, the inset's worth of bar below the labels read
   as bare page with the home indicator sitting on it. The answer then was to
   move the material up a step of the ramp, to --surface.

   That is what produced the colour this replaces, though not in the way
   anybody intended - see the note where --bar-bg used to be declared. The
   step never reached the owner app at all; what reached it was the
   storefront's brown, frozen into an inherited token.

   SO IT IS THE GROUND AGAIN, DELIBERATELY THIS TIME, and the trade is
   accepted rather than overlooked: the bar has no plane of its own, the band
   over the home indicator reads as page rather than as bar, and the hairline
   along the top is the only thing that says where the bar begins. What is
   bought for that is a bar with no colour in it but the accent on the tab she
   is on - two values on this screen instead of three, and each side of the
   product taking its own ground automatically, because --ink is read here at
   the point of use rather than inherited pre-resolved.

   OPAQUE, AND THE BLUR IS GONE WITH THE TRANSLUCENCY. A blur exists to keep a
   label legible over whatever is scrolling underneath; an opaque bar does
   that outright. Translucency over a ground the same colour as the bar buys
   nothing and muddies the labels when a receipt passes under them. */
/* THE BAR'S FOOTPRINT, PAINTED BY THE SHELL.
 *
 * #app ships with the hidden attribute and is only unhidden once the boot
 * fetches resolve, so for the first second or two of a cold load there is no
 * tab bar in the document at all and the bottom of the screen is the body's
 * backdrop: black, right down to the home indicator. That is the band. It is
 * not the bar's padding - the bar's own background paints through its padding
 * to the screen edge, measured pixel by pixel - it is the page showing through
 * because the bar does not exist yet.
 *
 * So the shell paints the bar's footprint itself, from the first frame, in the
 * bar's own colour. When the real bar arrives it lands exactly on top of it and
 * nothing moves. The height is the bar's own two terms, --barh and the inset,
 * not a number picked to look right.
 *
 * Only this shell. The storefront has no tab bar and must not get a strip. */
html.has-tabbar {
  /* ONE BAND, BECAUSE THE BAR IS NOW ONE COLOUR. This was three layers - a
     translucent surface over the frame's ink over the backdrop - reproducing
     a composite rather than guessing at it. The bar is opaque --ink now, so
     the footprint is opaque --ink too and the stack collapses to the band and
     the ground behind it. Still the same two terms for the height. */
  background:
    linear-gradient(var(--ink), var(--ink))
      bottom / 100% calc(var(--barh) + var(--safe-b)) no-repeat,
    var(--backdrop);
}

.tabbar {
  position: fixed; left: 50%; transform: translateX(-50%);
  bottom: 0;
  z-index: 30;
  width: 100%; max-width: 430px;
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
  padding: 0 0 var(--safe-b);
  /* Read here, not inherited. --ink is the owner app's #0e0e10 inside .tool
     and the customer's #1b1310 outside it, which is the "one bar, two
     palettes" rule below finally working: the bar takes the ground of
     whichever side it is standing on, because the cascade resolves it at the
     point of use. */
  background: var(--ink);
  border: 0;
  /* The only thing that says where the bar starts. --line2 rather than
     --line: at 0.16 against 0.09 it is the stronger of the two hairlines, and
     with the plane gone it is carrying the whole separation on its own. */
  border-top: 1px solid var(--line2);
  border-radius: 0;
  box-shadow: none;
}

/* ONE BAR, TWO PALETTES. Everything structural about the bottom bar is
   declared here and nowhere else - the same height, the same icon, the same
   label, the same weight on both sides of the product - and the only thing
   that differs between the owner app and the customer app is the value of
   --ink and --line underneath it, which .tool re-declares for its own ramp.
   The customer's bar used to be half a step bigger than the owner's for no
   reason anybody could name; now they are the same control. */
.tabbtn {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 2px;
  min-height: 52px; padding: 6px 2px;
  border: 0; background: none;
  border-radius: 0;
  color: var(--mauve2);
  font-family: var(--font-body);
  font-size: 9.5px;
  font-weight: 500;
  letter-spacing: 0.01em;
  cursor: pointer;
}

.tabbtn svg {
  width: 18px; height: 18px;
  fill: none; stroke: currentColor; stroke-width: 1.9;
  stroke-linecap: round; stroke-linejoin: round;
}

.tabbtn.on { color: var(--accent-text); }

/* SIX ACROSS, AND THE TIGHTEST LABEL IS SPANISH ON THE SMALLEST PHONE.

   Share joining the bar took each cell from a fifth of 430px to a sixth, and
   the longest label in the product is "Compartir" - longer than Pedidos,
   Dinero or Cocina, and nearly twice "Share". Measured in headless WebKit at
   320px, the narrowest width anybody still ships: the label is 46.5px in a
   49.3px cell, and 48.2px if the woff2 never arrives and Inter falls back to
   the system sans. It fits. It fits by 1.1px in the case that matters, which
   is not a margin, it is a coincidence - a slightly wider fallback on some
   Android build eats it.

   So two guards, and neither costs anything where there is room. `nowrap`
   because every label in this bar is one word: without it an overflowing word
   does not wrap, it bleeds sideways under its neighbours, and the failure is
   silent. Then a hair off the type below 350px, which buys the 320px case
   real headroom instead of a rounding error. Both bars use this rule - the
   cook's six and the customer's four - and the customer's is nowhere near
   tight, so nothing there moves. */
.tabbtn span { white-space: nowrap; }

@media (max-width: 349px) {
  .tabbtn { font-size: 9px; letter-spacing: 0; }
}

/* ------------------------------------------------------- category rail */

/* THE RAIL IS 52px TALL SO THE CHIP DOES NOT HAVE TO BE.
 *
 * A horizontal chip rail is an overflow:auto box, and CSS turns the other axis
 * into `auto` with it, which means it CLIPS to its padding box. A target
 * extended past that box with a pseudo-element is simply cut off - the first
 * attempt at this put 11px of negative margin on the chip and got a 34px
 * target back, because the flex line collapsed to 8px and took the clip box
 * with it.
 *
 * So the room is bought where the clip actually is: the rail carries half the
 * difference as real padding above and below, the chip stays 30px, and the
 * chip's 52px pseudo-element fills the rail exactly. The rail is the height a
 * 52px target needs. The chip is the size it should look. Neither is a
 * compromise, and that is the rule: extend the target, do not inflate the
 * visual. */
/* A RAIL SCROLLS ONE WAY, AND IT HAS TO SAY SO OUT LOUD.
 *
 * `overflow-x: auto` on its own does not mean "scroll horizontally". CSS says a
 * computed `visible` on one axis becomes `auto` when the other is not visible,
 * so declaring only overflow-x quietly made this a VERTICAL scroll container
 * too. That was harmless until the chips started carrying their 52px target on
 * a pseudo-element taller than the rail: four pixels of it hung below the
 * padding box, four pixels became scrollable overflow, and a drag on the chips
 * moved them up and down instead of sideways.
 *
 * Saying overflow-y out loud fixes both halves at once: the rail stops being a
 * vertical scroller, and the band is clipped to exactly the 52px the rail is
 * tall, which is what it was supposed to be. Same fix, same reason, as the
 * comment on .sheet further down.
 *
 * The negative block margin takes the target space back out of the LAYOUT
 * without taking it out of the rail: the rail is still 52px tall and still
 * clips its children at 52px, but it occupies only its 30px of visible chips
 * in the stack, so a rail sits on the same rhythm as everything else. */
.rail {
  display: flex; gap: var(--ctl-gap); align-items: center;
  overflow-x: auto;
  overflow-y: hidden;
  /* Contained at the source. A rail is the one thing on the page a thumb pans
     sideways on purpose, so it is the most likely place to run out of content
     mid-swipe, and the rest of that swipe should stop at the rail rather than
     chaining up to the document. This does NOT stop the iOS back gesture -
     nothing in CSS does - it only keeps the scroll where it started. */
  overscroll-behavior-x: contain;
  margin: calc(var(--stack) * -1) calc(var(--pad) * -1);
  padding: var(--stack) var(--pad);
}

/* Active chip is inverted cream, as in the reference — the rail stays legible
   whatever the accent is, and never competes with the price colour.

   Sized off the switch: --ctl-h tall, --ctl-fs type, --ctl-r corner. It used
   to be a 52px slab at 13px/700, which is a control the size of a button doing
   the job of a filter, and four of them ate a third of the screen.

   THE TARGET IS STILL 52px, and it is bought differently here than elsewhere:
   the rail is an overflow:auto scroller, and a pseudo-element taller than its
   parent gets clipped by that scrollport. So the rail itself carries vertical
   padding equal to the difference and the chip's own margin eats it back,
   which leaves a 52px band of rail that belongs to the chip under it. */
.chip {
  flex-shrink: 0;
  position: relative;
  display: inline-flex; align-items: center;
  padding: 0 var(--ctl-pad);
  min-height: var(--ctl-h);
  border-radius: var(--ctl-r);
  font-size: var(--ctl-fs);
  font-weight: var(--ctl-fw);
  letter-spacing: 0.01em;
  white-space: nowrap;
  cursor: pointer;
  background: var(--panel);
  color: var(--mauve);
  border: 1px solid var(--line);
  transition: transform 0.12s ease;
}

/* EXACTLY the target, not a pixel more.
   The rail's padding box is 11 + 30 + 11 = 52 by construction, so a 52px band
   centred on the chip fills it precisely. It used to be 8px taller as slack
   against a fractional centre, and those 8px were the whole of the rail's
   vertical scroll: an absolutely positioned child sticking out of a scroll
   container is scrollable overflow, and overflow-x: auto makes the other axis
   auto too. Clipping it with overflow-y: hidden stopped the panning; making it
   fit means there is nothing left to scroll to at all. */
.chip::before {
  content: '';
  position: absolute;
  left: 0; right: 0; top: 50%;
  transform: translateY(-50%);
  height: var(--tap);
}

.pill { position: relative; }

/* The status pill is not inside a scroller, so an expanded hit area is safe
   there and keeps the header compact. */
.pill::after {
  content: '';
  position: absolute;
  left: 0; right: 0;
  top: 50%;
  transform: translateY(-50%);
  height: var(--tap);
}

.chip:active { transform: scale(0.94); }
.chip[aria-pressed="true"] { background: var(--cream); color: var(--ink); border-color: var(--cream); }

/* A chip that does something rather than filtering something. It sits on the
   end of the section rail and opens the reorder sheet, so it reads as part of
   the rail without ever looking like a section you could be filtered to. */
.chip-act {
  gap: 5px;
  background: none;
  border-style: dashed;
  color: var(--mauve2);
}
.chip-act svg {
  width: 13px; height: 13px;
  fill: none; stroke: currentColor; stroke-width: 2.4;
  stroke-linecap: round; stroke-linejoin: round;
}

/* ---------------------------------------------------- section order sheet */

/* One row per section, dragged by the same grip the dishes use. Deliberately
   plainer than a dish row: there is nothing to edit here, only an order. */
.catlist { gap: 6px; }

.catrow {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px 8px 14px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
}

.catrow-name { flex: 1; min-width: 0; color: var(--cream); font-size: var(--fs-body); overflow-wrap: anywhere; }

.catrow-count {
  flex: none;
  font-family: var(--font-mono);
  font-size: var(--fs-tag);
  color: var(--mauve2);
}

/* The row being dragged. The lift is defined with the dish rows further down,
   beside the grip that does the dragging. */
.catlist li.dragging .catrow {
  border-color: var(--accent-line);
  box-shadow: var(--shadow-float);
}

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

.list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }

.item {
  display: flex; gap: 14px; align-items: flex-start;
  padding: 14px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  transition: border-color 0.18s ease;
}

/* SOLD OUT. One dish among many, and it should read as spent: dimmed hard,
   and its photograph drained below. */
.item.off { opacity: 0.55; }

/* THE KITCHEN IS NOT COOKING TODAY, which is a different statement and needs a
   lighter hand. This lands on every row at once, so .off's treatment would
   drain the whole menu and read as a kitchen that has gone rather than one that
   opens on Saturday - and the banner above it is already saying which.

   Barely dimmed, and the photographs keep their colour on purpose: the point of
   the page is still to make somebody want the food and come back for it. What
   takes the press away is the absence of .pick, not this. */
.item.shut { opacity: 0.88; }

.item:focus-within { border-color: var(--line2); }

/* Photo tile — the reference's radial disc, with the item initial standing in
   when there is no photo, which is the common case for a new stand. */
.item-tile {
  flex-shrink: 0;
  width: 64px; height: 64px;
  border-radius: var(--r-tile);
  display: grid; place-items: center;
  overflow: hidden;
  font-family: var(--font-display);
  font-size: 26px;
  color: var(--mauve);
  background: radial-gradient(70% 70% at 50% 35%, var(--disc1) 0%, var(--disc2) 100%);
  border: 1px solid var(--line2);
}

/* WHY THE PHOTO IS POSITIONED RATHER THAN SIZED.
 *
 * `width/height: 100%` on a grid item resolves against its GRID AREA, and the
 * tile is `display: grid; place-items: center` with an implicit `auto` row. An
 * auto row is sized by its content, so the height a percentage would resolve
 * against depends on the height of the thing asking - which is circular, so
 * `height: 100%` computed to `auto`. The image then took its own aspect ratio
 * off the resolved width, and `object-fit: cover` had nothing to do because the
 * box it was fitting was already the wrong shape.
 *
 * On the item sheet, whose tile is 352 x 150, a 600 x 1400 photo came out
 * 350 x 817 and `overflow: hidden` showed a 150px slice of it: the sliver.
 * A 900 x 900 photo came out 350 x 350 and lost its bottom half. Only a photo
 * that happened to match the tile's ratio ever looked right.
 *
 * Absolute positioning gives the image a definite containing block - the tile's
 * padding box - so both percentages resolve, and `cover` crops from the centre
 * at every shape: tall, wide and square. */
.item-tile { position: relative; }

.item-tile img {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  border-radius: inherit;
  display: block;
}

.item.off .item-tile { filter: grayscale(1); }

.item-main { flex: 1; min-width: 0; }
.item-top { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }

.sheet .item-name,
.sheet .item-desc,
.sheet .preset-name,
.sheet .preset-what { overflow-wrap: anywhere; }

/* THE DESCRIPTION IS THE WHOLE DESCRIPTION ONCE THE DISH IS OPEN.

   A seller wrote "Choose 1 or 2 flavors : Biscoff, Apple, Peach, Original,
   Strawberry, Oreo, Pineapple" and her customers could read half of it. The
   sheet cut the sentence at a pixel row - mid-word, no ellipsis, nothing to
   tap to see the rest - and how much survived changed with the phone and with
   the dish. That is an order placed against a list of flavours the customer
   never saw.

   Two rules met to do it. `.item-desc` carries `overflow: hidden` because in
   the LIST it is a one-line row, and that is right there. But a flex item
   whose overflow is not `visible` has an automatic minimum size of zero
   instead of its content, so in the sheet - a flex column with `max-height:
   90dvh` - the description became the one child that would give ground. Every
   other child either declares `flex: none`, is a flex container of its own,
   or is `.item-tile`, which is `flex-shrink: 0`. So the whole overflow of the
   sheet was charged to this one paragraph: it shrank until it was two lines,
   or one, or a 6px slice of letter-tops, and `overflow: hidden` then clipped
   what no longer fit. On a 390x640 phone it reached zero and the copy was
   gone entirely.

   The sheet already scrolls. Nothing in it needs to shrink to fit, so the
   description keeps its height and the sheet scrolls to it, which is what a
   scroll container is for. `overflow: visible` is not belt-and-braces: it is
   what restores the automatic minimum size, and there is nothing to clip
   here anyway. The list row above is untouched and still clamps to one line.

   The seller's text is never altered - not here, not on the way in, not in
   the database. This is only about which of it reaches the screen. */
.sheet .item-desc {
  flex: none;
  overflow: visible;
  text-overflow: clip;
  white-space: normal;
}

.item-name {
  font-family: var(--font-display);
  font-size: var(--fs-item);
  text-transform: uppercase;
  letter-spacing: 0.01em;
  color: var(--cream);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Price in accent, mono — the reference's one accent moment in a list row. */
.item-price {
  flex-shrink: 0;
  font-family: var(--font-mono);
  font-size: 14px;
  color: var(--accent-text);
}

.item-desc {
  margin: 4px 0 8px;
  color: var(--mauve);
  font-size: var(--fs-desc);
  line-height: 1.35;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.item-foot {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px;
}

.item-tags { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; min-width: 0; }

/* Outline-only tags; the colour carries the meaning. */
.tag {
  font-family: var(--font-mono);
  font-size: var(--fs-tag);
  line-height: 1;
  opacity: 0.95;
  border-radius: 5px;
  padding: 3px 6px;
  border: 1px solid currentColor;
  color: var(--mauve2);
  white-space: nowrap;
}

.tag.on { color: var(--lime); }
.tag.offsale { color: var(--coral); }

/* This is an editing surface, so every row carries explicit affordances.

   THE GAP IS LOAD-BEARING. Each of these buttons is drawn at 34px and carries
   a 52px target centred on it, so two adjacent centres have to be at least
   52px apart or the two targets overlap and a tap in the overlap goes to
   whichever one paints last - which is delete. 34 + 18 = 52, exactly
   adjacent, nothing overlapping, and the three of them together are still
   narrower than the three 44px buttons this replaced. */
.item-acts { display: flex; gap: 18px; flex-shrink: 0; }

.iconbtn {
  position: relative;
  width: 34px; height: 34px;
  display: grid; place-items: center;
  border-radius: var(--ctl-r);
  border: 1px solid var(--line);
  background: var(--panel);
  color: var(--mauve);
  cursor: pointer;
  transition: transform 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}

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

.iconbtn::after {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: var(--tap); height: var(--tap);
}

.iconbtn:active { transform: scale(0.94); }

/* THE COVER-PHOTO TOGGLE, ON when this dish is the one on her Browse card.
   The accent, because being chosen is a state of HER kitchen rather than a
   status of the dish - the on/off-menu tags beside it already own the status
   colours and must not be competing with this. Off, it looks like every other
   icon button on the row, which is correct: not being the cover is the
   ordinary state and should not be shouting. */
.iconbtn.cover.on {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 50%, transparent);
  background: color-mix(in srgb, var(--accent) 12%, var(--panel));
}

/* A LABEL THAT CAN EXPLAIN ITSELF.
   A title with a quiet glyph beside it that opens a note under it. `quiet` is
   already this stylesheet's word for a de-emphasised variant of a control
   (.savebtn.quiet), so it keeps that meaning here: no border, no panel, just
   the glyph.

   SMALLER THAN AN .iconbtn AND STILL AS THUMBABLE. 26px is what sits level
   with an uppercase .label instead of towering over it; the tap target is
   untouched, because .iconbtn::after above already stretches every one of
   these to var(--tap) regardless of the box it is drawn in. That pseudo-element
   is the whole reason this can be shrunk safely.

   The open state takes the accent, so the glyph is the thing that says the note
   below belongs to it - the same job .row-solo's chevron rotation does. */
.label-row { display: flex; align-items: center; gap: 6px; }

.iconbtn.quiet {
  width: 26px; height: 26px;
  border-color: transparent;
  background: transparent;
  color: var(--mauve2);
}
.iconbtn.quiet svg { width: 15px; height: 15px; }
.iconbtn.quiet:hover, .iconbtn.quiet:focus-visible { color: var(--mauve); }
.iconbtn.quiet[aria-expanded="true"] { color: var(--accent); }
.iconbtn.edit:hover, .iconbtn.edit:focus-visible { color: var(--accent); border-color: var(--accent-line); }
.iconbtn.del:hover, .iconbtn.del:focus-visible { color: var(--coral); border-color: color-mix(in srgb, var(--coral) 45%, transparent); }
.iconbtn.del[data-armed="1"] { color: var(--coral); border-color: var(--coral); background: color-mix(in srgb, var(--coral) 15%, var(--panel)); }

.empty {
  text-align: center;
  color: var(--mauve2);
  font-size: 14px;
  padding: 40px 22px;
  border: 1px dashed var(--line2);
  border-radius: var(--r-card);
  line-height: 1.6;
}

/* ---------------------------------------------------------------- forms */

.field { display: flex; flex-direction: column; gap: 7px; }

input, textarea, select {
  width: 100%;
  min-height: var(--tap);
  padding: 14px;
  border: 1px solid var(--line);
  border-radius: var(--r-input);
  background: var(--panel);
  color: var(--cream);
  font-family: var(--font-body);
  /* Never below --fs-field; the token says what iOS does otherwise. select
     is in here because it was not: it took the UA's own size, which is
     under 16px, so the one bare <select> on the address form zoomed. */
  font-size: var(--fs-field);
  outline: none;
}

textarea { min-height: 80px; resize: vertical; line-height: 1.4; }

input:focus, textarea:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-ring);
}

input::placeholder, textarea::placeholder { color: var(--mauve2); opacity: 0.7; }

.code-input {
  font-family: var(--font-mono);
  font-size: 28px;
  letter-spacing: 0.3em;
  text-align: center;
  padding-left: calc(14px + 0.3em); /* optical: absorbs the trailing letter-space */
}

.prefixed { position: relative; display: flex; align-items: center; }

.prefixed .prefix {
  position: absolute; left: 14px;
  font-family: var(--font-mono);
  color: var(--mauve2);
  pointer-events: none;
}

.prefixed input { padding-left: 32px; }

/* --------------------------------------------------------------- buttons */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 10px;
  min-height: var(--tap);
  padding: 0 20px;
  border: 1px solid transparent;
  border-radius: var(--r-control);
  font-family: var(--font-body);
  font-size: 16px;
  font-weight: 800;
  cursor: pointer;
  text-decoration: none; /* .btn is used on <a> too */
  transition: transform 0.12s ease, opacity 0.12s ease;
}

.btn:active { transform: scale(0.96); }
.btn[disabled] { opacity: 0.5; pointer-events: none; }
.btn.block { width: 100%; }

.btn.primary {
  background: var(--accent);
  color: var(--accent-ink);
  box-shadow: var(--shadow-cta);
}

.btn.ghost {
  background: var(--panel);
  color: var(--mauve);
  border-color: var(--line);
  font-weight: 600;
}

.btn.danger {
  background: transparent;
  color: var(--coral);
  border-color: color-mix(in srgb, var(--coral) 40%, transparent);
  font-weight: 600;
}

/* SAVING, SAID BY THE BUTTON SHE PRESSED. The word changes and a ring turns
   beside it. `disabled` on its own is a dim, and a dim on a sheet where
   nothing else moves reads as frozen rather than as busy - so a working button
   is deliberately brighter than a disabled one, not darker.

   TWO THINGS IT HAS TO GET PAST, both under `.tool`, which is the owner app's
   whole shell and so wins on specificity over any bare .btn rule:

     .tool .btn::before  is already spoken for. It is the 52px tap target that
                         lets these buttons be 38px, so the ring is ::after.
     .tool .btn.primary  sets the `background` SHORTHAND, which resets
                         background-image - so this cannot be a moving
                         background either, however tempting.

   Hence the doubled selectors. They are not belt and braces: the bare one is
   for the marketing pages, the .tool one for the app. */
.btn.working { opacity: 0.9; position: relative; }

.btn.working::after,
.tool .btn.working::after {
  content: "";
  position: absolute;
  left: 14px;
  top: 50%;
  margin-top: -7px;
  width: 14px;
  height: 14px;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  animation: lk-spin 0.62s linear infinite;
  z-index: 1;
}

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

@media (prefers-reduced-motion: reduce) {
  /* The ring stays and stops. It sits beside a button that says Saving and is
     shut, which is the part carrying the meaning. */
  .btn.working::after,
  .tool .btn.working::after { animation: none; }
}

/* --------------------------------------------------------------- swatches */

.swatches { display: flex; flex-wrap: wrap; gap: 14px; padding: 3px 0; }

.swatch {
  width: 44px; height: 44px;
  border: 0; border-radius: var(--r-pill);
  padding: 0;
  cursor: pointer;
  transition: transform 0.12s ease;
}

.swatch:active { transform: scale(0.94); }
.swatch[aria-pressed="true"] { outline: 2px solid var(--cream); outline-offset: 3px; }
.swatch:focus-visible { outline: 2px solid var(--cream); outline-offset: 3px; }

/* ---------------------------------------------------------------- switch */

.switch {
  display: flex; align-items: center; gap: 12px;
  min-height: var(--tap);
  font-size: var(--fs-body);
  cursor: pointer;
}

.switch input { position: absolute; opacity: 0; width: 0; height: 0; }

.switch .track {
  flex: none;
  width: 52px; height: 32px;
  border-radius: var(--r-pill);
  background: var(--panel);
  border: 1px solid var(--line);
  padding: 3px;
  transition: background 0.15s ease, border-color 0.15s ease;
}

.switch .knob {
  display: block;
  width: 24px; height: 24px;
  border-radius: var(--r-pill);
  background: var(--mauve2);
  transition: transform 0.15s ease, background 0.15s ease;
}

.switch input:checked + .track { background: var(--accent-glow-soft); border-color: var(--accent); }
.switch input:checked + .track .knob { transform: translateX(20px); background: var(--accent); }
.switch input:focus-visible + .track { box-shadow: 0 0 0 3px var(--accent-ring); }

/* A switch that cannot move has to look like one. It is still rendered rather
   than hidden, because the note beside it is explaining why it is stuck. */
.switch:has(input:disabled) { cursor: default; opacity: 0.55; }
.switch input:disabled + .track { pointer-events: none; }

/* ----------------------------------------------------------------- sheet */

.sheet-wrap {
  position: fixed; inset: 0; z-index: 60;
  display: flex; align-items: flex-end; justify-content: center;
}

.scrim {
  position: absolute; inset: 0;
  background: rgba(12, 8, 5, 0.66);
  animation: fade 0.25s ease;
}

/* THE SHEET IS PINNED TO ITS OWN BOX AND SCROLLS ONE WAY.
 *
 * It used to drag sideways on a phone, and the cause was two things meeting.
 *
 * First, `overflow-y: auto` on its own does not mean "scroll vertically". CSS
 * says a computed `visible` on one axis becomes `auto` when the other axis is
 * not `visible`, so declaring only overflow-y quietly made this a horizontal
 * scroll container too. Anything even a pixel too wide became a draggable
 * axis instead of being clipped.
 *
 * Second, something was too wide: a dish or option name with a long unbroken
 * word. `.choice-name` could shrink its box but its text still painted past
 * it, and `.choice` above it is a flex item whose automatic minimum size is
 * its content, so it refused to shrink and pushed the whole column out. On a
 * 414px screen that made the sheet 454px of scrollable width, which is where
 * the Add button's price and the option prices went.
 *
 * So: say overflow-x out loud, let the flex children shrink, and let long
 * words break rather than clipping them off the edge. */
.sheet {
  position: relative;
  width: 100%;
  max-width: 430px;
  min-width: 0;
  max-height: 90dvh;
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
  /* No top padding: the sticky bar below supplies it, and a padded scrollport
     would leave a band of content showing above a `top: 0` sticky child. */
  padding: 0 var(--pad) calc(24px + var(--safe-b));
  background: var(--surface);
  border: 1px solid var(--line2);
  border-bottom: 0;
  border-radius: var(--r-sheet) var(--r-sheet) 0 0;
  animation: sheet 0.34s var(--ease);
}

/* Every direct child can shrink to the sheet rather than the other way round,
   and nothing may be wider than it.

   `max-width: 100%` here is 100% of the sheet's CONTENT box, which is the
   sheet minus its two --pad gutters. That is right for content and wrong for
   the one child that is deliberately wider: .sheet-top bleeds out to the
   sheet's edges with a negative margin, and this rule was clamping it back,
   so the header bar stopped --pad short of the right edge and looked broken.
   The bar opts out below; nothing else does. */
.sheet > * { min-width: 0; max-width: 100%; }

.sheet h3 { font-size: var(--fs-head); }

/* A sheet whose title has a way back beside it. The button is the ordinary
   icon button, so it keeps its 52px target without the row getting taller. */
.sheet-head { display: flex; align-items: center; gap: 10px; }
.sheet-head h3 { flex: 1; min-width: 0; }

/* A sticky bar rather than a floating dot. The item sheet got long when
   options moved into it, and a close button that scrolls away at the top is a
   sheet you have to scroll back up to get out of - while one that floats over
   the content sits on top of whatever is at the right edge, which here is a
   delete button. The bar is opaque, so content passes under it and nothing is
   ever obscured. */
/* THE BAR SPANS THE SHEET, EDGE TO EDGE.
   The negative margin pulls it out over both gutters; the width below is what
   makes it actually get there. Without it the `max-width: 100%` above clamps
   the bar to the content box, and since the margin only moves its left edge
   the bar ends up one gutter short on the right - a header rule that stops
   before the corner, which is the thing that read as broken. */
.sheet-top {
  position: sticky;
  top: 0;
  z-index: 3;
  flex: none;
  margin: 0 calc(var(--pad) * -1);
  width: calc(100% + var(--pad) * 2);
  max-width: none;
  padding: 10px var(--pad);
  background: var(--surface);
  border-radius: var(--r-sheet) var(--r-sheet) 0 0;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  min-height: 44px;
  /* Just enough of an edge that content reads as passing under it rather than
     as vanishing into a band of the same colour. */
  box-shadow: 0 6px 10px -6px rgba(0, 0, 0, 0.45);
}

.grabber {
  position: absolute; top: 12px; left: 50%;
  transform: translateX(-50%);
  width: 40px; height: 4px;
  border-radius: var(--r-pill);
  background: var(--line2);
}

.sheet-close {
  position: relative;
  width: 30px; height: 30px;
  flex: none;
  display: grid; place-items: center;
  border: 0; border-radius: var(--r-pill);
  background: var(--panel);
  color: var(--cream);
  cursor: pointer;
  transition: transform 0.12s ease;
}

.sheet-close::after {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  width: var(--tap); height: var(--tap);
  transform: translate(-50%, -50%);
}
.sheet-close:active { transform: scale(0.94); }
.sheet-close svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 2.4; stroke-linecap: round; }

/* ---------------------------------------------------------------- ticket */

/* Receipt stock with the reference's sawtooth edge. The zigzag is an inline
   SVG so the drop-shadow follows the torn silhouette. */
/* No margin of its own. The receipt used to carry 2px above and 4px below,
   which put the Kitchen screen on 13 and 15 while everything around it was on
   --stack. A block in a stack is spaced by the stack. */
.ticket { filter: var(--drop-ticket); }

.ticket-edge { display: block; width: 100%; height: 7px; }
.ticket-edge path { fill: var(--paper); }
.ticket-edge.flip { transform: scaleY(-1); }

.ticket-body {
  font-family: var(--font-mono);
  background: var(--paper);
  color: var(--paperink);
  padding: 14px 18px;
  font-size: var(--fs-label);
  letter-spacing: var(--track-receipt);
}

.ticket-row { display: flex; justify-content: space-between; gap: 12px; }
/* Its children are flex items too, so the same automatic minimum applies:
   without this the row holding .line-opts on the status page is as wide as
   the longest unbroken answer. */
.ticket-row > * { min-width: 0; overflow-wrap: anywhere; }
.ticket-row + .ticket-row { margin-top: 8px; }

/* THE ORDER, ON THE CHECK.
   #pay-lines wraps the lines, which breaks the .ticket-row + .ticket-row
   adjacency the rest of the ticket spaces itself with, so the spacing is stated
   here instead of inherited. */
#pay-lines:not(:empty),
#co-lines:not(:empty) {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1.5px dashed var(--paperline);
}
/* The head already draws a rule under itself. On checkout the lines follow it
   directly, so their own rule would be a second dashed line against the first;
   on the payment screen there are rows in between and both are wanted. */
.ticket-head + #co-lines,
.ticket-head + #pay-lines {
  margin-top: 8px;
  padding-top: 0;
  border-top: 0;
}

#pay-lines .ticket-line + .ticket-line,
#pay-lines .ticket-sub + .ticket-line,
#co-lines .ticket-line + .ticket-line,
#co-lines .ticket-sub + .ticket-line { margin-top: 8px; }

/* Options and the customer's own note, under the line they belong to. Smaller
   and dimmer: they qualify the line above rather than competing with it. */
.ticket-sub {
  margin: 3px 0 0;
  padding-right: 52px;          /* clear of the price column */
  /* It carries the same answers, joined with commas, and a browser will
     not break inside a word for them. */
  overflow-wrap: anywhere;
  font-size: var(--fs-tag);
  letter-spacing: var(--track-cap);
  opacity: 0.72;
  line-height: 1.35;
}
.ticket-said { font-style: italic; }

.ticket-head {
  padding-bottom: 10px;
  margin-bottom: 10px;
  border-bottom: 1.5px dashed var(--paperline);
  text-transform: uppercase;
}

.ticket-total {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1.5px solid var(--paperink);
  font-size: 15px;
  font-weight: 500;
  text-transform: uppercase;
}

.ticket-row .v { text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ----------------------------------------------------------------- toast */

.toast {
  position: fixed;
  left: 50%; top: calc(72px + var(--safe-t));
  transform: translateX(-50%);
  z-index: 80;
  /* Purely informational: it floats over the chip rails and the order list,
     and must never swallow a tap meant for what is underneath it. */
  pointer-events: none;
  /* WIDTH, AND WHY IT HAS TO BE STATED.
     A fixed box with left:50% and no width shrink-to-fits into whatever is
     left of the viewport to its RIGHT - which is exactly half the screen. On a
     390px phone every toast was therefore capped at 195px and the max-width
     below never got a say. A one word toast fit anyway, which is why this
     survived: it only fails on a long message, and then it fails badly - the
     stock refusal wrapped to six lines in a 195px column and the pill radius
     rounded that into a circle sitting over the header. */
  width: max-content;
  max-width: min(92vw, 420px);
  padding: 12px 18px;
  /* NOT A PILL. 999px is right for one line and turns a two line box into a
     lozenge and a six line box into a circle. A plain radius reads the same at
     every length. */
  border-radius: 14px;
  background: var(--panel);
  border: 1px solid var(--line2);
  color: var(--cream);
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.02em;
  text-align: center;
  box-shadow: var(--shadow-float);
  animation: drop 0.18s ease-out;
}

.toast.bad { border-color: color-mix(in srgb, var(--coral) 55%, transparent); color: var(--coral); }

/* ---------------------------------------------------------------- motion */

@keyframes fade { from { opacity: 0; } }
@keyframes sheet { from { transform: translateY(100%); } }
@keyframes drop { from { transform: translate(-50%, -10px); opacity: 0; } }
@keyframes rise { from { opacity: 0; transform: translateY(14px); } }

.rise { animation: rise 0.5s cubic-bezier(0.2, 0.7, 0.2, 1) both; }

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

/* ==========================================================================
   Storefront + order loop.
   No new colours: everything below resolves to tokens already declared above.
   ========================================================================== */

/* ------------------------------------------------------------ store header */

.store-head {
  padding: 20px var(--pad) 16px;
  background: radial-gradient(120% 80% at 50% -10%, var(--heroglow) 0%, var(--ink) 62%);
  border-bottom: 1px solid var(--line);
}

/* The back control sits above the kitchen's name, on its own line, so it never
   competes with the logo or wraps into the title. Margin only: .sheet-close
   already carries the 30px visual and the 52px target underneath it. */
/* The way out on the left, the bell on the right. It was a bare block holding
   one button until alerts arrived; space-between is the whole change, and an
   absent bell leaves the back control exactly where it was. */
.store-nav { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
.store-nav[hidden] { display: none; }

/* ---------------------------------------------------------------- alerts

   THE BELL AND ITS PANEL, one component in two headers: Browse's topbar and
   the storefront's nav row. Both give it a corner and nothing else, so
   everything it needs to position itself is here rather than in either page.

   THE HOST IS THE POSITIONING CONTEXT, not the button, because the panel hangs
   off the corner rather than off the glyph - and on a narrow phone it is wider
   than the button it belongs to. */
/* NOT A POSITIONING CONTEXT, AND THAT IS THE FIX.
   The panel used to hang off this host, which put it wherever the bell
   happened to be. On Browse the bell sits BEFORE the Sign in pill, so its right
   edge is about a hundred pixels in from the screen - and a 300px panel
   anchored to it started off the left of a 390px phone, showing its right half
   only. The panel is positioned against the viewport now and the host just
   holds the button. */
.alerts { flex: none; }
.alerts[hidden] { display: none; }

.alerts-btn {
  position: relative;
  width: 34px; height: 34px;
  display: grid; place-items: center;
  border-radius: var(--ctl-r);
  border: 1px solid var(--line);
  background: var(--panel);
  color: var(--mauve);
  cursor: pointer;
}
.alerts-btn svg {
  width: 17px; height: 17px;
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}
/* The same trick .iconbtn uses: a 34px box with a full thumb behind it. */
.alerts-btn::after {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: var(--tap); height: var(--tap);
}
.alerts-btn:active { transform: scale(0.94); }
.alerts-btn[aria-expanded="true"] { color: var(--accent); border-color: var(--accent-line); }

/* THE COUNT, WHICH IS THE ONLY RED IN THE CUSTOMER CHROME. It sits on the
   corner of the glyph rather than beside it, so the button keeps its width
   whether there is a number in it or not - a bell that changes size when a
   drop lands would move the account pill next to it. */
.alerts-count {
  position: absolute;
  top: -5px; right: -5px;
  min-width: 17px; height: 17px;
  padding: 0 4px;
  display: grid; place-items: center;
  border-radius: 9px;
  background: var(--coral);
  color: #fff;
  font-family: var(--font-mono);
  font-size: 10px;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.alerts-count[hidden] { display: none; }

/* PINNED TO THE VIEWPORT, NOT TO THE BELL.

   left AND right are both set, so the box can never begin before one gutter or
   end after the other whatever the width of the screen or wherever the bell is
   in its header. max-width plus margin-left:auto then pulls it over to the
   right gutter, where the bell is, without ever letting it reach past the left
   one. Both gutters are --pad, so it is inset from both edges by the same
   amount as everything else on the page.

   The customer pages have no max-width shell - no .wrap anywhere in either of
   them - so the viewport IS the content box at every width, and fixed
   positioning cannot drift away from the layout on a desktop.

   `top` is the one thing script sets, because it is the one thing that depends
   on where the button ended up. See place() in alerts.js, which also keeps it
   right while the page scrolls under a sticky header. */
.alerts-panel {
  position: fixed;
  top: var(--alerts-top, 64px);
  left: var(--pad);
  right: var(--pad);
  margin-left: auto;
  z-index: 40;
  width: auto;
  max-width: 340px;
  max-height: min(60vh, 420px);
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: 12px;
  border-radius: var(--r-input);
  border: 1px solid var(--line);
  background: var(--panel);
  box-shadow: 0 18px 40px rgba(0, 0, 0, 0.45);
}
.alerts-panel[hidden] { display: none; }

.alerts-head { margin-bottom: 10px; }
.alerts-empty, .alerts-foot { color: var(--mauve2); font-size: var(--fs-sm); margin: 0; }
.alerts-foot { margin-top: 10px; font-family: var(--font-mono); font-size: var(--fs-tag); }

.alerts-item {
  display: block;
  padding: 10px 0;
  border-top: 1px solid var(--line);
  color: inherit;
  text-decoration: none;
}
.alerts-item:first-child { border-top: 0; }
.alerts-item-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.alerts-from { font-weight: 600; font-size: var(--fs-sm); }
.alerts-when { flex: none; color: var(--mauve2); font-family: var(--font-mono); font-size: var(--fs-tag); }
.alerts-body { margin: 2px 0 0; color: var(--mauve); font-size: var(--fs-sm); }
/* Unread is a mark rather than a colour change, so a read one is still legible
   rather than dimmed into looking disabled. */
.alerts-item.unread .alerts-from::before {
  content: '';
  display: inline-block;
  width: 6px; height: 6px;
  margin-right: 6px;
  border-radius: 50%;
  background: var(--coral);
  vertical-align: middle;
}

/* THE LAST NOTICE, UNDER HER MESSAGE AND QUIETER THAN IT. The message is the
   accent block she chose to leave up; this is a thing that already happened, so
   it takes the panel treatment instead of shouting in the same voice. */
.notecard.dropnote {
  background: var(--panel);
  border-color: var(--line);
  color: var(--mauve);
}

.store-head h1 { font-size: var(--fs-title, 26px); }

.store-meta {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px;
  margin-top: 10px;
}

/* Open/closed badge. Status colours, never the accent. */
.state {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 9px;
  border-radius: 6px;
  border: 1px solid currentColor;
  font-family: var(--font-mono);
  font-size: var(--fs-tag);
  letter-spacing: var(--track-cap);
  text-transform: uppercase;
  color: var(--mauve2);
}

.state .dot { width: 7px; height: 7px; border-radius: var(--r-pill); background: currentColor; }
.state.open { color: var(--fresh); }
.state.shut { color: var(--coral); }

.closed-banner {
  margin: 12px var(--pad) 0;
  padding: 12px 14px;
  border-radius: var(--r-input);
  border: 1px solid color-mix(in srgb, var(--coral) 30%, transparent);
  background: color-mix(in srgb, var(--coral) 10%, var(--surface));
  color: var(--coral);
  font-size: var(--fs-sm);
}

/* ---- asked for, on the orders rail ----------------------------------- */

/* A REQUEST IS NOT A RECEIPT, AND THIS IS THE WHOLE OF HOW SHE TELLS THEM
   APART AT A GLANCE.

   .ticket beside this is a slip of paper: --paper stock, --paperink, mono
   with receipt tracking, a drop shadow and torn zigzag edges top and bottom.
   It looks like the thing that comes out of a till because that is what an
   order is.

   Nothing has been bought here. No code, no total, no payment, no status
   from the order machine. So it is not printed - it is a note on the dark
   surface, in the app's own body type, with square-ish corners and a
   hairline and no shadow. Pale torn paper against a flat dark card is the
   difference that survives being scrolled past, which is the test a rail
   sets.

   ANSWERED ONES GO QUIET. A price she has sent is waiting on somebody else,
   so it keeps its place - she may want to change it - and stops competing
   with the ones waiting on her. */
.reqcard {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 14px 16px;
  border-radius: var(--r-card);
  border: 1px solid var(--line2);
  background: var(--surface);
}

.reqcard.answered { border-color: var(--line); opacity: 0.82; }

.reqcard-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.reqcard-when { font-family: var(--font-mono); font-size: var(--fs-tag); color: var(--mauve2); }

/* THEIR WORDS, AND THE LOUDEST THING ON THE CARD. It is the only part she
   needs to read to know what she is being asked, and it is prose rather
   than a field, so it is body type at reading size rather than mono. */
.reqcard-what {
  margin: 0;
  font-size: var(--fs-body);
  line-height: 1.4;
  color: var(--cream);
  overflow-wrap: anywhere;
}

.reqcard-who { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.reqcard-who > span { color: var(--mauve); font-size: var(--fs-sm); }
.reqcard-tel { color: var(--accent-text); font-size: var(--fs-sm); text-decoration: none; }

.reqcard-when-for { margin: 0; font-size: var(--fs-desc); color: var(--mauve2); }

/* What she already said, when she has said it. On the panel step so it
   reads as a thing that came back rather than as more of the request. */
.reqcard-quote {
  display: flex; align-items: baseline; gap: 10px;
  padding: 8px 10px;
  border-radius: var(--r-input);
  background: var(--panel);
}

.reqcard-price {
  flex: none;
  font-family: var(--font-mono);
  font-size: var(--fs-item);
  color: var(--accent-text);
}

.reqcard-covers {
  min-width: 0;
  font-size: var(--fs-desc);
  color: var(--mauve);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Name a price leads; No is the quiet one beside it. Both are real 52px
   targets because .btn already carries that. */
.reqcard-acts { display: flex; gap: 10px; }
.reqcard-acts .btn { min-height: 44px; font-size: var(--fs-sm); }
.reqcard-acts .grow { flex: 1 1 auto; }

/* What they asked for, at the top of the quote sheet, so she is not pricing
   from memory with the card scrolled away behind it. */
.quote-what {
  margin: 0;
  padding: 12px 14px;
  border-radius: var(--r-input);
  background: var(--panel);
  color: var(--cream);
  font-size: var(--fs-sm);
  line-height: 1.45;
  overflow-wrap: anywhere;
}

/* ---- where a truck will be, on its own storefront --------------------- */

/* THE FIRST THING UNDER THE HEADER, and for a truck with no menu it is the
   page. Drawn as a card rather than as another banner: the two banners above
   it are exceptions - switched off, not cooking today - and this is the
   ordinary state of a truck rather than something having gone wrong.

   FILLED WITH HER OWN COLOUR, which is the whole point of it being a block
   rather than a line. It was an accent left edge on --surface, which made it
   one more grey card in a column of grey cards; the one thing a customer
   opens a truck's page to read should not be the same colour as everything
   else on it.

   --accent-ink IS NOT A GUESS. applyAccent computes it beside --accent on
   this very page - the same derivation the owner app uses, so a kitchen
   looks identical on both sides - by walking a few dark tints of her colour
   until one clears 4.5:1 on the fill, and flipping to cream when none does.
   Measured across all seven swatches: 4.80 at worst (#cf6152), 13.20 at best
   (the cream one, where the ink goes dark), every one of them AA.

   NO OPACITY ANYWHERE IN HERE, and that is a measurement rather than a
   preference. The quieter lines wanted to be the ink at 70%, which on the
   worst swatch is 3.20 - large-text only, and two of these lines are 15px
   and 12.5px. 85% is still 4.02. There is 6.7% of headroom over AA at full
   strength and none below it, so every line is full ink and the hierarchy
   comes from the type scale instead: mono caps, then display, then body,
   then small. */
.stopcard {
  margin: 12px var(--pad) 0;
  padding: 14px 16px;
  border-radius: var(--r-card);
  border: 1px solid transparent;
  background: var(--accent);
  color: var(--accent-ink);
  /* Centred, and this card only. Every line in here is one short statement -
     a label, a day, a pitch - rather than prose somebody reads along, and
     centring is what makes the block read as a notice rather than as the
     start of a column of text. Set on the container because none of the
     four children declares its own alignment; nothing else on the
     storefront is centred and nothing else inherits from this. */
  text-align: center;
}

.stopcard[hidden] { display: none; }

/* .label is --mauve2 by default, which is a grey chosen against the dark
   ramp and unreadable on a filled block. Everything in here overrides to the
   ink, including this. */
.stopcard .label {
  display: block;
  margin-bottom: 8px;
  color: inherit;
}

.stopcard-when {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--fs-item);
  letter-spacing: 0.01em;
  text-transform: uppercase;
  line-height: 1.15;
  color: inherit;
}

.stopcard-where {
  margin: 4px 0 0;
  font-size: var(--fs-body);
  line-height: 1.35;
  color: inherit;
}

/* Only for a truck with no menu - see renderStop. A page with no Add button
   has to say why or it reads as one that failed to load. */
.stopcard-note {
  margin: 10px 0 0;
  padding-top: 10px;
  border-top: 1px solid color-mix(in srgb, var(--accent-ink) 25%, transparent);
  font-size: var(--fs-desc);
  line-height: 1.45;
  color: inherit;
}

/* WHAT THE KITCHEN WANTED TO SAY, under everything that says whether you can
   order from her.

   NOT A .closed-banner, AND FILLED FOR THE SAME REASON THE STOP CARD IS.
   Those two are coral: closed, paused, something is wrong. This is neither an
   alarm nor an error - it is a cook talking - so it wears her colour rather
   than a warning one, and --accent-ink is the readable pair applyAccent
   computes beside it on this very page. Measured across all seven swatches:
   4.80 at worst, 13.20 on the cream one. No opacity anywhere in here for the
   same reason as the stop card - 85% of the ink on the worst swatch is 4.02,
   and this is 13px text.

   Sentence case and body type rather than the display face: it is her words,
   and uppercasing somebody's sentence changes how it sounds. Wraps to as many
   lines as it needs, which at 140 characters is three on a 390px phone. */
.notecard {
  margin: 12px var(--pad) 0;
  padding: 12px 14px;
  border-radius: var(--r-input);
  border: 1px solid transparent;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: var(--fs-sm);
  /* Inter ships here as one variable face declared 400 700, so 600 is a real
     interpolated weight rather than a browser-synthesised bold. It is the
     whole of what she wanted to say and it is thirteen pixels on a filled
     block; the weight is what stops it reading as a caption under the cards
     above it. Contrast is unchanged by weight - still 4.80 on the worst
     swatch, which clears AA for small text regardless. */
  font-weight: 600;
  line-height: 1.45;
}

.notecard[hidden] { display: none; }

/* TWO FILLED CARDS IN A COLUMN, AND WHY THEY DO NOT READ AS ONE.

   On a truck both of these are the seller's accent, stacked, so the colour
   cannot be what separates them. Three things do it, and the first is the
   one carrying the weight:

   PROXIMITY, WHICH BEATS COLOUR FOR GROUPING. The gap between them is 18px
   against the 12-14px of padding inside each, so there is more air between
   the cards than within either. That is the whole rule: two blocks with more
   space between them than inside them read as two blocks whatever colour
   they are. At the default 12px the gap was tighter than the padding and
   they would have grouped.

   TWO SILHOUETTES. --r-card on the stop card against --r-input here, 18px
   of corner against 12px. Different shapes, not just different sizes.

   TWO TEXTURES, which was already true and is now load-bearing: the stop
   card is a mono uppercase label over a display-caps line over a body line,
   and this is one sentence-case paragraph with no label. Nothing about them
   is typographically alike. */
.stopcard + .notecard { margin-top: 18px; }

/* ------------------------------------------------ the price she sent back */

/* THE THIRD FILLED CARD, and the only one of the three that is ever alone
   on its page. The stop card and the note card have to be told apart from
   each other because they stack; this one sits on a view of its own where
   nothing else is filled, so the accent is doing the simpler job it does
   on a button - this is the thing you came here to read.

   Left-aligned, unlike the stop card. That card is three short statements
   and centring makes it a notice; this one carries a sentence in her words
   about what the price covers, and prose reads down a left edge. */
.quotecard {
  padding: var(--pad);
  border-radius: var(--r-card);
  background: var(--accent);
  color: var(--accent-ink);
}

.quotecard[hidden] { display: none; }

/* --mauve2 is picked against the dark ramp and is unreadable on a filled
   block, the same override the stop card needs and for the same reason. */
.quotecard .label {
  display: block;
  margin-bottom: 10px;
  color: inherit;
}

.quotecard-price {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--fs-hero);
  line-height: 1;
  color: inherit;
}

.quotecard-covers {
  margin: 10px 0 0;
  font-size: var(--fs-body);
  line-height: 1.4;
  color: inherit;
  /* Her words, typed into a phone. */
  overflow-wrap: anywhere;
}

/* SMALLER, AND NOT FAINTER. The obvious way to demote a deadline on a
   filled card is opacity, and it is the one thing that cannot be done
   here: applyAccent picks --accent-ink to clear AA against --accent, and
   any opacity over it throws that measurement away. Size carries the
   hierarchy and the contrast stays the computed one. */
.quotecard-holds {
  margin: 12px 0 0;
  font-size: var(--fs-sm);
  line-height: 1.35;
  color: inherit;
}

.quotecard-holds[hidden] { display: none; }

/* What they asked for, in their words - the same treatment the seller sees
   it in on her own card, because it is the same text. */
.q-what {
  margin: 0;
  font-size: var(--fs-body);
  line-height: 1.4;
  color: var(--cream);
  overflow-wrap: anywhere;
}

/* The day they asked for, under their own words. Quieter than the ask
   itself, because it is a detail of it rather than a second thing. */
.q-when {
  margin: 0;
  font-size: var(--fs-sm);
  line-height: 1.35;
  color: var(--mauve);
}

.q-when[hidden] { display: none; }

/* The page's answer when there is nothing to tap: it ran out, they already
   said yes, they already said no. */
.q-said {
  margin: 0;
  font-size: var(--fs-body);
  line-height: 1.4;
  color: var(--mauve);
}

.q-said[hidden] { display: none; }

/* ------------------------------------------------------------- menu rows */

/* Same card as the owner list; the trailing control is an add button rather
   than edit/delete. */
/* The dish photograph at the top of the item sheet. It was a fixed 150px set
   inline on the element; it is the same 16:10 frame the owner's own photo
   editor uses, so a seller sees the shape she is shooting for. */
.sheet-photo {
  width: 100%;
  aspect-ratio: 16 / 10;
  height: auto;
  border-radius: var(--r-tile);
  font-size: 56px;
}

/* THE THUMBNAIL, BLURRED, UNDER THE PHOTOGRAPH.

   The list has already downloaded it, so this is the one thing that can fill
   the sheet in the frame it opens without costing a request. Blurred at 18px
   it is a wash of the dish's own colours rather than a picture - which is the
   point, because the sharp photograph that lands on top is framed differently
   and a recognisable image underneath would read as a change.

   On ::before so the blur cannot reach the <img> or the letter: filter on the
   tile itself would blur its children too. scale(1.12) hides the soft edge a
   blur leaves at the boundary. Nothing here paints at all until store.js sets
   --behind, so a dish with no thumbnail is unchanged. */
.sheet-photo::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  background-image: var(--behind, none);
  background-size: cover;
  background-position: center;
  filter: blur(18px);
  transform: scale(1.12);
  border-radius: inherit;
}

/* Above the wash, and above the letter, which both sit at the default layer. */
.sheet-photo img { z-index: 1; }

.item.pick { cursor: pointer; }
.item.pick:active { transform: scale(0.985); }

.item-add {
  flex-shrink: 0; align-self: center;
  position: relative;
  width: 34px; height: 34px;
  display: grid; place-items: center;
  border-radius: var(--ctl-r);
  border: 1px solid var(--accent-line);
  background: var(--accent-glow-soft);
  color: var(--accent-text);
  cursor: pointer;
  transition: transform 0.12s ease;
}

.item-add::after {
  content: '';
  position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: var(--tap); height: var(--tap);
}

.item-add:active { transform: scale(0.94); }
.item-add svg { width: 17px; height: 17px; fill: none; stroke: currentColor; stroke-width: 2.4; stroke-linecap: round; }


/* -------------------------------------------------------------- qty stepper */

/* Built like the switch: a tray at --ctl-r-out holding two segments at
   --ctl-r. It was a 56px pill with two 44px discs in it, taller than the row
   it sat under. Both buttons keep a full 52px target. */
.stepper {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px;
  padding: 2px;
  border: 1px solid var(--line);
  border-radius: var(--ctl-r-out);
  background: var(--panel);
  width: fit-content;
}

.stepper button {
  position: relative;
  width: 38px; height: var(--ctl-h);
  flex-shrink: 0;
  display: grid; place-items: center;
  border: 0; border-radius: var(--ctl-r);
  background: var(--surface);
  color: var(--cream);
  cursor: pointer;
  transition: transform 0.12s ease;
}

/* A shade over 52, so a fractional centre can never leave the real target a
   pixel short of it. Nothing sits close enough above or below a stepper for
   the overhang to reach another control. */
.stepper button::after {
  content: '';
  position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: calc(var(--tap) + 4px); height: calc(var(--tap) + 4px);
}

.stepper button:active { transform: scale(0.94); }
.stepper button[disabled] { opacity: 0.35; pointer-events: none; }
.stepper svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 2.6; stroke-linecap: round; }

.stepper .qty {
  font-family: var(--font-body);
  font-size: 15px;
  font-weight: 650;
  letter-spacing: 0.01em;
  min-width: 26px;
  text-align: center;
}

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

/* Fixed to the bottom of the storefront, ON the safe area, with the content
   scrolling under it: the same bar the owner app's tab bar is, in the same
   place, with the same hairline, the same blur and - see the tab bar above -
   the same raised material rather than the page ground, so it is visible as a
   bar all the way down to the physical bottom of the screen. */
.cartbar {
  position: fixed; left: 50%; transform: translateX(-50%); bottom: 0; z-index: 30;
  width: 100%; max-width: 430px;
  padding: 10px var(--pad) calc(10px + var(--safe-b));
  background: color-mix(in srgb, var(--surface) 92%, transparent);
  border-top: 1px solid var(--line2);
  backdrop-filter: saturate(160%) blur(18px);
  -webkit-backdrop-filter: saturate(160%) blur(18px);
}

@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .cartbar { background: var(--surface); }
}

/* THE ROOM IS RESERVED ONLY WHILE THE BAR IS ACTUALLY THERE.
 *
 * This used to reserve a cart bar's worth of room unconditionally, on the
 * reasoning that a stranded last dish is worse than a little air. It is not a
 * little air: the cart bar is only up once something is in the cart, so every
 * storefront a customer opens for the first time ended in 90px of nothing.
 * Measured from the render at 390x844: 124px of bare ground below the last
 * painted pixel with an empty cart, against 11px on the owner app.
 *
 * store.js puts .has-cartbar on the root for exactly as long as the bar is up,
 * so the reservation and the bar are the same fact. */
#view-menu .tab { padding-bottom: calc(var(--barclear) + var(--safe-b)); }
.has-cartbar #view-menu .tab { padding-bottom: calc(var(--cartbarh) + var(--safe-b) + var(--barclear)); }

/* Every other storefront view ends in an ordinary button in the flow, so it
   only needs the ordinary breathing room. */
#view-cart .tab,
#view-checkout .tab,
#view-pay .tab,
#view-status .tab { padding-bottom: calc(24px + var(--safe-b)); }

.cartbar .btn { width: 100%; justify-content: space-between; }
.cartbar .count {
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  opacity: 0.85;
}

/* ------------------------------------------------------------ cart lines */

.line {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 12px 0;
}

.line + .line { border-top: 1px dashed var(--paperline); }
.line-main { flex: 1; min-width: 0; }
.line-name { font-weight: 500; }
.line-note { font-size: var(--fs-tag); opacity: 0.75; margin-top: 2px; }
.line-cost { flex-shrink: 0; text-align: right; }

/* Stepper sized down for use inside the receipt. */
.line-qty { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }

.line-qty button {
  position: relative;
  width: 26px; height: 26px;
  display: grid; place-items: center;
  border: 1px solid var(--paperline);
  border-radius: 6px;
  background: transparent;
  color: var(--paperink);
  cursor: pointer;
}

.line-qty button::after {
  content: '';
  position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: var(--tap); height: var(--tap);
}

.line-qty svg { width: 13px; height: 13px; fill: none; stroke: currentColor; stroke-width: 2.6; stroke-linecap: round; }
.line-qty .n { min-width: 20px; text-align: center; font-weight: 500; }

/* --------------------------------------------------------- pickup slots */

/* Same arithmetic as the notice presets: 30px of slot plus 22px of row gap is
   52px between the centres of two wrapped rows, which is what stops the second
   row's targets covering the first row's. */
.slots { display: flex; flex-wrap: wrap; gap: calc(var(--tap) - var(--ctl-h) + 2px) var(--ctl-gap); }

/* The day above its own row of times. Written once per day instead of stamped
   onto every chip, which is what lets a chip stay just a time and fit at 375px.
   Same size and weight as the other small labels on this screen, so it reads as
   a heading and never as another chip. */
.slot-day {
  margin: 0 0 var(--stack);
  color: var(--mauve2);
  font-size: var(--ctl-fs);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

/* The gap between one day's row and the next day's heading. The first heading
   gets none: the field label above it already supplies that space. */
.slots + .slot-day { margin-top: calc(var(--stack) + 10px); }

/* Tight to the wheel. A full stack here plus the rail's own padding read as a
   hole between the day and the time; the two margins collapse, so this is the
   one that decides the gap. */
#slot-days { margin-bottom: 6px; }

/* THE TIME WHEEL.
   Three rows tall whatever the day holds: eight slots or eighty, the field is
   the same height, which is why this replaced a grid that grew with the day.
   One column, full width of the content area, so there is nothing to overflow
   sideways. */
.wheel {
  position: relative;
  /* Tight to the rail above. The rail carries its own padding, so a full stack
     on top of that read as a hole between the day and the time. */
  margin-top: 6px;
  /* The frame lives here, not on the scrolling view, so the band can sit
     between this background and the rows. With the background on the view the
     band painted underneath an opaque surface and was invisible. */
  border-radius: var(--r-input);
  background: var(--panel);
  border: 1px solid var(--line);
  overflow: hidden;
}

.wheel-view {
  position: relative;
  z-index: 1;
  height: 132px;                    /* 3 rows of 44 */
  overflow-y: auto;
  overflow-x: hidden;
  /* The pan belongs to the wheel. Without this a drag that starts on it scrolls
     the checkout page behind it once the wheel hits an end. */
  overscroll-behavior-y: contain;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  background: transparent;
  cursor: grab;
}
.wheel-view::-webkit-scrollbar { display: none; }
.wheel-view:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.wheel-row {
  height: 44px;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-mono, var(--font-body));
  font-size: 17px;
  color: var(--mauve2);
  /* Faded away from the centre, and the fade is the only thing that moves: the
     row that is chosen is the one under the band. */
  opacity: 0.45;
  transition: opacity 0.12s ease, color 0.12s ease;
  user-select: none; -webkit-user-select: none;
}
.wheel-row.on {
  opacity: 1;
  color: var(--cream);
  font-weight: 600;
}
.wheel-blank { height: 44px; }

/* The band the chosen value sits in. Painted only, and out of the way of the
   pointer, so it never swallows a drag meant for the list under it. */
.wheel-band {
  position: absolute;
  left: 0; right: 0; top: 44px;     /* 1 row down: the centre of three */
  height: 44px;
  /* Above the wheel's own background, below the rows, and never in the way of
     the pointer: a drag that starts on the band is a drag on the list. */
  z-index: 0;
  pointer-events: none;
  border-top: 1px solid var(--line2);
  border-bottom: 1px solid var(--line2);
  /* Enough to read as a band at arm's length in a bright kitchen. At 6% it was
     technically present and invisible, which is the same as absent. */
  background: color-mix(in srgb, var(--cream) 12%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--cream) 6%, transparent);
}

/* A day with one time is a fact, not a choice. */
.wheel-only {
  margin: var(--stack) 0 0;
  font-family: var(--font-mono, var(--font-body));
  font-size: 17px;
  color: var(--cream);
}

/* The day chip carries a word and a date, stacked. */
/* THE DAY CHIP IS 36 TALL, NOT 52.
   It was min-height: var(--tap), which inflated the visible chip to the size of
   its own hit target and left the two lines of text swimming in it. The target
   is not the chip: .chip::before is a 52px band centred on it, and at 36 tall
   that band reaches 8px past each edge, which the rail's padding below covers
   exactly. Same construction as every other chip, two lines instead of one. */
.chip-day {
  flex-direction: column;
  justify-content: center;
  gap: 1px;
  min-height: 36px;
  padding-top: 4px; padding-bottom: 4px;
  line-height: 1.15;
}

/* 8 + 36 + 8 = 52, so the 52px target band fills the rail's padding box
   precisely and there is nothing for the rail to scroll to vertically. */
#slot-days { padding-top: 8px; padding-bottom: 8px; }
.chip-day .chip-word { font-size: var(--ctl-fs); font-weight: 700; }
.chip-day .chip-date { font-size: 10.5px; opacity: 0.72; letter-spacing: 0.01em; }

/* The read-back above the button that commits to it. */
#slot-chosen:not(:empty) { margin: 0 0 10px; color: var(--cream); }

/* A day's hours, on the row that switches that day on. Hidden until the day is
   ticked, so an unticked week reads as seven plain rows rather than fourteen
   empty time fields waiting to be filled in. */
.day-times {
  display: flex; align-items: center; gap: 6px;
  margin-top: 8px;
}
.day-times[hidden] { display: none; }
.day-times input {
  min-height: var(--ctl-h);
  padding: 0 8px;
  border: 1px solid var(--line);
  border-radius: var(--ctl-r);
  background: var(--panel);
  color: var(--cream);
  /* --ctl-fs is 11.5px and this is focusable, so tapping a time zoomed the
     whole seller app. */
  font-size: var(--fs-field);
}
.day-to { color: var(--mauve2); font-size: var(--ctl-fs); }

/* ONE SELLING DAY, IN THREE GROUPS.

   WHAT IT WAS. Five controls - a day, two times, sometimes a limit, a
   location and a Remove button - each at flex-basis 100% in a wrapping row.
   Five full-width things in a stack, all the same width and all the same
   weight, with nothing saying which of them belonged together. The location
   box had no label at all and was told apart from the day select only by
   what happened to be inside it, and Remove was a .btn.ghost: 52px tall,
   drawn exactly as loud as the day the row is about.

   WHAT IT IS. A column of three labelled groups - which day, what hours,
   where - using the same .label the rest of this form heads its fields
   with. The heading of the first group shares its line with Remove, which
   is how a control that throws the row away stops needing a full-width slot
   of its own.

   THE GAP DOES THE GROUPING. 14px between groups against 6px inside one, so
   the three read as three before anybody has read a word of them. */
.sched-row {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 14px;
}

.sched-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }

.sched-group { display: flex; flex-direction: column; gap: 6px; }

/* REMOVE, DEMOTED TO A CORNER. Small to look at and a full 52px under a
   thumb, the same trick .item-add and the stepper use - the visual is never
   inflated to buy the target. --mauve2 until it is reached for, because it
   is the one control on this row nobody is looking for. */
.sched-drop {
  position: relative;
  flex: none;
  width: 26px; height: 26px;
  display: grid; place-items: center;
  padding: 0;
  border: 0;
  background: none;
  border-radius: var(--ctl-r);
  color: var(--mauve2);
  cursor: pointer;
  transition: color 0.12s ease;
}

.sched-drop::after {
  content: '';
  position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: var(--tap); height: var(--tap);
}

.sched-drop:hover, .sched-drop:focus-visible { color: var(--coral); }

.sched-drop svg {
  width: 14px; height: 14px;
  fill: none; stroke: currentColor; stroke-width: 2.2; stroke-linecap: round;
}

/* The day control fills its group. `width` rather than the flex-basis this
   used to carry: the group is a COLUMN now, so a basis of 100% would have
   been a height. .row-main has min-width:0, which on a phone collapsed the
   date input to "10/" and left a seller unable to read the day she had just
   chosen. The date input is styled here rather than borrowing .day-times
   input, which is scoped to that element and would drag the times' layout
   along with it. */
.sched-group > .row-main { width: 100%; min-width: 0; }
.sched-group > .row-main input[type="date"] {
  width: 100%;
  min-height: var(--ctl-h);
  padding: 0 8px;
  border: 1px solid var(--line);
  border-radius: var(--ctl-r);
  background: var(--panel);
  color: var(--cream);
  /* NOT `font: inherit`. The shorthand took the 15px around it, which
     is a focusable field under the floor - and being a shorthand it is
     invisible to anything grepping for font-size. */
  font-family: inherit;
  font-size: var(--fs-field);
}
.sched-group .day-times { margin-top: 0; flex-wrap: wrap; row-gap: 6px; }

/* WHERE SHE WILL BE THAT DAY. Full width because it is a sentence rather
   than a number: "Wren Brewing lot" does not fit beside two times on a
   phone. It is inside a labelled group now, so the box itself no longer has
   to carry the job of saying what it is. */
.day-where-input {
  width: 100%;
  min-height: var(--ctl-h);
  padding: 0 8px;
  border: 1px solid var(--line);
  border-radius: var(--ctl-r);
  background: var(--panel);
  color: var(--cream);
  font-family: var(--font-body);
  font-size: var(--fs-field);
}

/* The base showing through is the inheritance made visible, and it is drawn
   as the quiet thing it is rather than as text she typed. The sentence under
   the box is what actually says so; this only shows the value. */
.day-where-input::placeholder { color: var(--mauve2); opacity: 1; }

/* NOT SAVED YET, beside the button that would fix it. Amber, matching the
   dot on the tab, so the two are one signal in two places rather than two
   signals. Margin-bottom of nothing: it sits directly on the Save button,
   because a state about a control belongs against the control. */
.unsaved {
  color: var(--warn);
  margin: 0 0 -2px;
}

.unsaved[hidden] { display: none; }

/* The character count and Clear, on one line under the box. The count is
   quiet and on the left; Clear is a real control and sits where a Save would
   - she is more likely to reach for it than to read the number. */
.msg-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-top: 7px;
}

.msg-foot .btn { min-height: var(--ctl-h); padding: 0 var(--ctl-pad); font-size: var(--ctl-fs); }

/* ---- what kind of seller this is ------------------------------------- */

/* A SEGMENTED PAIR, NOT A SWITCH. "Truck: off" does not say that the other
   thing is a kitchen, so both answers are on screen and one of them is
   chosen - the same shape, and the same four numbers, as every other
   segmented control in the product. Two columns at any width: there are
   exactly two answers and they are short. */
.kindpick {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--ctl-gap);
  padding: 3px;
  border: 1px solid var(--line);
  border-radius: var(--ctl-r-out);
  background: var(--panel);
}

.kindopt {
  display: flex; flex-direction: column; gap: 2px;
  padding: 9px 10px;
  border: 0;
  border-radius: var(--ctl-r);
  background: none;
  color: var(--mauve);
  text-align: left;
  cursor: pointer;
  transition: background 0.12s ease, color 0.12s ease;
}

.kindopt[aria-checked="true"] { background: var(--surface); color: var(--cream); }

.kindopt-name { font-size: var(--ctl-fs); font-weight: var(--ctl-fw); }
.kindopt-sub { font-size: var(--fs-tag); color: var(--mauve2); line-height: 1.3; }
.kindopt[aria-checked="true"] .kindopt-sub { color: var(--mauve); }

/* A row that stacks rather than putting its control on the right: the picker
   is two columns wide and a label beside it would leave neither room. */
.row-stack { flex-direction: column; align-items: stretch; }
.row-stack .row-label { margin-bottom: 7px; }

/* A schedule row's weekday picker gets the same full line the date input
   does - they are the same field asked two ways.

   RESCOPED FROM .date-row, which was on every schedule row despite its name
   and so was carrying the weekly select too. The name now means what it
   says: .sched-row is any row, and there is no date-only container left. */
.sched-group > .row-main select {
  width: 100%;
  min-height: var(--ctl-h);
  padding: 0 8px;
  border: 1px solid var(--line);
  border-radius: var(--ctl-r);
  background: var(--panel);
  color: var(--cream);
  /* NOT `font: inherit`. The shorthand took the 15px around it, which
     is a focusable field under the floor - and being a shorthand it is
     invisible to anything grepping for font-size. */
  font-family: inherit;
  font-size: var(--fs-field);
}
/* Why a repeating row has no limit, on its own line under it. */
/* The line under a control that explains it. Sits immediately after the
   thing it is about - under the day for "no order limit", under the location
   box for what empty means - rather than at the bottom of the row where it
   used to be, two controls away from either. */
.sched-why {
  color: var(--mauve2);
  font-size: var(--fs-desc);
  line-height: 1.35;
}

/* The preview: one line per day, what it is and what it offers. */
.sched-prev-day { display: flex; justify-content: space-between; gap: 12px; padding: 6px 0; }
.sched-prev-day + .sched-prev-day { border-top: 1px solid var(--line); }
.sched-prev-when { color: var(--cream); }
.sched-prev-times { color: var(--mauve2); white-space: nowrap; }

/* The order cap sits beside the times. Narrow on purpose: it holds a number
   somebody types, not a value that needs room to be read. Only a dated row
   is given one - see drawSchedule - so this needs no date-only selector. */
.day-cap { display: flex; align-items: center; gap: 6px; }
.day-cap input {
  width: 68px;
  min-height: var(--ctl-h);
  padding: 0 8px;
  border: 1px solid var(--line);
  border-radius: var(--ctl-r);
  background: var(--panel);
  color: var(--cream);
  /* NOT `font: inherit`. The shorthand took the 15px around it, which
     is a focusable field under the floor - and being a shorthand it is
     invisible to anything grepping for font-size. */
  font-family: inherit;
  font-size: var(--fs-field);
}

/* A DAY THAT HAS FILLED, still on the screen. Struck through rather than
   removed: a customer sent a link for the fourth has to learn it sold out, and
   a day that vanishes reads as the kitchen cancelling. */
.chip-day.sold-out,
.slot-day.sold-out { opacity: 0.55; }
.chip-day.sold-out { pointer-events: none; }
/* The strike goes on the day and its date - the thing that is no longer
   available - and never on the words saying so. */
.chip-day.sold-out .chip-word,
.chip-day.sold-out .chip-date { text-decoration: line-through; }
.chip-gone {
  display: block;
  margin-top: 2px;
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--mauve2);
}

.slot {
  position: relative;
  min-height: var(--ctl-h);
  padding: 0 var(--ctl-pad);
  display: inline-flex; align-items: center;
  border-radius: var(--ctl-r);
  border: 1px solid var(--line);
  background: var(--panel);
  color: var(--mauve);
  font-family: var(--font-mono);
  font-size: var(--ctl-fs);
  cursor: pointer;
  transition: transform 0.12s ease;
}

.slot::after {
  content: '';
  position: absolute; left: 0; right: 0; top: 50%;
  transform: translateY(-50%);
  height: var(--tap);
}

.slot:active { transform: scale(0.94); }
.slot[aria-pressed="true"] { background: var(--cream); color: var(--ink); border-color: var(--cream); }

/* ------------------------------------------------------------ order code */

.code-badge {
  font-family: var(--font-display);
  font-size: 52px;
  letter-spacing: 0.08em;
  text-align: center;
  color: var(--paperink);
  line-height: 1.1;
}

/* -------------------------------------------------------------- statuses */

/* Status colours are fixed tokens so they mean the same thing in every shop. */
.st-pending { color: var(--lime); }
.st-confirmed { color: var(--accent-text); }
.st-ready { color: var(--fresh); }
.st-picked_up { color: var(--mauve2); }
.st-cancelled { color: var(--coral); }

.status-line {
  display: flex; align-items: center; gap: 10px;
  font-family: var(--font-mono);
  font-size: var(--fs-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}

.status-line .dot {
  width: 9px; height: 9px; border-radius: var(--r-pill);
  background: currentColor;
  box-shadow: 0 0 8px currentColor;
}

/* Progress rail on the customer status page. */
.track { display: flex; flex-direction: column; gap: 0; margin: 4px 0; }

.step {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 0 0 18px;
  position: relative;
}

.step:last-child { padding-bottom: 0; }

.step .bead {
  flex-shrink: 0;
  width: 18px; height: 18px;
  border-radius: var(--r-pill);
  border: 2px solid var(--line2);
  background: var(--surface);
  margin-top: 1px;
}

/* The connector between beads. */
.step:not(:last-child)::before {
  content: '';
  position: absolute;
  left: 8px; top: 20px; bottom: 4px;
  width: 2px;
  background: var(--line2);
}

.step.done .bead { border-color: var(--fresh); background: var(--fresh); }
.step.done:not(:last-child)::before { background: var(--fresh); }
.step.at .bead { border-color: var(--fresh); background: var(--surface); box-shadow: 0 0 10px var(--fresh); }
.step .what { font-size: var(--fs-body); color: var(--mauve2); }
.step.done .what, .step.at .what { color: var(--cream); }
.step .when { font-size: var(--fs-tag); font-family: var(--font-mono); color: var(--mauve2); margin-top: 2px; }

/* ------------------------------------------------------- owner order rail */

/* The rail is a list of receipts inside one block, so it does its own spacing
   rather than inheriting the stack's - but it uses the same token. */
.order-card { margin-bottom: var(--stack); }

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

.order-code {
  font-family: var(--font-display);
  font-size: var(--fs-head);
  letter-spacing: 0.05em;
  color: var(--paperink);
}

.order-when { font-family: var(--font-mono); font-size: var(--fs-tag); color: var(--paperink); opacity: 0.7; }

.order-who {
  display: flex; justify-content: space-between; gap: 10px;
  font-size: var(--fs-label);
  margin-top: 2px;
}

/* The .st-* colours above are tuned for the dark surface and wash out on cream
   receipt stock, so on paper each one is mixed down toward --paperink. Same
   tokens, same meaning, legible on both grounds. */
.order-state.st-pending { color: color-mix(in srgb, var(--lime) 40%, var(--paperink)); }
.order-state.st-confirmed { color: color-mix(in srgb, var(--accent) 45%, var(--paperink)); }
.order-state.st-ready { color: color-mix(in srgb, var(--fresh) 40%, var(--paperink)); }
.order-state.st-picked_up { color: var(--paperink); opacity: 0.55; }
.order-state.st-cancelled { color: color-mix(in srgb, var(--coral) 55%, var(--paperink)); }

.order-state {
  display: inline-block;
  padding: 3px 9px;
  border-radius: var(--r-pill);
  border: 1.5px solid currentColor;
  font-size: var(--fs-tag);
  letter-spacing: var(--track-cap);
  text-transform: uppercase;
}

.order-acts { display: flex; gap: 10px; margin-top: 12px; }
.order-acts .btn { flex: 1; font-size: var(--fs-body); }
.order-acts .btn.ghost { flex: 0 0 auto; padding: 0 16px; }

/* The Orders filters. Same rail, same chips, same one-way scroll, same reason
   for the padding and for pulling it back out of the layout. */
.rail-filters {
  display: flex; gap: var(--ctl-gap); align-items: center;
  overflow-x: auto;
  overflow-y: hidden;
  margin: calc(var(--stack) * -1) calc(var(--pad) * -1);
  padding: var(--stack) var(--pad);
}

/* A quiet dot next to the Orders tab when something new lands. */
.tabbtn { position: relative; }

.tabbtn .badge {
  position: absolute;
  top: 6px; left: 50%;
  margin-left: 10px;
  min-width: 18px; height: 18px;
  padding: 0 5px;
  border-radius: var(--r-pill);
  background: var(--lime);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: 10px;
  line-height: 18px;
  text-align: center;
}

/* NOT A COUNT, SO NOT A NUMBER. The Orders badge says how many tickets are
   waiting; this says one thing is true of the Kitchen tab and there is
   nothing to count. Same slot and same geometry so the bar does not shift
   between them, drawn down to a dot.

   AMBER, WHICH IS THE RAMP'S OWN WORD FOR IT. The tool palette says it in as
   many words: green is open, running, go; red is stop, 86, cancelled; amber
   is timing and warning. Unsaved work is neither running nor stopped - it is
   waiting on her, which is what amber means here and nowhere else. */
.tabbtn .badge.dot {
  min-width: 8px; width: 8px; height: 8px;
  top: 9px;
  margin-left: 12px;
  padding: 0;
  background: var(--warn);
}

/* ------------------------------------------------------------ pay notice */

.notice {
  padding: 14px 16px;
  border-radius: var(--r-input);
  border: 1px dashed var(--line2);
  background: var(--panel);
  color: var(--mauve);
  font-size: var(--fs-sm);
  line-height: 1.5;
}

.notice b { color: var(--cream); font-weight: 600; }

/* ---- what to make -------------------------------------------------------

   Her open orders added up, over the tickets they came from. It is a count
   sheet and it is read standing up with both hands busy, so the NUMBER leads
   every row and sits in one column: she reads down the left edge and gets
   the quantities without reading a single dish name. Mono, because mono is
   the only thing that keeps 8 and 12 in the same column.

   NO PANEL AND NO BORDER AROUND THE WHOLE THING. #order-demand is a notice -
   dashed, boxed, something has gone wrong. This is neither a warning nor an
   interruption; it is the top of the list it belongs to, so it is separated
   by space and a rule rather than made into a card sitting on the page. */
.make {
  margin: 0 0 var(--stack);
  padding-bottom: 14px;
  border-bottom: 1px solid var(--line2);
}

.make > .label { margin: 0 0 var(--stack); }

/* The day headings are .slot-day, the same class and the same words the
   ticket list uses below - one vocabulary for a pickup day across the whole
   product. Only the spacing above a repeat heading is this file's business. */
.make .slot-day { margin-bottom: 8px; }
.make-item + .slot-day { margin-top: 16px; }

.make-item + .make-item { margin-top: 10px; }

.make-line {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin: 0;
  color: var(--cream);
  font-size: var(--fs-body);
}

.make-name { font-weight: 600; }

/* THE COLUMN. Fixed width rather than flex-none so 8 and 12 line up on their
   left edge down the whole sheet, including the indented choice rows - those
   share the class and therefore the column, which is what makes a group's
   counts scannable against the dish total above them. */
.make-qty {
  flex: none;
  min-width: 2.2em;
  /* RIGHT, SO THE NUMBERS SHARE AN EDGE AND THE NAMES SHARE ANOTHER. Left
     aligned, 8 and 12 start together and end ragged, and the dish names after
     them step in and out by a digit. min-width rather than width because a
     kitchen with a 136 in it must never clip the count - the column grows and
     that day's names sit one digit further in, which is the right trade. */
  text-align: right;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--cream);
}

.make-group {
  margin: 8px 0 3px;
  padding-left: calc(2.2em + 10px);
  font-family: var(--font-mono);
  font-size: var(--fs-tag);
  letter-spacing: var(--track-cap);
  text-transform: uppercase;
  color: var(--mauve2);
}

.make-choices {
  list-style: none;
  margin: 0;
  padding: 0 0 0 calc(2.2em + 10px);
}

.make-choices li {
  display: flex;
  align-items: baseline;
  gap: 10px;
  color: var(--mauve);
  font-size: var(--fs-sm);
}

/* A choice count is subordinate to the dish count above it and is coloured
   to say so - same column, quieter ink. */
.make-choices .make-qty { color: var(--mauve); }

/* ONE LINE PER ANSWER, NEVER COUNTED TOGETHER: two cakes reading the same
   words are two cakes. Under one .make-group heading, exactly like the choice
   tallies, because the question is asked once on the menu and belongs over
   the answers rather than restamped down the left of each one.

   THE WORDS COME FIRST AND TAKE THE ROOM. They are what she is piping, so
   they lead the line and wrap onto as many as they need - never truncated,
   because an elided answer sends her back to the ticket this screen exists to
   save her opening. The count and the order code trail behind, and neither is
   pushed to the far edge: `margin-left: auto` on the code is what stranded it
   alone on a second line as soon as an answer was long enough to wrap. */
.make-answers {
  list-style: none;
  margin: 0;
  padding: 0 0 0 calc(2.2em + 10px);
}

.make-answers li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 8px;
  padding: 3px 0;
  font-size: var(--fs-sm);
  color: var(--cream);
}

.make-ans-text { font-weight: 600; }

.make-ans-qty,
.make-ans-code {
  flex: none;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--mauve2);
}

.make > .muted { margin: 12px 0 0; }

/* Receipt footnote for the seller-side fee. */
.ticket-note {
  margin-top: 10px;
  padding-top: 8px;
  border-top: 1px dashed var(--paperline);
  font-size: var(--fs-tag);
  letter-spacing: var(--track-cap);
  opacity: 0.75;
}

/* ==========================================================================
   Photos: item tile, shop logo, upload progress, drag handle.
   No new colours. The empty tile is the same radial disc as .item-tile.
   ========================================================================== */

.photo-tile,
.logo-tile {
  position: relative;
  display: block;
  padding: 0;
  overflow: hidden;
  cursor: pointer;
  color: var(--mauve2);
  background: radial-gradient(70% 70% at 50% 35%, var(--disc1) 0%, var(--disc2) 100%);
  border: 1px solid var(--line2);
  border-radius: var(--r-tile);
  transition: transform 0.12s ease, border-color 0.18s ease;
}

/* Wide enough to judge a photo of a plate by, short enough that the rest of
   the editor is still on screen under it. */
.photo-tile { width: 100%; aspect-ratio: 16 / 10; }
.logo-tile { flex: none; width: 84px; height: 84px; }

.photo-tile:active,
.logo-tile:active { transform: scale(0.98); }
.photo-tile:hover, .photo-tile:focus-visible,
.logo-tile:hover, .logo-tile:focus-visible { border-color: var(--accent-line); }

/* Positioned, for the same reason .item-tile img is: a photograph fills the
   frame it was given at every shape, and never sizes itself from its own
   aspect ratio. Both of these already have a definite height, so this is
   belt and braces rather than a fix - but it is the same rule everywhere a
   photograph meets a frame, which is what stops the next one going wrong. */
.photo-tile img,
.logo-tile img {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  border-radius: inherit;
  display: block;
}

.photo-empty {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 8px;
  font-family: var(--font-mono);
  font-size: var(--fs-tag);
  letter-spacing: var(--track-cap);
  text-transform: uppercase;
}

.photo-empty svg {
  width: 26px; height: 26px;
  fill: none; stroke: currentColor; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
}

/* Upload progress, pinned to the bottom edge of the tile it belongs to. */
.photo-bar {
  position: absolute; left: 0; right: 0; bottom: 0;
  height: 5px;
  background: rgba(12, 8, 5, 0.66);
}

.photo-fill {
  display: block;
  height: 100%;
  width: 0;
  background: var(--accent);
  transition: width 0.18s ease;
}

.logo-row { display: flex; align-items: center; gap: 14px; }
.logo-row .muted { flex: 1; min-width: 0; }

/* Drag handle. touch-action is what stops the page scrolling instead of the
   row moving once a finger is down on it. */
.iconbtn.grip { cursor: grab; touch-action: none; }
.iconbtn.grip:active { cursor: grabbing; }

.list li.dragging { position: relative; z-index: 5; }

.list li.dragging .item {
  border-color: var(--accent-line);
  box-shadow: var(--shadow-float);
}

/* ------------------------------------------------------- install steps */

.steps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 12px; }

.steps li {
  display: flex; align-items: center; gap: 10px;
  color: var(--mauve);
  font-size: var(--fs-sm);
}

.step-n {
  flex: none;
  width: 22px; height: 22px;
  display: grid; place-items: center;
  border-radius: var(--r-pill);
  background: var(--panel);
  border: 1px solid var(--line);
  color: var(--cream);
  font-family: var(--font-mono);
  font-size: var(--fs-tag);
}

.inline-glyph {
  width: 16px; height: 16px;
  vertical-align: -3px;
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}

/* --------------------------------------------------------- store logo */

.store-id { display: flex; align-items: center; gap: 12px; }

.store-logo {
  flex: none;
  width: 46px; height: 46px;
  border-radius: var(--r-tile);
  object-fit: cover;
  border: 1px solid var(--line2);
}
/* ==========================================================================
   Marketing site. Same language as the app: Anton display, the warm bloom,
   receipt tickets, brick accent.

   WHAT IS DELIBERATELY NOT UNIFORM HERE. Every section used to be the same
   shape - mono eyebrow, Anton headline, grid of rounded cards - and a page
   built that way reads as a template however good the words are. So the
   sections below each get their own geometry, and no two that touch share
   one:

     hero      full-bleed photograph, asymmetric, receipt over the edge
     problem   two columns, a DM thread on one side and bare lines on the other
     steps     light cream ground, one horizontal flow, no boxes at all
     trucks    full-bleed photograph, one line over it, nothing else
     demo      asymmetric and mirrored: the phone leads, the words follow
     pricing   two plans and a strip of arithmetic under them
     faq       one narrow column
     close     centred, on the bloom

   There are exactly two mono eyebrows on the whole page, in the hero and over
   the pricing. The rest of the sections open on their headline.
   ========================================================================== */

.site { background: var(--ink); min-height: 100dvh; }

.wrap { width: 100%; max-width: 1060px; margin: 0 auto; padding: 0 var(--pad); }
.wrap-narrow { max-width: 720px; }

/* The nav is sticky, so an anchor jump has to stop short of it or the section
   heading lands underneath. */
.band[id], section[id] { scroll-margin-top: 72px; }

/* Grid children default to min-width:auto, which lets a wide child (the demo
   phone, a long word) hold the column open and push the page sideways. */
.hero-grid > *,
.demo-grid > *,
.plans > *,
.flow > *,
.problem-grid > * { min-width: 0; }

html { scroll-behavior: smooth; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

/* ------------------------------------------------------------ photography */

/* One shape for every photograph on the site. The frame holds the aspect
   ratio and a warm ground, so the page is laid out identically whether the
   file is there, still loading, or missing altogether.

   MISSING FILES. marketing.js marks the frame .img-out when an image errors.
   The picture goes, the frame stays, and the section keeps its geometry - the
   page never collapses around a 404. */
.shot {
  position: relative;
  overflow: hidden;
  background: radial-gradient(70% 70% at 50% 35%, var(--disc1) 0%, var(--disc2) 100%);
}

.shot img {
  display: block;
  width: 100%; height: 100%;
  object-fit: cover;
  /* Fades in rather than snapping, and stays put if it never arrives. */
  opacity: 0;
  transition: opacity 0.4s ease;
}

.shot img.ready { opacity: 1; }

/* Set by marketing.js on the frame when the file did not load. The picture
   goes; the frame, its ground and its size stay. */
.img-out img { display: none; }

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

/* A photograph the full width of the viewport, with the text sitting on it.
   The scrim is a token gradient rather than a flat wash so the top of the
   picture stays readable while the bottom carries type. */
.bleed { position: relative; isolation: isolate; }

.bleed .shot { position: absolute; inset: 0; }

.bleed .scrim-layer {
  position: absolute; inset: 0;
  background:
    linear-gradient(180deg,
      color-mix(in srgb, var(--ink) 55%, transparent) 0%,
      color-mix(in srgb, var(--ink) 30%, transparent) 38%,
      color-mix(in srgb, var(--ink) 88%, transparent) 100%);
}

.bleed .wrap { position: relative; z-index: 1; }

/* -------------------------------------------------------------- site nav */

.site-nav {
  position: sticky; top: 0; z-index: 40;
  padding-top: var(--safe-t);
  background: color-mix(in srgb, var(--ink) 92%, transparent);
  border-bottom: 1px solid var(--line);
  backdrop-filter: blur(10px);
}

.nav-inner { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 64px; }

.nav-brand {
  display: flex; align-items: center; gap: 10px;
  color: var(--cream);
  text-decoration: none;
  font-size: 22px;
}

/* The brand mark. It used to be a monochrome glyph tinted with --accent-ink
   and set on an --accent tile, because the glyph had no colour of its own. The
   real logo is brick, so the tile goes: an accent mark on an accent tile
   would be invisible. It now sits on the page ground as itself. */
.nav-mark {
  flex: none;
  width: 34px; height: 34px;
  display: grid; place-items: center;
}

.nav-mark img { width: 100%; height: 100%; object-fit: contain; display: block; }

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

.nav-links a {
  display: inline-flex; align-items: center;
  min-height: var(--tap);
  padding: 0 12px;
  color: var(--mauve);
  text-decoration: none;
  font-size: var(--fs-sm);
  font-weight: 600;
}

.nav-links a:hover { color: var(--cream); }

/* Written as a descendant so it outweighs the plain nav link above it without
   reaching for !important. */
.nav-links a.nav-cta {
  color: var(--accent-ink);
  font-size: var(--fs-sm);
  padding: 0 20px;
  /* A BUTTON IS ONE LINE. Wrapped, "Open a kitchen" became a two line block
     56px tall sitting next to a 52px pill, which is the one thing in the nav
     that looked unconsidered. There is no width at which breaking this label
     reads better than shortening it, so it never breaks and the narrow rule
     below decides what it says instead. */
  white-space: nowrap;
}

.nav-links a.btn.ghost.nav-cta { color: var(--cream); }

/* The wide label is the one the button carries by default; the narrow one is
   dark until the media query below turns them over. Declared here rather than
   only in the query so a page that uses the markup is never briefly showing
   both labels at once. */
.nav-cta-short { display: none; }

/* THE LANGUAGE SWITCH, IN THE NAV RATHER THAN THE FOOTER. Somebody who cannot
   read the hero should not have to scroll the whole page to find the way out
   of it, which is what the storefront's footer placement would mean here.

   A <button> and not an <a>, and that is load-bearing: the rule below hides
   every .nav-links a that is not the CTA at 520px, and a phone is exactly
   where this matters most. Bordered so it reads as a control next to two
   plain text links, and quiet so it does not compete with the CTA. */
.nav-lang {
  appearance: none; -webkit-appearance: none;
  display: inline-flex; align-items: center;
  min-height: var(--tap);
  padding: 0 12px;
  background: none;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  color: var(--mauve);
  font-family: var(--font-body);
  font-size: var(--fs-sm);
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
}

.nav-lang:hover { color: var(--cream); }

/* Narrow: the brand text goes and the two survivors - the switch and the CTA -
   tighten up, so all three fit a 320px nav without wrapping it. */
@media (max-width: 520px) {
  .nav-lang { padding: 0 10px; font-size: var(--fs-tag); }
  /* Level with the pill beside it: same 10px inset, so the two controls read
     as a pair rather than as a button and an afterthought. */
  .nav-links a.nav-cta { padding: 0 12px; }

  /* THE SHORT LABEL, AND WHY THE BUTTON IS NOT JUST ALLOWED TO BE WIDE.

     Three things share this row: the wordmark, the language pill and this. At
     390px the nav is 354px wide, and "Open a kitchen" takes 120 of it - which
     leaves "LITTLE KITCHEN" 113px of the 123 it needs and breaks it over two
     lines. Spanish is worse: "Abrir una cocina" is 129 and leaves 108.

     So the button says less rather than the wordmark breaking. "Start" is 56px
     and "Empezar" 74, and the wordmark is back on one line at 360 as well as
     390, in both languages. Nothing above 520 changes. */
  .nav-cta-full { display: none; }
  .nav-cta-short { display: block; }
}

/* On a phone the text links crowd the button out, so the button stays.

   SIX HUNDRED AND NOT 520, because the row is three links now that
   /kitchen-on-your-phone sits in it, and three do not fit where two did. Below
   600 every item wraps to two lines and then to three - "On / your / phone" -
   which has stopped being a nav and become a paragraph with links in it.
   Measured on the front page, which carries the longest row: three-line
   wrapping is there at 560 and gone by 590, so the collapse goes above it.
   Spanish is a shade shorter across the whole row, so English is the case that
   sets the number. */
@media (max-width: 600px) {
  .nav-links a:not(.nav-cta) { display: none; }
}

/* ==========================================================================
   1. HERO - a full-bleed photograph with the words on it and the receipt
   hanging off the right edge. Asymmetric on purpose: 1.15fr / 0.85fr, and
   the ticket is allowed to overlap the column boundary.
   ========================================================================== */

.hero {
  position: relative; isolation: isolate;
  padding: 52px 0 60px;
  /* The house bloom stays under the photograph, so a missing hero.jpg leaves
     the section looking like every other hero in the product rather than
     leaving a hole. */
  background: radial-gradient(120% 80% at 50% -10%, var(--heroglow) 0%, var(--ink) 62%);
}

.hero .shot { position: absolute; inset: 0; }

.hero .scrim-layer {
  position: absolute; inset: 0;
  background:
    linear-gradient(180deg,
      color-mix(in srgb, var(--ink) 74%, transparent) 0%,
      color-mix(in srgb, var(--ink) 62%, transparent) 55%,
      var(--ink) 100%),
    radial-gradient(90% 70% at 18% 40%,
      color-mix(in srgb, var(--ink) 80%, transparent) 0%,
      transparent 70%);
}

.hero-grid { position: relative; z-index: 1; display: grid; gap: 34px; align-items: center; }

/* ---- both doors, in the space under the nav -----------------------------

   index.html carries why they are there and why only one of them is a link;
   this is how they sit.

   A FILLED BRICK BLOCK, NOT BRICK TEXT. Brick is #a8402f and this sits on a
   photograph under a scrim - as type it measures about 3:1 there and fails AA
   outright, which is the whole reason --accent-text exists and why the quiet
   links in .hero-cta use it instead. Filling keeps the colour the actual
   brick and puts --accent-ink's cream on it, which is the pairing the ramp is
   declared with.

   TWO SHAPES, ONE BLOCK. On a phone it is a stacked pair of lines that hugs
   its own longest line. On a wide screen it is a single rule-width row under
   the nav. Same element and same markup order either way - only the direction
   the lines run changes - so there is one of these to keep right. */
.hero-doors-wrap {
  /* .hero .shot and .hero .scrim-layer are absolutely positioned over the
     whole section, so anything meant to be read above them needs its own
     stacking position. .hero-grid carries the identical pair. */
  position: relative;
  z-index: 1;
  /* TIGHT ON PURPOSE, AND THE NUMBER IS A BUDGET RATHER THAN A TASTE.

     The arrow takes 29px of width out of the text column on a phone, and the
     English buyer line needs 322px to sit on one line where there are now 297.
     So it wraps to two, and this block is one line taller than it was before
     the arrow came out of the sentence. The headline is not allowed to move
     down for that, so the line it gained is paid for here and in the padding
     below: block plus margin comes to 96px against the 97px the stacked
     version cost, which puts the h1 a pixel higher than it was rather than
     eleven lower. The wide layout overrides both, where there is room. */
  margin-bottom: 10px;
}

.hero-doors {
  display: flex;
  align-items: center;
  gap: 14px;
  width: fit-content;
  max-width: min(100%, 42ch);
  padding: 6px 13px 7px;
  border-radius: var(--r-input);
  background: var(--accent);
  color: var(--accent-ink);
}

/* min-width:0 so a long line wraps inside the flex item instead of holding
   the box open past its max-width. */
.hero-doors-lines { min-width: 0; }

.hero-door-sell {
  font-size: var(--fs-body);
  font-weight: 700;
  line-height: 1.3;
}

/* Smaller, and stepped back without leaving the brick - held at 0.9 rather
   than mixed toward it, so there is one colour on this block to keep right. */
.hero-door-eat {
  font-size: var(--fs-desc);
  line-height: 1.35;
  opacity: 0.9;
}

/* The link inherits the cream it is printed on rather than taking
   --accent-text, which is a lightened brick meant for the dark ground and
   would be close to unreadable here. The underline is what marks it. */
.hero-door-eat a {
  color: inherit;
  font-weight: 700;
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* A SCROLL CUE, NOT A WORD IN THE SENTENCE.

   It used to be inline, after "Start here". Once that sentence wrapped on a
   phone the arrow landed at the end of a line with the buyer line directly
   beneath it, and it read as pointing at that - which is the one thing it is
   not for. As a flex item on the right edge it is clear of every line of type,
   centred against both of them by the box's own align-items, and there is no
   text it can be read as belonging to.

   THE ONLY MOVING THING ABOVE THE FOLD, so it is slow and small: 4px, two
   seconds, eased at both ends so it has no hard turn at either extreme. One
   axis only - a horizontal component is exactly what turns a bob into a shake.
   It is a flex item rather than absolutely positioned precisely so the bob can
   own `transform`: centring it with a translate would have to be undone and
   re-applied inside the keyframes, and the next person to touch either would
   get a jump. */
.hero-door-arrow {
  flex: none;
  display: block;
  width: 15px; height: 15px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.1;
  stroke-linecap: round;
  stroke-linejoin: round;
  animation: lk-bob 2s ease-in-out infinite;
}

@keyframes lk-bob {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(4px); }
}

@media (prefers-reduced-motion: reduce) {
  /* The arrow stays and stops. It is pointing down at the pitch, which is the
     whole of what it says; the bob was only ever how it asked to be noticed. */
  .hero-door-arrow { animation: none; }
}

.hero-h1 {
  font-size: clamp(42px, 9vw, 72px);
  margin: 16px 0 0;
  max-width: 13ch;
  text-shadow: 0 2px 18px rgba(0, 0, 0, 0.45);
}

.lede {
  margin-top: 18px;
  max-width: 44ch;
  color: var(--cream);
  font-size: 17px;
  line-height: 1.55;
}

.hero-cta { display: flex; flex-direction: column; align-items: flex-start; gap: 12px; margin-top: 28px; }
.hero-eat { margin-top: 4px; color: var(--mauve); }
/* The second quiet line under the CTA. Same treatment as .hero-eat and no
   margin of its own, so the pair sits on the flex gap and reads as two lines
   of one aside rather than two separate remarks. */
.hero-install { color: var(--mauve); }
.hero-eat a, .hero-install a { color: var(--accent-text); }
.hero-cta .btn { padding: 0 34px; }
.hero-cta .cap { color: var(--cream); opacity: 0.8; }

/* Free-standing printed receipt, per the reference: tilt plus a real shadow. */
.hero-art { display: flex; justify-content: center; }

/* A drop-shadow rather than a box-shadow, so it follows the torn edge instead
   of drawing a rectangle behind it. Same blacks as --shadow-float. */
.ticket-tilt {
  max-width: 340px;
  width: 100%;
  transform: rotate(-1.5deg);
  filter: drop-shadow(0 18px 40px rgba(0, 0, 0, 0.55));
}

/* ==========================================================================
   2. THE PROBLEM - no cards. One headline, a phone showing a real mess of a
   DM thread, and the rest of it as bare hanging lines beside it. Showing it
   is shorter than describing it, so the thread carries most of the section.
   ========================================================================== */

.band { padding: 58px 0; }

.sec-h2 {
  font-size: clamp(28px, 5.4vw, 44px);
  margin: 0 0 26px;
  max-width: 18ch;
}

.problem { background: var(--ink); border-top: 1px solid var(--line); }
.problem-grid { display: grid; gap: 34px; align-items: start; }
.problem .sec-h2 { max-width: 15ch; }

/* Hanging lines: the number sits in the margin, the sentence keeps the
   measure. No border, no fill, nothing round. */
.pains { list-style: none; margin: 0; padding: 0; display: grid; gap: 0; }

.pain {
  display: grid;
  grid-template-columns: 2.6ch 1fr;
  gap: 14px;
  padding: 15px 0;
  border-top: 1px solid var(--line);
  color: var(--mauve);
  font-size: var(--fs-body);
  line-height: 1.5;
}

.pain:first-child { border-top: 0; padding-top: 0; }

/* The first one is the one everybody recognises, so it is set at reading
   size and the rest fall back to a list under it. */
.pain-lead {
  color: var(--cream);
  font-size: 19px;
  line-height: 1.4;
  padding-bottom: 18px;
}

.pain-n {
  font-family: var(--font-mono);
  font-size: var(--fs-label);
  letter-spacing: var(--track-cap);
  color: var(--accent-text);
  padding-top: 5px;
}

.pain-lead .pain-n { padding-top: 7px; }

.band-note {
  margin-top: 24px;
  color: var(--mauve2);
  font-size: var(--fs-sm);
  max-width: 46ch;
}

/* ---- the inbox -----------------------------------------------------------

   NOT a thread. Six separate one-to-one conversations, which is the actual
   shape of the problem: every person is talking to her privately, none of
   them can see the others, and the order is spread across all six. A single
   scrolling feed with different names in it reads as a group chat and says
   the opposite of what this section is for.

   Narrow: the inbox alone, full width, no device frame. It is already being
   read on a phone.
   Wide: the inbox with one of its rows opened beside it, which is the proof
   that the rows are separate conversations rather than one feed. */

/* DECORATIVE MOCKUPS ARE INERT.
   The inbox, the thread, the three step screens and the hero receipt are
   pictures of the product, not the product. pointer-events:none is the whole
   fix in one line: a wheel over them scrolls the page instead of a container
   inside them, a click lands on the page rather than focusing or scrolling
   something in the mockup, and nothing in them can be dragged or selected.
   They are aria-hidden already, so nothing here is reachable by keyboard
   either. overflow:clip on the frames means a stray click can never scroll a
   hidden overflow, which is the "jumps when clicked" failure.

   The demo storefront is deliberately NOT in this list. It is the one mockup
   on the page that is real and meant to be tapped. */
.inbox-art,
.flow-screen,
.hero-art {
  pointer-events: none;
  user-select: none;
  -webkit-user-select: none;
  overscroll-behavior: contain;
}

.inbox-art img,
.flow-screen img,
.hero-art img {
  -webkit-user-drag: none;
  user-drag: none;
  pointer-events: none;
}

.inbox,
.flow-screen,
.device { overflow: clip; }

.inbox-art { display: grid; gap: 16px; }

.inbox {
  border-radius: var(--r-card);
  border: 1px solid var(--line2);
  background: var(--ink);
  overflow: hidden;
}

.inbox-top {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 13px 16px;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
}

.inbox-top .cap { color: var(--mauve); letter-spacing: var(--track-cap); }

/* The unread count, as a real badge rather than a sentence. */
.inbox-badge {
  flex: none;
  min-width: 22px;
  padding: 2px 7px;
  border-radius: var(--r-pill);
  background: var(--coral);
  color: #2b1109;
  font-family: var(--font-mono);
  font-size: var(--fs-tag);
  font-weight: 500;
  text-align: center;
}

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

.inbox-row {
  display: grid;
  grid-template-columns: 38px 1fr auto;
  align-items: center;
  gap: 12px;
  padding: 12px 16px;
  border-top: 1px solid var(--line);
}

.inbox-row:first-child { border-top: 0; }

/* An initial, not a photograph: these are six imaginary people and a real
   face in a 38px circle is exactly the crop this page is fixing elsewhere. */
.inbox-who {
  width: 38px; height: 38px;
  display: grid; place-items: center;
  border-radius: var(--r-pill);
  background: var(--panel);
  border: 1px solid var(--line2);
  color: var(--mauve);
  font-family: var(--font-mono);
  font-size: 14px;
}

/* The number nobody has saved. It is a different kind of row and it looks it. */
.inbox-who-x { color: var(--mauve2); background: transparent; border-style: dashed; }

.inbox-main { min-width: 0; display: block; }

.inbox-line {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
}

.inbox-name {
  color: var(--cream);
  font-size: 14.5px;
  font-weight: 600;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.inbox-when {
  flex: none;
  font-family: var(--font-mono);
  font-size: var(--fs-tag);
  letter-spacing: 0.06em;
  color: var(--mauve2);
}

/* One line, clipped. A fragment of an order is the point: she cannot see the
   whole thing without opening it, and there are five more behind it. */
.inbox-frag {
  display: block;
  margin-top: 3px;
  color: var(--mauve);
  font-size: 13px;
  line-height: 1.35;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.inbox-row.unread .inbox-name { color: var(--cream); }
.inbox-row.unread .inbox-frag { color: var(--cream); }

.inbox-dot {
  flex: none;
  width: 9px; height: 9px;
  border-radius: var(--r-pill);
  background: var(--coral);
}

/* The opened conversation. Hidden until there is room for it to be a second
   thing rather than a repeat of the first. */
/* The opened conversation lives in a phone. Without it the bubbles sit on the
   page with nothing holding them and read as a layout that has come apart.
   Only ever drawn on a wide screen: below 1200px this whole block is display
   none, so the frame cannot appear on the device it is a picture of. */
.inbox-open { display: none; }

.device {
  border-radius: 26px;
  border: 1px solid var(--line2);
  background: var(--ink);
  box-shadow: 0 18px 40px rgba(0, 0, 0, 0.45);
  /* The bezel. A hairline inside the border reads as glass in a case. */
  outline: 6px solid var(--surface);
  outline-offset: -1px;
  padding: 0 0 16px;
}

/* Status bar. Drawn, not iconography: three shapes in CSS, no extra request. */
.device-bar {
  display: flex; align-items: center; justify-content: space-between;
  padding: 12px 18px 8px;
}

.device-time {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.04em;
  color: var(--cream);
}

.device-status { display: flex; align-items: center; gap: 5px; }

/* Signal: a stepped bar. */
.device-sig {
  width: 13px; height: 9px;
  background: linear-gradient(to top, var(--mauve) 0 3px, transparent 3px) 0 100% / 3px 3px no-repeat,
              linear-gradient(to top, var(--mauve) 0 6px, transparent 6px) 5px 100% / 3px 6px no-repeat,
              linear-gradient(to top, var(--mauve) 0 9px, transparent 9px) 10px 100% / 3px 9px no-repeat;
}

/* Wifi: a dot with an arc over it. */
.device-wifi {
  width: 11px; height: 9px;
  border: 2px solid var(--mauve);
  border-bottom: 0; border-left-color: transparent; border-right-color: transparent;
  border-radius: 11px 11px 0 0;
  position: relative;
}

.device-wifi::after {
  content: '';
  position: absolute;
  left: 50%; bottom: -2px;
  transform: translateX(-50%);
  width: 3px; height: 3px;
  border-radius: var(--r-pill);
  background: var(--mauve);
}

/* Battery: a body and a nub. */
.device-batt {
  width: 18px; height: 9px;
  border: 1px solid var(--mauve);
  border-radius: 3px;
  position: relative;
  background: linear-gradient(to right, var(--mauve) 0 60%, transparent 60%) no-repeat;
  background-size: calc(100% - 4px) calc(100% - 4px);
  background-position: 2px 2px;
}

.device-batt::after {
  content: '';
  position: absolute;
  right: -3px; top: 50%;
  transform: translateY(-50%);
  width: 2px; height: 4px;
  border-radius: 0 2px 2px 0;
  background: var(--mauve);
}

.device .inbox-open-top { padding: 0 16px 12px; margin: 0 2px; }
.device .inbox-open-body { padding: 14px 16px 0; }

.inbox-open-top {
  display: flex; align-items: center; gap: 10px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--line);
}

.inbox-open-body { padding-top: 14px; display: flex; flex-direction: column; gap: 8px; }

.msg {
  max-width: 86%;
  padding: 10px 13px;
  border-radius: 16px;
  font-size: 13.5px;
  line-height: 1.45;
  color: var(--cream);
  background: var(--panel);
  border-bottom-left-radius: 5px;
  align-self: flex-start;
}

/* The seller's own replies, so it reads as a conversation rather than as a
   list of demands. */
.msg.mine {
  align-self: flex-end;
  background: var(--accent);
  color: var(--accent-ink);
  border-bottom-left-radius: 16px;
  border-bottom-right-radius: 5px;
}

.thread-time {
  align-self: center;
  font-family: var(--font-mono);
  font-size: 9.5px;
  letter-spacing: var(--track-cap);
  color: var(--mauve2);
  padding: 4px 0;
}


/* ==========================================================================
   3. HOW IT WORKS - the light cream ground. The whole page is one warm brown
   otherwise, and the eye needs somewhere to rest. Receipt stock, because that
   is already where printed things live in this product.

   The three steps are a horizontal flow, not three cards: a rule runs through
   the numbers, and the type does the separating.
   ========================================================================== */

.band-paper {
  background: var(--paper);
  color: var(--paperink);
  border-top: 1px solid var(--paperline);
  border-bottom: 1px solid var(--paperline);
}

.band-paper .sec-h2 { color: var(--paperink); max-width: 20ch; }
.band-paper .label { color: #7a6a5c; }
.band-paper .muted { color: #6b5a4c; }
.band-paper .band-note { color: #7a6a5c; }

/* The three steps. Not three equal boxes: each one carries the real screen
   that step produces, and the numeral is set large enough to do the
   sequencing on its own.

   NARROW (the default): one column. The numeral is big, the screen is full
   width and flat, and there is no device frame anywhere, because the reader
   is holding the device.
   WIDE: three columns laid out like three phones on a counter, with a rule
   running across the tops of the numerals. */

.flow {
  display: grid;
  gap: 34px;
  margin: 8px 0 0;
  padding: 0;
  list-style: none;
}

.flow-step { position: relative; }

.flow-head { display: flex; align-items: baseline; gap: 14px; margin-bottom: 10px; }

/* Big enough to be the ordering device by itself. Outlined rather than
   filled, so three of them down a page do not read as three buttons. */
.flow-n {
  flex: none;
  font-family: var(--font-display);
  font-size: 44px;
  line-height: 0.9;
  letter-spacing: 0.01em;
  color: transparent;
  -webkit-text-stroke: 1.5px #c08a5e;
  text-stroke: 1.5px #c08a5e;
}

.flow-h3 {
  font-size: clamp(21px, 4.6vw, 26px);
  color: var(--paperink);
  margin: 0;
}

.flow-step > p {
  color: #6b5a4c;
  font-size: var(--fs-body);
  line-height: 1.55;
  max-width: 40ch;
  margin: 0;
}

/* The screen. A dark panel on the cream ground, which is what a phone
   actually looks like lying on a counter. No bezel, no notch, no rounded
   device shell: it is the content, not a picture of a handset. */
.flow-screen {
  margin-top: 18px;
  padding: 14px;
  border-radius: var(--r-card);
  background: var(--ink);
  border: 1px solid var(--line2);
  display: flex; flex-direction: column; gap: 10px;
}

.fs-label { color: var(--mauve2); letter-spacing: var(--track-cap); }

/* --- step 1: menu rows --- */

.fs-item {
  display: grid;
  grid-template-columns: 52px 1fr;
  gap: 12px;
  align-items: center;
  padding: 10px;
  border-radius: var(--r-tile);
  background: var(--surface);
  border: 1px solid var(--line);
}

.fs-dim { opacity: 0.55; }

.fs-tile {
  width: 52px; height: 52px;
  border-radius: 12px;
  overflow: hidden;
  background: radial-gradient(70% 70% at 50% 35%, var(--disc1) 0%, var(--disc2) 100%);
  border: 1px solid var(--line2);
}

.fs-tile img { width: 100%; height: 100%; object-fit: cover; display: block; }

.fs-item-main { min-width: 0; display: block; }
.fs-row { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }

.fs-name {
  color: var(--cream);
  font-size: 14px; font-weight: 600;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.fs-price {
  flex: none;
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--accent-text);
}

.fs-desc {
  display: block;
  margin-top: 2px;
  color: var(--mauve);
  font-size: 12px; line-height: 1.35;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.fs-tags { display: flex; gap: 6px; margin-top: 7px; }

.fs-tag {
  font-family: var(--font-mono);
  font-size: var(--fs-tag);
  letter-spacing: 0.06em;
  color: var(--mauve2);
  border: 1px solid var(--line2);
  border-radius: var(--r-pill);
  padding: 2px 8px;
}

/* --- step 2: the link --- */

.fs-link {
  display: flex; flex-direction: column; gap: 8px;
  padding: 14px;
  border-radius: var(--r-tile);
  background: var(--surface);
  border: 1px solid var(--accent-line);
}

.fs-url { color: var(--accent-text); font-size: 17px; line-height: 1.25; word-break: break-all; }
.fs-copy { color: var(--mauve2); letter-spacing: var(--track-cap); }

.fs-share { display: flex; flex-wrap: wrap; gap: 7px; }

.fs-chip {
  font-size: 12px;
  color: var(--mauve);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  padding: 6px 12px;
}

/* --- step 3: the ticket --- */

/* The ticket brings its own paper, so it needs no panel under it. */
.fs-ticket { max-width: 100%; }
.fs-ticket .ticket-body { padding: 12px 14px; }

/* ==========================================================================
   4. TRUCKS - one full-bleed photograph and one sentence. No eyebrow, no
   list, no card. It is the break in the page as much as it is a section.
   ========================================================================== */

.trucks { min-height: 62vh; display: flex; align-items: flex-end; padding: 88px 0 44px; }

.trucks .sec-h2 { font-size: clamp(30px, 6.4vw, 56px); margin: 0 0 18px; max-width: 16ch; }
.trucks p { color: var(--cream); font-size: 16px; line-height: 1.55; max-width: 44ch; opacity: 0.92; }
.trucks .sec-h2 { text-shadow: 0 2px 18px rgba(0, 0, 0, 0.5); }

/* Two short facts under the line, in mono, separated by a rule rather than
   put in boxes. */
.truck-facts {
  list-style: none;
  margin: 24px 0 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: 0 26px;
}

.truck-facts li {
  font-family: var(--font-mono);
  font-size: var(--fs-label);
  letter-spacing: var(--track-cap);
  text-transform: uppercase;
  color: var(--cream);
  padding: 10px 0;
  border-top: 1px solid var(--line2);
  flex: 1 1 220px;
}

/* ==========================================================================
   5. DEMO - mirrored against the hero: the phone is on the left and the words
   follow it, where the hero put the words first. Sits on --backdrop, the one
   step darker than the sections either side of it.
   ========================================================================== */

.band-demo { background: var(--backdrop); }
.demo-grid { display: grid; gap: 34px; align-items: center; }
.demo-copy .btn { margin-top: 6px; }
.demo-copy .sec-h2 { max-width: 16ch; }

.ticks { list-style: none; margin: 18px 0 22px; padding: 0; display: flex; flex-direction: column; gap: 10px; }

.ticks li {
  position: relative;
  padding-left: 24px;
  color: var(--mauve);
  font-size: var(--fs-sm);
  line-height: 1.5;
}

/* A rule in the margin, not a tick.
   A green or orange checkmark beside a feature is the most generic thing a
   pricing page can do, and this page already spends its numerals on the five
   pains and the three steps. So the feature lists get the third typographic
   device instead: a short accent rule hung in the margin, set on the first
   line's optical centre. Drawn in CSS, no icon and no request. */
.ticks li::before {
  content: '';
  position: absolute;
  left: 0; top: 0.72em;
  width: 13px; height: 2px;
  border-radius: 1px;
  background: var(--accent-text);
}

.band-paper .ticks li { color: #6b5a4c; }
.band-paper .ticks li::before { background: #a8552a; }

.phone-wrap { display: flex; flex-direction: column; align-items: center; gap: 12px; }

/* The demo runs at real phone width so the tap targets are honestly sized. */
.phone {
  width: 100%;
  max-width: min(380px, 100%);
  height: clamp(480px, 72vh, 640px);
  border-radius: 30px;
  border: 1px solid var(--line2);
  background: var(--ink);
  box-shadow: var(--shadow-frame);
  overflow: hidden;
}

.phone-screen { position: relative; height: 100%; overflow: hidden; }

/* A toast raised by something inside the phone belongs to the phone. Fixed, it
   would land in the middle of the marketing page with nothing around it to say
   what had just refused. */
.phone-screen .toast { position: absolute; top: 12px; }

/* THE LABEL, ABOVE THE MENU. It says the same thing the owner app's storefront
   preview says about the example kitchen it opens, in the same words, because
   it is the same disclosure. It sits above the phone rather than under it: a
   caption under a 640px demo is read after somebody has already decided Rosa
   is a real cook, which is too late to be a label. */
.demo-cap {
  margin: 0;
  max-width: 46ch;
  color: var(--mauve2);
  font-size: var(--fs-sm);
  line-height: 1.5;
  text-align: left;
  align-self: stretch;
}

.demo-cap .cap { display: block; color: var(--accent-text); margin-bottom: 4px; }

/* The walkthrough, offered beside the bullet that promises it. */
.demo-help { margin: 14px 0 0; font-size: var(--fs-sm); }
.demo-help a { color: var(--accent-text); }

.demo-view { display: flex; flex-direction: column; height: 100%; min-height: 0; }

.demo-head {
  flex: none;
  padding: 18px 16px 14px;
  background: radial-gradient(120% 80% at 50% -10%, var(--heroglow) 0%, var(--ink) 62%);
  border-bottom: 1px solid var(--line);
}

.demo-name { font-size: 24px; }

.demo-logo {
  flex: none;
  width: 56px; height: 56px;
  border-radius: var(--r-tile);
  overflow: hidden;
  background: radial-gradient(70% 70% at 50% 35%, var(--disc1) 0%, var(--disc2) 100%);
  border: 1px solid var(--line2);
}

/* A DELIBERATE CROP, not a centre one. rosa.jpg is a woman holding a tray:
   its centre is the tray, so object-position pulls the frame up to her face.
   The whole photograph is shown at size in .demo-who above; this is the
   thumbnail of it, and a thumbnail has to be of something. */
.demo-logo img {
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: 50% 18%;
  display: block;
}

.demo-tagline { margin-top: 6px; }
.demo-head .store-meta { margin-top: 10px; }

/* ---- Rosa, whole frame -------------------------------------------------

   She used to exist only as a 40px tile, which cropped her face in half and
   lost the tray she is holding. The photograph is square and she fills it, so
   showing it square at size IS the whole frame: nothing is cropped here. */

.demo-who {
  margin: 0 0 22px;
  display: grid;
  gap: 14px;
  justify-items: start;
}

.who-shot {
  display: block;
  width: min(260px, 62vw);
  aspect-ratio: 1;
  border-radius: var(--r-card);
  overflow: hidden;
  background: radial-gradient(70% 70% at 50% 35%, var(--disc1) 0%, var(--disc2) 100%);
  border: 1px solid var(--line2);
}

.who-shot img {
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
  opacity: 0;
  transition: opacity 0.4s ease;
}

.who-shot img.ready { opacity: 1; }

/* Same contract as every other photograph on this page: a file that is not
   there takes itself off and leaves the frame's own ground. */
.who-shot.img-out img { display: none; }

.who-cap { display: grid; gap: 5px; }
.who-cap .cap { color: var(--accent-text); letter-spacing: var(--track-cap); }
.who-line { color: var(--mauve); font-size: var(--fs-sm); line-height: 1.45; }

/* ---- the price moving --------------------------------------------------

   THE NUMBER THAT MOVES IS THE ONE IN THE LIST. While a dish is open its own
   row shows the running price rather than the sticker price, so the thing
   that changes as somebody picks a size is the thing they were already
   looking at, and the panel below does not have to repeat the dish's name and
   price back at them to have somewhere to put it.

   A one-shot animation, applied whenever the number differs from the last one
   drawn. It is the whole reason the demo opens with a dish already open. */

.demo-view .item-price { display: inline-block; }

.demo-view .item-price.bumped {
  animation: price-bump 0.42s ease;
  color: var(--accent-text);
}

@keyframes price-bump {
  0% { transform: scale(1); }
  38% { transform: scale(1.26); }
  100% { transform: scale(1); }
}

.demo-top {
  flex: none;
  display: flex; align-items: center; gap: 12px;
  padding: 12px 14px;
  background: var(--ink);
  border-bottom: 1px solid var(--line);
}

.demo-top h3 { font-size: var(--fs-head); }

.demo-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 12px 14px 16px;
  display: flex; flex-direction: column; gap: 10px;
}

/* The frame has a fixed height, so its children would otherwise flex-shrink to
   fit instead of letting the column scroll. The chip rail is the one that
   notices first: it gets squashed to a sliver. */
.demo-scroll > * { flex: none; }

/* The rail bleeds to the frame edge, matching the storefront. */
.demo-scroll .rail { margin: 0 -14px; padding: 2px 14px 6px; }
.demo-scroll .list { gap: 10px; }

/* The demo's dish photograph needs nothing of its own: .item-tile img is
   already absolutely filling its tile wherever a tile appears. */
.item-tile.has-photo { font-size: 0; background: var(--disc2); }

.demo-bar {
  flex: none;
  padding: 12px 14px calc(12px + var(--safe-b));
  background: var(--ink);
  border-top: 1px solid var(--line);
}

.demo-bar .btn { width: 100%; justify-content: space-between; }
.demo-bar .count { font-family: var(--font-mono); font-size: var(--fs-sm); opacity: 0.85; }

/* The demo is the one mockup on this page that is real, so it keeps its
   scrolling. What it does not keep is `overscroll-behavior: contain`, which
   .sheet sets because a modal sheet in the actual app must not scroll the page
   behind it. On a marketing page that same rule means a wheel over the demo
   dead ends and the page stops moving, which is the scroll trap. It chains
   instead: it takes the wheel while it has somewhere to go and hands it back
   to the page the moment it does not. */
.demo-scroll { overscroll-behavior: auto; }

.demo-top .sheet-close { width: 52px; height: 52px; }
.demo-code-note { text-align: center; border: 0; margin: 2px 0 12px; padding: 0; }

/* ---- the dish, opened in place ----------------------------------------

   The options used to arrive as a sheet over the whole phone, which is the
   one thing in this demo that read as a mockup rather than as a storefront: a
   popup covers the kitchen it came from, so while it was up there was no
   Rosa, no tagline and no menu behind it, only a floating card.

   Now the panel is the second half of the row that was tapped. The row keeps
   the top corners, the panel keeps the bottom ones, and the seam between them
   is a rule, so the two read as one card in a list rather than as a card and
   a thing under it. */
.demo-detail {
  padding: 14px;
  display: flex; flex-direction: column; gap: 12px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-top: 0;
  border-bottom-left-radius: var(--r-card);
  border-bottom-right-radius: var(--r-card);
}

.item.open {
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
  border-bottom-color: var(--line2);
}

/* The row clamps the description to one line and the panel below prints the
   whole of it, so while a dish is open the clipped copy comes off rather than
   sitting two lines above the same sentence finished. */
.item.open .item-desc { display: none; }

/* One control, two states: the plus that opened the dish turns forty five
   degrees into the close that puts it away. */
.item-add svg { transition: transform 0.18s ease; }
.item-add.open svg { transform: rotate(45deg); }

/* The demo is the one mockup that is really tapped, so its controls carry the
   product's 52px floor. The app's own sheets use a 44px stepper, which is fine
   under a thumb inside a full screen sheet but not inside a 380px picture of
   one on a marketing page. */
.demo-detail .stepper button { width: 52px; height: 52px; }

/* The row clamps its description to one line so the list stays a list. Down
   here there is room for the whole sentence, which is half the reason a dish
   opens at all. */
.demo-detail .item-desc { white-space: normal; margin: 0; }

/* The panel is a run of options ending in a button, and the button is the end
   of it: no extra gap above, no rule under. */
.demo-detail .btn { width: 100%; }

/* ==========================================================================
   6. PRICING - two plans and, under them, the arithmetic in one line. The
   number that decides it is $580 of orders in a month, so that number is set
   as type rather than hidden in a paragraph.
   ========================================================================== */

.pricing { border-top: 1px solid var(--line); }
.plans { display: grid; gap: 14px; margin-top: 8px; }

.plan {
  padding: 26px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  display: flex; flex-direction: column;
}

/* The second plan is outlined in the accent rather than filled: the page keeps
   exactly one lit thing per screenful, and that is the button. */
.plan-up { border-color: var(--accent-line); }

.plan-name {
  font-family: var(--font-mono);
  font-size: var(--fs-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--mauve2);
}

.plan-price { font-size: 54px; margin: 12px 0 0; display: flex; align-items: baseline; gap: 10px; }

.plan-per {
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  letter-spacing: var(--track-cap);
  color: var(--mauve2);
  text-transform: none;
}


.plan .ticks { flex: 1 1 auto; }
/* Two paragraphs of prose in a plan card need air between them. */
.plan .muted + .muted { margin-top: 12px; }
.plan .muted + .btn { margin-top: 18px; }

/* Full width, but split so the
   label and the prose each get a column. Left as one full width paragraph it
   ran to about 158 characters a line; capped to a measure it left two thirds
   of its own box empty. Two columns fixes both. */
/* The add-on line, between the two plans and the note about getting paid. It
   is prose rather than a card on purpose: a third box in a section headed "Two
   ways to pay" reads as a third way to pay. */
.plan-addon { margin: 20px 0 0; max-width: 62ch; }

.pay-note { margin-top: 20px; display: grid; gap: 6px; }
.pay-note b { display: block; }
/* The column is wide; the line inside it still is not. */
.pay-note p { margin: 0; max-width: 58ch; }

@media (min-width: 940px) {
  .pay-note { grid-template-columns: 0.85fr 1.15fr; gap: 32px; align-items: start; }
}

/* ==========================================================================
   7. FAQ - one narrow column and nothing else.
   ========================================================================== */

.faq { border-bottom: 1px solid var(--line); }

.faq summary {
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
  min-height: var(--tap);
  padding: 14px 0;
  cursor: pointer;
  list-style: none;
  color: var(--cream);
  font-size: 17px;
  font-weight: 600;
}

.faq summary::-webkit-details-marker { display: none; }

.faq-chevron {
  flex: none;
  width: 20px; height: 20px;
  fill: none; stroke: var(--mauve2); stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
  transition: transform 0.18s ease;
}

.faq[open] .faq-chevron { transform: rotate(180deg); }
.faq p { padding: 0 0 18px; max-width: 62ch; line-height: 1.6; color: var(--mauve); font-size: var(--fs-sm); }
.faq p a { color: var(--accent-text); }

/* ==========================================================================
   8. CLOSE - centred on the bloom, the same one the hero opens with.
   ========================================================================== */

.band-close {
  background: radial-gradient(120% 90% at 50% 110%, var(--heroglow) 0%, var(--ink) 62%);
  border-top: 1px solid var(--line);
}

.closer { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 6px; }
.closer .sec-h2 { max-width: 20ch; margin-bottom: 8px; }
.closer .btn { margin-top: 20px; padding: 0 34px; }
.closer .cap { margin-top: 14px; }

/* ---------------------------------------------------------------- footer */

.site-foot {
  padding: 34px 0 calc(34px + var(--safe-b));
  border-top: 1px solid var(--line);
  background: var(--backdrop);
}

.foot-inner { display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: space-between; gap: 20px; }
.foot-id { display: flex; align-items: center; gap: 12px; }
.foot-name { font-size: 20px; }
.foot-id .cap { display: block; margin-top: 4px; }

/* GROUPED, AND LAID OUT AS COLUMNS RATHER THAN AS A WRAPPING ROW.

   Seven links in one `flex-wrap` row wrapped into three ragged rows whose
   right edges landed 124px, 82px and 151px short of the margin, with no
   relationship between the things that ended up next to each other. Columns
   fix both halves of that: the group is what wraps, so a link is never
   orphaned onto a line of its own, and every row inside a group starts at the
   same left edge and runs the full width of it, so there is no ragged edge
   left to be uneven.

   The columns themselves are `auto-fit`, so this is one declaration for every
   width: three across on a desktop, two on a phone, one if the box ever gets
   narrower than a single link. */
.foot-links {
  /* COLUMNS, NOT A GRID. A grid puts one group per cell, so at two columns the
     four link group and the two link group sat side by side and left a 150px
     hole under the short one. Multicolumn flows the groups instead and
     balances the height, so two columns come out as four links beside two and
     two, which is even. `150px 3` is the whole responsive rule: as many
     columns as fit at 150px, never more than three. */
  columns: 150px 3;
  column-gap: 20px;
  max-width: 620px;
  /* A flex item will not shrink past its own min-content by default, and the
     min-content of a three column layout is three columns: without this the
     footer forced the page 133px wider than the phone instead of reflowing. */
  flex: 1 1 300px;
  min-width: 0;
}

/* A group is never split across a column break: a heading at the foot of one
   column with its links at the head of the next is the ragged edge again in a
   different direction. */
.foot-group {
  display: flex; flex-direction: column;
  break-inside: avoid;
  margin-bottom: 18px;
}

.foot-group-head {
  color: var(--mauve);
  padding-bottom: 4px;
  border-bottom: 1px solid var(--line);
}

.foot-links a {
  display: flex; align-items: center;
  /* The product's floor, and the reason these are rows and not a sentence of
     links: a 52px target needs a line to itself. */
  min-height: var(--tap);
  color: var(--mauve2);
  text-decoration: none;
  font-size: var(--fs-sm);
}

.foot-links a:hover { color: var(--cream); }

/* The line that has to be on every page: what we are, and what we are not. */
.foot-legal {
  margin-top: 22px;
  padding-top: 18px;
  border-top: 1px solid var(--line);
  color: var(--mauve2);
  font-size: var(--fs-desc);
  line-height: 1.6;
  /* One declaration, not two. There was a 74ch here overriding a 68ch above
     it, which is why capping the measure appeared to do nothing. */
  max-width: 62ch;
}

.foot-legal a { color: var(--mauve); }

/* ------------------------------------------------------------- desktop */

/* ==========================================================================
   MARKETING, NARROW FIRST

   Everything above this point is the narrow layout, because that is what
   almost everybody gets. The blocks below are the widening, not the design.

   Two rules the whole page obeys on a phone:

   1. NO DEVICE FRAMES. A rendered handset inside a handset is a smaller
      screen and a picture of the thing the reader is holding. Below 720px
      the demo loses its bezel and its fixed height and becomes ordinary
      page content; the inbox and the three steps never had one.
   2. NOTHING FIXED-HEIGHT. The browser chrome appears and disappears on
      scroll, so anything sized against the viewport reflows under the
      reader. The demo is content-height on a phone and only takes a
      viewport-relative height once there is a window to put it in.
   ========================================================================== */

/* The demo, unframed. This is the default and the wide version overrides it. */
.phone {
  width: 100%;
  max-width: none;
  height: auto;
  border-radius: var(--r-card);
  border: 1px solid var(--line2);
  background: var(--ink);
  box-shadow: none;
  overflow: hidden;
}

/* Content height on a phone, with a floor so a one dish category still looks
   like a storefront rather than a stub. There is no ceiling: the dish opens
   inside the list now, so the block simply grows by the height of its options
   the way any other run of page content does. */
.phone-screen {
  position: relative;
  height: auto;
  min-height: var(--demo-frame-h);
  max-height: none;
  overflow: hidden;
}

.demo-view { min-height: 520px; }

@media (min-width: 720px) {
  .band { padding: 80px 0; }
  .hero { padding: 68px 0 80px; }
  .plans { grid-template-columns: 1fr 1fr; }

  /* Three screens on a counter. The rule runs across the tops of the
     numerals and stops at the last one. */
  .flow { grid-template-columns: repeat(3, 1fr); gap: 0 28px; align-items: start; }
  .flow-step { padding: 30px 0 0; }

  .flow-step::before {
    content: '';
    position: absolute;
    left: 0; right: -28px; top: 0;
    height: 1.5px;
    background: var(--paperline);
  }

  .flow-step:last-child::before { right: 0; }

  .flow-head { display: block; margin-bottom: 8px; }
  .flow-n { display: block; font-size: 38px; margin-bottom: 10px; }
  .flow-step > p { max-width: 32ch; }

  /* The device frame comes back once there is a window to put it in.

     NOTHING INSIDE THE DEMO SCROLLS AT THIS WIDTH. The phone is sized by its
     content rather than by the viewport, so the options sheet is shown whole
     and there is no inner scrollport for a wheel to fall into. That is what
     stops the demo swallowing the scroll: not overscroll-behavior, which
     WebKit ignores in the middle of a gesture, but having nowhere to scroll.
     A wheel over the demo moves the page, because the page is the only thing
     that can move. */
  .phone {
    max-width: min(380px, 100%);
    height: auto;
    border-radius: 30px;
    box-shadow: var(--shadow-frame);
  }

  .phone-screen { height: auto; min-height: var(--demo-frame-h); }
  .demo-view { min-height: var(--demo-frame-h); }

  /* The dish opens inside the list, so there is no second box to take out of
     the flow and nothing here that has to know whether one is open. The phone
     simply grows by the height of the panel. */
  .demo-scroll { overflow-y: visible; flex: 0 0 auto; }

  .trucks { padding: 120px 0 56px; }
}

@media (min-width: 940px) {
  /* Asymmetric, and not the same asymmetry twice: the hero leans left, the
     demo leans right and puts its picture first. */
  .hero-grid { grid-template-columns: 1.15fr 0.85fr; gap: 52px; }
  /* The storefront is content height now that a dish opens inside it rather
     than over it, so on a wide screen it is taller than the words beside it.
     Centred, the copy floated in the middle of a column of nothing; started,
     it reads level with the label and the kitchen's own header, which is
     where somebody looks first. */
  .demo-grid { grid-template-columns: 1.05fr 0.95fr; gap: 56px; align-items: start; }
  .hero { padding: 92px 0 104px; }

  /* LONG AND SKINNY, UNDER THE NAV. The stacked phone shape would be a small
     brick square floating in the left column of a 1060px hero; at this width
     there is a whole empty band under the nav and the two doors fit on one
     line of it with room over.

     EDGE FOR EDGE WITH THE NAV, which costs nothing to get right because the
     wrapper is a .wrap like the nav's own and inherits the same measure and
     the same gutter. The box fills it rather than hugging its text.

     THE NEGATIVE MARGIN IS THE "move it up" HALF. The hero's 92px of top
     padding is sized for a headline starting there, and dropping a row into
     the top of it would push everything down by the row's full height. Pulling
     up by 38 puts the band in the gap the nav already left and hands the rest
     of the height back, so the headline below ends up a little higher than it
     was rather than lower. */
  .hero-doors-wrap { margin-top: -38px; margin-bottom: 30px; }

  .hero-doors {
    width: auto;
    max-width: none;
    gap: 20px;
    padding: 9px 18px;
  }

  /* The two doors run side by side instead of stacking. Baseline rather than
     centre, so the 15px line and the 12.5px line sit on one rule the way two
     sentences on a line should, and the arrow stays centred against the pair
     because that is the parent's business, not this one's. */
  .hero-doors-lines {
    flex: 1;
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 4px 20px;
  }

  /* The problem section leans to the art now rather than away from it. It used
     to be 0.9fr/1.1fr, which left the inbox about 215px wide once it had been
     split in two, and every order fragment truncated to "2 plates for frid...".
     A fragment that cannot be read is not evidence of anything. */
  .problem-grid { grid-template-columns: 1.05fr 0.95fr; gap: 56px; align-items: center; }

  /* Source order is copy, then phone, so the words are read first on a narrow
     screen. On a wide one the phone leads. */
  .demo-grid .demo-copy { order: 2; }
  .demo-grid .phone-wrap { order: 1; }

  /* The receipt is allowed over the column boundary. It is the strongest
     thing on the page and a gutter should not contain it. */
  .hero-art { justify-content: flex-end; margin-right: -28px; }
  .ticket-tilt { max-width: 380px; }

  /* Rosa sits beside her caption once the column is wide enough for both. The
     caption is capped so a one line note does not sit in a 400px column
     looking abandoned. */
  .demo-who { grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 18px; }
  .who-shot { width: 150px; }
  .who-cap { max-width: 34ch; }

  /* THE THREE STEPS, ALIGNED. Each step is a column of its own height, so the
     three screens used to start and finish at three different places and read
     as a mistake. Now every step fills the row, every screen is pushed to the
     bottom of it, and the screens share a height, so their tops and bottoms
     both line up across the section. */
  .flow-step { display: flex; flex-direction: column; height: 100%; }
  /* 250px is the natural height of the tallest of the three, the ticket, so
     matching it makes all three identical instead of nearly identical. */
  .flow-screen { margin-top: auto; min-height: 250px; }

  /* The breakeven strip earns its width instead of leaving half of itself
     empty: the sum on one side, what it means on the other. */
}

/* The opened conversation appears beside the inbox only once there is room for
   both to be legible. Between 940 and 1200 the inbox takes the whole column
   and says the same thing on its own; a second panel there just made both of
   them too narrow to read. */
@media (min-width: 1200px) {
  .inbox-art { grid-template-columns: 1fr 0.78fr; gap: 22px; align-items: start; }
  .inbox-open { display: block; }
}

/* ---- very wide ----------------------------------------------------------

   Past about 1400px the page was not getting better, it was getting more
   margin. The wrap opens up a little so the two column sections have somewhere
   to go, and the measures above stay where they are, so nothing gains a longer
   line out of it. Wide screens get more room for the pictures, not more words
   per line. */
@media (min-width: 1400px) {
  .wrap { max-width: 1180px; }
  .band { padding: 96px 0; }
  .hero { padding: 112px 0 124px; }
  .hero-grid { gap: 64px; }
  .problem-grid, .demo-grid { gap: 72px; }
  .flow { gap: 0 36px; }
  .flow-step::before { right: -36px; }
  .who-shot { width: 170px; }
  .phone { max-width: 400px; }
}

/* ==========================================================================
   Share kit. The link, the QR, the post image, the words. Existing tokens
   only; the only new shapes are the paper frames the code and image sit in.
   ========================================================================== */

/* The link, big enough to read across a kitchen and tappable anywhere. */
.linkbox {
  position: relative;
  display: flex; flex-direction: column; gap: 10px;
  width: 100%;
  padding: 18px;
  text-align: left;
  cursor: pointer;
  color: var(--cream);
  background: var(--surface);
  border: 1px solid var(--accent-line);
  border-radius: var(--r-card);
  transition: transform 0.12s ease, border-color 0.18s ease;
}

.linkbox:active { transform: scale(0.985); }

.linkbox-url {
  font-size: 22px;
  line-height: 1.25;
  color: var(--accent-text);
  word-break: break-all;
}

.linkbox-hint {
  display: flex; align-items: center; gap: 7px;
  font-family: var(--font-mono);
  font-size: var(--fs-tag);
  letter-spacing: var(--track-cap);
  text-transform: uppercase;
  color: var(--mauve2);
}

.linkbox-hint svg {
  width: 14px; height: 14px;
  fill: none; stroke: currentColor; stroke-width: 1.8;
  stroke-linecap: round; stroke-linejoin: round;
}

/* Receipt stock again: paper is where printable things live in this product. */
.qr-frame {
  display: grid; place-items: center;
  padding: 14px;
  background: var(--paper);
  border-radius: var(--r-tile);
}

/* The canvas is drawn at print size and shown small. */
.qr-frame canvas {
  width: 100%;
  max-width: 260px;
  height: auto;
  display: block;
  image-rendering: pixelated;   /* keeps the modules square when scaled down */
}

.shot-frame {
  display: grid; place-items: center;
  padding: 12px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-tile);
}

.shot-frame canvas {
  width: auto;
  max-width: 100%;
  max-height: 380px;
  height: auto;
  display: block;
  border-radius: 8px;
}

/* ------------------------------------------------------- guided first run */

.onboard { max-width: 430px; }

.onboard-top { display: flex; flex-direction: column; gap: 8px; width: 100%; }

/* One bar, used by the walkthrough and by the checklist under it, so "how far
   along am I" looks the same in both places. */
.onboard-bar {
  height: 5px;
  border-radius: var(--r-pill);
  background: var(--panel);
  overflow: hidden;
}

.onboard-fill {
  display: block;
  height: 100%;
  width: 0;
  border-radius: var(--r-pill);
  background: var(--accent);
  transition: width 0.35s var(--ease);
}

.onboard .brand h1 { font-size: var(--fs-title); }
.onboard .brand .muted { max-width: 34ch; }

.onboard-acts { width: 100%; }
.onboard-acts .btn { flex: 1; }
.onboard-foot { color: var(--mauve2); text-align: center; }

/* --------------------------------------------------------- notice picker */

/* Presets rather than a number field. Every chip is a duration somebody would
   actually pick, written the way they would say it. */
/* Not a scroller, so the pills can extend their own targets past their boxes.
   The row gap is what keeps two wrapped rows' targets from overlapping:
   30px of pill plus 22px of gap is 52px between centres, exactly. */
.notice-picker {
  display: flex;
  flex-wrap: wrap;
  gap: calc(var(--tap) - var(--ctl-h) + 2px) var(--ctl-gap);
}

.notice-picker .chip { flex: 0 0 auto; }

/* ------------------------------------------------------------- checklist */

.setup-card { border-color: var(--accent-line); }

.checklist { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }

.check {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 10px 0;
  min-height: var(--tap);
}
.check + .check { border-top: 1px solid var(--line); }

.check-box {
  flex: 0 0 22px;
  height: 22px;
  margin-top: 2px;
  border-radius: var(--r-pill);
  border: 2px solid var(--line2);
  display: grid;
  place-items: center;
}
.check.done .check-box { border-color: var(--fresh); background: var(--fresh); }
.check-box svg {
  width: 13px; height: 13px;
  fill: none; stroke: var(--ink);
  stroke-width: 3; stroke-linecap: round; stroke-linejoin: round;
}

/* THE NEXT ONE TO DO, and it is the only thing on this card competing for
   attention. A left edge in the accent rather than a filled row: four unticked
   lines all shouting is the state this replaces, and a highlight loud enough
   to be a button would make the other three look broken rather than later. */
.check.next {
  border-left: 3px solid var(--accent);
  margin-left: -13px;
  padding-left: 10px;
}
/* The rule between rows is what separates them; the marked row already has an
   edge and a second line above it reads as a box half-drawn. */
.check.next + .check, .check:has(+ .check.next) { border-top-color: transparent; }

/* WHO CAN REACH HER. Two lines, above the list. Quiet by construction: this is
   a statement of fact she needs early, not an alarm - the warn tone is the
   body colour with a coloured edge, never a red slab, because "you are not in
   Browse yet" is a normal thing for a kitchen three minutes old. */
.setup-visibility {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 10px 12px;
  border-radius: var(--r-input);
  background: var(--surface);
  border: 1px solid var(--line);
}
.setup-vis-line {
  margin: 0;
  font-size: var(--fs-desc);
  line-height: 1.45;
  color: var(--mauve);
  padding-left: 10px;
  border-left: 2px solid var(--line2);
}
.setup-vis-line.good { border-left-color: var(--fresh); }
.setup-vis-line.warn { border-left-color: var(--warn); }
/* An empty line still draws its edge, which reads as a stray mark. */
.setup-vis-line:empty { display: none; }

.check-main { flex: 1; min-width: 0; }
.check-title { color: var(--cream); font-size: var(--fs-body); }
.check.done .check-title { color: var(--mauve2); text-decoration: line-through; }
.check-blurb { color: var(--mauve); font-size: var(--fs-desc); line-height: 1.4; margin-top: 3px; }

.check-go {
  flex: 0 0 auto;
  min-height: var(--ctl-h);
  padding: 0 var(--ctl-pad);
  font-size: var(--ctl-fs);
  font-weight: var(--ctl-fw);
  position: relative;
}
/* The button is 40px so the row does not tower; the tap target is still 52. */
.check-go::after {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  width: 100%; height: var(--tap);
  transform: translate(-50%, -50%);
}

/* ------------------------------------------------------------ view switch */

/* Only ever rendered for an account that owns a kitchen. In the owner app that
   is everybody here; in the customer app it is gated on the account. */
/* ---- the side switch ----------------------------------------------------

   One account, two sides, and this is how you cross between them. It replaces
   the old one-way "switch to my kitchen" link, which only appeared for an
   account that already owned a kitchen: the same control now serves everybody,
   including somebody who has never signed in.

   It is the first thing inside #app on both sides, so it is in the header on
   both and behind nothing on either. A segmented pair rather than a toggle,
   because the two sides are places, not an on and an off, and the one you are
   on reads as a label instead of a link. */

/* THIS IS THE CONTROL EVERYTHING ELSE IS SIZED FROM, so it is written in the
   same four tokens as its copies rather than in numbers of its own. It used to
   be 62px of pill across the top of every screen for a thing that is
   navigation; it is now a segmented control that sits IN the header instead of
   being the header, on both sides of the product. */
.sideswitch {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 2px;
  /* 12 top, 10 bottom. The switch is the first thing on the page and its
     target reaches 26px above and below its centre: above, a browser with no
     safe-area inset gives it nothing, and below, the sticky topbar paints over
     it. These two numbers are the room those two need. */
  margin: 12px var(--pad) 10px;
  padding: 2px;
  border-radius: var(--ctl-r-out);
  background: var(--panel);
  border: 1px solid var(--line);
  width: fit-content;
}

.sideswitch-opt {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  min-height: var(--ctl-h);
  padding: 0 var(--ctl-pad);
  border-radius: var(--ctl-r);
  text-decoration: none;
  text-align: center;
  color: var(--mauve);
  font-size: var(--ctl-fs);
  font-weight: var(--ctl-fw);
  transition: background 0.16s ease, color 0.16s ease;
}

/* Small to look at, 52px to hit. */
.sideswitch-opt::before {
  content: '';
  position: absolute;
  left: 0; right: 0; top: 50%;
  transform: translateY(-50%);
  height: var(--tap);
}

/* Hover tints the side you are NOT on. An option that has committed is
   already dark-on-cream, so tinting its text cream paints it out - which is
   what happened the moment .pending shipped, because a.sideswitch-opt:hover
   outranks .sideswitch-opt.pending and a tap leaves the pointer sitting on
   what it just tapped. (The current side escapes this only because it has
   no href.) */
a.sideswitch-opt:not(.pending):hover,
a.sideswitch-opt:not(.pending):focus-visible { color: var(--cream); }
a.sideswitch-opt:active { transform: scale(0.985); }

/* Inverted cream for the side you are on, the same way the category chips and
   the Back of House tabs mark their current one. It stays legible whatever a
   kitchen has set its accent to. */
.sideswitch-opt[aria-current="page"] {
  background: var(--cream);
  color: var(--ink);
  cursor: default;
}

/* ---- the switch mid-switch ----------------------------------------------

   Tapping a side is a navigation, and a navigation shows nothing at all
   until the new document commits - which is one network round trip away at
   best, and on a cold mobile connection is DNS, TCP and TLS before the
   request is even sent. All of that used to happen behind a switch that
   still had the OLD side marked, so the only honest reading of it was that
   the tap had missed.

   THE MARK MOVES ON THE TAP. Same inverted cream the current side wears, so
   the switch answers with the one gesture it already uses to say which side
   you are on, rather than inventing a spinner for it. */
.sideswitch-opt.pending {
  background: var(--cream);
  color: var(--ink);
  /* NO FADE ON THIS ONE. .sideswitch-opt carries a 160ms background
     transition, which is right for hover and wrong here: measured across
     the crossfade, the mark was still invisible at 0ms and only half moved
     at 40ms, so the first tenth of a second after the tap looked the same
     as a tap that had not registered - which is the entire thing being
     fixed. Hover keeps its ease; the commit snaps. */
  transition: none;
}

/* And the side being left lets go of it in the same frame. Two marked
   options would be a worse lie than none. */
.sideswitch[data-pending="1"] .sideswitch-opt[aria-current="page"]:not(.pending) {
  background: transparent;
  color: var(--mauve);
  transition: none;
}

/* STILL GOING, NOT ARRIVED. Moving the mark alone would say the switch had
   already happened; the sweep under it is the part that says it is
   underway. ::before is the 52px tap target, so this is ::after. */
.sideswitch-opt.pending::after {
  content: '';
  position: absolute;
  left: var(--ctl-pad);
  right: var(--ctl-pad);
  bottom: 5px;
  height: 2px;
  border-radius: 2px;
  background: linear-gradient(90deg, transparent 0%, currentColor 50%, transparent 100%);
  background-size: 60% 100%;
  background-repeat: no-repeat;
  opacity: 0.45;
  animation: sideswitch-wait 1000ms linear infinite;
}

@keyframes sideswitch-wait {
  from { background-position: -60% 0; }
  to { background-position: 160% 0; }
}

/* The fact still has to be readable without the motion, so the bar stays
   and only the sweep goes. */
@media (prefers-reduced-motion: reduce) {
  .sideswitch-opt.pending::after {
    animation: none;
    background: currentColor;
    opacity: 0.30;
  }
}

/* ---- the business side, signed out --------------------------------------

   Four lines of what this is, above the email field. */

/* The four lines under the headline on the sign in screen. The screen is
   centred, so the list is centred in it - as a BLOCK, with its own lines still
   ragged-right and its rules still hung in the same margin. Centring the text
   itself would put the accent rule in a different place on every line. */
.pitch {
  margin: 2px 0 14px;
  width: fit-content;
  max-width: 100%;
  margin-inline: auto;
}
.pitch li { font-size: var(--fs-sm); line-height: 1.45; }
.pitch li { color: var(--mauve); }

/* ONE PLACEMENT, CENTRED, EVERY SCREEN, BOTH SIDES.
   It was centred on the sign in screen and left aligned everywhere else, which
   made it read as two different controls. Where a control sits is not a fact
   about the screen it is on. */
.sideswitch { margin-inline: auto; }

/* ------------------------------------------------- options: the owner side */

/* One group is one decision. It sits on --panel so it reads as a block inside
   the sheet's --surface, the same relationship an input already has. */
.opt-group {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-input);
  padding: 12px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.opt-head { display: flex; align-items: center; gap: 8px; }
.opt-head .grow { min-width: 0; }

/* The group's name is edited in place. Bare on the panel rather than in an
   input box, so a group reads as a heading and not as another form field. */
.opt-title {
  flex: 1;
  min-width: 0;
  font-family: var(--font-display);
  text-transform: uppercase;
  letter-spacing: 0.01em;
  /* --fs-item is 17px at the root and 15px under .tool, and this input only
     ever appears under .tool - so it was a 15px focusable field. max() keeps
     the intended size wherever it already clears the floor. */
  font-size: max(var(--fs-item), var(--fs-field));
  color: var(--cream);
  background: transparent;
  border: 1px solid transparent;
  border-radius: 8px;
  padding: 6px 8px;
  min-height: var(--tap);
  outline: none;
}
.opt-title:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-ring); }

/* Min, max and required on one line. They are the whole type system. */
.opt-rules { display: flex; align-items: flex-end; gap: 10px; flex-wrap: wrap; }
.opt-rules .field { gap: 5px; }
.opt-rules .switch { flex: 1 1 130px; min-width: 0; }
.opt-num { flex: 0 0 72px; }
.opt-num input { text-align: center; padding-left: 8px; padding-right: 8px; min-height: var(--tap); }

.opt-shape {
  font-family: var(--font-mono);
  font-size: var(--fs-tag);
  letter-spacing: var(--track-cap);
  text-transform: uppercase;
  color: var(--mauve2);
}

/* The group's shape and its saved mark on one line. This is the only row in a
   group card with slack in it, which is why the mark lives here. */
.opt-shape-row { display: flex; align-items: baseline; gap: 8px; }

/* SAVED, SAID BY THE ROW. Every field in the options editor saves when she
   leaves it - she pressed nothing, so there is nothing to put a spinner on and
   a toast per field would be a stream of them. This is the quiet version: it
   fades up in the row that saved, it is empty and unmeasurable the rest of the
   time, and it is written rather than merely shown so role="status" has
   something to announce. */
.opt-saved {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: var(--fs-tag);
  letter-spacing: var(--track-cap);
  text-transform: uppercase;
  color: var(--lime);
  white-space: nowrap;
  opacity: 0;
  transition: opacity 0.18s ease;
}
.opt-saved.on { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .opt-saved { transition: none; }
}

.opt-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }

/* Two lines on purpose. The name is the field a seller is actually typing in
   and it gets the whole width; the price and the sold-out toggle are secondary
   and sit under it. Five controls on one 414px line leaves "Pickled onion"
   showing as "Pickle". */
.opt-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 8px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 6px 8px;
}
.opt-row.off { opacity: 0.62; }
.opt-row.off .opt-label { text-decoration: line-through; }

/* WHAT THE CUSTOMER WILL GET, IN THE EDITOR. A real input, disabled, so a
   seller who has just made a question can see that it is a box to type in
   rather than another list she has forgotten to fill. Dead on purpose:
   there is nothing here for her to set. */
.opt-answer-demo { padding: 0 8px 8px; }

.opt-answer-demo input {
  width: 100%;
  min-height: 38px;
  padding: 9px 12px;
  border: 1px dashed var(--line2);
  border-radius: 8px;
  background: transparent;
  color: var(--mauve2);
  /* Disabled, so it can never take focus and never zooms - but it is a
     picture of what the customer gets, and the customer gets 16px. */
  font-size: var(--fs-field);
}

/* THE TILE ON EACH CHOICE IN THE EDITOR.

   Empty on most rows and that is the resting state, not a gap to be
   filled: a pepperoni topping needs no portrait. So it is a quiet outline
   with a camera in it rather than a dashed box demanding attention, and it
   is the first thing in the row because it is the first thing she will
   want to change on a group of themes. */
.opt-photo {
  position: relative;
  flex: 0 0 40px;
  width: 40px;
  height: 40px;
}

.opt-photo-pick {
  width: 100%;
  height: 100%;
  padding: 0;
  display: grid;
  place-items: center;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--panel);
  color: var(--mauve2);
  cursor: pointer;
  /* NO overflow: hidden HERE. The enlarged tap target below is a ::before on
     this element, and clipping the element clips the target back to 40px -
     which is the whole thing it exists to avoid. The image rounds its own
     corners instead. */
}

/* STROKED, NOT FILLED. Every glyph in the sprite is an open path drawn
   with a stroke - i-x is literally two lines - so fill: currentColor draws
   the × as nothing at all and the camera as a solid blob. Same five
   declarations .iconbtn svg uses, for the same sprite. */
.opt-photo-pick svg {
  width: 17px; height: 17px;
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}
.opt-photo-pick img {
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
  border-radius: 7px;
}
.opt-photo-pick:hover, .opt-photo-pick:focus-visible { border-color: var(--line2); color: var(--cream); }
.opt-photo.has .opt-photo-pick { border-color: var(--line2); }

/* Sits ON the tile rather than beside it: the row already carries a name, a
   price, a sold-out toggle and a delete, and a fifth control in the flow
   would be the one that pushes the row to a third line on a phone. Nested
   buttons are not legal, which is why the tile is a div holding two. */
.opt-photo-x {
  position: absolute;
  /* Above the tap target that reaches under it, so the corner is a remove
     and not another way to open the picker. */
  z-index: 1;
  top: -6px;
  right: -6px;
  width: 20px;
  height: 20px;
  padding: 0;
  display: grid;
  place-items: center;
  border: 1px solid var(--line2);
  border-radius: var(--r-pill);
  background: var(--surface);
  color: var(--mauve);
  cursor: pointer;
}
.opt-photo-x svg {
  width: 11px; height: 11px;
  fill: none; stroke: currentColor; stroke-width: 2.4;
  stroke-linecap: round; stroke-linejoin: round;
}
.opt-photo-x:hover, .opt-photo-x:focus-visible { color: var(--cream); border-color: var(--mauve); }

/* The tile is 40px and the row's own targets are 52px, so the hit area is
   extended rather than the tile grown - the same trick .sideswitch-opt uses
   for the same reason. */
.opt-photo-pick::before {
  content: '';
  position: absolute;
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  width: var(--tap);
  height: var(--tap);
}

.opt-foot {
  flex: 1 1 100%;
  display: flex;
  align-items: center;
  gap: 8px;
  padding-left: 2px;
}
.opt-foot .opt-acts { margin-left: 0; }

.opt-row input[type="text"] {
  background: transparent;
  border: 1px solid transparent;
  border-radius: 8px;
  color: var(--cream);
  font-size: 16px;   /* 16 keeps iOS from zooming the page on focus */
  padding: 8px;
  min-width: 0;
  min-height: var(--tap);
  outline: none;
}
.opt-row input[type="text"]:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-ring);
}
.opt-row .opt-label { flex: 1 1 120px; min-width: 0; }
.opt-group, .opt-head, .opt-list, .opt-row { min-width: 0; }
.opt-title { overflow-wrap: anywhere; }

/* The delta keeps its sign in view: a customer reading "+$2.00" and a seller
   reading "-1.00" are looking at the same column. */
.opt-delta { flex: 0 0 104px; position: relative; display: flex; align-items: center; }
.opt-delta input { width: 100%; padding-left: 22px; font-family: var(--font-mono); text-align: right; }
.opt-delta .prefix {
  position: absolute;
  left: 8px;
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  color: var(--mauve2);
  pointer-events: none;
}

.opt-acts { display: flex; gap: 6px; flex-shrink: 0; margin-left: auto; }

/* Sold out for today, without deleting it off the menu. Outline only, the way
   every other status tag in this product is. */
.opt-stock {
  min-height: 38px;
  padding: 0 14px;
  border-radius: var(--r-pill);
  font-family: var(--font-mono);
  font-size: var(--fs-tag);
  letter-spacing: var(--track-cap);
  text-transform: uppercase;
  background: transparent;
  border: 1px solid var(--accent-line);
  color: var(--accent-text);
  cursor: pointer;
  transition: transform 0.12s ease;
  position: relative;
}
.opt-stock::after {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  width: var(--tap); height: var(--tap);
  transform: translate(-50%, -50%);
}
.opt-stock:active { transform: scale(0.94); }
.opt-stock[aria-pressed="false"] {
  border-color: color-mix(in srgb, var(--coral) 55%, transparent);
  color: var(--coral);
}

/* Saved sets. A tray inside the sheet rather than a second sheet on top of
   the first, because stacked sheets on a phone are a trap to get out of. */
.preset-tray {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-input);
  padding: 12px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.preset-row {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: var(--tap);
  padding: 6px 0;
}
.preset-row + .preset-row { border-top: 1px solid var(--line); }
.preset-main { flex: 1; min-width: 0; }
.preset-name { font-weight: 600; color: var(--cream); }
.preset-what {
  font-family: var(--font-mono);
  font-size: var(--fs-tag);
  color: var(--mauve2);
  margin-top: 2px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* --------------------------------------------- options: the customer side */

.optgroup { display: flex; flex-direction: column; gap: 7px; min-width: 0; margin-bottom: 16px; }

.optgroup-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.optgroup-head .label { min-width: 0; overflow-wrap: anywhere; }

.optgroup-rule {
  font-family: var(--font-mono);
  font-size: var(--fs-tag);
  letter-spacing: var(--track-cap);
  text-transform: uppercase;
  color: var(--mauve2);
  flex-shrink: 0;
}
.optgroup-rule.needed { color: var(--accent-text); }

/* A choice is a whole row, not a small control with a label beside it. The
   label element is the tap target, so the 52px is the thing a thumb hits. */
/* min-width lets the row shrink; the name inside it wraps rather than sitting
   off the edge. A dish called SundaysOnlyExtraLargeFamilyPortion has to fit. */
.choice {
  display: flex;
  align-items: center;
  gap: 12px;
  min-width: 0;
  min-height: var(--tap);
  padding: 8px 12px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-input);
  cursor: pointer;
  transition: border-color 0.18s ease, background 0.18s ease;
}
.choice input { position: absolute; opacity: 0; width: 0; height: 0; }

.choice .box {
  flex: 0 0 22px;
  height: 22px;
  border: 2px solid var(--line2);
  background: var(--surface);
  display: grid;
  place-items: center;
  transition: border-color 0.15s ease, background 0.15s ease;
}
/* Round for a pick-one, square for a pick-many: the same shorthand every
   other form on a phone uses, driven off max_select rather than a type. */
.choice.one .box { border-radius: var(--r-pill); }
.choice.many .box { border-radius: 6px; }

.choice .box::after {
  content: '';
  width: 10px;
  height: 10px;
  border-radius: inherit;
  background: var(--accent);
  transform: scale(0);
  transition: transform 0.15s var(--ease);
}
.choice input:checked ~ .box { border-color: var(--accent); }
.choice input:checked ~ .box::after { transform: scale(1); }
.choice input:checked ~ .choice-name { color: var(--cream); }
.choice:has(input:checked) { border-color: var(--accent-line); background: var(--accent-glow-soft); }
.choice input:focus-visible ~ .box { box-shadow: 0 0 0 3px var(--accent-ring); }

/* The line under the stepper. .muted rather than .cap: this is a sentence,
   and .cap is 10px mono with caps tracking on it - right for a label over a
   field, wrong for something anybody has to read. Sits with the control it
   qualifies rather than at the foot of the sheet. */
.qty-note { margin: 8px 0 0; line-height: 1.4; }

.qty-note[hidden] { display: none; }

/* THE BOX FOR A QUESTION. Sits where the list of choices sits, under the
   same heading and the same rule, because to a customer it is the same
   thing: a group asking for one answer. Full width - a name on a cake is
   longer than a topping - and the 52px minimum every other control here
   has, since it is typed into on a phone. */
.optanswer {
  width: 100%;
  min-height: var(--tap);
  padding: 12px;
  border: 1px solid var(--line);
  border-radius: var(--r-input);
  background: var(--panel);
  color: var(--cream);
  font-size: var(--fs-field);
}

.optanswer::placeholder { color: var(--mauve2); }
.optanswer:focus-visible { border-color: var(--accent); outline: none; box-shadow: 0 0 0 3px var(--accent-ring); }

/* THE CHOICE'S PICTURE, WHERE SHE HAS GIVEN ONE.

   Between the control and the name, so the eye reaches it on the way to
   the word rather than after it - on a group of cake themes the picture is
   what is being chosen and the name is the label for it.

   Square and cropped, because these are shot by a cook on a phone and
   arrive in whatever shape the plate was. flex: none so a long theme name
   takes the wrapping and the picture keeps its size. */
.choice-photo {
  flex: 0 0 44px;
  width: 44px;
  height: 44px;
  border-radius: 8px;
  object-fit: cover;
  background: var(--surface);
}

.choice-name {
  flex: 1;
  min-width: 0;
  overflow-wrap: anywhere;
  color: var(--mauve);
  font-size: var(--fs-body);
}

.choice-delta {
  flex-shrink: 0;
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  color: var(--accent-text);
}

.choice.gone { opacity: 0.45; cursor: not-allowed; }
.choice.gone .choice-name { text-decoration: line-through; }
.choice.gone .choice-delta { color: var(--mauve2); }

/* A group that still needs an answer says so rather than leaving somebody
   poking a dead Add button. */
.optgroup.unmet .choice { border-color: color-mix(in srgb, var(--accent) 28%, transparent); }

/* Chosen options under a line, on both the dark ticket and the paper one. */
/* ANYTHING HERE CAN BE TYPED BY A CUSTOMER NOW.

   These lines used to be option names, which a seller writes and keeps
   short. One of them can now be an answer - eighty characters of whatever
   somebody wants written on a cake - and eighty characters with no space
   in them is a single unbreakable word. A flex item's automatic minimum
   size is its longest word, so the column refused to shrink, pushed
   .line-main past its flex basis, and took the whole cart sideways: 665px
   of scroll in a 390px viewport, receipt and Checkout button off the right
   edge.

   min-width: 0 lets the column shrink and overflow-wrap gives it somewhere
   to break. Both are needed - either alone still overflows. */
.line-opts {
  font-family: var(--font-mono);
  font-size: var(--fs-tag);
  opacity: 0.8;
  margin-top: 2px;
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
  overflow-wrap: anywhere;
}

/* The same for the two lines either side of it, which are also flex items
   holding text nobody here controls the length of. */
.line-name, .line-note { min-width: 0; overflow-wrap: anywhere; }

/* ---------------------------------------------------------------- ratings */

/* Sits between the storefront header and the menu. Not a .tab: that class
   carries the tab bar's 104px of bottom padding, which here would open a hole
   the size of a phone screen between the ratings and the first dish. */
.store-ratings { padding: var(--pad) var(--pad) 0; }

.rating-head { display: flex; align-items: center; gap: 14px; }

.rating-score { font-size: var(--fs-hero); line-height: 1; color: var(--cream); }

/* Stars are drawn from the glyph sheet and filled with the accent, so a
   kitchen's ratings look like the rest of its page. */
.rating-stars { display: flex; gap: 3px; }
.rating-stars svg {
  width: 15px;
  height: 15px;
  fill: var(--line2);
  stroke: none;
  display: block;
}
.rating-stars svg.on { fill: var(--accent); }

/* Chosen to lead. Filled rather than outlined, because it is a state she set
   rather than an action still available. */
.rating-feature { margin-top: 10px; }
.rating-feature.on {
  border-color: color-mix(in srgb, var(--fresh) 55%, transparent);
  color: var(--fresh);
}

.rating-item { padding: 12px 0; }
.rating-item + .rating-item { border-top: 1px solid var(--line); }
.rating-item .rating-stars { margin-bottom: 6px; }
.rating-said { color: var(--cream); font-size: var(--fs-body); line-height: 1.45; white-space: pre-wrap; }
.rating-when {
  font-family: var(--font-mono);
  font-size: var(--fs-tag);
  letter-spacing: var(--track-cap);
  text-transform: uppercase;
  color: var(--mauve2);
  margin-top: 4px;
}

/* The owner's answer, indented under the comment it answers. */
.rating-reply {
  margin-top: 10px;
  padding-left: 12px;
  border-left: 2px solid var(--accent-line);
  display: flex;
  flex-direction: column;
  gap: 3px;
}
.rating-reply .muted { white-space: pre-wrap; }

.rating-replybox { display: flex; flex-direction: column; gap: 8px; margin-top: 10px; }
.rating-replybox textarea { min-height: 64px; }

/* The customer's picker. Five 52px targets, which is also why they are not
   crammed into a row of small glyphs. */
.stars { display: flex; gap: 6px; }

.stars button {
  width: var(--tap);
  height: var(--tap);
  border: 1px solid var(--line);
  background: var(--panel);
  border-radius: var(--r-input);
  display: grid;
  place-items: center;
  cursor: pointer;
  color: var(--mauve2);
  transition: transform 0.12s ease, color 0.15s ease, border-color 0.15s ease;
}
.stars button svg { width: 24px; height: 24px; fill: currentColor; stroke: none; }
.stars button:active { transform: scale(0.94); }
.stars button[aria-checked="true"] { color: var(--accent); border-color: var(--accent-line); }
.stars button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }



/* ------------------------------------------------- save / more, storefront */

/* THREE SMALL THINGS, NOT THREE BUTTONS.
   These were 52px slabs across the top of somebody's menu: the first thing a
   customer saw on a page whose job is to show them food. They are now the same
   compact control as everything else, and they carry different weight from
   each other, because they are not equally important. Save is the one anybody
   taps, More kitchens is a way out, and Report is a thing almost nobody
   presses and nobody should be invited to. The 52px target is extended below,
   so all three are still honestly tappable. */
.store-acts { display: flex; gap: var(--ctl-gap); flex-wrap: wrap; align-items: center; margin-top: 10px; }

.savebtn {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: var(--ctl-h);
  padding: 0 var(--ctl-pad);
  border: 1px solid var(--line);
  border-radius: var(--ctl-r);
  background: var(--panel);
  color: var(--mauve);
  font-family: var(--font-body);
  font-size: var(--ctl-fs);
  font-weight: var(--ctl-fw);
  letter-spacing: 0.01em;
  text-decoration: none;
  cursor: pointer;
  transition: transform 0.12s ease, color 0.15s ease, border-color 0.15s ease;
}

.savebtn::before {
  content: '';
  position: absolute;
  left: 0; right: 0; top: 50%;
  transform: translateY(-50%);
  height: var(--tap);
}

.savebtn:active { transform: scale(0.96); }
.savebtn svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linejoin: round; }

/* Report. It has to be findable and it must not be inviting: no fill, no
   border, and the quietest text on the header. Same 52px target as the other
   two, because a person who does need it is often in a hurry. */
.savebtn.quiet {
  background: none;
  border-color: transparent;
  color: var(--mauve2);
  padding: 0 6px;
}
.savebtn.quiet svg { width: 13px; height: 13px; opacity: 0.8; }

/* Filled once it is saved, which is the whole signal. */
.savebtn[aria-pressed="true"] {
  color: var(--accent-text);
  border-color: var(--accent-line);
  background: var(--accent-glow-soft);
}
.savebtn[aria-pressed="true"] svg { fill: currentColor; stroke: none; }

/* ---------------------------------------------------------- pay methods */

#sheet-options { min-width: 0; }

.paylist { display: flex; flex-direction: column; gap: 8px; }

/* One way to pay is a statement, not a question: the control stays but reads
   as a fact rather than as a choice nobody gets to make. */
.paylist .choice.only { cursor: default; }
.paylist .choice.only .box { border-color: var(--accent); }
.paylist .choice.only .box::after { transform: scale(1); }


/* --------------------------------------------------------- running balance */

/* At the top of the Money screen, and only there. It used to sit above every
   tab, which put a running bill on the screen she opens to cook. Being inside
   the tab now, it lays out with the rest of it: no margin of its own and full
   width of the column. */
.balance {
  display: flex;
  align-items: baseline;
  gap: 10px;
  width: 100%;
  min-height: 40px;
  padding: 7px 12px;
  border: 1px solid var(--line);
  border-radius: var(--ctl-r-out);
  background: var(--panel);
  cursor: pointer;
  text-align: left;
  transition: border-color 0.15s ease, transform 0.12s ease;
}
.balance:active { transform: scale(0.99); }

.balance-label {
  font-family: var(--font-mono);
  font-size: var(--fs-tag);
  letter-spacing: var(--track-cap);
  color: var(--mauve2);
  flex-shrink: 0;
}
.balance-amount { font-size: var(--fs-body); color: var(--cream); }
/* The hint is the only part of this pill that can run long, so it is the part
   that gives way. flex-shrink:0 was refusing to let it, and the sentence was
   being clipped by the pill's own radius rather than ending in an ellipsis. */
.balance-hint {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: var(--fs-tag);
  letter-spacing: var(--track-cap);
  text-transform: uppercase;
  color: var(--mauve2);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Something is owed. Accent, not alarm: it is a bill, not a fire. */
.balance.due { border-color: var(--accent-line); background: var(--accent-glow-soft); }
.balance.due .balance-label,
.balance.due .balance-hint { color: var(--accent-text); }
.balance.due .balance-amount { color: var(--accent-text); }

.locked-banner {
  margin: 10px var(--pad) 0;
  padding: 12px 14px;
  border: 1px solid color-mix(in srgb, var(--coral) 45%, transparent);
  border-radius: var(--r-input);
  background: color-mix(in srgb, var(--coral) 12%, transparent);
  color: var(--coral);
  font-size: var(--fs-sm);
  line-height: 1.5;
}

/* THE THREE ANSWERS, AS A LEDGER.
   Label on the left, figure on the right, in mono so a column of amounts lines
   up on the decimal. The outstanding row is the only one that ever colours,
   and only when there is something outstanding. */
.ledger {
  margin: 0;
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 7px 12px;
  padding-top: 10px;
  border-top: 1px solid var(--line);
}

.ledger dt {
  color: var(--mauve2);
  font-size: var(--fs-desc);
}

.ledger dd {
  margin: 0;
  text-align: right;
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  color: var(--cream);
}

.ledger dd.quiet { color: var(--mauve2); }
.ledger dd.owing { color: var(--accent-text); }

/* ------------------------------------------------------------- the bill */

/* PAYOUTS. It borrows the billing card's shapes on purpose - same head, same
   big figure, same ledger - because it is the same kind of thing one block
   lower: money, named, with a date. Only two rules are its own.

   The button is .ghost, not .primary and not the bare .btn it started as. The
   primary action on this screen is paying a bill that is owed; opening a list
   to read is not something she needs pushing towards. The bare .btn renders
   filled and pale, which in the first screenshots drew the eye harder than the
   bill above it - the outline is the right volume for "there is more here". */
#payout-card .btn { margin-top: 2px; }

/* PAUSED IS NOT AN ERROR, so it is not red. Stripe pauses payouts while it
   waits on something, which is a state to notice rather than a failure to
   panic about; the notification banner above is where the actual task is. */
.cap.warn { color: var(--amber, #d9a441); }

.bill-amount {
  font-size: var(--fs-hero);
  line-height: 1;
  color: var(--cream);
  margin: 2px 0 0;
}

.bill-row { display: flex; align-items: center; gap: 12px; padding: 10px 0; }
.bill-row + .bill-row { border-top: 1px solid var(--line); }
.bill-main { flex: 1; min-width: 0; }
.bill-period { color: var(--cream); font-size: var(--fs-sm); }
.bill-note {
  font-family: var(--font-mono);
  font-size: var(--fs-tag);
  color: var(--mauve2);
  margin-top: 2px;
}
.bill-fee { flex-shrink: 0; color: var(--accent-text); font-size: var(--fs-body); }

/* ------------------------------------------------------------ the plan */

/* Two rows, pick one. The same whole-row .choice the storefront uses for
   options, given a second line so a plan can say what it costs and who it is
   cheaper for in the same target. Still 52px at the minimum. */

.planpick { display: flex; flex-direction: column; gap: 8px; }

.planpick .choice { align-items: flex-start; padding: 12px; }
.planpick .choice .box { margin-top: 2px; }

.plan-row { flex: 1; min-width: 0; }

.plan-row-top {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
}

.plan-row-name { color: var(--cream); font-size: var(--fs-body); font-weight: 600; min-width: 0; }
.plan-row-cost {
  flex-shrink: 0;
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  color: var(--accent-text);
}

.plan-row-note {
  margin-top: 4px;
  color: var(--mauve2);
  font-size: var(--fs-desc);
  line-height: 1.45;
}

/* THE WAY OUT, said on the row she is on.

   Set apart from .plan-row-note with a rule above it rather than a colour or a
   warning tint: this is not a problem and must not read as one. She is being
   told where the door is, not that something is wrong, and an alarmed-looking
   line here would make a working plan feel broken. */
.plan-row-exit {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--line);
  color: var(--mauve);
  font-size: var(--fs-desc);
  line-height: 1.45;
}

/* Which one is running right now, as against which one is ticked. They differ
   for exactly as long as a switch is queued. */
.plan-row-tag {
  display: inline-block;
  margin-top: 6px;
  font-family: var(--font-mono);
  font-size: var(--fs-tag);
  letter-spacing: var(--track-cap);
  text-transform: uppercase;
  color: var(--mauve2);
  border: 1px solid var(--line2);
  border-radius: var(--r-pill);
  padding: 2px 8px;
}

.plan-row-tag.now { color: var(--accent-text); border-color: var(--accent-line); }

/* Something is wrong with the monthly payment. Coral, the same colour a
   cancelled order already wears, rather than a new alarm colour nobody has
   seen before - and it replaces the RUNNING NOW tag rather than sitting beside
   it, because on an unpaid plan that tag is a false statement. */
.plan-row-tag.warn {
  color: var(--coral);
  border-color: color-mix(in srgb, var(--coral) 45%, transparent);
}

/* The saving. Accent rather than alarm, and only ever shown when the other
   plan is genuinely cheaper on this kitchen's own numbers. */
.plan-advice { border-style: solid; border-color: var(--accent-line); }
.plan-advice b { color: var(--accent-text); }

.planpick .choice.busy { opacity: 0.55; pointer-events: none; }

/* --------------------------------------------------------------- stats */

/* The four stat tiles that lived here are gone, and so is the CSS for them.
   Back of House shows orders, sales and average as they happen, and the bill
   above answers what she owes; four boxes repeating those numbers were the
   loudest thing on this screen and answered nothing. */

/* Thirty days. Bars rather than a line: a kitchen has days it is shut, and a
   line drawn through a gap tells a lie about them. */
/* THE CHART THAT LIVED HERE IS GONE, and so is its CSS: the plot, the bars,
   the peak label, the average line and the axis. At a home cook's volume a
   range holds one to three bars and the whole thing reads as a rendering
   failure. The note above windowTotals in src/index.js says what brings it
   back and at what volume; when it does, this is the block to restore.

   What replaced it is one line of three figures. */
/* A BARE LINE OF TEXT IN A STACK HAS TO HUG ITS OWN INK.
   At the inherited 1.45 the box stood 3px taller than the glyphs top and
   bottom, so the gaps a person SEES around this line came out at 14 and 13
   against 11 everywhere else on the screen. Same trap the screen title had:
   half-leading paints nothing and the stack cannot see it. */
.totals {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 16px;
  color: var(--mauve2);
  font-size: var(--fs-sm);
  line-height: 1.1;
}

.totals b {
  font-family: var(--font-mono);
  font-weight: 400;
  color: var(--cream);
}

/* The money one is the figure she came for. */
.totals span:nth-child(2) b { color: var(--accent-text); }

/* HER DISHES, OVER THE SELECTED PERIOD.
   Name and money on one line because money per dish is the number she came
   for, then a bar to compare them by, then the count. The bar is scaled to
   whichever column the toggle is sorting on, so the longest bar is always the
   row at the top. */
.dishes { display: flex; flex-direction: column; gap: 10px; }

.dish { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.dish + .dish { padding-top: 10px; border-top: 1px solid var(--line); }

.dish-top { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }

.dish-name {
  min-width: 0;
  color: var(--cream);
  font-size: var(--fs-body);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.dish-money { flex: none; color: var(--accent-text); font-size: var(--fs-sm); }

/* The count is a subtitle, not a second number competing with the money.
   The change sits on the same line, hard right, so the column of arrows
   reads down the list. */
.dish-foot {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  min-width: 0;
}

.dish-qty { color: var(--mauve2); font-size: var(--fs-desc); }

/* HOW IT DID AGAINST LAST TIME.

   Four states and three colours, because two of the four say the same
   thing: "new" and "level" are both "there is no movement to report", and
   colouring either of them would put a verdict on a dish that has not
   earned one. Green and coral are the pair this product already uses for
   up and down - the ticket rail and the heat trend both speak in them - so
   nothing new is introduced here.

   The sign is in the text rather than in an icon. A row already carries
   money, a count and a name; a fourth glyph on it is noise, and "+40%"
   says the direction without one. */
.dish-change {
  flex: none;
  font-family: var(--font-mono);
  font-size: var(--fs-tag);
  letter-spacing: var(--track-cap);
  color: var(--mauve2);
}

.dish-change.up { color: var(--fresh); }
.dish-change.down { color: var(--coral); }
/* "new" and the neutral dash both mean "no verdict", so neither is
   coloured - the default --mauve2 is the point. */

/* Ranked bars: name, bar, number. Straight off the reference's top sellers. */
.bars { display: flex; flex-direction: column; gap: 10px; }

.bar-row { display: flex; align-items: center; gap: 10px; min-width: 0; }

.bar-name {
  flex: 0 0 118px;
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  color: var(--cream);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.bar-track {
  flex: 1;
  min-width: 0;
  height: 8px;
  border-radius: var(--r-pill);
  background: var(--panel);
  overflow: hidden;
}
.bar-fill { display: block; height: 100%; border-radius: var(--r-pill); background: var(--accent); }
.bar-value { flex: 0 0 auto; min-width: 30px; text-align: right; font-size: var(--fs-sm); color: var(--mauve2); }

/* --------------------------------------------------------- hot and cold */

/* Gone with the cards. "Selling fast" and "Going quiet" told a seller what to
   do about two dishes; the dish list on the Money screen shows her every one
   of them with the money beside it, and Back of House covers what is moving
   during service. */

/* ---------------------------------------------------------------- info */

.info-lead { color: var(--cream); font-size: var(--fs-body); line-height: 1.55; }
.info-q { color: var(--cream); font-size: var(--fs-body); margin-top: 12px; }
.info-q:first-of-type { margin-top: 0; }

/* ------------------------------------------------------------ six tabs */

/* The owner bar carries six. Each is still a 52px-tall target and the labels
   drop a point so nothing wraps at 390. */

/* ---------------------------------------------------------------- legal */

.legal { max-width: 66ch; }
.legal p { margin: 0 0 14px; color: var(--mauve); line-height: 1.65; }
.legal p a { color: var(--accent-text); }
.legal-h { font-size: var(--fs-head); margin: 34px 0 10px; color: var(--cream); }
.legal-foot { color: var(--mauve2); margin: 28px 0 20px; }
.legal .btn { margin-top: 10px; }

/* A numbered section rule, so a long document can be pointed at by number in
   an email without anybody having to count headings. */
.legal-h .legal-n {
  font-family: var(--font-mono);
  font-size: var(--fs-label);
  letter-spacing: var(--track-label);
  color: var(--accent-text);
  display: block;
  margin-bottom: 8px;
  text-transform: none;
}

.legal ul { margin: 0 0 16px; padding-left: 20px; color: var(--mauve); line-height: 1.65; }
.legal li { margin-bottom: 8px; }
.legal li::marker { color: var(--mauve2); }

/* .ticks brings its own margin rule and hangs it in the padding, so it must
   not also take the document list's indent. */
.legal .ticks { padding-left: 0; }
.legal .ticks li { margin-bottom: 0; }

/* ---- a walkthrough with pictures --------------------------------------

   Four steps, each one a paragraph and the screen it is describing. On a
   phone the screen follows its own words; once there is a column to spare it
   sits beside them, which keeps a step to one glance instead of two.

   NAMED `walk-`, NOT `step-`. .steps, .step and .step-n are all taken: the
   owner app's own install instructions and the customer app's order progress
   rail use them, and .steps li beats a bare .step on specificity, so writing
   this as .step would have both lost here and quietly restyled two screens in
   the product this page is not allowed to touch.

   The pictures are 9:16 phone screens, so they are deliberately NOT given the
   width of the column: a 600px tall picture of a phone between two paragraphs
   turns four short steps into four screenfuls of scrolling. They are held to
   a width where the thing being circled is still legible and the step still
   reads as one thing. */
.walk { list-style: none; margin: 26px 0 34px; padding: 0; display: flex; flex-direction: column; gap: 32px; }

.walk-step { display: grid; gap: 14px; }
.walk-copy p { margin: 0; }
.walk-n { display: block; color: var(--accent-text); font-size: var(--fs-label); letter-spacing: var(--track-label); }
.walk-h { font-size: var(--fs-head); margin: 8px 0 8px; color: var(--cream); }

/* The same frame contract as every other picture on this site: the frame
   holds the shape and a warm ground, so the page is laid out identically
   whether the file is there, still arriving, or missing. */
.walk-shot {
  margin: 0;
  width: min(280px, 100%);
  border-radius: var(--r-card);
  overflow: hidden;
  border: 1px solid var(--line2);
  background: radial-gradient(70% 70% at 50% 35%, var(--disc1) 0%, var(--disc2) 100%);
}

.walk-shot img { display: block; width: 100%; height: auto; }

@media (min-width: 720px) {
  .walk-step { grid-template-columns: minmax(0, 1fr) 240px; align-items: center; gap: 30px; }
  .walk-shot { width: 240px; }
  /* A step with no picture beside it, which is every step on
     /kitchen-on-your-phone: the drawings belong to the customer's guide and
     would be lying there. The copy takes the whole column rather than leaving
     the 240px picture gutter standing empty next to it. */
  .walk-step.walk-bare { grid-template-columns: minmax(0, 1fr); }
}

/* The contents, at the top of a document nobody wants to scroll. Bare links
   in one column, not a card. */
.legal-toc { list-style: none; margin: 0 0 34px; padding: 0; border-top: 1px solid var(--line); }

.legal-toc li { margin: 0; }

.legal-toc a {
  display: flex; align-items: center; gap: 12px;
  min-height: var(--tap);
  padding: 6px 0;
  border-bottom: 1px solid var(--line);
  color: var(--mauve);
  text-decoration: none;
  font-size: var(--fs-sm);
  line-height: 1.4;
}

.legal-toc a:hover { color: var(--cream); }

.legal-toc .n {
  font-family: var(--font-mono);
  font-size: var(--fs-label);
  letter-spacing: var(--track-cap);
  color: var(--mauve2);
  flex: none;
  width: 2.4ch;
}

/* The one thing in a legal document that somebody must not scroll past: the
   arbitration clause, and on the help page the line about state rules. */
.legal-flag {
  margin: 22px 0 24px;
  padding: 18px 20px;
  border: 1px solid var(--accent-line);
  border-left: 3px solid var(--accent);
  border-radius: var(--r-input);
  background: var(--surface);
}

.legal-flag p { margin: 0 0 10px; color: var(--cream); }
.legal-flag p:last-child { margin-bottom: 0; }

.legal-flag .label { display: block; margin-bottom: 10px; color: var(--accent-text); }

/* The help page's one table, for what a state asks of you. Scrolls inside
   itself rather than pushing the page sideways. */
.legal-table-wrap { overflow-x: auto; margin: 0 0 20px; }

.legal-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-sm);
  color: var(--mauve);
  min-width: 420px;
}

.legal-table th,
.legal-table td {
  text-align: left;
  padding: 12px 14px 12px 0;
  border-bottom: 1px solid var(--line);
  vertical-align: top;
  line-height: 1.5;
}

.legal-table th {
  font-family: var(--font-mono);
  font-size: var(--fs-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--mauve2);
  font-weight: 400;
  white-space: nowrap;
}

.legal-table td:first-child { color: var(--cream); white-space: nowrap; padding-right: 24px; }

/* One line, where it is actually read: under the Place order button, and on
   the kitchen's own page. */
/* ------------------------------------------------------ affiliate page

   Reuses the owner screen's vocabulary - .own, .own-stat, .own-row - because
   it is the same kind of screen: dense, read-only, one person's numbers. Only
   what that vocabulary does not already have is here. */

/* Room under the header for a page that opens on a link rather than a list. */
.aff .own-top { margin-bottom: 18px; }

.aff-share {
  display: flex; flex-direction: column; gap: 8px;
  margin-bottom: 22px;
}

/* The rate and the count toward the higher one, side by side on anything wide
   enough and stacked on a phone. .own-split already does exactly this. */
.aff .own-split { margin-bottom: 22px; }

/* A kitchen row. The figures wrap rather than scroll: four short numbers on a
   390px screen is two rows of two, which is readable, and a horizontal
   scroller inside a list row is not. */
.aff-row + .aff-row { margin-top: 10px; }

.aff-row .own-figures {
  display: flex; flex-wrap: wrap; gap: 14px 26px;
  margin-top: 10px;
}

/* A rung not yet reached. Dimmed, not hidden: the ladder has to show what is
   ahead, and a tier that looks the same as an earned one would read as money
   already owed. */
.aff-bonus-todo { opacity: 0.45; }

/* The terms below an affiliate's numbers. It carries .legal as well, which is
   what styles the shared partial's prose - so the same file reads the same way
   here and on the public page, and this only has to draw the rule that
   separates terms from figures. The max-width .legal sets is right for both:
   a wall of terms at full page width is not read. */
.aff-terms {
  margin-top: 34px;
  padding-top: 22px;
  border-top: 1px solid var(--line);
}

/* The language switch, at the foot of the storefront. Quiet: it is a thing you
   look for once and then never again, and it must not compete with the menu. */
.lang-switch {
  appearance: none; -webkit-appearance: none;
  display: block;
  margin: 14px auto 0;
  padding: 8px 16px;
  background: none;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  color: var(--mauve);
  font: inherit;
  font-size: var(--fs-sm);
  cursor: pointer;
}

.disclaimer {
  font-family: var(--font-mono);
  font-size: var(--fs-tag);
  line-height: 1.5;
  letter-spacing: 0.04em;
  color: var(--mauve2);
}
.disclaimer a { color: var(--mauve); }

/* UNDER THE SIGN-IN BUTTON, and centred because it is the last thing on a
   short centred screen rather than a footnote at the end of a long one. The
   cards above it carry their own width; this sits under whichever of the two
   is showing, so it is spaced from the card rather than from the viewport. */
.signin-legal {
  margin-top: 14px;
  text-align: center;
  text-wrap: balance;
}
/* The two links are the point of the line, so they are not left to be guessed
   at from colour alone at this size. */
.signin-legal a { text-decoration: underline; text-underline-offset: 2px; }

/* The storefront's copy of it, in its footer position: separated from the last
   dish by a rule rather than by a gap, so it reads as the end of the page
   rather than as one more thing on the menu. */
.store-foot {
  margin-top: 10px;
  padding-top: 14px;
  border-top: 1px solid var(--line);
}

/* ------------------------------------------------------- the customer app */

/* ---- where she is browsing ----------------------------------------------

   The first control on the customer's first screen, because the first thing
   anybody wants to know about food they are going to go and collect is where
   it is. A row, not a chip: it holds two lines and it is the only thing on
   this screen that opens a sheet. */
.placebar {
  position: relative;
  display: flex; align-items: center; gap: 10px;
  width: 100%;
  min-height: var(--tap);
  padding: 6px 12px;
  border: 1px solid var(--line);
  border-radius: var(--r-input);
  background: var(--panel);
  color: var(--cream);
  text-align: left;
  cursor: pointer;
  transition: border-color 0.15s ease;
}
.placebar:active { border-color: var(--accent-line); }

.placebar-pin {
  flex: none;
  width: 17px; height: 17px;
  fill: none; stroke: var(--accent-text); stroke-width: 1.9;
  stroke-linecap: round; stroke-linejoin: round;
}

.placebar-main { flex: 1; min-width: 0; }

.placebar-cap {
  display: block;
  font-family: var(--font-body);
  font-size: var(--fs-tag);
  color: var(--mauve2);
  line-height: 1.2;
}

.placebar-name {
  display: block;
  font-size: var(--fs-body);
  font-weight: 600;
  color: var(--cream);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* Nothing in her town yet. Said once, above the list of everywhere else, in
   the accent rather than in an alarm colour: it is a fact about how new this
   is, not a failure. */
.browse-note {
  padding: 10px 12px;
  border-radius: var(--r-input);
  border: 1px solid var(--accent-line);
  background: var(--accent-glow-soft);
  color: var(--accent-text);
  font-size: var(--fs-sm);
  line-height: 1.45;
}

/* The tick beside the place she is on is .row-tick, already declared with the
   other picker rows further down. Nothing to add here. */


/* Search. One field, full width, with the glyph inside it rather than beside
   it, so the whole bar is the target. */
.searchbar {
  position: relative;
  display: flex;
  align-items: center;
}

/* A field you type in is a 52px target because it IS 52px: it does not shrink
   the way a chip does. What it does take from the control language is the
   corner - a full pill sitting above a row of 7px chips was the one thing left
   on this screen speaking a different shape. */
.searchbar input {
  width: 100%;
  min-height: var(--tap);
  padding: 12px 42px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-input);
  color: var(--cream);
  font-family: var(--font-body);
  font-size: 16px;   /* 16 keeps iOS from zooming the page on focus */
  outline: none;
}
.searchbar input::placeholder { color: var(--mauve2); }
.searchbar input:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-ring);
}
.searchbar input::-webkit-search-cancel-button { display: none; }

.searchbar-glyph {
  position: absolute;
  left: 15px;
  width: 18px; height: 18px;
  fill: none; stroke: var(--mauve2);
  stroke-width: 2; stroke-linecap: round;
  pointer-events: none;
}

.searchbar-clear {
  position: absolute;
  right: 8px;
  width: 30px; height: 30px;
  display: grid; place-items: center;
  border: 0; border-radius: var(--ctl-r);
  background: transparent;
  color: var(--mauve);
  cursor: pointer;
}
.searchbar-clear::after {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  width: var(--tap); height: var(--tap);
  transform: translate(-50%, -50%);
}
.searchbar-clear svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 2.4; stroke-linecap: round; }

/* ------------------------------------------------------------ kitchen card */

/* 12px between cards was right when a card ended in a full-width button and
   needed separating from the next one. Now that each ends in a line of text,
   10px reads as the same break and shows more of the list. */
.kitchens { display: flex; flex-direction: column; gap: 10px; }

/* The seller's own colour, handed in per card. It falls back to the app's
   accent so a kitchen with no colour set still reads as one of these. */
.kcard {
  --kaccent: var(--accent);
  /* A COLUMN NOW, NOT A ROW. The card used to be a tile beside three lines of
     text. It is a photograph with the words underneath, because in a scrolling
     list the picture is what is being compared and a 116px square shares the
     row with the text rather than leading it.

     NO PADDING ON THE CARD ITSELF, and overflow hidden: the photograph runs to
     all three of its edges and is clipped by the card's own corner radius, so
     there is one rounded shape rather than a rounded picture inside a rounded
     box. The text below brings its own padding. */
  display: flex;
  flex-direction: column;
  position: relative;
  padding: 0;
  overflow: hidden;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  text-decoration: none;
  color: inherit;
  transition: transform 0.18s ease, border-color 0.18s ease;
}
.kcard:active { transform: scale(0.985); }
.kcard:hover, .kcard:focus-visible { border-color: color-mix(in srgb, var(--kaccent) 45%, transparent); }

/* THE PHOTOGRAPH.

   16:9 AND FIXED, declared as an aspect-ratio rather than a height, so the
   space is reserved before a single byte of the picture has arrived. This is
   most of what makes a lazy-loaded list feel solid on a slow phone: the cards
   below do not jump as the ones above fill in, and nothing the customer was
   about to tap moves out from under the thumb.

   THE 800px CUT IS DELIBERATE, and wider than this ever renders: 390px of
   viewport at 3x is 1170 device pixels, so a 800px source is soft at the very
   top end and exact everywhere else, while a full-size kitchen photo would be
   two megabytes to draw a thumbnail. See tools/backfill-cards.mjs.

   THE WASH UNDERNEATH IS NOT A PLACEHOLDER, it is the card for a kitchen that
   has photographed nothing. Her own colour falling into the app's disc, with
   her initial cut out of it - a thing that looks made rather than missing.
   Because it is painted by the element itself it also covers the gap while a
   photo loads, so there is never a grey hole and never a broken frame. */
.kcard-shot {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background:
    radial-gradient(75% 100% at 24% 6%,
      color-mix(in srgb, var(--kaccent) 70%, transparent) 0%, transparent 72%),
    linear-gradient(160deg, var(--disc1) 0%, var(--disc2) 100%);
  display: grid;
  place-items: center;
}
.kcard-shot-letter {
  font-family: var(--font-display);
  font-size: clamp(54px, 17vw, 88px);
  line-height: 1;
  color: var(--cream);
  /* Quiet enough to be a texture rather than a headline - it is standing in
     for a picture, not competing with the name below it - but not so quiet
     that the frame reads as a picture that failed to arrive, which at 0.22 on
     an olive accent it did. */
  opacity: 0.34;
  user-select: none;
}
/* The picture sits ON the wash rather than replacing it, so the moment it
   errors and removes itself (see kitchenCard) the fallback is already drawn
   underneath with nothing to re-render. */
.kcard-shot img {
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
}

/* A SCRIM AT THE TOP, AND ONLY AT THE TOP. The badge and the heart sit over a
   photograph nobody has vetted - a white plate under either of them would take
   the text with it. A short gradient from the top edge buys both of them a
   dark backing without greying the food, which a flat overlay would.

   It is above the picture and below the controls. */
.kcard-shot::after {
  content: '';
  position: absolute;
  inset: 0 0 auto 0;
  height: 58%;
  z-index: 2;
  pointer-events: none;
  background: linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%);
}

/* OPEN OR CLOSED, ON THE PICTURE. Same status colours and same shape as the
   storefront's .state - this is that badge, moved - with a blur pad behind it
   so it holds up over a bright photo as well as the scrim does over a dark
   one. Never the accent: whether a kitchen is trading is not branding. */
.kcard-badge {
  position: absolute;
  z-index: 3;
  top: 10px; left: 10px;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 9px;
  border-radius: 6px;
  border: 1px solid currentColor;
  background: rgba(12, 9, 8, 0.55);
  backdrop-filter: blur(6px);
  font-family: var(--font-mono);
  font-size: var(--fs-tag);
  letter-spacing: var(--track-cap);
  text-transform: uppercase;
  color: var(--mauve2);
}
.kcard-badge .dot { width: 7px; height: 7px; border-radius: var(--r-pill); background: currentColor; }
.kcard-badge.open { color: var(--fresh); }
.kcard-badge.shut { color: var(--coral); }

/* THE TEXT, AND IT IS THE PART THAT HAD ROOM TO GIVE.

   Five lines under a photograph is a lot of card, and each one was separated
   as though it were its own paragraph. They are not: the name, what she
   cooks, where and what it costs, and when it is ready are one block about
   one kitchen, and they read faster stacked closer than spread out.

   gap 4px -> 2px, and the padding loses 2px top and 3px bottom. With the
   button and the "no ratings yet" line gone, a card is about 80px shorter,
   which is most of a sixth card on a 390px screen. */
.kcard-main {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 10px 14px 11px;
}

/* ---- a truck's card ---------------------------------------------------- */

/* VISIBLY A TRUCK, NOT A KITCHEN CARD WITH DIFFERENT WORDS.

   Three things do it, and the first two are structural rather than
   decorative so it reads at a glance and before anything is read at all:

     the first line is a WHEN and a WHERE, not a name - a kitchen's card
     opens with what it is called, a truck's with where it will be;

     that line sits on the accent, in mono caps, so the block has a
     different texture from the sentence-case name under it;

     and the tile is a pill rather than a square, which is the only
     silhouette difference available on a card that is otherwise the same
     shape - visible in a scrolling list from the corner of an eye.

   The accent is the seller's own --kaccent, so a row of trucks is still a
   row of different kitchens rather than a row of this app. */
/* The pill tile is gone with the tile, so the truck's silhouette difference is
   now the accent rule down the photograph's bottom edge - the same trick at the
   same place in the scroll, and visible from the corner of an eye. */
.kcard.truck .kcard-shot { border-bottom: 2px solid var(--kaccent); }

/* THE FIRST LINE IS A ROW: the tag, then the day. Wrapping, and the wrap is
   load-bearing rather than a safety net - at phone widths these two usually
   do not share a line.

   MEASURED, in headless Chrome against this stylesheet with the real woff2
   faces, with --ktile pinned to what each width would actually compute:

     320px  text column 144px   tag 84px (en) / 126px (es)   two lines
     360px  text column 184px   tag 84px (en) / 126px (es)   en one line
                                                             with a short day

   So the Spanish tag clears the narrowest column by 18px and the English by
   60px, nothing overflows at any width, and the page never scrolls
   sideways. What the two-word tag costs is a line: at 320px the day drops
   under the tag in both languages, which it did not do when this said
   "Truck". The card is ~14px taller on the narrowest phone and nothing is
   cut off.

   CAMIÓN DE COMIDA IS THE LONGEST STRING THIS LINE CAN HOLD at 126px of
   144px. A tag longer than that overflows the column at 320px, because
   .kcard-kind is flex:none and will not shrink - re-measure before changing
   either translation. */
.kcard-stop {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 2px 7px;
  margin-bottom: 2px;
}

/* THE WORD, DRAWN AS A FILLED PILL. Solid rather than outlined so it holds
   its shape at the edge of vision while a list is moving - an outline at
   10px is a smudge - and the same rounded accent shape as the tile beside
   it, so the two read as one signal.

   --kaccent-ink IS SET PER CARD, by inkOn() in browse.js, and a fixed one
   does not work: --ink against brick is 3.00, which fails. The pick is
   whichever of the ramp's darkest and lightest reads better on her fill -
   4.78 at worst across the seven swatches, 16.05 at best. The fallback below
   is only for a kitchen with no accent stored, where --kaccent is the
   product's own and --ink is right for it. */
.kcard-kind {
  flex: none;
  padding: 2px 7px;
  border-radius: var(--r-pill);
  background: var(--kaccent);
  color: var(--kaccent-ink, var(--ink));
  font-family: var(--font-mono);
  font-size: var(--fs-tag);
  letter-spacing: var(--track-cap);
  text-transform: uppercase;
  line-height: 1.4;
}

.kcard-stop-when {
  font-family: var(--font-mono);
  font-size: var(--fs-tag);
  letter-spacing: var(--track-cap);
  text-transform: uppercase;
  color: var(--kaccent);
}

.kcard-stop-where {
  font-size: var(--fs-desc);
  line-height: 1.3;
  color: var(--cream);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* NOTHING SCHEDULED. The line stays and goes quiet rather than coming off:
   a truck that has not said where it will be is answering the question, and
   an absent line would make the card read as a kitchen's. */
.kcard-stop.none .kcard-stop-when { color: var(--mauve2); }
.kcard-stop.none .kcard-stop-where { color: var(--mauve2); }
/* And the tag does not, on purpose - see stopLine(). It says what she is,
   which is still true when she has not said where she will be. */

/* The badge has moved onto the photograph, so the name has the row to itself
   and no longer has to leave a gap on the right for anything - which is also
   what makes room for it to wrap rather than be cut. */
.kcard-top { display: flex; min-width: 0; }

/* A KITCHEN'S NAME IS NEVER TRUNCATED. It wraps.

   This used to be one nowrap line with an ellipsis on the end, which turned
   "Sincerely Brooklyn's Kitchen" into "Sincerely Brooklyn's Kit..." on a
   390px phone. A tagline can afford that - it is a description and the first
   few words carry it - but a name is the thing she chose and the thing a
   customer is scanning for, and half of it is not a shorter version of it.
   There is room: the card is the full width of the list now that the photo
   is above the text rather than beside it, and a second line costs about
   22px on the one card in ten that needs one.

   NO LINE CLAMP EITHER, deliberately. Clamping at two lines would put the
   ellipsis back for anything longer, which is the same defect with a higher
   threshold.

   break-word rather than anywhere: it wraps at spaces like ordinary text and
   only breaks inside a word when a single word is wider than the card, which
   is the one case where the alternative is a page that scrolls sideways. */
.kcard-name {
  font-family: var(--font-display);
  text-transform: uppercase;
  letter-spacing: 0.01em;
  font-size: var(--fs-item);
  line-height: 1.15;
  color: var(--cream);
  min-width: 0;
  white-space: normal;
  overflow-wrap: break-word;
}

.kcard-tag {
  color: var(--mauve);
  font-size: var(--fs-desc);
  /* Was 1.35. A single clipped line does not need leading for a second one,
     and the 5px it was reserving showed up as a gap under the name. */
  line-height: 1.25;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* THE META ROW. Where it is, what it costs to start, how it rated, how much
   notice it needs - in that order, and separated by a dot the markup does not
   contain. A separator that is an element is a separator somebody has to
   remember to leave off the last one; a generated one cannot get that wrong
   however the card is assembled above.

   IT SITS ON THE BOTTOM of the text column (margin-top: auto), so on every card
   in a list the meta row lines up with the bottom edge of the photo whatever
   else is above it. */
.kcard-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 2px 0;
  margin-top: auto;
  /* Was 6px, on top of the column's own gap. The meta row is the same block
     as the two lines above it, not a footer under them. */
  padding-top: 3px;
  font-family: var(--font-mono);
  font-size: var(--fs-tag);
  letter-spacing: var(--track-cap);
  text-transform: uppercase;
  color: var(--mauve2);
}
/* THE DOT TRAILS ITS ITEM, IT DOES NOT LEAD THE NEXT ONE. Both put a dot
   between every pair; only this one survives wrapping. As ::before on the
   following item, a row that wraps starts the new line with a floating dot,
   which reads as a bullet list somebody broke. As ::after on the preceding
   item it wraps with the item it belongs to and leaves the dot at the end of
   the line, where it reads as "continued". :last-child keeps it off the end. */
.kcard-meta > * { display: inline-flex; align-items: center; }
.kcard-meta > *:not(:last-child)::after {
  content: '';
  width: 3px; height: 3px;
  margin: 0 7px;
  border-radius: 50%;
  background: currentColor;
  opacity: 0.45;
  flex: 0 0 auto;
}

/* The town and the pickup corner are now alternatives rather than neighbours -
   the card prints whichever of the two the customer does not already know (see
   kitchenCard in browse.js) - so they are set identically and only one of them
   is ever in the row. */
.kcard-city, .kcard-where { color: var(--mauve2); }

/* WHAT IT COSTS TO START, and the one thing in the row worth reading first.
   The seller's own colour, so the number carries her accent rather than the
   app's, and the same weight as the name to lift it off the mauve around it. */
.kcard-price { color: var(--kaccent); font-weight: 600; }

.kcard-rating { display: flex; align-items: center; gap: 5px; color: var(--kaccent); }
.kcard-rating .rating-stars svg { width: 11px; height: 11px; }
.kcard-rating .rating-stars svg.on { fill: var(--kaccent); }

/* THE HEART, on the photograph's top-right, opposite the open/closed badge.
   It used to sit over the text column and the name row had to be padded away
   from it; there is nothing to collide with up here. It carries its own dark
   pad for the same reason the badge does - it is over a stranger's photo. */
.kcard-unsave {
  position: absolute;
  z-index: 3;
  top: 10px; right: 10px;
  width: 30px; height: 30px;
  display: grid; place-items: center;
  border: 0; border-radius: var(--ctl-r);
  background: rgba(12, 9, 8, 0.55);
  backdrop-filter: blur(6px);
  color: var(--cream);
  cursor: pointer;
}
.kcard-unsave::after {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  width: var(--tap); height: var(--tap);
  transform: translate(-50%, -50%);
}
.kcard-unsave svg { width: 17px; height: 17px; fill: currentColor; stroke: none; }
.kcard-unsave:active { transform: scale(0.94); }

/* WHEN SHE COULD EAT. Set brighter than the meta row above it and in the
   seller's own colour, because of everything on this card it is the line most
   likely to decide the tap - "open" and "ready at 2:30" are different facts and
   only the second one is a plan.

   It clips rather than wraps. A long weekday in Spanish plus a 24-hour time is
   the widest this gets and it clears 320px; anything longer is a locale we have
   not seen and it loses its tail rather than the card losing its shape. */
.kcard-when {
  display: flex;
  align-items: center;
  gap: 6px;
  /* Was 8px. It keeps a little more air than the lines above it, because it
     is the one line here that is a different KIND of fact - everything above
     describes the kitchen, this one is a time somebody can act on. */
  margin-top: 5px;
  font-family: var(--font-mono);
  font-size: var(--fs-tag);
  letter-spacing: var(--track-cap);
  text-transform: uppercase;
  color: var(--kaccent);
  min-width: 0;
}
.kcard-when svg {
  flex: 0 0 auto;
  width: 13px; height: 13px;
  fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round;
}
.kcard-when > span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ------------------------------------------------------------- order card */

.ocard {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  padding: 14px;
  margin-bottom: 12px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.ocard-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }

.ocard-kitchen {
  font-family: var(--font-display);
  text-transform: uppercase;
  font-size: var(--fs-item);
  color: var(--cream);
  text-decoration: none;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ocard-code { font-size: var(--fs-sm); color: var(--accent-text); flex-shrink: 0; }

.ocard-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  flex-wrap: wrap;
}
.ocard-when {
  font-family: var(--font-mono);
  font-size: var(--fs-tag);
  letter-spacing: var(--track-cap);
  color: var(--mauve2);
}

.ocard-lines {
  border-top: 1px solid var(--line);
  padding-top: 8px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.ocard-line {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  font-size: var(--fs-sm);
  color: var(--mauve);
}
.ocard-line .v { flex-shrink: 0; font-family: var(--font-mono); }

.ocard-total {
  border-top: 1px solid var(--line);
  padding-top: 8px;
  color: var(--cream);
  font-weight: 600;
}

.ocard-acts { display: flex; gap: 10px; margin-top: 4px; }
.ocard-acts .btn { flex: 1; font-size: var(--fs-body); }
.ocard-acts .btn.ghost { flex: 0 0 auto; padding: 0 16px; }

/* THE CUSTOMER APP'S TABS, AND ONLY THE CUSTOMER APP'S.
 *
 * This was written as `#tab-browse, #tab-saved, #tab-orders, #tab-info`, and
 * two of those four ids also exist in the owner app. An id selector outweighs
 * `.tool .tab`, so a rule meant for browse was quietly setting the gap on the
 * owner's Orders and Info screens and beating the tool's own density - which is
 * exactly the bleed between the two visual languages that is not allowed to
 * happen. The owner app carries .tool and the customer app does not, so that
 * is what the scope is written against. */
#app:not(.tool) > .tab { gap: var(--stack); }

/* A quieter affordance for the one nobody should feel invited to press. It is
   already borderless and unfilled where .savebtn is defined; this is what it
   does when somebody does reach for it. */
.savebtn.quiet:hover, .savebtn.quiet:focus-visible { color: var(--coral); }

/* --------------------------------------------------- order from an email */

/* The order the seller tapped through to from an email, ringed for a few
   seconds so they can pick it out of the rail without reading every code. */
.spot {
  border-radius: var(--r-card);
  animation: spot 4s ease;
}

@keyframes spot {
  0%, 70% { box-shadow: 0 0 0 4px var(--accent-line), 0 0 28px var(--accent-glow); }
  100% { box-shadow: 0 0 0 4px transparent, 0 0 28px transparent; }
}

/* ==========================================================================
   BACK OF HOUSE

   The $5 add-on: the screen service runs on. Its own room, and it looks like
   it. The rest of the app is a warm brown kitchen at home; this is read one
   handed, standing up, next to a hot pan, often on a cracked screen in bad
   light, so it goes cold and hard: near black ground, near white type, the
   biggest text and the largest targets in the product.

   The reference does the same thing - After Hours gives Back of House its own
   neutral --bh ramp, distinct from the purple app chrome - and this is that
   move re-valued, not a new idea.

   EVERY interactive element here is at least 52px tall. The rail is set to be
   read at arm's length across a counter.
   ========================================================================== */

.boh {
  --bh: #0a0a0c;
  --bh2: #16161a;
  --bh3: #23232a;
  --bhline: rgba(255, 255, 255, 0.13);
  --bhline2: rgba(255, 255, 255, 0.22);
  --bhtext: #f4f4f6;
  --bhmut: #9b9ba6;

  /* ---- THE SCALE, AS PROPERTIES, BECAUSE IT HAS TWO SETTINGS ----
     Roomy here: the service screen on a tablet propped on a shelf, read
     across a counter, where a 38px order code is the whole point.
     A phone gets the compact set below, and so does the demo frame - the
     same numbers in both, declared once, because they are the same
     decision about the same screen. */
  --bt-pad: 16px;          /* ticket */
  --bt-code: 38px;
  --bt-who: 17px;
  --bt-qty: 26px;
  --bt-line: 19px;
  --bt-opts: 15px;
  --bt-note: 15px;
  --bt-act: 60px;          /* the advance button, never under 44 */
  --bt-gap: 12px;          /* between cards in a pane */
  --b86-h: 56px;           /* 86 row, never under 44 - it is the button */
  --b86-name: 17px;
  --b86-sub: 13px;
  --bhe-h: 60px;           /* heat row */
  --bhe-name: 17px;
  --bcall-pad: 18px;       /* hero call-out */
  --bcall-name: 30px;
  --bcall-sub: 14px;
  --blead-pad: 20px;       /* today */
  --blead-v: clamp(44px, 13vw, 64px);
  --bstat-h: 92px;
  --bstat-v: 28px;
  --bstat-pad: 14px;

  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  flex-direction: column;
  background: var(--bh);
  color: var(--bhtext);
  overflow: hidden;
}

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

.boh-top {
  flex: none;
  /* 14px so the back arrow's 52px target has room above it even with no
     safe-area inset to sit on. */
  padding: calc(var(--safe-t) + 14px) 14px 0;
  background: var(--bh2);
  border-bottom: 1px solid var(--bhline);
}

.boh-top-row { display: flex; align-items: center; gap: 12px; }

.boh-back {
  position: relative;
  flex: none;
  width: 36px; height: 36px;
  display: grid; place-items: center;
  border-radius: var(--ctl-r);
  border: 1px solid var(--bhline);
  background: var(--bh3);
  color: var(--bhtext);
  cursor: pointer;
}

.boh-back::after {
  content: '';
  position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: var(--tap); height: var(--tap);
}

.boh-back svg {
  width: 19px; height: 19px;
  fill: none; stroke: currentColor; stroke-width: 2.4;
  stroke-linecap: round; stroke-linejoin: round;
}

.boh-id { flex: 1; min-width: 0; }

.boh-title {
  display: block;
  font-family: var(--font-body);
  font-size: 12px;
  font-weight: 650;
  letter-spacing: 0.06em;
  color: var(--bhtext);
}

.boh-sub {
  display: block;
  margin-top: 2px;
  font-size: 12px;
  color: var(--bhmut);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.boh-open {
  flex: none;
  display: inline-flex; align-items: center;
  min-height: var(--ctl-h);
  font-family: var(--font-body);
  font-size: 12px;
  font-weight: var(--ctl-fw);
  color: var(--bhtext);
  background: var(--bh3);
  border: 1px solid var(--bhline);
  border-radius: var(--ctl-r);
  padding: 0 var(--ctl-pad);
}

/* ---- the four panes ---- */

/* THE FOUR PANES, AS ONE SEGMENTED CONTROL.
   Four 52px slabs at 16/700 was a second navigation bar under the first one,
   and it pushed the tickets - the thing this screen exists to show - below the
   fold. It is now literally the side switch: a tray at --ctl-r-out holding
   four segments at --ctl-r, --ctl-h tall, --ctl-fs type. Each segment keeps a
   full 52px target through the pseudo-element below.

   Back of House is read at arm's length, so this is the one place the type
   comes back up a step from the switch's 11.5px: the rest of the screen is
   deliberately the biggest in the product, and a 11.5px pane label under a
   28px ticket would read as a different app. */
.boh-tabs {
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
  gap: 2px;
  /* 16 above: these segments reach 26px up from their own centres and the back
     arrow reaches 26px down from its, and 16px is where the two stop meeting. */
  margin: 16px 0 12px;
  padding: 2px;
  border-radius: var(--ctl-r-out);
  background: var(--bh3);
  border: 1px solid var(--bhline);
}

.boh-tab {
  position: relative;
  min-height: 32px;
  padding: 0 6px;
  border-radius: var(--ctl-r);
  border: 0;
  background: none;
  color: var(--bhmut);
  font-size: 13px;
  font-weight: var(--ctl-fw);
  cursor: pointer;
}

.boh-tab::before {
  content: '';
  position: absolute;
  left: 0; right: 0; top: 50%;
  transform: translateY(-50%);
  height: var(--tap);
}

/* A quiet raised fill for the pane you are on, the way the switch marks its
   side. It used to invert to near white, which at four across read as a
   selected slab rather than as a place. */
.boh-tab.on { background: var(--bh); color: var(--bhtext); }

.boh-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: 12px 14px calc(var(--safe-b) + 24px);
}

/* Room for the offer bar when it is up, so the last ticket is never under it. */
.boh-body { padding-bottom: calc(var(--safe-b) + 150px); }

.boh-pane { display: flex; flex-direction: column; gap: var(--bt-gap); }

.boh-empty {
  margin: 40px 0;
  text-align: center;
  color: var(--bhmut);
  font-size: 16px;
  line-height: 1.5;
}

.boh-pane-note {
  margin: 10px 0 0;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.14em;
  color: var(--bhmut);
}

/* ---- today ---- */

.boh-lead {
  padding: var(--blead-pad);
  border-radius: 16px;
  background: var(--bh2);
  border: 1px solid var(--bhline);
}

.boh-lead-label {
  display: block;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.14em;
  color: var(--bhmut);
}

/* The number she actually came for. */
.boh-lead-value {
  display: block;
  margin-top: 8px;
  font-family: var(--font-display);
  font-size: var(--blead-v);
  line-height: 1;
  letter-spacing: 0.01em;
  color: var(--bhtext);
}

.boh-lead-sub { display: block; margin-top: 8px; font-size: 14px; color: var(--bhmut); }

.boh-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; }

.boh-stat {
  padding: var(--bstat-pad);
  border-radius: 14px;
  background: var(--bh2);
  border: 1px solid var(--bhline);
  min-height: var(--bstat-h);
  display: flex; flex-direction: column; gap: 4px;
}

.boh-stat-label {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.12em;
  color: var(--bhmut);
}

.boh-stat-value {
  font-family: var(--font-display);
  font-size: var(--bstat-v);
  line-height: 1;
  color: var(--bhtext);
}

.boh-stat-sub { font-size: 11px; color: var(--bhmut); line-height: 1.3; }

/* The way back to the free screen, said plainly. */
.boh-link {
  margin-top: 6px;
  min-height: 52px;
  padding: 14px;
  text-align: left;
  border-radius: 12px;
  border: 1px dashed var(--bhline2);
  background: transparent;
  color: var(--bhmut);
  font-size: 13px;
  line-height: 1.45;
  cursor: pointer;
}

/* ---- heat ---- */

/* THE TWO CALL-OUTS ARE THE ANSWER, so they are the loudest thing on the pane.
   They were a tinted box with a coloured word in it, which is the same
   information at the volume of a footnote: somebody glancing at this while
   plating has to be able to read "birria is carrying tonight, flan is dead"
   without stopping to parse a list. Orange for the one that is selling, blue
   for the one that is not, and the count and the move on both. */
.boh-call {
  padding: var(--bcall-pad);
  border-radius: 16px;
}

/* THE TWO CALL-OUTS, ON THE SCREEN'S OWN PALETTE.

   These were an orange-to-red gradient and a blue-to-navy one, with 30px
   coloured glows under them. Nothing else in the product has a gradient or
   a hue like either, and on a screen whose whole language is a dark card
   with a coloured EDGE - the ticket rail sorts itself by border-left, the
   86ed rows are a coral tint with a coral border - two saturated slabs read
   as something pasted in from another app.

   So they join the family that was already here. .boh-call.short got this
   right first: a dark tint of the colour, a border in it, and a label in
   it. Hot is coral, the same coral the pending tickets and the 86ed rows
   use, down to the literal values so a later reader can see they are one
   thing. Cold is simply quiet - the ordinary card - because "the coldest
   dish" is the unremarkable one and dressing it up was always the odder of
   the two choices. */
.boh-call.hot {
  background: #24100c;
  border: 1px solid rgba(240, 96, 78, 0.45);
}

.boh-call.cold {
  background: var(--bh2);
  border: 1px solid var(--bhline);
}

/* MORE ORDERS THAN STOCK. Amber, and deliberately not the red the 86ed rows
   and the hottest-dish call-out already use: sold out is a fact and this is a
   decision she still has time to make. Literal rather than var(--warn), which
   is declared inside .tool and is not in scope on this screen. */
.boh-call.short { background: #2a1f08; border: 1px solid rgba(251, 191, 36, 0.45); }
.boh-call.short .boh-call-label { color: #ffd98a; }
.boh-call.short .boh-call-name { font-size: 16px; }

.boh-call-label {
  display: block;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.14em;
}

/* Both grounds are dark now, so both take the screen's own text colours -
   the same pair every other card here uses - and only the label carries the
   hue. #ffb08e is the coral the 86ed rows already speak in. */
.boh-call.hot .boh-call-label { color: #ffb08e; font-weight: 600; letter-spacing: 0.16em; }
.boh-call.hot .boh-call-name { color: var(--bhtext); }
.boh-call.hot .boh-call-sub { color: #d2705c; }

.boh-call.cold .boh-call-label { color: var(--bhmut); font-weight: 600; letter-spacing: 0.16em; }
.boh-call.cold .boh-call-name { color: var(--bhtext); }
.boh-call.cold .boh-call-sub { color: var(--bhmut); }

.boh-call-name {
  display: block;
  margin-top: 8px;
  font-family: var(--font-display);
  font-size: var(--bcall-name);
  line-height: 1.05;
  color: var(--bhtext);
}

.boh-call-sub { display: block; margin-top: 4px; font-size: var(--bcall-sub); color: var(--bhmut); }

.boh-heat-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--b86-gap, 6px); }

/* Same again, and worse. The rank is mono 13px and the list is capped at 12
   rows, so the widest this ever holds is "12" at 16px of ink - and it sat in a
   30px cell, left aligned, leaving 34px of dead space on every row. 20px
   clears two digits, and right aligning it puts a 1 and a 12 in the same
   place so the gap before the dish is the column gap and nothing else. */
.boh-heat-row {
  display: grid;
  grid-template-columns: 20px 1fr;
  align-items: center;
  gap: 10px;
  min-height: var(--bhe-h);
  padding: 7px 11px;
  border-radius: 12px;
  background: var(--bh2);
  border: 1px solid var(--bhline);
}

.boh-heat-rank { font-family: var(--font-mono); font-size: 13px; color: var(--bhmut); text-align: right; }
.boh-heat-main { min-width: 0; }

.boh-heat-name {
  display: block;
  font-size: var(--bhe-name);
  font-weight: 600;
  color: var(--bhtext);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* THE LINE, RANKED BY HEAT. A row is a name, where it sits, and how far along
   the day it got - the bar is the number, and the number beside it is there
   for anybody who wants the exact one. Both the bar and the word take their
   colour from the row's position, computed in boh-view.js, so a row cannot
   say Sizzling in the colour of a cold one. */
.boh-heat-top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}

.boh-heat-temp {
  flex: none;
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.04em;
}

.boh-heat-bar { display: flex; align-items: center; gap: 9px; margin-top: var(--bhe-barmt, 7px); }

.boh-heat-track {
  flex: 1;
  min-width: 0;
  height: 8px;
  border-radius: 99px;
  background: var(--bh);
  overflow: hidden;
}

.boh-heat-fill { display: block; height: 100%; border-radius: 99px; }

/* Drawn rather than set: an arrow glyph falls back to whatever face has one
   and arrives at a different size and weight per platform. */
.boh-heat-trend { flex: none; position: relative; width: 13px; height: 13px; }

.boh-heat-trend.up::before,
.boh-heat-trend.down::before {
  content: '';
  position: absolute;
  left: 2px;
  border-left: 4.5px solid transparent;
  border-right: 4.5px solid transparent;
}

.boh-heat-trend.up::before { top: 3px; border-bottom: 6px solid var(--fresh); }
.boh-heat-trend.down::before { top: 4px; border-top: 6px solid var(--coral); }

.boh-heat-trend.flat {
  color: var(--bhmut);
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 13px;
  text-align: center;
}

/* NO READING YET, AND IT MUST NOT LOOK LIKE ONE.

   A hollow ring, in the muted ink the flat dash uses: not a triangle, so it
   cannot be mistaken for a direction, and not the dash either, because a dash
   is a measurement that came back level and this is the absence of one. Drawn
   at the same 13px box as the arrows so the column stays aligned. */
.boh-heat-trend.none::before {
  content: '';
  position: absolute;
  top: 3px; left: 3px;
  width: 7px; height: 7px;
  box-sizing: border-box;
  border: 1.5px solid var(--bhmut);
  border-radius: var(--r-pill);
}

.boh-heat-qty {
  flex: none;
  width: 26px;
  text-align: right;
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--bhmut);
}

/* ---- 86 and the quoted wait ---- */

.boh-wait {
  padding: var(--bwait-pad, 14px);
  border-radius: 14px;
  background: var(--bh2);
  border: 1px solid var(--bhline);
}

.boh-wait-label {
  display: block;
  font-family: var(--font-body);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--bhmut);
}

.boh-wait-row {
  display: grid;
  grid-template-columns: 38px 1fr 38px;
  align-items: center;
  gap: 12px;
  margin: var(--bwait-rowm, 10px 0 6px);
}

/* Two 64px squares was a numeric keypad for a control that moves one value up
   and down. 38 x 30, the stepper's proportions, with the 52px target extended
   past the visible box in both directions. */
.boh-step {
  position: relative;
  width: 38px; height: var(--ctl-h);
  border-radius: var(--ctl-r);
  border: 1px solid var(--bhline2);
  background: var(--bh3);
  color: var(--bhtext);
  font-size: 17px;
  line-height: 1;
  cursor: pointer;
  display: grid; place-items: center;
}

.boh-step::after {
  content: '';
  position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: var(--tap); height: var(--tap);
}

.boh-step:disabled { opacity: 0.4; }

/* The number itself stays big. It is what she is reading across the kitchen,
   and it is the one thing on this control that is not a control. */
.boh-wait-value {
  text-align: center;
  font-family: var(--font-display);
  /* Still the biggest thing in this block and still read across a kitchen;
     it just stops being the tallest thing on the 86 pane. */
  font-size: var(--bwait-v, 28px);
  color: var(--bhtext);
}

.boh-wait-sub { display: block; font-size: var(--bwait-sub, 12px); color: var(--bhmut); }

.boh-86-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--b86-gap, 6px); }

.boh-86 {
  width: 100%;
  min-height: var(--b86-h);
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 7px 11px;
  text-align: left;
  border-radius: 12px;
  background: var(--bh2);
  border: 1px solid var(--bhline);
  color: var(--bhtext);
  cursor: pointer;
}

.boh-86-main { min-width: 0; display: block; }

.boh-86-name {
  display: block;
  font-size: var(--b86-name); font-weight: 600;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.boh-86-sub { display: block; margin-top: 1px; font-size: var(--b86-sub); color: var(--bhmut); }
.boh-86-sub.short { color: #f0b429; }

.boh-86-flag {
  flex: none;
  display: inline-flex; align-items: center;
  min-height: var(--ctl-h);
  font-family: var(--font-body);
  font-size: var(--ctl-fs);
  font-weight: var(--ctl-fw);
  letter-spacing: 0.04em;
  padding: 0 var(--ctl-pad);
  border-radius: var(--ctl-r);
  border: 1px solid var(--bhline2);
  color: var(--bhmut);
}

/* An 86ed dish reads as off at a glance, from across the kitchen. */
.boh-86.off { background: #24100c; border-color: rgba(240, 96, 78, 0.45); }
.boh-86.off .boh-86-name { color: #ff9b86; text-decoration: line-through; }
.boh-86.off .boh-86-sub { color: #d2705c; }
.boh-86.off .boh-86-flag { color: #ffb08e; border-color: rgba(240, 96, 78, 0.5); }

/* ---- the rail ---- */

.boh-ticket {
  padding: var(--bt-pad);
  border-radius: 16px;
  background: var(--bh2);
  border: 1px solid var(--bhline);
  border-left: 5px solid var(--bhmut);
}

/* The status is carried by the edge, so a glance down the rail sorts itself. */
.boh-ticket.st-pending { border-left-color: var(--coral); }
.boh-ticket.st-confirmed { border-left-color: var(--warn, #ffc24b); }
.boh-ticket.st-ready { border-left-color: var(--fresh); }

.boh-ticket-top { display: flex; align-items: baseline; gap: 9px; }

/* Read at arm's length across a counter. This is the biggest text in the app. */
.boh-code {
  font-family: var(--font-display);
  font-size: var(--bt-code);
  line-height: 1;
  letter-spacing: 0.02em;
  color: var(--bhtext);
}

.boh-waited { font-family: var(--font-mono); font-size: 14px; color: var(--bhmut); }

/* A flex item in the code row now, not a paragraph under it. flex: 1 takes
   the slack so the waited time stays hard right, and min-width: 0 plus the
   ellipsis is what keeps a long name from pushing the time off the card. */
.boh-who {
  margin: 0;
  flex: 1;
  min-width: 0;
  font-size: var(--bt-who);
  font-weight: 600;
  color: var(--bhtext);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.boh-lines { list-style: none; margin: var(--bt-linesmt, 10px) 0 0; padding: 0; display: flex; flex-direction: column; gap: 7px; }

/* THE QUANTITY COLUMN, AND WHY IT IS RIGHT ALIGNED.
   It has to clear the widest thing that can land in it, and that is "50 x" -
   MAX_QTY_PER_LINE - which measures 36px of ink. It was 42px, and because the
   content sat at the LEFT of that cell, every ordinary line left the slack on
   the inside: 23px of "2 x" against a 42px cell put a 29px empty channel
   between the numeral and the dish name.
   Aligning the quantity to the right of its cell is what actually fixes that.
   The multiplication sign now lands in the same place whatever the number is,
   so the gap before the dish is the 10px column gap and nothing else - 10px on
   a 1, 10px on a 50 - and the numerals line up down a ticket the way a column
   of quantities should. 38px is the worst case plus two. */
.boh-line { display: grid; grid-template-columns: 38px 1fr; gap: 10px; align-items: start; }

.boh-qty {
  display: flex;
  align-items: baseline;
  justify-content: flex-end;
  gap: 2px;
  color: var(--bhtext);
}

.boh-qty-n { font-family: var(--font-display); font-size: var(--bt-qty); line-height: 1.1; }
/* Anton has no multiplication sign, so this one is set in the mono face at a
   size that sits under the numeral rather than fighting it. */
.boh-qty-x { font-family: var(--font-mono); font-size: 14px; color: var(--bhmut); }

.boh-line-body { min-width: 0; }
.boh-line-name { display: block; font-size: var(--bt-line); line-height: 1.25; color: var(--bhtext); }
.boh-line-opts { display: block; margin-top: 2px; font-size: var(--bt-opts); color: var(--bhmut); line-height: 1.35; }

/* The line note is the thing that gets missed at the window, so it is the
   loudest part of the ticket rather than grey text under it. */
.boh-line-note {
  display: inline-block;
  margin-top: 6px;
  padding: 5px 10px;
  border-radius: 8px;
  background: rgba(240, 96, 78, 0.16);
  border: 1px solid rgba(240, 96, 78, 0.45);
  color: #ffb08e;
  font-size: var(--bt-note);
  font-weight: 600;
  line-height: 1.3;
}

.boh-order-note {
  margin: 12px 0 0;
  padding: 10px 12px;
  border-radius: 10px;
  background: var(--bh3);
  color: var(--bhtext);
  font-size: var(--bt-note);
  line-height: 1.4;
}

.boh-advance {
  width: 100%;
  min-height: var(--bt-act);
  margin-top: var(--bt-actmt, 10px);
  border: 0;
  border-radius: 14px;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: 18px;
  font-weight: 800;
  cursor: pointer;
}

.boh-advance:disabled { opacity: 0.45; }

/* ---- the offer ---- */

.boh-offer {
  flex: none;
  display: flex; flex-direction: column; gap: 12px;
  padding: 16px 14px calc(var(--safe-b) + 16px);
  background: var(--bh2);
  border-top: 1px solid var(--bhline2);
}

/* SHE REACHED FOR SOMETHING THIS BAR PAYS FOR.

   bohAsk() puts .asked on here when an unentitled tap lands on an 86 toggle
   or the wait stepper. It is a mark, not an alarm: the bar is already on
   screen and already says the price, so all this has to do is tell her eye
   which of the two things she is looking at just answered her.

   THE ANIMATION IS THE WHOLE EFFECT AND IT IS REMOVABLE. Nothing about the
   bar's resting state changes, so under reduced motion the class simply does
   nothing and the toast plus the scroll still carry the message. */
@keyframes boh-asked {
  0%   { box-shadow: 0 0 0 0 var(--accent-ring); }
  35%  { box-shadow: 0 0 0 3px var(--accent-ring); }
  100% { box-shadow: 0 0 0 0 transparent; }
}

.boh-offer.asked { animation: boh-asked 1.1s var(--ease); }

@media (prefers-reduced-motion: reduce) {
  .boh-offer.asked { animation: none; }
}

.boh-offer-text { display: block; }
.boh-offer-lead { display: block; font-size: 17px; font-weight: 700; color: var(--bhtext); }
.boh-offer-sub { display: block; margin-top: 4px; font-size: 14px; line-height: 1.4; color: var(--bhmut); }
.boh-offer-btn { min-height: 56px; font-size: 17px; }

/* ---- the service screen inside the demo frame ------------------------------

   .boh is `position: fixed; inset: 0` because in the owner app it IS the
   screen - it covers the kitchen she was looking at. On /back-of-house it is a
   thing on a page, so it is placed instead of pinned. Nothing else about it
   changes: same palette, same 52px targets, same column.

   IT NEEDS A HEIGHT. The storefront demo's frame grows with its content and
   deliberately has no inner scrollport, so a wheel over it moves the page. A
   service screen cannot work that way - it is a fixed header, a scrolling
   middle and a bar pinned at the bottom, and a rail that cannot scroll is not
   a rail. So this frame is the one place on the marketing site with something
   to scroll inside it, and it is kept short enough that there is always page
   above and below it to catch the wheel. */
.phone.boh-frame { height: var(--demo-frame-h); }

/* The price, under the frame. It is page content, not screen content: it used
   to be the screen's own offer bar and on a phone it ate the top ticket. */
.boh-sell { width: 100%; max-width: min(380px, 100%); margin-top: 4px; }
.boh-sell-lead { margin: 0; color: var(--cream); font-weight: 700; line-height: 1.4; }
.boh-sell-sub { margin: 4px 0 12px; color: var(--mauve2); font-size: var(--fs-sm); }
.boh-sell .btn { width: 100%; }
.phone.boh-frame .phone-screen { height: 100%; min-height: 0; }
.phone-screen .boh { position: absolute; z-index: 1; }

/* PHONE LAYOUT IN A PHONE, whatever the window is doing.

   Back of House widens at 560px and again at 720px, and those are viewport
   queries - correct in the app, where the screen IS the viewport, and wrong
   here, where it is a 380px frame on a 1320px page. Without this the four
   stat columns arrive in a box too narrow to hold them.

   Container queries are what this wants, but retargeting the rules above would
   mean rewriting the screen's whole responsive story to serve a marketing
   page. Overriding for the frame is the smaller change and it is contained. */
.phone-screen .boh-stats { grid-template-columns: repeat(2, 1fr); }

/* ---- THE COMPACT SCALE: A PHONE, AND THE DEMO FRAME ------------------------

   ONE SET OF NUMBERS, TWO PLACES THAT NEED THEM. A phone running the real
   service screen and a 380px mock on a marketing page are the same problem -
   a rail that shows one ticket - and they used to be two answers: the frame
   had its own overrides and the real screen had none, on the reasoning that
   the real one is held at arm's length. That reasoning holds for a tablet on
   a shelf. It does not hold for the phone in her apron, which is what this
   is actually used on, and where a 559px body showed two tickets out of
   eight and five 86 rows out of fifteen.

   TWO SELECTORS BECAUSE THEY CANNOT BE ONE. The real screen is the viewport,
   so it needs a width query; the demo frame is 380px inside a desktop page,
   so a width query never sees it and it needs the class. Both set the same
   properties and nothing else, so there is one scale to change.

   WHAT DOES NOT SHRINK. --bt-act and --b86-h stop at 44px: the advance
   button and the 86 rows are what a cook hits mid-service with wet hands.
   Every other control here already carries its own 52px target through a
   pseudo-element - .boh-step::after, .boh-back::after, .boh-tab::before -
   which no amount of type scaling touches. */
.phone-screen .boh {
  --bt-pad: 9px;
  --bt-code: 26px;
  --bt-who: 14px;
  --bt-qty: 18px;
  --bt-line: 15px;
  --bt-opts: 12.5px;
  --bt-note: 12.5px;
  /* THE ONE NUMBER THE FRAME DOES NOT SHARE. 44px is a floor for a finger,
     and nothing in here is touched by one: this is a picture of a phone on
     a marketing page, at 380px inside a desktop layout. Held to 44 it cost
     the frame a whole ticket, which is the opposite of what the demo is
     for. The real screen keeps the floor, in the media query below. */
  --bt-act: 34px;
  --bt-gap: 7px;
  --b86-h: 44px;
  --b86-name: 14px;
  --b86-sub: 11.5px;
  --bhe-h: 44px;
  --bhe-name: 14px;
  --bcall-pad: 11px;
  --bcall-name: 20px;
  --bcall-sub: 12px;
  --blead-pad: 12px;
  --blead-v: clamp(30px, 9vw, 40px);
  --bstat-h: 58px;
  --bstat-v: 19px;
  --bstat-pad: 9px;
  --bhe-barmt: 4px;
  --bt-linesmt: 7px;
  --bt-actmt: 7px;
  --bwait-pad: 9px;
  --bwait-rowm: 6px 0 3px;
  --b86-gap: 4px;
  --bwait-sub: 11px;
  --bwait-v: 21px;
}

@media (max-width: 430px) {
  .boh {
  --bt-pad: 9px;
  --bt-code: 26px;
  --bt-who: 14px;
  --bt-qty: 18px;
  --bt-line: 15px;
  --bt-opts: 12.5px;
  --bt-note: 12.5px;
  --bt-act: 44px;
  --bt-gap: 7px;
  --b86-h: 44px;
  --b86-name: 14px;
  --b86-sub: 11.5px;
  --bhe-h: 44px;
  --bhe-name: 14px;
  --bcall-pad: 11px;
  --bcall-name: 20px;
  --bcall-sub: 12px;
  --blead-pad: 12px;
  --blead-v: clamp(30px, 9vw, 40px);
  --bstat-h: 58px;
  --bstat-v: 19px;
  --bstat-pad: 9px;
  --bhe-barmt: 4px;
  --bt-linesmt: 7px;
  --bt-actmt: 7px;
  --bwait-pad: 9px;
  --bwait-rowm: 6px 0 3px;
  --b86-gap: 4px;
  --bwait-sub: 11px;
  --bwait-v: 21px;
  }

  /* The header and the body padding give back the rest. They are chrome,
     and on a phone the chrome was costing most of a ticket. */
  .boh-top { padding: calc(var(--safe-t) + 10px) 12px 0; }
  .boh-body { padding: 9px 12px calc(var(--safe-b) + 150px); }
}

/* The over-demand call-out carries a dish name rather than a number, so it
   sizes off the same scale as everything else. */
.boh-call.short .boh-call-name { font-size: var(--bcall-sub); }

.phone-screen .boh-body { padding-left: 12px; padding-right: 12px; }
/* In the app that bottom padding clears a bar that floats over the list. Here
   the bar is a sibling in the column and clears itself. */
.phone-screen .boh-body { padding-bottom: calc(var(--safe-b) + 24px); }

/* Nothing is blurred and nothing is padlocked. The screen is live and real;
   the controls are simply inert until she says yes. */
/* WITHOUT THE ADD-ON, AND ONLY WHAT IS ACTUALLY DEAD IS DRAWN AS DEAD.

   All three were dimmed together, back when all three were disabled. The 86
   toggles and the wait stepper are tappable now - they answer with the offer
   rather than doing nothing - and a control at 0.55 that responds to a press
   is telling two stories at once. So they paint at full strength and the
   advance button, which really is inert until she pays, keeps the dim. */
.boh.preview .boh-advance { opacity: 0.55; }

/* ---- the way in, on the orders tab ---- */

/* THE DOOR TO BACK OF HOUSE, DRAWN AS A CARD AND NOT AS A BILL.

   WHAT IT LOOKED LIKE. An accent-bordered slab on a cool-grey gradient -
   #23232a to #17171c, off the warm ramp entirely and a survivor of the
   reference this product was lifted from - carrying a 21px display title, one
   pixel under the 22px of the section headings it sat above. Three separate
   ways of shouting on a screen somebody opens to cook, for an add-on she does
   not pay for.

   The accent border is the specific problem. At the default accent it is
   brick, which next to --coral's tomato reads as the same family, and coral
   on this product means closed, cancelled, sold out and danger. A door to an
   optional feature was wearing the colour of a problem.

   So it is now the card every other card is: --surface behind, --line around
   it. Nothing about it says urgent, because nothing about it is. */
.boh-enter {
  width: 100%;
  min-height: 60px;
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 13px 16px;
  text-align: left;
  cursor: pointer;
  border-radius: var(--r-card);
  color: var(--cream);
  background: var(--surface);
  border: 1px solid var(--line);
}

.boh-enter:active { transform: scale(0.99); }
.boh-enter-main { min-width: 0; }

/* UNDER A SECTION HEADING, DELIBERATELY. .listhead h3 is --fs-head at 22px;
   this is --fs-item, the size a dish name is drawn at, so the card reads as
   one thing on the Orders tab rather than as a second section competing with
   it. */
.boh-enter-title {
  display: block;
  font-family: var(--font-display);
  font-size: var(--fs-item);
  letter-spacing: 0.01em;
  text-transform: uppercase;
  line-height: 1.15;
}

/* The price, and the quietest line on the card. It was --accent-text, which
   put the one coloured thing in the block on the charge. */
.boh-enter-sub {
  display: block;
  margin-top: 4px;
  font-family: var(--font-mono);
  font-size: var(--fs-tag);
  letter-spacing: var(--track-cap);
  text-transform: uppercase;
  color: var(--mauve2);
}

.boh-enter-badge {
  flex: none;
  min-width: 34px;
  padding: 7px 10px;
  border-radius: var(--r-pill);
  background: var(--accent);
  color: var(--accent-ink);
  font-family: var(--font-mono);
  font-size: 14px;
  text-align: center;
}

/* Wider screens: the panes get room rather than getting bigger. */
@media (min-width: 560px) {
  .boh-body { padding-left: 20px; padding-right: 20px; }
  .boh-stats { grid-template-columns: repeat(4, 1fr); }
  .boh-offer { flex-direction: row; align-items: center; justify-content: space-between; }
  .boh-offer-btn { flex: none; min-width: 220px; }
}

/* ==========================================================================
   STRIPE EMBEDDED COMPONENTS

   A page Stripe requires us to host, because our connected accounts have no
   Stripe dashboard of their own. Everything inside #stripe-embed is Stripe's
   and cannot be styled from here - Connect.js takes appearance options at
   init, not CSS. What is styled here is the frame around it, so a cook lands
   somewhere that looks like the rest of her app rather than on a bare panel.

   The component inherits font-family and background from its parent, which is
   the one hook CSS has: set those on the body below and Stripe matches.
   ========================================================================== */

/* Fixed and over everything, the way #view-boh is. A view that is merely the
   next block in the document sits BELOW the screen it replaced - which is what
   happened first time: the component mounted and painted correctly, a full
   screen further down the page than anybody would scroll. */
.stripe-view {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  flex-direction: column;
  background: var(--ink);
}

.stripe-top {
  flex: none;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: calc(var(--safe-t) + 14px) var(--pad) 14px;
  border-bottom: 1px solid var(--line);
}

.stripe-back {
  flex: none;
  width: 44px; height: 44px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--line2);
  border-radius: var(--r-pill);
  background: var(--surface);
  color: var(--cream);
}

.stripe-back svg { width: 20px; height: 20px; fill: currentColor; }

.stripe-id { min-width: 0; }
.stripe-title { display: block; font-size: 19px; font-weight: 700; color: var(--cream); }
.stripe-sub { display: block; margin-top: 2px; font-size: var(--fs-sm); color: var(--mauve2); }

/* Stripe's component reads these two off its parent. */
.stripe-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 16px var(--pad) calc(var(--safe-b) + 32px);
  background: var(--ink);
  font-family: var(--font-body);
}

#stripe-embed { margin-top: 12px; }

/* ==========================================================================
   THE OWNER'S SCREEN

   /owner. Not part of the app and not on the marketing site: a private tool for
   the one person running the platform, on the same tokens as everything else so
   it reads as the same product.

   It is scanned, not read. The summary comes before the detail, anything
   needing a person is lifted above the list, and state is a coloured chip
   rather than a number somebody has to interpret. Semantic colour here (good,
   warn, bad) is separate from the brick accent and never stands in for it.
   ========================================================================== */

.own {
  max-width: 1000px;
  margin: 0 auto;
  padding: calc(var(--safe-t) + 28px) var(--pad) calc(var(--safe-b) + 72px);
}

.own-top {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 16px;
  padding-bottom: 20px;
  border-bottom: 1px solid var(--line);
}

.own-eyebrow {
  display: block;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.18em;
  color: var(--mauve2);
}

.own-h1 { margin: 6px 0 0; font-size: clamp(30px, 6vw, 44px); line-height: 1; }
.own-zone { font-family: var(--font-mono); font-size: 12px; color: var(--mauve2); }

.own-locked { padding: 60px 0; display: grid; gap: 16px; justify-items: start; }
.own-locked-lead { margin: 0; font-size: 18px; color: var(--cream); }

/* WHICH VIEW, not which timeframe, and the difference has to be visible.

   The owner screen had no view switcher before this: .own-ranges below is a
   TIMEFRAME picker that refetches, and a second row of the same pills would
   have put two identical controls one above the other meaning different
   things. So this borrows the segmented-control shape Back of House already
   uses for its panes - a recessed tray with one raised segment - and repaints
   it in the owner's warm palette. Same shape, same control vars, different
   ground; nothing new was drawn.

   Two colours do the work: the tray is --surface, one step down from the page,
   and the segment you are on is --panel, the lightest flat chrome. */
.own-tabs {
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
  gap: 2px;
  margin: 22px 0 4px;
  padding: 2px;
  border-radius: var(--ctl-r-out);
  background: var(--surface);
  border: 1px solid var(--line);
}

.own-tab {
  position: relative;
  min-height: 36px;
  padding: 0 10px;
  border-radius: var(--ctl-r);
  border: 0;
  background: none;
  color: var(--mauve);
  font-family: var(--font-body);
  font-size: var(--fs-sm);
  font-weight: var(--ctl-fw);
  cursor: pointer;
}

/* The segment is 36px to look at and a full tap target to hit, the same trick
   .boh-tab uses so a thin control is not a thin thing to press. */
.own-tab::before {
  content: '';
  position: absolute;
  left: 0; right: 0; top: 50%;
  transform: translateY(-50%);
  height: var(--tap);
}

.own-tab.on { background: var(--panel); color: var(--cream); }

.own-ranges { display: flex; flex-wrap: wrap; gap: 8px; margin: 22px 0 20px; }

/* The same control, under the list it orders rather than above the numbers it
   does not. Tighter above than below because it hangs off the KITCHENS
   heading: it belongs to the list, and the gap should say so. */
.own-sorts { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 14px; }

.own-range {
  min-height: 38px;
  padding: 0 14px;
  border-radius: var(--r-pill);
  border: 1px solid var(--line2);
  background: transparent;
  color: var(--mauve);
  font-size: var(--fs-sm);
}

.own-range.on { background: var(--cream); border-color: var(--cream); color: var(--ink); font-weight: 600; }

/* THE FILTER CHIP, WHICH IS THE SAME CONTROL AND NOT THE SAME GROUP.
   It shares every rule .own-range has because to a thumb it is the same thing;
   it has its own class only so paintOn's sweep over .own-range cannot switch
   it off when a sort is chosen. Its `on` state is the accent rather than the
   cream the sorts use, so a glance tells a narrowed list from a re-ordered
   one without reading either. */
.own-toggle {
  min-height: 38px;
  padding: 0 14px;
  border-radius: var(--r-pill);
  border: 1px solid var(--line2);
  background: transparent;
  color: var(--mauve);
  cursor: pointer;
}
.own-toggle.on {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
  font-weight: 600;
}

/* The Stripe status is the one stat with a state rather than a quantity, so it
   is the one that takes a colour. Exactly the three .own-chip already uses on
   this screen - var(--fresh), the same #e0a54a, var(--coral) - because a state
   should not change colour depending on which control it is drawn in. */
.own-stat-value.good { color: var(--fresh); }
.own-stat-value.warn { color: #e0a54a; }
.own-stat-value.bad  { color: var(--coral); }

.own-totals {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 1px;
  margin: 0 0 22px;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  overflow: hidden;
}

.own-stat { background: var(--surface); padding: 14px 16px; display: block; }
.own-stat-label {
  display: block;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.14em;
  color: var(--mauve2);
  margin-bottom: 5px;
}
.own-stat-value {
  display: block;
  font-size: 21px;
  font-weight: 700;
  color: var(--cream);
  font-variant-numeric: tabular-nums;
}
.own-stat-sub { display: block; margin-top: 3px; font-size: 12px; color: var(--mauve2); }

/* Lifted above the list so it is not something you have to notice. */
.own-attention {
  padding: 14px 16px;
  margin: 0 0 22px;
  border-radius: var(--r-card);
  border: 1px solid color-mix(in srgb, var(--coral) 45%, transparent);
  background: color-mix(in srgb, var(--coral) 10%, var(--surface));
}
.own-attention-head {
  margin: 0 0 8px;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.14em;
  color: var(--coral);
}
.own-attention-row { margin: 0 0 4px; font-size: var(--fs-sm); color: var(--cream); }
.own-attention-row:last-child { margin-bottom: 0; }

/* A row that can be cleared from where it is read. An unpaid bonus is the only
   thing in this block that has an action, because it is the only one where the
   whole of the fix is "send it and say so" - a kitchen that owes us is not
   settled by a button here. */
.own-attention-act {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px 16px;
  flex-wrap: wrap;
  padding: 6px 0;
}

.own-attention-act .own-act { flex: none; }

/* Two groups in one block - bonuses, then kitchens - so the second heading
   needs air above it that the first does not. */
.own-attention-row + .own-attention-head { margin-top: 14px; }

/* A REPORT, WITH THE WORDS IN IT.

   Deliberately roomier than a kitchen row: the comment is the whole reason
   this screen exists and is the one thing here that must not be truncated or
   squeezed. Unsafe food takes the coral edge, everything else stays neutral,
   so a list of five is scannable for the one that cannot wait. */
.own-reports { display: flex; flex-direction: column; gap: 10px; margin: 0 0 22px; }

/* The programme in one line, above the affiliates it is evidence about. Same
   three-up shape as the platform totals, because it answers the same kind of
   question and should be read the same way. */
.own-split {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 1px;
  margin: 0 0 14px;
  border-radius: var(--r-card);
  overflow: hidden;
  background: var(--line);
}
.own-split .own-stat { background: var(--surface); margin: 0; }

.own-affiliates { display: flex; flex-direction: column; gap: 10px; margin: 0 0 22px; }

.own-report {
  padding: 14px 16px;
  border-radius: var(--r-card);
  border: 1px solid var(--line);
  background: var(--surface);
}
.own-report.bad {
  border-color: color-mix(in srgb, var(--coral) 45%, transparent);
  background: color-mix(in srgb, var(--coral) 8%, var(--surface));
}

.own-report-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  margin-bottom: 6px;
}
.own-report-shop { font-weight: 600; color: var(--cream); }
.own-report-when {
  font-family: var(--font-mono); font-size: 11px;
  letter-spacing: 0.08em; color: var(--mauve2); white-space: nowrap;
}

.own-report-reason { margin: 0 0 8px; font-size: var(--fs-sm); color: var(--mauve); }
.own-report.bad .own-report-reason { color: var(--coral); }

/* Their own words, their own line breaks, not shortened. */
.own-report-comment {
  margin: 0 0 10px;
  padding: 10px 12px;
  border-radius: 10px;
  background: var(--panel);
  color: var(--cream);
  font-size: var(--fs-sm);
  line-height: 1.5;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.own-report-none { margin: 0 0 10px; font-size: var(--fs-sm); color: var(--mauve2); }

.own-report-foot { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; }
.own-report-code {
  font-family: var(--font-mono); font-size: 11px;
  letter-spacing: 0.06em; color: var(--mauve2);
}
.own-report-link { font-size: var(--fs-sm); color: var(--mauve); }

/* The count sits on the same line as the heading, so the heading says how
   many as well as what. */
.own-listhead .cap { margin-left: 10px; letter-spacing: 0; text-transform: none; }

.own-listhead {
  margin: 0 0 10px;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.14em;
  color: var(--mauve2);
}

.own-list { display: flex; flex-direction: column; gap: 12px; }

/* ---- activation, on the owner screen -------------------------------- */

/* THE FUNNEL. Same tile vocabulary as the money totals above it, because it is
   the same kind of reading: a number with a label and a line of context. */
.own-funnel .own-stat-value { font-size: 20px; }

.own-sub {
  margin: -4px 0 10px;
  color: var(--mauve2);
  font-size: 12px;
  font-family: var(--font-mono);
  letter-spacing: var(--track-cap);
  text-transform: uppercase;
}

/* A KITCHEN'S PROGRESS, LEFT TO RIGHT, WRAPPING ON A PHONE.

   Each step is a dot, a word and a date. The dot is now, the date is when it
   first happened, and they are allowed to disagree - see activationStrip().
   Scrolls rather than compresses: seven steps squeezed onto 414px would be
   seven illegible words, and horizontal scroll inside the row keeps the page
   itself from ever scrolling sideways. */
.own-activation {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 14px;
  padding: 10px 0 2px;
  border-top: 1px solid var(--line);
  margin-top: 10px;
}

.own-step { display: inline-flex; align-items: center; gap: 6px; min-width: 0; }

.own-step-dot {
  flex: 0 0 8px;
  width: 8px; height: 8px;
  border-radius: var(--r-pill);
  border: 1.5px solid var(--line2);
}
.own-step.done .own-step-dot { background: var(--fresh); border-color: var(--fresh); }

.own-step-label { font-size: 12px; color: var(--mauve2); white-space: nowrap; }
.own-step.done .own-step-label { color: var(--cream); }

/* The date, and it is the quietest thing in the row on purpose: "unknown" is
   the commonest value on this platform today and it must not read as an
   error. */
.own-step-when {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--mauve2);
  opacity: 0.75;
  white-space: nowrap;
}

.own-activation-note {
  flex: 1 1 100%;
  margin: 4px 0 0;
  font-size: 12px;
  line-height: 1.45;
  color: var(--mauve2);
}

/* A FOOTNOTE UNDER A FIGURE, AND DELIBERATELY NOT .own-sub.

   .own-sub is a label: uppercase mono at 12px with caps tracking, which is
   right for four words naming a denominator and unreadable for a sentence
   explaining what was left out of it. This is the sentence. It tucks up under
   the tiles - .own-totals carries 22px beneath itself - so the caveat reads as
   belonging to the number above rather than floating between two blocks. */
.own-caveat {
  margin: -14px 0 22px;
  font-size: 12px;
  line-height: 1.45;
  color: var(--mauve2);
}

.own-row {
  padding: 16px;
  border-radius: var(--r-card);
  border: 1px solid var(--line);
  background: var(--surface);
}

/* A hidden kitchen is still readable, just plainly not live. */
.own-row.off { opacity: 0.62; }

.own-row-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.own-who { min-width: 0; }
.own-name { display: block; font-size: 18px; font-weight: 700; color: var(--cream); }
.own-slug { display: block; margin-top: 2px; font-family: var(--font-mono); font-size: 12px; color: var(--mauve2); }
.own-plan { flex: none; font-family: var(--font-mono); font-size: 12px; color: var(--mauve); }

.own-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }

.own-chip {
  font-family: var(--font-mono);
  font-size: 10.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 3px 8px;
  border-radius: 4px;
  border: 1px solid var(--line2);
  color: var(--mauve);
}
.own-chip.good { border-color: color-mix(in srgb, var(--fresh) 50%, transparent); color: var(--fresh); }
.own-chip.warn { border-color: color-mix(in srgb, #e0a54a 55%, transparent); color: #e0a54a; }
.own-chip.bad  { border-color: color-mix(in srgb, var(--coral) 55%, transparent); color: var(--coral); }

/* NOT A STATUS, A LABEL. Every other chip is a fact about how a kitchen is
   doing and takes a semantic colour; this one says the row is not a seller at
   all. Filled rather than outlined so it reads as a stamp on the row instead
   of another condition in the same list. */
.own-chip.test {
  border-color: transparent;
  background: var(--mauve);
  color: var(--ink, #1b1310);
  font-weight: 700;
}

.own-figures {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
  gap: 1px;
  margin-top: 14px;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: var(--r-input);
  overflow: hidden;
}
.own-figures .own-stat { background: var(--panel); padding: 11px 13px; }
.own-figures .own-stat-value { font-size: 17px; }

/* ==========================================================================
   THE PLATFORM SCREEN ON A PHONE

   Everything here is a size, not a rearrangement: the same blocks in the same
   order, drawn to fit a hand. It is the only max-width query in this section
   and it stops one pixel below the 560px the Back of House stats already open
   up at, so there is a single boundary in the product rather than two that
   nearly agree. Nothing above 559px is touched.

   WHAT WAS WRONG. At 390px the five totals fell into two columns, which is
   three rows and, because five is odd, an empty sixth cell showing as a bare
   slab of --line. That is 299px. The timeframe pills, at 38px tall and 13px
   type, needed two rows for four words. Between them they pushed the KITCHENS
   heading to 612px, so the list this screen exists to show started below the
   fold on every phone made.
   ========================================================================== */
@media (max-width: 559px) {

  /* THREE THEN TWO, which is five with no hole in it.

     Six tracks, the first three chips spanning two each and the last two
     spanning three each. The wide row is the lower one deliberately: it holds
     WE TOOK and OWED, whose sub-lines are the longest, and at half the width
     they stay on one line. 299px becomes 192px.

     THIS COUNTS TO FIVE. renderTotals() appends exactly five, and if a sixth
     is ever added it lands in the wide row and this needs revisiting. */
  .own-totals { grid-template-columns: repeat(6, minmax(0, 1fr)); }
  .own-totals .own-stat { grid-column: span 2; padding: 11px 12px; }
  .own-totals .own-stat:nth-child(4),
  .own-totals .own-stat:nth-child(5) { grid-column: span 3; }
  .own-totals .own-stat-value { font-size: 19px; }
  .own-totals .own-stat-sub { font-size: 11px; line-height: 1.35; }

  /* CAME BACK HAS TWO CELLS, and two spanning two each would leave a third of
     the row as a bare slab of --line - the same hole the rule above exists to
     close. Three and three is one even row. It counts to two: renderRepeat()
     appends exactly two tiles, and the note under the heading is where
     anything further goes. */
  .own-repeat .own-stat { grid-column: span 3; }

  /* The referral split is the same shape answering the same kind of question,
     so it tightens with it. It has four cells, not five, so two and two is
     already even and only the sizes change. */
  .own-split .own-stat { padding: 11px 12px; }
  .own-split .own-stat-value { font-size: 19px; }
  .own-split .own-stat-sub { font-size: 11px; line-height: 1.35; }

  /* A ROW OF CHIPS IS A RAIL, the same one the storefront pans its categories
     on. Wrapping is what made these two rows tall; a rail is one row that
     scrolls if it has to, and at 390px neither of these has to - the four
     timeframes measure 298px of the 354px available and the four sorts 301px.
     Below about 340px they start to scroll, which is the point of it.

     The rail rules are copied rather than shared because .rail is not
     breakpoint-scoped and wearing it would resize these chips on the desktop
     too. Same numbers, same reasoning, stated where it applies.

     overflow-y is said out loud for the reason the .rail comment gives: a
     computed visible on one axis becomes auto when the other is not, so
     declaring only overflow-x would quietly make these vertical scrollers and
     the 52px tap band below would be what they scrolled. */
  .own-ranges,
  .own-sorts {
    flex-wrap: nowrap;
    gap: var(--ctl-gap);
    overflow-x: auto;
    overflow-y: hidden;
    overscroll-behavior-x: contain;
    /* The rail is --tap tall inside and --ctl-h tall in the layout: the
       padding buys the band, the negative margin gives the space back to the
       stack, and the horizontal pair lets it bleed to the page edge so a chip
       scrolled to the end is not clipped by the container's padding. */
    padding: var(--stack) var(--pad);
    margin: calc(22px - var(--stack)) calc(var(--pad) * -1) calc(20px - var(--stack));
  }

  .own-sorts { margin-top: calc(var(--stack) * -1); margin-bottom: calc(14px - var(--stack)); }

  /* Chip sizing, and the 52px target bought the way .chip buys it: extend the
     target, do not inflate the visual. Without the pseudo-element these would
     be 30px things to press, which is the one way this change could make the
     screen worse than it was. */
  .own-range {
    position: relative;
    flex-shrink: 0;
    min-height: var(--ctl-h);
    padding: 0 var(--ctl-pad);
    font-size: var(--ctl-fs);
    font-weight: var(--ctl-fw);
    white-space: nowrap;
  }

  .own-range::before {
    content: '';
    position: absolute;
    left: 0; right: 0; top: 50%;
    transform: translateY(-50%);
    height: var(--tap);
  }
}

.own-owed { margin: 12px 0 0; font-size: var(--fs-sm); color: var(--mauve); }
.own-owed.bad { color: var(--coral); }

.own-acts { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }

.own-act {
  min-height: 40px;
  padding: 0 14px;
  border-radius: var(--r-pill);
  border: 1px solid var(--line2);
  background: transparent;
  color: var(--cream);
  font-size: var(--fs-sm);
}
.own-act:disabled { opacity: 0.5; }

.own-empty { color: var(--mauve2); }

/* ==========================================================================
   SETTINGS ROWS

   Taken from the mockups' iOS Settings language: grouped inset lists, a quiet
   uppercase header above each group, a row that puts the label on the left and
   its current value on the right with a chevron, and a footnote under the
   group rather than a paragraph inside it.

   NOT taken: the coloured icon square on every row. Five icon colours is five
   accents, and this product has one. The rows carry their meaning in words.
   Also not taken: the system font and the blue. Anton still sets headings,
   Inter still sets rows, DM Mono still sets the group headers, and the only
   accent is brick.
   ========================================================================== */

.group-head {
  display: block;
  margin: 18px 0 8px;
  padding: 0 4px;
  font-family: var(--font-mono);
  font-size: var(--fs-tag);
  letter-spacing: var(--track-cap);
  text-transform: uppercase;
  color: var(--mauve2);
}

.rowgroup {
  border-radius: var(--r-card);
  border: 1px solid var(--line);
  background: var(--surface);
  overflow: hidden;
}

/* The row. A button, an anchor or a plain div, all the same shape. */
.row {
  width: 100%;
  display: flex; align-items: center; gap: 12px;
  min-height: 56px;
  padding: 12px 14px;
  border: 0;
  border-top: 1px solid var(--line);
  background: none;
  color: var(--cream);
  font-family: var(--font-body);
  font-size: var(--fs-body);
  text-align: left;
  cursor: pointer;
}

.rowgroup > .row:first-child,
.rowgroup > *:first-child .row { border-top: 0; }

/* .row is worn by buttons and, for the one link out of this app, by an
   anchor. Both press the same way; only the anchor needs its underline
   taking off. */
a.row { text-decoration: none; }
button.row:active,
a.row:active { background: var(--panel); }
.row[disabled] { opacity: 0.5; cursor: default; }

.row-main { flex: 1; min-width: 0; }
.row-label { display: block; color: var(--cream); }

.row-sub {
  display: block;
  margin-top: 2px;
  color: var(--mauve2);
  font-size: var(--fs-desc);
  line-height: 1.35;
}

/* The current setting, on the right, where the mockups put it. Truncates
   rather than wrapping: the row is a summary and the detail is one tap away. */
.row-value {
  flex: none;
  max-width: 48%;
  color: var(--mauve);
  font-family: var(--font-mono);
  font-size: var(--fs-desc);
  text-align: right;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.row-chev {
  flex: none;
  width: 16px; height: 16px;
  fill: none; stroke: var(--mauve2); stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
  transform: rotate(-90deg);
}

/* The footnote under a group, in the mockups' voice: what happens, not what
   to do. */
.group-note {
  display: block;
  margin: 8px 4px 0;
  color: var(--mauve2);
  font-size: var(--fs-desc);
  line-height: 1.5;
}

/* ---- small pills, full targets -------------------------------------------

   The prep time picker was thirteen pills at 52px each, a wall of them. The
   mockups show three common choices and put the rest behind a row. These are
   the three.

   The visible pill is 38px. The TARGET is 52px, extended by a pseudo-element
   that reaches above and below the pill without moving anything in the layout.
   Small to look at, full size to hit. */
/* Row gap is the 52px arithmetic, column gap is just spacing: the pills are
   wide, so their side-by-side targets never meet. */
.chiprow {
  display: grid; grid-template-columns: repeat(3, 1fr);
  gap: calc(var(--tap) - var(--ctl-h) + 2px) var(--ctl-gap);
}

.chip-sm {
  position: relative;
  min-height: var(--ctl-h);
  padding: 0 var(--ctl-pad);
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--ctl-r);
  border: 1px solid var(--line);
  background: var(--panel);
  color: var(--mauve);
  font-family: var(--font-body);
  font-size: var(--ctl-fs);
  font-weight: var(--ctl-fw);
  cursor: pointer;
  white-space: nowrap;
}

.chip-sm::before {
  content: '';
  position: absolute;
  left: 0; right: 0;
  top: 50%;
  transform: translateY(-50%);
  height: var(--tap);
}

/* The chosen one is a quiet raised fill, the way the switch marks its side,
   not a slab of accent. Accent on a filter reads as an action. */
.chip-sm[aria-pressed="true"] {
  background: var(--cream);
  border-color: var(--cream);
  color: var(--ink);
}

/* The nine browse buckets, as a wrapping row of the same compact pill the
   notice presets use. Multi-select, so more than one can read as chosen at
   once, and the row gap is the 52px arithmetic like every other wrapped row of
   controls in the product. */
.kindrow {
  display: flex;
  flex-wrap: wrap;
  gap: calc(var(--tap) - var(--ctl-h) + 2px) var(--ctl-gap);
}

/* ---- the storefront preview ---------------------------------------------

   Her own live storefront, in an iframe, inert. Not a mock and not a second
   renderer that could drift from the real one: it is the page a customer
   loads, so sold out items, a closed kitchen and a quoted wait are all
   whatever they actually are right now. */

.preview {
  position: fixed;
  inset: 0;
  z-index: 70;
  display: flex;
  flex-direction: column;
  background: var(--backdrop);
}

.preview-bar {
  flex: none;
  display: flex; align-items: center; gap: 12px;
  padding: calc(var(--safe-t) + 10px) 14px 10px;
  background: var(--ink);
  border-bottom: 1px solid var(--line);
}

.preview-back {
  flex: none;
  width: 52px; height: 52px;
  display: grid; place-items: center;
  border-radius: 14px;
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--cream);
  cursor: pointer;
}

.preview-back svg {
  width: 22px; height: 22px;
  fill: none; stroke: currentColor; stroke-width: 2.2;
  stroke-linecap: round; stroke-linejoin: round;
}

.preview-id { flex: 1; min-width: 0; }

.preview-title {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--fs-tag);
  letter-spacing: var(--track-cap);
  text-transform: uppercase;
  color: var(--accent-text);
}

.preview-sub {
  display: block;
  margin-top: 2px;
  color: var(--mauve2);
  font-size: var(--fs-desc);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.preview-stage { flex: 1; min-height: 0; position: relative; background: var(--ink); }

/* Interactive on purpose. It is the real storefront and she can tap through
   it exactly as a customer would; what stops it doing harm is in store.js,
   not here. See the preview gate in api(). */
.preview-stage iframe {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  border: 0;
}

.preview-note {
  flex: none;
  padding: 10px 14px calc(var(--safe-b) + 12px);
  background: var(--ink);
  border-top: 1px solid var(--line);
  color: var(--mauve2);
  font-size: var(--fs-desc);
  line-height: 1.45;
  text-align: center;
}

/* ---- rows that hold a control -------------------------------------------

   A settings row can be a link to somewhere else, or it can be the control
   itself. These are the second kind: the label sits where a label sits and the
   field sits under it, inside the same 56px row shape, so a group of inputs
   reads as one list rather than as a stack of cards. */

/* A FIELD INSIDE A SETTINGS ROW IS NOT A BOX YOU AIM AT.
   The row is the target, and it is a <label>, so a tap anywhere along it puts
   the caret in here. But the input was inheriting the form block's 52px
   minimum, which it spent as empty space under the text: every text row on the
   Kitchen screen was 96px tall to hold one line of typing.

   The type stays at 16px. That is what stops iOS zooming the page on focus and
   it is the one number in this rule that is not negotiable. */
.row-input {
  display: block;
  width: 100%;
  min-height: 24px;
  margin-top: 3px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--cream);
  font-family: var(--font-body);
  /* 16px stops iOS zooming the page when a field takes focus. */
  font-size: 16px;
  line-height: 1.35;
  outline: none;
  resize: none;
}

.row-input::placeholder { color: var(--mauve2); }

/* Two lines of room rather than the form block's five. A pickup note is a
   sentence, and the field grows when somebody writes more than one. */
textarea.row-input { min-height: 44px; }

.row .prefixed { margin-top: 4px; background: none; border: 0; padding: 0; }
.row .prefixed input { font-size: 16px; }

/* A group that holds something other than rows: pills, swatches. */
.rowgroup-pad { padding: 14px; display: flex; flex-direction: column; gap: 12px; }

/* A row inside a padded group, pulled back out to the group's edges so its
   separator still runs the full width. */
.row-flush { margin: 0 -14px -14px; border-top: 1px solid var(--line); }

/* A row that is its own group. */
/* A row that is its own group. The margin separates it from the group above it
   inside the settings form, where the form does not space its own children. */
.row-solo {
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  background: var(--surface);
  margin-top: 18px;
}

/* Except in a stack, where the stack does the spacing. Without this the plan
   row on the Money screen sat 29px below the bill card while every other gap
   on that screen was 11: the row's own margin, added to the stack's gap. */
.tab > .row-solo { margin-top: 0; }

.row-solo[aria-expanded="true"] .row-chev { transform: rotate(0deg); }

/* The way to reach a person, drawn wherever something to do with money has
   gone wrong. Quieter than the rows around it on purpose: it is a fallback,
   not a feature, and it should not compete with Pay now.

   margin-top is zeroed for the same reason .tab > .row-solo zeroes it - these
   are always drawn inside a stack, and the row's own margin on top of the
   stack's gap is the 29px-instead-of-11px bug noted above. */
.help-row { margin-top: 0; background: transparent; }
.help-row .row-label { font-weight: 600; }

/* ASKING TO BE FILMED. Drawn inside a stack, so the row's own 18px margin is
   zeroed for the same reason .help-row zeroes it.

   TWO OF ITS THREE STATES ARE NOT BUTTONS, and `.row` carries cursor: pointer
   unconditionally - it was written for rows that are all controls. A div with
   that class inherits the pointer and the press affordance while doing
   nothing, which is the same false promise the storefront's menu rows used to
   make. So the cursor comes off explicitly here rather than being left to the
   element type to imply. */
.feature-row { margin-top: 0; }
.feature-row.locked,
.feature-row.done { cursor: default; }

/* Not yet something there would be anything to film. Dimmed, but nowhere near
   as far as a sold-out dish: this is a thing she can still get to today by
   adding one photograph, and the sub line under it says so. */
.feature-row.locked { opacity: 0.72; }

/* She asked. The tick is the same one the notice sheet marks a chosen row
   with, so "this is settled" looks the same in both places. */
.feature-row.done .row-label { color: var(--fresh); }

/* An empty slot must not spend a stack gap on nothing. */
#bill-help:empty { display: none; }

/* The refusal, under the field rather than in the toast strip along the bottom
   of the screen - which on a phone is behind the keyboard she is still typing
   on. --bad is the same red the rest of the tool uses for a refusal. */
.help-error {
  margin: 0;
  padding: 10px 12px;
  border: 1px solid var(--coral);
  border-radius: var(--r-card);
  color: var(--coral);
  font-size: 14px;
  line-height: 1.45;
}

/* The switch, without the label text it normally carries: in a row the label
   is already the row's own. */
.switch-bare { flex: none; display: inline-flex; align-items: center; }
.switch-bare .track { margin: 0; }

/* The tick beside the chosen row in a picker. */
.row-tick {
  flex: none;
  width: 18px; height: 18px;
  fill: none; stroke: var(--accent-text); stroke-width: 2.4;
  stroke-linecap: round; stroke-linejoin: round;
}

/* A COMPACT CONTROL'S TARGET HAS TO WIN AGAINST WHAT IS NEXT TO IT.
 *
 * Every control below is drawn smaller than 52px and buys the rest with a
 * pseudo-element that reaches past its own box. That reach lands on top of
 * whatever is above or below it in the flow - a field label, a caption - and
 * since none of those were positioned, THEY painted last and swallowed the
 * edge of the target. A pickup slot measured 51px of reach and the storefront
 * stepper 50, both stolen by the label of the next field down.
 *
 * One line each, and the invisible target wins. Nothing visible moves: these
 * elements have no background of their own that could cover a neighbour.
 */
.slot, .stepper, .chip-sm, .savebtn, .check-go { position: relative; z-index: 1; }

/* ---- hit targets on the compact controls --------------------------------

   Every control in this product is a 52px target even when it is drawn
   smaller. These four are drawn smaller on purpose: the open/closed pill, the
   fees bar, the small icon buttons and the "Do it" links on the checklist all
   sit inside dense rows where a 52px box would push everything apart.

   So the box stays the size it is and the TARGET is extended with a
   pseudo-element, the same way .chip-sm does it. ::after is already spoken for
   on .pill and .iconbtn, so these use ::before. Nothing moves; the finger just
   has more room than the eye needs. */

.pill, .iconbtn, .balance, .btn.check-go { position: relative; }

.pill::before,
.iconbtn::before,
.balance::before,
.btn.check-go::before {
  content: '';
  position: absolute;
  left: 0; right: 0;
  top: 50%;
  transform: translateY(-50%);
  height: var(--tap);
  /* Behind the content, so it never eats a click meant for a child. */
  z-index: 0;
}

/* ==========================================================================
   THE TOOL

   Everything below is scoped to .tool, which only ever sits on the owner app's
   root. The marketing site and the storefront never carry it, so nothing here
   can reach them, and nothing they define is restated here: this block only
   overrides.

   The seller's own accent still arrives as an inline style on <html> from
   applyAccent(), so it wins over these tokens by specificity. That is
   deliberate and harmless: inside the tool the accent is used for one thing,
   the current tab, and every other coloured thing on screen is a status.

   WHY IT LOOKS DIFFERENT
   Ground goes neutral and near black. Type goes to Inter almost everywhere,
   with Anton kept for screen titles and genuinely large numbers, and DM Mono
   kept only for a slug, a code or a price where the tabular shape is the
   point. Everything tightens: smaller sizes, less padding, hairline rules
   instead of gaps between floating cards. Big rounded buttons go.
   ========================================================================== */

.tool {
  /* A neutral ramp with real steps between the levels. The warm cocoa one is
     three shades of the same brown; this is a ground, a surface and a raised
     surface you can actually tell apart at arm's length in a bright kitchen. */
  --backdrop: #08080a;
  --ink: #0e0e10;
  --surface: #17171a;
  --panel: #202024;
  --disc1: #26262b;
  --disc2: #161619;

  --line: rgba(255, 255, 255, 0.09);
  --line2: rgba(255, 255, 255, 0.16);

  /* Near white, then two greys. No warmth in any of them. */
  --cream: #f2f2f4;
  --mauve: #a8a8b0;
  --mauve2: #74747d;

  /* MEANING, NOT DECORATION. Green is open, running, go. Red is stop, 86,
     cancelled. Amber is timing and warning. There is no fourth colour and
     there are no decorative icon tints. */
  --fresh: #4ade80;
  --coral: #f87171;
  --warn: #fbbf24;
  --lime: #4ade80;

  /* Tighter type. The mockups sit at 17 for a row label and 10 to 12 for
     everything secondary, and almost nothing is bigger than that. */
  --fs-hero: 30px;
  --fs-title: 21px;
  --fs-head: 17px;
  --fs-item: 15px;
  --fs-body: 14px;
  --fs-sm: 13px;
  --fs-desc: 12px;
  --fs-label: 11px;
  --fs-tag: 10px;

  /* Less rounding. A tool is squarer than a brand. */
  --r-sheet: 20px;
  --r-card: 12px;
  --r-tile: 10px;
  --r-control: 10px;
  --r-input: 8px;

  --pad: 14px;

  /* No accent glow anywhere in here: it was the thing that made every button
     look like a marketing CTA. */
  --shadow-cta: none;

  background: var(--backdrop);
}

/* ---- type ---------------------------------------------------------------

   Anton was on every heading, every item name, every price and every label,
   which is what made the tool read as a poster. It now appears on exactly two
   things: a screen title and a big number. Everything else is Inter. */

.tool .display {
  font-family: var(--font-body);
  font-weight: 650;
  letter-spacing: -0.01em;
  text-transform: none;
  line-height: 1.25;
}

/* The two places Anton earns its keep. */
.tool .view > .listhead > h3.display,
.tool .bignum {
  font-family: var(--font-display);
  font-weight: 400;
  letter-spacing: 0.01em;
  text-transform: uppercase;
  line-height: 1.05;
}

/* Mono survives where the shape of the characters is the information: a slug,
   an order code, a price in a column. Not on labels. */
.tool .label,
.tool .cap {
  font-family: var(--font-body);
  font-size: var(--fs-label);
  font-weight: 550;
  letter-spacing: 0.01em;
  text-transform: none;
  color: var(--mauve2);
}

/* The quiet group header keeps its uppercase, because that is what makes a
   settings list scannable, but in Inter at a small size rather than in mono. */
.tool .group-head {
  font-family: var(--font-body);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--mauve2);
  margin: 16px 0 6px;
}

/* ---- density ------------------------------------------------------------

   Cards floating in gaps become groups separated by hairlines. */

.tool .tab { padding: var(--stack) var(--pad) calc(var(--barh) + var(--safe-b) + var(--barclear)); }

.tool .card {
  border-radius: var(--r-card);
  padding: 12px 14px;
  gap: 10px;
  box-shadow: none;
}

.tool .rowgroup { border-radius: var(--r-card); }
.tool .row { min-height: 50px; padding: 9px 12px; font-size: var(--fs-item); }
.tool .row-sub { font-size: 11.5px; }
.tool .row-value { font-size: 12px; font-family: var(--font-body); }

/* NO PADDING. It carried 2px above and 4px below, and padding on a text-only
   block paints nothing - so the gap a person SEES under a screen title was
   4px wider than every other gap on the page. Measured from the render: 15px
   under the Money heading against 11px everywhere else. A block in a stack is
   spaced by the stack, and a heading has enough air of its own in its leading. */
.tool .listhead { padding: 0; }
.tool .listhead h3 { font-size: var(--fs-title); }

/* The menu row. It was a 64px photo tile in 14px of padding, so five dishes
   filled a screen and the sixth needed a scroll. 46px tile, 10px of padding,
   which is still a picture you can judge a plate by at the size a list row
   wants to be. */
.tool .item { padding: 10px; gap: 10px; }
.tool .item-tile { width: 46px; height: 46px; font-size: 20px; }
.tool .item-desc { margin: 2px 0 6px; }
.tool .list { gap: 6px; }

/* The .tool block's own rule, applied where it had been missed: Anton is for a
   screen title and a big number, and a dish name in a list is neither. It was
   the single loudest thing left in here - five uppercase condensed headlines
   down a menu - and it is the reason the tool still read as a poster. The
   price keeps mono, because a column of prices is exactly the case mono is
   kept for. */
.tool .item-name {
  font-family: var(--font-body);
  font-weight: 600;
  letter-spacing: -0.01em;
  text-transform: none;
  font-size: var(--fs-item);
}

.tool .item-price { font-size: 13px; }

/* ---- controls -----------------------------------------------------------

   No big rounded pill buttons. A primary action is a compact button; a
   secondary one is a row or an inline link. The 52px target is kept by the
   same pseudo-element trick used elsewhere, so the button can be 38px and
   still be honest under a thumb. */

.tool .btn {
  position: relative;
  min-height: 38px;
  padding: 0 14px;
  border-radius: var(--r-control);
  font-size: 14px;
  font-weight: 600;
  box-shadow: none;
}

.tool .btn::before {
  content: '';
  position: absolute;
  left: 0; right: 0; top: 50%;
  transform: translateY(-50%);
  height: var(--tap);
  z-index: 0;
}

/* TWO STACKED BUTTONS NEED 52px BETWEEN THEIR CENTRES.
   The button is 38px and carries its 52px target on a pseudo-element, so a
   pair of them needs 14px of separation or the lower one's target covers the
   upper one's bottom edge and eats the tap. .stack already gives 14px;
   .tool .card tightened its gap to 10px, and this is the 4px back.

   Scoped to a card's own children on purpose. Written as a bare `.btn + .btn`
   it also caught Accept-order-next-to-Cancel on every ticket, where the two
   buttons are side by side and the margin only made the row taller. */
.tool .card > .btn + .btn { margin-top: 4px; }

.tool .btn.primary { background: var(--accent); color: var(--accent-ink); border-color: transparent; }
.tool .btn.ghost { background: var(--panel); color: var(--cream); border-color: var(--line2); }
.tool .btn.block { width: 100%; }

/* A destructive action is a plain red link, not a filled slab. */
.tool .btn.danger {
  background: none;
  border-color: transparent;
  color: var(--coral);
  font-weight: 600;
  padding: 0 4px;
}

/* ---- status colour ------------------------------------------------------ */

.tool .pill { font-size: var(--ctl-fs); font-family: var(--font-body); font-weight: var(--ctl-fw); letter-spacing: 0; text-transform: none; }
.tool .pill[aria-pressed="true"] { color: var(--fresh); border-color: color-mix(in srgb, var(--fresh) 45%, transparent); }

.tool .st-pending { color: var(--warn); }
.tool .st-confirmed { color: var(--warn); }
.tool .st-ready { color: var(--fresh); }
.tool .st-cancelled { color: var(--coral); }

/* ---- the side switch ----------------------------------------------------

   Its shape is now the shared --ctl-* tokens, which were taken from this
   control in the first place, so there is nothing left to restate here except
   the one thing that genuinely differs between the two languages: the brand
   inverts the side you are on to cream, and the tool raises it a step on the
   neutral ramp instead, because near-white on near-black at this size is a
   glare rather than a state. */

.tool .sideswitch-opt[aria-current="page"] { background: var(--surface); color: var(--cream); }

/* Same reason, same places: a chip, a preset pill and a pickup slot mark
   themselves by rising a step rather than by inverting. */
.tool .chip[aria-pressed="true"],
.tool .chip-sm[aria-pressed="true"],
.tool .slot[aria-pressed="true"] {
  background: var(--surface);
  border-color: var(--line2);
  color: var(--cream);
}

/* ---- the bottom bar -----------------------------------------------------

   Nothing. The bar is declared once, in tokens, and .tool's own --ink and
   --line are what make it read as the tool's bar rather than the brand's.
   Anything restated here would be a second bar to keep in step with the first.
*/

/* ---- receipts stay paper ------------------------------------------------

   The torn ticket is the one warm thing kept inside the tool. It is a printed
   object, it is how this product has always drawn an order, and a neutral
   version of it would just be a grey box. */

/* Mono, inside the tool, is for a slug, an order code, and the printed rows
   of a receipt. Everything else that had picked it up goes to Inter. */
.tool .balance-label,
.tool .balance-hint,
.tool .boh-enter-sub,
.tool .boh-enter-title,
.tool .order-when,
.tool .order-state,
.tool .badge,
.tool .demo-cap,
.tool .step-n,
.tool .foot-name {
  font-family: var(--font-body);
  letter-spacing: 0.01em;
  text-transform: none;
}

.tool .balance-label,
.tool .boh-enter-sub { font-size: 11px; font-weight: 550; }
.tool .balance-hint { font-size: 11px; }
.tool .order-when { font-size: 11.5px; }
.tool .order-state { font-size: 11px; font-weight: 650; }

/* The one number on the Orders screen that is worth setting large: what she
   owes. Anton, because it is a number and it is the point of the row. */
.tool .balance-amount { font-family: var(--font-display); font-size: 17px; letter-spacing: 0.01em; }

/* The slug keeps mono: the shape of the characters is the information. */
.tool .row .prefixed input,
.tool #shop-slug,
.tool #tk-slug,
.tool .linkbox-url { font-family: var(--font-mono); }

/* The offer bar once she is subscribed. Same control, same place, quieter:
   there is nothing being sold any more, only a fact and a way out. */
.boh-offer.on {
  background: var(--bh2);
  border-top-color: var(--bhline);
}

.boh-offer.on .boh-offer-lead { font-size: 15px; }
.boh-offer.on .boh-offer-btn { min-width: 120px; }

/* Cancel is a plain bordered button, not a red alarm and not a shout. It is a
   normal thing to do and it looks like one. */
.boh-offer .btn.ghost {
  background: var(--bh3);
  border: 1px solid var(--bhline2);
  color: var(--bhtext);
}
