/* ==========================================================================
   Wole — design system root
   Loaded on every page. Page-specific rules live in their own file.

   The system is built on one artifact: the kitchen ticket. It is the thing
   this platform actually moves — customer to rail to chef to oven to box to
   door — so it is also the visual motif, and monospace is the data face
   because order numbers, timers and prices are printed matter.
   ========================================================================== */

:root {
  /* Ink and paper. Char is warm near-black (burnt crust), not neutral grey. */
  --ink:        #1a1614;
  --ink-soft:   #6b625c;
  --ink-faint:  #9c938b;
  --paper:      #f7f4ef;   /* semolina */
  --surface:    #ffffff;
  --line:       #e5dfd5;
  --line-soft:  #f0ebe3;

  /* Heat. --brand is overridden per tenant from the database. */
  --brand:      #d62828;
  --brand-ink:  #ffffff;
  --ember:      #ff7a1a;   /* oven glow — motion and heat states only */
  --basil:      #2f6d4f;   /* ready, success, delivered */
  --night:      #16110f;   /* kitchen at night — chrome, builder stage, KDS */
  --night-soft: #241c18;

  --radius:     10px;
  --radius-lg:  18px;

  /*
    A type scale with real jumps, not a gentle ramp.

    **The two heading steps are clamped and the rest are not** (2026-09-05).
    Every value here was a fixed rem until the audit, and `--step-4` at 3 rem is
    48 px on a 320 px phone — where the page's own gutters leave 280 px and the
    word *Kumppanisopimuksen* wants 391. The ceiling is the figure they already
    were, so nothing moves on a laptop; the floor is what a Finnish compound
    needs to fit a small phone. Body steps are left alone: a paragraph wraps.
  */
  --step--1:  0.8125rem;
  --step-0:   1rem;
  --step-1:   1.1875rem;
  --step-2:   1.5rem;
  --step-3:   clamp(1.55rem, 1.1rem + 2.2vw, 2.125rem);
  --step-4:   clamp(2rem, 1.35rem + 3.2vw, 3rem);
  --step-5:   clamp(2.75rem, 7vw, 5rem);

  --sans: "Helvetica Neue", Helvetica, Arial, ui-sans-serif, system-ui, sans-serif;
  --mono: ui-monospace, "SF Mono", "Cascadia Mono", Consolas, "Liberation Mono", Menlo, monospace;

  --shadow-card: 0 1px 2px rgb(26 22 20 / 6%), 0 8px 24px -12px rgb(26 22 20 / 18%);
  --shadow-lift: 0 2px 4px rgb(26 22 20 / 8%), 0 18px 40px -18px rgb(26 22 20 / 28%);

  /* --- Direction B ------------------------------------------------------- --

     The customer-facing look, chosen 2026-08-11: true white ground, layered
     soft shadow, larger radii, and the search as one floating pill.

     These are additions rather than edits. Slice N builds the two new public
     pages on them; slice R moves the storefront, the checkout, the tracker and
     the review page over. Redefining --paper here instead would have restyled
     the kitchen board and five admin screens in a slice that was supposed to
     touch neither.

     **The kitchen-ticket motif stops being the brand.** The palette above is
     named semolina, ember and basil because the design system was built on one
     artifact — a pizza order. That does not describe an all-in-one till for a
     sushi restaurant. Mono keeps the jobs it earns (money, order numbers,
     timers) and stops being the voice of the product, which is why there is no
     --mono in any token below.
     ----------------------------------------------------------------------- */

  --b-ground:   #ffffff;   /* the page itself */
  --b-raised:   #ffffff;   /* a card sitting on it, separated by shadow not fill */
  --b-sunken:   #f7f6f4;   /* a well: map frame, inactive tab, empty state */
  --b-line:     #eceae6;   /* the hairline that survives on white */
  --b-line-str: #ddd9d2;   /* a border that has to be seen */

  --b-radius:    14px;
  --b-radius-lg: 20px;
  --b-radius-pill: 999px;

  /*
    Two shadows, both layered. The tight one grounds the element; the wide,
    heavily negative-spread one is the light. A single blurred shadow is the
    thing that makes a white card look like a 2013 bootstrap panel.
  */
  --b-shadow:      0 1px 2px rgb(26 22 20 / 5%), 0 14px 34px -18px rgb(26 22 20 / 30%);
  --b-shadow-lift: 0 2px 6px rgb(26 22 20 / 7%), 0 26px 56px -22px rgb(26 22 20 / 34%);
}

/* --- Reset ---------------------------------------------------------------- */

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

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: var(--step-0);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

img, svg { max-width: 100%; display: block; }
button, input, select, textarea { font: inherit; color: inherit; }

h1, h2, h3, h4, p, ul, ol, figure { margin: 0; }
ul, ol { padding: 0; list-style: none; }

a { color: inherit; text-decoration-thickness: 1px; text-underline-offset: 3px; }

:focus-visible {
  outline: 2.5px solid var(--ember);
  outline-offset: 2px;
  border-radius: 3px;
}

/* --- Type ----------------------------------------------------------------- */

.display {
  font-size: var(--step-5);
  font-weight: 800;
  line-height: 0.94;
  letter-spacing: -0.035em;
  text-wrap: balance;
}

h1 { font-size: var(--step-4); font-weight: 800; letter-spacing: -0.03em; line-height: 1.05; }
h2 { font-size: var(--step-3); font-weight: 700; letter-spacing: -0.02em; line-height: 1.12; }
h3 { font-size: var(--step-1); font-weight: 700; letter-spacing: -0.01em; }

/*
  A heading in Finnish is one word (2026-09-05).

  *Kumppanisopimuksen*, *Pöytävaraukset*, *Tietosuojaseloste*, *Myyntiraportti*,
  *Sopimustekstit* — a compound language writes single words where English
  writes three, and a single word cannot wrap. At 320 px, nine pages pushed the
  whole document sideways rather than the heading: `/ehdot/kumppani` by 91 px,
  the bookings sheet by 90, the console's contract editor by 77, the sales report
  by 54. Two of them, `/ehdot` and `/tietosuoja`, are linked from every footer in
  the product, so this reached customers.

  **Both halves are needed and they do different jobs.** The clamp on the scale
  above stops the common case by making the type fit the phone; this stops the
  uncommon one — a shop called *Kahvila-konditoria-leipomo* — by letting a word
  that still cannot fit break rather than hang off the edge. `hyphens` breaks it
  politely where a browser knows how, `overflow-wrap` unconditionally where it
  does not, and neither does anything at all to a heading that already fits.
*/
h1,
h2,
h3 {
  overflow-wrap: break-word;
  hyphens: auto;
}

/* The data face. Order numbers, money, timers, codes — printed matter. */
.mono,
.money,
.order-no,
.timer {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}

.order-no { font-weight: 700; }

/* An eyebrow marks a section's role. It is a label, not decoration. */
.eyebrow {
  font-family: var(--mono);
  font-size: var(--step--1);
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--ink-faint);
}

.lede { font-size: var(--step-1); color: var(--ink-soft); max-width: 58ch; }
.muted { color: var(--ink-soft); }

/* --- The impersonation banner --------------------------------------------- --

   A superadmin is wearing somebody else's identity. This is a full-width bar
   above everything, on every page, because the failure it prevents is an admin
   forgetting — reading a shop's numbers as the platform's, or making a change
   they think they are making as themselves. A toast disappears and a corner
   badge is furniture; a bar that pushes the whole page down is neither.

   Amber rather than red. Red is this product's brand colour and also its error
   colour, and the state is neither of those: nothing has gone wrong, but
   something is true that must not be forgotten.
   -------------------------------------------------------------------------- */

.impbar {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.6rem 1.25rem;
  background: #2b2009;
  color: #ffd88a;
  font-size: var(--step--1);
  border-bottom: 2px solid var(--ember);
}

.impbar__dot {
  width: 0.55rem;
  height: 0.55rem;
  border-radius: 50%;
  background: var(--ember);
  flex: none;
  animation: impbar-blink 2s ease-in-out infinite;
}

@keyframes impbar-blink { 50% { opacity: 0.3; } }

@media (prefers-reduced-motion: reduce) {
  .impbar__dot { animation: none; }
}

.impbar__text { flex: 1; min-width: 0; }
.impbar__shop { color: #fff; font-weight: 650; }

.impbar__exit {
  flex: none;
  border: 1px solid currentColor;
  background: none;
  color: inherit;
  border-radius: var(--b-radius-pill);
  padding: 0.35rem 0.9rem;
  font-size: var(--step--1);
  font-weight: 650;
  cursor: pointer;
}

.impbar__exit:hover { background: rgb(255 216 138 / 14%); }
.impbar__exit:disabled { opacity: 0.5; cursor: default; }

/* --- The shop card (direction B) ------------------------------------------ --

   One restaurant, as the finder shows it and as a city page shows it. It lives
   here rather than in finder.css because three pages render it, and a shared
   component in a page stylesheet is the mistake that left the kitchen board
   unstyled once — see the note at the top of assets/css.

   The whole card is one link. A card with a link *inside* it gives a thumb a
   small target inside a large clickable-looking area, which is the most
   irritating twenty pixels on any directory.
   -------------------------------------------------------------------------- */

.shoplist { display: flex; flex-direction: column; gap: 0.75rem; }

.shopcard {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 0.9rem 1.1rem;
  background: var(--b-raised);
  border: 1px solid var(--b-line);
  border-radius: var(--b-radius);
  box-shadow: var(--b-shadow);
  text-decoration: none;
  transition: box-shadow 0.16s ease, transform 0.16s ease, border-color 0.16s ease;
}

.shopcard:hover {
  box-shadow: var(--b-shadow-lift);
  border-color: var(--b-line-str);
  transform: translateY(-1px);
}

/* The mark leads the card, so a company is picked out by its brand rather than
   read for by name (slice Y-D, Y13). `.shopcard` is already a flex row with a
   gap, so the mark needs no layout of its own — only a size. */
.shopcard__logo { width: 2.75rem; height: 2.75rem; }

/* A closed shop is listed on purpose — a missing row reads as "not on Wole",
   which is a different and wrong message. It is quietened, not hidden. */
.shopcard--shut { background: var(--b-sunken); box-shadow: none; }
/* The mark is quietened with the rest of the card rather than removed: a shop
   shut this evening is still the same shop tomorrow. */
.shopcard--shut .shopcard__logo { opacity: 0.55; }
.shopcard--shut .shopcard__name { color: var(--ink-soft); }

.shopcard__main { flex: 1; min-width: 0; }

.shopcard__name {
  font-size: var(--step-1);
  font-weight: 650;
  letter-spacing: -0.015em;
  margin-bottom: 0.2rem;
}

.shopcard__meta {
  font-size: var(--step--1);
  color: var(--ink-soft);
  display: flex;
  flex-wrap: wrap;
  gap: 0.15rem 0.5rem;
}

/* Separators are drawn, not typed. A literal "·" between spans becomes a
   stranded bullet the moment one of them is absent. */
.shopcard__meta > * + *::before { content: '·'; margin-right: 0.5rem; color: var(--ink-faint); }

.shopcard__tags {
  font-size: var(--step--1);
  color: var(--ink-faint);
  margin-top: 0.2rem;
}

.shopcard__aside { text-align: right; flex-shrink: 0; }

.shopcard__state {
  display: inline-block;
  font-size: var(--step--1);
  font-weight: 650;
  padding: 0.3rem 0.65rem;
  border-radius: var(--b-radius-pill);
  background: #f0f6f2;
  color: var(--basil);
  white-space: nowrap;
}

.shopcard__state--shut { background: #f1efec; color: var(--ink-soft); }
.shopcard__fee { font-size: var(--step--1); color: var(--ink-soft); margin-top: 0.35rem; }

@media (max-width: 33rem) {
  .shopcard { align-items: flex-start; }
  .shopcard__aside { text-align: left; }

  /*
    The separators go. A drawn "·" belongs between two items on one line, and on
    a narrow card the meta wraps — which strands a bullet at the start of the
    second line, looking like a bullet list that lost its first item. The gap
    alone separates them once they are stacked.
  */
  .shopcard__meta { gap: 0.1rem 0.65rem; }
  .shopcard__meta > * + *::before { content: none; }
}

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

.page {
  max-width: 1180px;
  margin-inline: auto;
  padding: 2rem 1.25rem 5rem;
}

/*
  How tall the dark bar is, named once (owner, 2026-09-05: pages that scroll on
  past the footer into nothing).

  A full-height screen — the kitchen board is the only one — measured itself as
  `100vh` and then had the nav above it and the footer below it, so every such
  page was taller than the window by the height of both and scrolled into empty
  space. `--chrome-h` is what those pages subtract, and `dvh` is what stops a
  phone's collapsing address bar reintroducing the same gap.
*/
:root { --chrome-h: 4.25rem; }

/*
  ALIASES FOR TOKENS SEVEN STYLESHEETS ASK FOR AND NOBODY EVER DEFINED.

  Found on 2026-09-05 while chasing the owner's *"entire POS page needs rework
  since it currently feels and looks horrible"*. `pos.css` styles nine surfaces
  with `border-radius: var(--r-md)` and `var(--r-sm)`; neither token exists, so
  every one of them had square corners. `admin.css` asks for `--radius-sm`,
  `--radius-s`, `--radius-m` and `--accent`; `books.css`, `choose.css` and
  `invoice.css` ask for `--muted`; `setup.css` for `--font-mono`.

  A missing custom property is not an error in CSS. The declaration is dropped
  and the element keeps the initial value — square corners, black text, no
  accent — which is exactly the class of fault nobody can see in a diff and
  everybody can see on a screen.

  Defined as aliases rather than as new values on purpose: these are other names
  for tokens the design system already has, and giving them their own numbers
  would be a second design system growing quietly beside the first one.
*/
:root {
  --r-sm:      8px;
  --r-md:      var(--radius);
  --radius-s:  8px;
  --radius-sm: 8px;
  --radius-m:  var(--radius);
  --accent:    var(--brand);
  --muted:     var(--ink-soft);
  --surface-2: var(--line-soft);
  --mark:      var(--brand);
  --ok:        var(--basil);
  --font-mono: var(--mono);
  --step--2:   0.72rem;
}

.page--wide { max-width: 1440px; }
.page--narrow { max-width: 640px; }

.stack > * + * { margin-top: 1rem; }
.stack-lg > * + * { margin-top: 2.5rem; }

/* --- Site chrome ---------------------------------------------------------- */

.site-nav {
  display: flex;
  align-items: center;
  gap: 1.25rem;
  padding: 0.875rem 1.25rem;
  background: var(--night);
  color: var(--paper);
}

.site-nav__brand {
  font-weight: 800;
  letter-spacing: -0.03em;
  font-size: var(--step-1);
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
}

/* A filled dot in the tenant's colour: the whole nav mark, nothing more. */
.site-nav__dot {
  width: 0.7rem;
  height: 0.7rem;
  border-radius: 50%;
  background: var(--brand);
  flex: none;
}

/* --- The hamburger --------------------------------------------------------- */

/*
  One menu, every role, every width (slice U, 2026-08-11).

  The owner's nav was ten links across the top of every screen, permanently,
  most of which are opened once a month. A visible bar for the short public
  navs and a panel for the long private ones would have been two components,
  and the one that is not looked at is the one that rots.

  It is a `<details>` element. The browser owns the open state, which is what
  makes it work with JavaScript off and what keeps it out of the `hidden`-versus-
  `display` trap this project has paid for twice. `nav.js` only adds manners.
*/
.navmenu {
  margin-left: auto;
  position: relative;
}

.navmenu__toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: var(--radius);
  cursor: pointer;
  position: relative;

  /* The default disclosure triangle, in both engines that draw one. */
  list-style: none;
}

.navmenu__toggle::-webkit-details-marker { display: none; }

.navmenu__toggle:hover { background: rgb(247 244 239 / 10%); }
.navmenu__toggle:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/*
  Three bars from one element: the middle is the box, the other two are its
  pseudo-elements. Drawn rather than an inline SVG, so the colour follows the
  chrome — the light chrome remaps it below without touching this markup.
*/
.navmenu__bars,
.navmenu__bars::before,
.navmenu__bars::after {
  display: block;
  width: 1.15rem;
  height: 2px;
  border-radius: 1px;
  background: currentcolor;
  transition: transform 0.16s ease, opacity 0.12s ease;
}

.navmenu__bars { position: relative; color: rgb(247 244 239 / 88%); }

.navmenu__bars::before,
.navmenu__bars::after {
  content: "";
  position: absolute;
  left: 0;
}

.navmenu__bars::before { top: -0.36rem; }
.navmenu__bars::after  { top: 0.36rem; }

/* Open, the bars become a cross. The middle bar goes rather than rotating with
   them, because two lines crossing at the centre is a cleaner X than three. */
.navmenu[open] .navmenu__bars { background: transparent; }
.navmenu[open] .navmenu__bars::before { transform: translateY(0.36rem) rotate(45deg); }
.navmenu[open] .navmenu__bars::after  { transform: translateY(-0.36rem) rotate(-45deg); }

/* A count that must be visible while the panel is shut — otherwise the one
   thing that needs answering today is behind a tap nobody takes. */
.navmenu__badge {
  position: absolute;
  top: -0.15rem;
  right: -0.15rem;
  min-width: 1.05rem;
  height: 1.05rem;
  padding: 0 0.25rem;
  border-radius: 999px;
  background: var(--brand);
  color: #fff;
  font-size: 0.65rem;
  font-weight: 700;
  line-height: 1.05rem;
  text-align: center;
}

/*
  THE PANEL IS A SHEET FROM THE TOP OF THE SCREEN, AT EVERY WIDTH.

  Owner, 2026-09-05: *"menu is getting too long and currently cannot be
  scrolled. We need to update menu visually to be div that comes top of the
  screen and is scrollable."*

  It was a dropdown pinned under the hamburger, sized `max-height: calc(100vh -
  5rem)`. That is a scroll only while the panel's own containing block is the
  viewport — inside the sticky bar it is not, so an owner's menu grew past the
  fold and the last entries were unreachable at exactly the moment the menu
  gained groups.

  `position: fixed` takes the viewport as the containing block whatever the bar
  around it does, and `overscroll-behavior: contain` stops a flick at the end of
  the list scrolling the page behind it.
*/
.navmenu__panel {
  position: fixed;
  top: 3.5rem;
  right: 0.5rem;
  left: auto;
  z-index: 60;
  width: min(22rem, calc(100vw - 1rem));
  max-height: calc(100dvh - 4.5rem);
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  margin: 0;
  padding: 0.4rem;
  border-radius: var(--radius);
  background: var(--night);
  border: 1px solid rgb(247 244 239 / 14%);
  box-shadow: 0 18px 40px rgb(0 0 0 / 35%);
}

.navmenu__link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.6rem 0.75rem;
  border-radius: calc(var(--radius) - 2px);
  color: rgb(247 244 239 / 82%);
  text-decoration: none;
  font-size: var(--step--1);
  font-weight: 600;
}

.navmenu__link:hover { background: rgb(247 244 239 / 10%); color: var(--paper); }

.navmenu__link[aria-current="page"] {
  color: var(--paper);
  background: rgb(247 244 239 / 7%);
}

/*
  A group is a labelled block rather than a second disclosure: everything a
  shop sets up once is under `Tuotteet` and `Asetukset`, and putting those
  behind another tap would be the same complaint one level down.
*/
.navmenu__group {
  margin-top: 0.35rem;
  padding-top: 0.35rem;
  border-top: 1px solid rgb(247 244 239 / 12%);
}

.navmenu__grouplabel {
  margin: 0.2rem 0.75rem 0.3rem;
  color: rgb(247 244 239 / 45%);
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
}

.navmenu__group .navmenu__link { padding-left: 1.15rem; }

.navmenu__rule {
  height: 1px;
  margin: 0.35rem 0.25rem;
  background: rgb(247 244 239 / 14%);
}

/* A number beside an entry: what is waiting on the page that answers it. */
.navmenu__count {
  min-width: 1.25rem;
  padding: 0 0.35rem;
  border-radius: 999px;
  background: var(--brand);
  color: #fff;
  font-size: 0.7rem;
  font-weight: 700;
  text-align: center;
  flex: none;
}

/*
  On a phone the sheet is the width of the screen. A 22rem menu hanging off the
  right edge of a 360px screen is a column of text with nothing beside it.
*/
@media (max-width: 599px) {
  .navmenu__panel {
    left: 0.5rem;
    right: 0.5rem;
    width: auto;
  }
}

/*
  The light chrome, for the public pages built in direction B.

  Opted into per page with `$pageChrome = 'light'` in header.php, so the working
  screens — the kitchen board, the driver queue, every admin page — keep the
  dark bar that separates a tool from the room it is used in. A page that says
  nothing is unchanged.
*/
/*
  Direction B reaches the whole customer surface by REMAPPING the old token
  names, not by editing four page stylesheets.

  `storefront.css`, `checkout.css`, `tracker.css` and `account.css` between them
  reference `--paper`, `--surface`, `--line`, `--radius` and the two shadows a
  dozen times. Rewriting each of those by hand would be a dozen chances to miss
  one, and would leave the two systems interleaved in files that are hard to
  read afterwards. Pointing the old names at the new values inside this scope
  moves every one of them at once, and moves them back the moment a page stops
  opting in.

  **`--night`, `--brand` and `--ember` are deliberately untouched.** The
  tracker's scenes and the builder's stage are dark on purpose — they are a
  photograph's backdrop, not chrome — and the tenant's brand colour is data.
*/
.chrome-light {
  --paper:   var(--b-ground);
  --surface: var(--b-raised);
  --line:      var(--b-line);
  --line-soft: var(--b-line);

  --radius:    var(--b-radius);
  --radius-lg: var(--b-radius-lg);

  --shadow-card: var(--b-shadow);
  --shadow-lift: var(--b-shadow-lift);

  background: var(--b-ground);
}

.chrome-light .site-nav {
  background: var(--b-ground);
  color: var(--ink);
  border-bottom: 1px solid var(--b-line);
}

/*
  The hamburger on a light page.

  The bars follow `currentcolor`, so only the toggle's colour has to move — but
  the panel does not follow the chrome. It stays dark on purpose: a menu that
  floats over the page it belongs to needs to read as a layer above it, and a
  white panel on a white ground is a border doing all the work.
*/
.chrome-light .navmenu__bars { color: var(--ink); }
.chrome-light .navmenu__toggle:hover { background: rgb(22 17 15 / 7%); }

.chrome-light .site-footer {
  background: var(--b-ground);
  border-top: 1px solid var(--b-line);
}

.site-footer {
  padding: 2.5rem 1.25rem;
  border-top: 1px solid var(--line);
  color: var(--ink-soft);
  font-size: var(--step--1);
}

/*
  Every link in the footer is a real tap target (2026-09-05).

  They were 15 px tall on a phone, on all 46 pages — a third under the 24 px
  floor — and the two-letter language links were 15 px SQUARE. That switcher is
  the only way to change language in the product, so it was at once the smallest
  control here and one of the most consequential.

  Padding rather than a min-height on the anchors: these sit inside running
  paragraphs separated by `·`, and a block-level target would break the line
  those separators belong to.
*/
.site-footer a {
  display: inline-block;
  padding-block: 0.45rem;
}


.site-footer__lang {
  display: flex;
  gap: 0.35rem;
}

.site-footer__lang a {
  min-width: 2.75rem;
  padding: 0.45rem 0.6rem;
  border: 1px solid transparent;
  border-radius: 0.5rem;
  text-align: center;
  text-decoration: none;
  letter-spacing: 0.06em;
}

/* Which language is being read, so the pair states a fact rather than offering
   two identical links. */
.site-footer__lang a[aria-current] {
  border-color: currentColor;
  color: var(--ink);
  font-weight: 650;
}

/* --- Buttons -------------------------------------------------------------- */

.btn {
  --btn-bg: var(--surface);
  --btn-fg: var(--ink);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.7rem 1.15rem;
  min-height: 2.75rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--btn-bg);
  color: var(--btn-fg);
  font-weight: 650;
  font-size: var(--step-0);
  letter-spacing: -0.01em;
  /* A button that happens to be an <a> must not wear the global link
     underline — it reads as a link inside a button otherwise. */
  text-decoration: none;
  cursor: pointer;
  transition: transform 120ms ease, box-shadow 160ms ease, background-color 160ms ease;
}

.btn:hover { box-shadow: var(--shadow-card); }
.btn:active { transform: translateY(1px); }

.btn[disabled],
.btn[aria-disabled="true"] {
  opacity: 0.45;
  cursor: not-allowed;
  box-shadow: none;
  transform: none;
}

.btn--primary {
  --btn-bg: var(--brand);
  --btn-fg: var(--brand-ink);
  border-color: transparent;
}

.btn--ghost { --btn-bg: transparent; }

/*
  The button that ends something (2026-09-05).

  `.btn--danger` was written onto three controls and defined nowhere: the
  customer's own *erase my data*, the console's *erase this customer*, and the
  console home's version of the same. All three are irreversible — this project
  anonymises rather than deletes precisely because they cannot be taken back —
  and all three were rendering as ordinary buttons, indistinguishable from
  *Save*.

  Outlined rather than filled, and it is the one place that is not a hierarchy
  decision: a solid red button is the shape of a PRIMARY action, and the primary
  action on a page holding this one is always the other one. It reads as a
  warning at the moment of the press, which is when it is read.
*/
.btn--danger {
  --btn-bg: transparent;
  --btn-fg: var(--danger, #8f2a0c);
  border-color: currentColor;
}

.btn--danger:hover,
.btn--danger:focus-visible {
  --btn-bg: #ffe9e2;
}

/*
  The action on a menu card. Outlined at rest, because a grid of eighteen cards
  painted in brand red is eighteen equally loud primaries and no hierarchy at
  all. It fills the moment it is the thing being pointed at — and the whole card
  is a target, so this is the affordance rather than the only way in.
*/
.btn--card {
  --btn-bg: transparent;
  --btn-fg: var(--brand);
  border-color: currentColor;
  font-weight: 700;
}

.btn--card:hover,
.btn--card:focus-visible,
.dish:hover .btn--card {
  --btn-bg: var(--brand);
  --btn-fg: var(--brand-ink);
  border-color: transparent;
}

.btn--card[disabled] {
  --btn-fg: var(--ink-faint);
  border-color: var(--line);
  cursor: not-allowed;
}

.btn--lg { padding: 0.95rem 1.6rem; min-height: 3.25rem; font-size: var(--step-1); }
.btn--block { width: 100%; }

/* --- Forms ---------------------------------------------------------------- */

.field { display: block; }

.field__label {
  display: block;
  font-size: var(--step--1);
  font-weight: 650;
  margin-bottom: 0.35rem;
}

.field__hint { font-size: var(--step--1); color: var(--ink-soft); margin-top: 0.3rem; }

/*
  A set of tick-boxes that reads as one answer — the store's cuisines today,
  and whatever the setup wizard asks next. Named `pick` rather than `chip`
  because `.chip` is already the status pill, and two components sharing a class
  is how one of them silently inherits the other's colour.

  The native checkbox stays in the DOM and stays focusable; it is the thing
  screen readers and the keyboard operate. Only its appearance is replaced.
*/
.pickset { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.5rem; }

.pick {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.45rem 0.8rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  font-size: var(--step--1);
  cursor: pointer;
  user-select: none;
  transition: border-color 0.12s ease, background 0.12s ease;
}

.pick input { margin: 0; accent-color: var(--brand); }
.pick:hover { border-color: var(--ink-faint); }
.pick:has(input:checked) { border-color: var(--ink); background: var(--paper); font-weight: 650; }
.pick:has(input:focus-visible) { outline: 2px solid var(--brand); outline-offset: 2px; }

/*
  A consent tick-box: one checkbox and a sentence that wraps.

  Deliberately NOT `.pick`. That one is a pill in a `.pickset` — a short label
  on one line, sized to its text, meant to sit in a row of siblings. A consent
  line is a full-width paragraph with a control at its start, and forcing it
  through the pill would put a contract inside a rounded chip.

  It lives here rather than in `legal.css` or `checkout.css` because both of
  those pages use it, and this project has paid three times for a shared
  component filed under one of its consumers — `.cartbar` sat in `checkout.css`
  and the storefront's cart went unstyled for months.
*/
.consent {
  display: flex;
  align-items: flex-start;
  gap: 0.7rem;
  padding: 0.85rem 1rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--paper);
  font-size: var(--step--1);
  line-height: 1.55;
  cursor: pointer;
}

/* The control must not shrink when the sentence beside it wraps to three lines. */
.consent input {
  margin: 0;
  flex: none;
  width: 1.05rem;
  height: 1.05rem;
  margin-top: 0.15rem;
  accent-color: var(--brand);
}

.consent:has(input:focus-visible) { outline: 2px solid var(--brand); outline-offset: 2px; }
.consent:has(input:checked) { border-color: var(--ink); }
.consent a { color: inherit; text-underline-offset: 2px; }

/* An unanswered required consent, marked by the page that needs it. */
.consent.is-missing { border-color: #d1503a; background: #ffe9e2; }

.input,
.select,
.textarea {
  width: 100%;
  padding: 0.7rem 0.85rem;
  min-height: 2.75rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  transition: border-color 140ms ease, box-shadow 140ms ease;
}

.input:focus,
.select:focus,
.textarea:focus {
  border-color: var(--ink);
  outline: none;
  box-shadow: 0 0 0 3px rgb(255 122 26 / 22%);
}

.textarea { min-height: 6rem; resize: vertical; }

/* --- Cards and the ticket motif ------------------------------------------- */

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: 1.25rem;
}

/*
  The ticket: the platform's signature object. A docket with a torn bottom
  edge, used wherever an order is represented as a physical thing — the
  kitchen board, the tracker's first scene, receipts, order history.
  The tear is a mask so it works on any background.
*/
.ticket {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--line);
  border-bottom: none;
  border-radius: var(--radius) var(--radius) 0 0;
  padding: 1rem 1.1rem 1.35rem;
  box-shadow: var(--shadow-card);
}

/*
  The tear hangs below the card, so the card has to leave room for it.

  `.ticket` had no bottom margin and `::after` sits 9 px under it, so the
  scalloped edge painted straight across whatever came next — on the receipt
  page that was **Noutonumero 343**, cut in half by decoration (2026-09-05
  audit). The margin is the tear's height plus a hair, written next to the
  number it has to clear rather than somewhere a later edit could separate them.
*/
.ticket { margin-bottom: 12px; }

.ticket::after {
  content: "";
  position: absolute;
  inset-inline: -1px;
  bottom: -9px;
  height: 10px;
  background: var(--surface);
  border-inline: 1px solid var(--line);
  /* Scalloped tear — a repeating notch along the bottom edge. */
  mask-image: radial-gradient(9px 10px at 9px -1px, transparent 0 9px, #000 9.5px);
  mask-size: 18px 10px;
  mask-repeat: repeat-x;
}

.ticket__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  padding-bottom: 0.6rem;
  margin-bottom: 0.7rem;
  border-bottom: 1px dashed var(--line);
}

.ticket__no { font-size: var(--step-2); }

.ticket__lines { font-size: var(--step--1); }
.ticket__lines li + li { margin-top: 0.45rem; }

/* Modifiers read the way a cook reads them: additions and removals, plainly. */
.mod { font-family: var(--mono); font-size: var(--step--1); display: block; padding-left: 0.9rem; }
.mod--add { color: var(--basil); }
.mod--remove { color: var(--brand); text-transform: uppercase; }

/* --- Store switcher ------------------------------------------------------- */
/* Shared: the storefront, the kitchen board and the owner pages all pick a store. */

.storepick { display: flex; gap: 0.4rem; flex-wrap: wrap; }

.storepick__opt {
  padding: 0.4rem 0.85rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  font-size: var(--step--1);
  font-weight: 650;
  text-decoration: none;
  white-space: nowrap;
}

.storepick__opt.is-active {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--paper);
}

/* --- Choice pills --------------------------------------------------------- */
/*
  One row of mutually exclusive choices: a pizza's sizes in the builder, and at
  the checkout the fulfilment toggle, the ASAP/scheduled toggle, the saved
  addresses and the payment methods.

  These lived in builder.css until 2026-08-15, which meant the checkout — which
  does not load that file — drew four rows of unstyled buttons, and cart.js
  wrote `is-active` onto a class with no rule behind it. A shared component
  belongs here; that rule is in MAP.md and this is the second time it has been
  paid for.
*/

.sizes { display: flex; gap: 0.4rem; flex-wrap: wrap; }

.size-pill {
  padding: 0.55rem 0.9rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  font-weight: 650;
  font-size: var(--step--1);
  cursor: pointer;
  transition: background-color 140ms ease, border-color 140ms ease, color 140ms ease;
}

.size-pill:hover { border-color: var(--ink-soft); }

.size-pill.is-active {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--paper);
}

/* --- Status chips --------------------------------------------------------- */

.chip {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.25rem 0.6rem;
  border-radius: 999px;
  background: var(--line-soft);
  color: var(--ink-soft);
  font-size: var(--step--1);
  font-weight: 650;
  white-space: nowrap;
}

.chip::before {
  content: "";
  width: 0.45rem;
  height: 0.45rem;
  border-radius: 50%;
  background: currentcolor;
  flex: none;
}

.chip--waiting { background: #fff2e0; color: #a2540c; }
.chip--cooking { background: #ffe9e2; color: #b2350f; }
.chip--ready   { background: #e4f2ea; color: var(--basil); }
.chip--moving  { background: #e6eefb; color: #1d4f9c; }
.chip--done    { background: var(--line-soft); color: var(--ink-soft); }
.chip--dead    { background: var(--line-soft); color: var(--ink-faint); }

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

.banner {
  padding: 0.85rem 1.1rem;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--surface);
  font-size: var(--step--1);
}

.banner--closed { background: #fff2e0; border-color: #f2d5ac; color: #7d400a; }
/*
  Amber, the same as `--closed`, and a separate name on purpose. They mean
  different things — one is "this shop is shut", the other is "something here
  needs your attention" — and a state that borrows another state's class is a
  state that silently changes when that one is restyled.
*/
.banner--warn   { background: #fff2e0; border-color: #f2d5ac; color: #7d400a; }
.banner--error  { background: #ffe9e2; border-color: #f3c3b5; color: #8f2a0c; }
.banner--ok     { background: #e4f2ea; border-color: #b9dcc8; color: #1d5138; }

/*
  A banner that belongs to the page shell rather than to a form. It sits under
  the nav on every page the owner opens, so it carries the page's own gutter
  instead of inheriting a card's.
*/
/*
  A banner belonging to the page shell rather than to a form: it sits between
  the nav and whatever page follows, so it lines up with `.page`'s own gutter
  (1180px wide inside 1.25rem of padding) instead of running edge to edge.
*/
.site-banner {
  width: min(1180px, 100% - 2.5rem);
  margin: 1rem auto 0;
}

/* --- Utilities ------------------------------------------------------------ */

.row { display: flex; gap: 0.75rem; align-items: center; flex-wrap: wrap; }
.row--between { justify-content: space-between; }
.row--center { justify-content: center; }
.push { margin-left: auto; }

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

/* --- Money fields ---------------------------------------------------------
   Prices are typed in euros. The symbol sits in the field rather than in the
   label, so the owner can see they are entering 13,90 and not 1390 while their
   eyes are on the box they are typing into.
   -------------------------------------------------------------------------- */

.field--money { position: relative; }

.input--money {
  padding-right: 2.1rem;
  font-family: var(--mono);
  text-align: right;
}

.field--money::after {
  content: "€";
  position: absolute;
  right: 0.8rem;
  bottom: 0.72rem;
  color: var(--ink-faint);
  font-family: var(--mono);
  pointer-events: none;
}

/* --- Catalogue list -------------------------------------------------------
   The owner's menu and ingredient pages. One line per thing, everything else
   behind a modal: a pizzeria has thirty toppings and a dozen pizzas, and
   showing every field of every one of them at once turns a two-second price
   change into a scavenger hunt.
   -------------------------------------------------------------------------- */

.catalist {
  list-style: none;
  margin: 0;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  overflow: hidden;
}

.catalist__row {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  padding: 0.6rem 0.85rem;
  border-top: 1px solid var(--line-soft);
}

.catalist__row:first-child { border-top: 0; }
.catalist__row:hover { background: var(--paper); }

/* A thing that is off tonight reads as off at a glance, without hiding it. */
.catalist__row.is-off { background: repeating-linear-gradient(
  135deg, transparent, transparent 7px, var(--line-soft) 7px, var(--line-soft) 14px); }
.catalist__row.is-off .catalist__name { color: var(--ink-soft); text-decoration: line-through; }

.catalist__art {
  flex: 0 0 auto;
  width: 2.5rem; height: 2.5rem;
  border-radius: 50%;
  background: var(--paper) center/cover no-repeat;
  border: 1px solid var(--line);
}

.catalist__name { font-weight: 600; }
.catalist__meta { flex: 1 1 auto; min-width: 0; }

.catalist__sub {
  display: block;
  font-size: var(--step--1);
  color: var(--ink-soft);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.catalist__price {
  font-family: var(--mono);
  font-size: var(--step--1);
  white-space: nowrap;
}

.catalist__actions { display: flex; gap: 0.35rem; flex: 0 0 auto; align-items: center; }

/* Secondary controls in a dense list. A row is scanned, not read, so the
   buttons must not outweigh the name and the price they sit beside. */
.btn--sm {
  padding: 0.3rem 0.7rem;
  min-height: 2rem;
  font-size: var(--step--1);
}

/* The availability toggle shows the current state; pressing it changes that
   state. Green for on sale, hatched grey for off — legible at a glance down a
   column of thirty toppings. */
.btn--toggle[aria-pressed="true"] {
  --btn-bg: #e4f2ea;
  color: var(--basil);
  border-color: #bfe0cd;
}

.btn--toggle[aria-pressed="false"] {
  --btn-bg: var(--line-soft);
  color: var(--ink-soft);
}

/* A category heading names the group; its kind is a footnote about pricing. */
.catsection__kind {
  font-family: var(--sans);
  font-size: var(--step--1);
  font-weight: 400;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-left: 0.6rem;
  vertical-align: middle;
}

.catalist__empty { padding: 1.1rem 0.85rem; color: var(--ink-soft); }

.chip--off { background: var(--line-soft); color: var(--ink-soft); }
.chip--off::before { background: var(--ink-faint); }

/* --- Modal ----------------------------------------------------------------
   A native <dialog>: the browser already provides Esc, the focus trap and the
   inert background, and every one of those is a thing a hand-rolled overlay
   gets subtly wrong.
   -------------------------------------------------------------------------- */

.modal {
  width: min(46rem, calc(100vw - 2rem));
  max-height: calc(100vh - 4rem);
  padding: 0;
  border: 0;
  border-radius: var(--radius-lg);
  background: var(--surface);
  color: var(--ink);
  box-shadow: var(--shadow-lift);
}

.modal::backdrop { background: rgb(26 22 20 / 45%); }

.modal__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1.1rem 1.3rem;
  border-bottom: 1px solid var(--line);
  position: sticky;
  top: 0;
  background: var(--surface);
  z-index: 1;
}

.modal__title { margin: 0; font-size: var(--step-1); }
.modal__body { padding: 1.3rem; overflow-y: auto; }
.modal__body > * + * { margin-top: 1.1rem; }

.modal__foot {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
  justify-content: flex-end;
  padding: 1rem 1.3rem;
  border-top: 1px solid var(--line);
}

/*
  The question `window.confirm()` used to ask (2026-09-05).

  Narrower than an editing modal because it holds one sentence and its answers,
  and a sentence set 46rem wide is a sentence nobody reads at a counter. Built
  by `assets/js/dialog.js`; everything else it wears is `.modal` above.

  `pre-line` because the messages were written for a native box and some of them
  have a blank line in them — the reorder warning names the items that are gone,
  then leaves a gap, then quotes the new total.
*/
.modal--ask { width: min(28rem, calc(100vw - 2rem)); }

.modal__ask {
  margin: 0;
  white-space: pre-line;
}

/*
  On a counter phone the answers are stacked and full width, because the two
  buttons of a confirmation are the whole reason the dialog is on screen and a
  thumb should not have to find the smaller half of a 320 px row.

  `column-reverse` rather than `column`, so the way out sits at the bottom: the
  bottom of a phone is where a thumb already is, and the button it lands on by
  accident should be the one that does nothing. It is the same reasoning as the
  focus rule in `dialog.js` — on a destructive question the safe answer is the
  one nearest the hand.
*/
@media (max-width: 26rem) {
  .modal--ask .modal__foot { flex-direction: column-reverse; }
  .modal--ask .modal__foot .btn { width: 100%; }
}

/* --- Standard topping library --------------------------------------------- */

.libpick {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
  gap: 0.4rem 1rem;
}

.libpick__have { color: var(--ink-faint); }

/*
  Priced rows (slice X1): the tick and the name on one line, the money box on
  the next, so a column stays narrow enough that thirty-nine of them still fit
  the grid above rather than forcing one topping per row.
*/
.libpick--priced { grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr)); }

.libpick__row {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  padding: 0.35rem 0;
}

.libpick__row .input--money { max-width: 7rem; }
.libpick__row.libpick__have { color: var(--ink-faint); }

/*
  What each run of ingredients is for (slice Y-B, Y7).

  The library has always carried `applies_to` and the picker showed one flat run
  of thirty-nine names, so an owner learned that half of them were burger parts
  only after importing them. A heading per group is the whole fix.
*/
.libpick__head {
  margin: 1rem 0 0.35rem;
  font-size: var(--step--1);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-soft);
}

.libpick__head:first-of-type { margin-top: 0.5rem; }

/* The optional per-size ladder, shut by default: most shops price a topping
   once and the size multiplier does the rest. */
.libpick__ladder { margin-bottom: 0.5rem; }

.libpick__ladder > summary {
  padding: 0.4rem 0;
  cursor: pointer;
  font-size: var(--step--1);
  color: var(--ink-soft);
}

.libpick__ladderrow {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr));
  gap: 0.5rem;
  margin-top: 0.35rem;
}

@media (max-width: 640px) {
  .catalist__row { flex-wrap: wrap; }
  .catalist__meta { flex-basis: 100%; order: 2; }
  .modal { width: 100vw; max-width: 100vw; height: 100vh; max-height: 100vh; border-radius: 0; }

  /*
    …but not a question (2026-09-05, D6). A full-screen sheet is right for an
    editing form, which is a page's worth of fields; a confirmation is one
    sentence and two buttons, and blown up to the whole screen it reads as
    having navigated somewhere rather than as having been asked something. It
    stays a card over the page it belongs to, which is the whole reason this
    stopped being `window.confirm()`.
  */
  .modal--ask {
    width: min(28rem, calc(100vw - 2rem));
    max-width: calc(100vw - 2rem);
    /*
      `fit-content`, not `auto`. A modal dialog is absolutely positioned with
      both block edges pinned, so `height: auto` there means *stretch to the
      space* — the browser's own sheet says `fit-content` for exactly this
      reason, and writing `auto` quietly turns it off. Measured: 600 px of card
      around 206 px of question.
    */
    height: fit-content;
    max-height: calc(100vh - 4rem);
    border-radius: var(--radius-lg);
  }
}

/* --- Delivery bands -------------------------------------------------------
   Shared by the storefront and checkout, so it lives here rather than in a page
   sheet — a shared component parked in storefront.css once left the kitchen
   board unstyled, because that page does not load it.                         */

.bandview {
  margin: 2rem 0;
  display: grid;
  gap: 0.75rem;
}

.bandview__title {
  font-size: var(--step-1, 1.1rem);
  margin: 0;
}

.bandview__table {
  width: 100%;
  border-collapse: collapse;
}

.bandview__row > * {
  padding: 0.45rem 0.5rem;
  border-bottom: 1px solid var(--line-soft);
  text-align: left;
  font-weight: 400;
}

.bandview__row .money { text-align: right; white-space: nowrap; }

.bandview__free {
  color: var(--ink-soft);
  font-size: var(--step--1);
  text-align: right;
}

/* The band the customer's own address fell into. */
.bandview__row.is-here > * {
  background: var(--line-soft);
  font-weight: 650;
}

.bandview__note,
.bandview__you {
  margin: 0;
  font-size: var(--step--1);
  color: var(--ink-soft);
}

.bandview__you {
  color: var(--ink);
  font-weight: 650;
}

/* --- Leaflet containment ---------------------------------------------------
   Leaflet numbers its own furniture in the hundreds — panes at 400, controls at
   800, the corner containers at 1000 — and `.leaflet-container` is positioned
   but sets no z-index, so it opens no stacking context and every one of those
   numbers competes in the page root. Against the builder's 50 the map wins, and
   a customer composing a pizza gets the delivery map drawn across the dialog.

   Fixing this by out-bidding Leaflet is a race nobody wins: the next component
   would have to clear 1000 too. Giving the map its own stacking context settles
   it once — everything Leaflet draws is then ordered *within* the map and the
   whole map sits at 0, under every layer of ours.

   `isolation: isolate` is what creates the context; the z-index is there so the
   rule still reads as deliberate to someone who removes the isolation.         */

.leaflet-container {
  position: relative;
  z-index: 0;
  isolation: isolate;
}

/* --- Category nav ----------------------------------------------------------
   Sticky, and horizontally scrollable rather than wrapping: a shop with twelve
   categories must not push the first dish below the fold. In app.css because
   the owner's menu editor is the obvious second home for it.
   -------------------------------------------------------------------------- */

.catnav {
  position: sticky;
  top: 0;
  z-index: 30;
  display: flex;
  gap: 0.5rem;
  margin: 0 -1.25rem 2rem;
  padding: 0.75rem 1.25rem;
  overflow-x: auto;
  scrollbar-width: none;
  background: color-mix(in srgb, var(--paper) 88%, transparent);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--line);
}

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

.catnav__chip {
  flex: 0 0 auto;
  padding: 0.4rem 0.9rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink-soft);
  font-size: var(--step--1);
  font-weight: 650;
  text-decoration: none;
  white-space: nowrap;
  transition: background-color 140ms ease, color 140ms ease, border-color 140ms ease;
}

.catnav__chip:hover { color: var(--ink); border-color: var(--ink-faint); }

.catnav__chip.is-current {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--paper);
}

/* A jump must not land the heading underneath the bar that did the jumping. */
.menu-group { scroll-margin-top: 4.5rem; }

/* --- Cart pill -------------------------------------------------------------
   Shared by the storefront and, on a phone, the checkout — which is why it is
   here and not in either page's file. It lived in checkout.css until
   2026-08-10, so the one page that needed it most never loaded it and the
   storefront's cart was an unstyled row of text.
   -------------------------------------------------------------------------- */

.cartbar {
  position: fixed;
  left: 50%;
  bottom: 1.1rem;
  z-index: 40;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  max-width: calc(100vw - 2rem);
  padding: 0.6rem 0.6rem 0.6rem 1rem;
  border-radius: 999px;
  background: var(--night);
  color: var(--paper);
  box-shadow: var(--shadow-lift);
  transform: translateX(-50%);
  animation: cartbar-in 260ms cubic-bezier(.2, .9, .3, 1) both;
}

.cartbar[hidden] { display: none; }

@keyframes cartbar-in {
  from { opacity: 0; transform: translate(-50%, 18px); }
  to   { opacity: 1; transform: translate(-50%, 0); }
}

.cartbar__count {
  display: grid;
  place-items: center;
  min-width: 1.6rem;
  height: 1.6rem;
  padding-inline: 0.35rem;
  border-radius: 999px;
  background: var(--brand);
  color: var(--brand-ink);
  font-weight: 700;
  font-size: var(--step--1);
}

.cartbar__label { font-size: var(--step--1); }

/* The running total. Display only — cart_quote() decides what is owed. */
.cartbar__total {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  letter-spacing: -0.02em;
  /* "10,90 €" broke across two lines inside the pill without this. */
  white-space: nowrap;
}

/* How far the basket is from the shop's minimum. Sits beside the total rather
   than replacing it: the customer needs both numbers to know what to add. */
.cartbar__short {
  font-size: var(--step--1);
  color: var(--ember);
  white-space: nowrap;
}

.cartbar__short[hidden] { display: none; }

.cartbar.is-bumped { animation: cartbar-bump 340ms cubic-bezier(.34, 1.4, .64, 1); }

@keyframes cartbar-bump {
  0%   { transform: translate(-50%, 0) scale(1); }
  40%  { transform: translate(-50%, -5px) scale(1.04); }
  100% { transform: translate(-50%, 0) scale(1); }
}

/* The shortfall is the first thing to go when the pill runs out of room. */
@media (max-width: 420px) {
  .cartbar__short { display: none; }
}

/* --- Phone verification (slice P) ----------------------------------------- */

/*
  One panel, two callers. At the checkout it sits inside the ticket below Place
  order and appears only once the server has said a code is needed, so it reads
  as the last thing between a composed order and the kitchen. On `register.php`
  it is the whole step, and `.verify--page` drops the rule that separated it.

  It lives here rather than in `checkout.css` because `register.php` loads no
  page stylesheet — the shared-component rule, paid for once already when a rule
  left in `storefront.css` unstyled the kitchen board.
*/
.verify {
  display: flex;
  flex-direction: column;
  gap: .6rem;
  margin-top: .9rem;
  padding-top: .9rem;
  border-top: 1px solid var(--line);
}

/*
  A class that sets `display` beats the `hidden` attribute. The slot picker cost
  this project that bug on 2026-08-09 and the till's pay-on-delivery button on
  2026-08-11; this rule is why it does not cost it a third time.
*/
.verify[hidden] { display: none; }

/* Standing on its own: nothing above it to be separated from. */
.verify--page {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}

.verify__title {
  margin: 0;
  font-size: 1rem;
}

.verify__lede,
.verify__msg,
.verify__why {
  margin: 0;
  font-size: .85rem;
}

.verify__msg:empty { display: none; }

.verify__msg { color: var(--ember); }

/* Six digits, spaced out and large: this is read off a phone screen and typed
   with a thumb, often while standing up. */
.verify__code {
  font-size: 1.5rem;
  letter-spacing: .35em;
  text-align: center;
}

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

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

/* --- The privacy notice (slice W11c) --------------------------------------- */

/* A bar, not a modal. It gates nothing, so it must not block anything either —
   a customer mid-order should be able to ignore it entirely. */
.privacybar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 40;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 1rem;
  padding: 0.85rem 1.1rem;
  background: var(--night);
  color: #fff;
  font-size: 0.92rem;
}

.privacybar p { margin: 0; }
.privacybar a { color: #fff; }
.privacybar .btn { margin-left: auto; background: #fff; color: var(--night); border: 0; }

@media (max-width: 600px) {
  .privacybar .btn { margin-left: 0; }
}

/* --- Address suggestions ---------------------------------------------------
   Moved out of checkout.css in slice X10, when the superadmin console started
   asking the same question of the same endpoint. One list, one look: the
   customer's checkout and the operator's new-company form are different pages
   with the same control, and two copies of these rules would drift.

   The positioned parent is the caller's — `.addrsearch` on the checkout,
   `.geopick` on an admin field — because the two wrap different amounts of
   form around the input. */
.suggest {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  /*
    Above the basket pill and the privacy bar, both of which are 40 (2026-09-05
    audit).

    At 20 it lost to `.cartbar`, and on a 390 px checkout the pill painted over
    the last two of five addresses and swallowed taps meant for them — while the
    customer was typing the one thing a delivery cannot be placed without. A
    transient list somebody has opened outranks a bar that is always there;
    everything else on the page still passes under both.
  */
  z-index: 60;
  margin: 0.2rem 0 0;
  padding: 0.25rem;
  list-style: none;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 0.6rem;
  box-shadow: 0 12px 28px rgb(0 0 0 / 12%);
  max-height: 18rem;
  overflow-y: auto;
}

.suggest[hidden] { display: none; }

.suggest__item {
  display: grid;
  gap: 0.1rem;
  padding: 0.5rem 0.6rem;
  border-radius: 0.4rem;
  cursor: pointer;
}

.suggest__item.is-active,
.suggest__item:hover { background: var(--line-soft); }

.suggest__name { font-weight: 650; }

.suggest__addr {
  font-size: var(--step--1);
  color: var(--ink-soft);
}

/* The credit a road-drawn delivery area owes OpenStreetMap. Small, quiet, and
   present only when a polygon was actually drawn — ODbL asks for attribution,
   not for a banner. */
.bandview__credit {
  margin-top: 0.4rem;
  font-size: var(--step--2, 0.75rem);
  color: var(--ink-soft);
}

/* --- A shop's mark ---------------------------------------------------------
   One component, four surfaces: the storefront header, the finder card, the
   receipt and the settings page. `shop_mark_html()` in lib/tenant.php draws it,
   and the size lives here rather than there — a receipt and a map pin want
   different dimensions and neither is that function's business.

   The monogram tile paints `--mark`, which the shop's own row supplies. A
   generic mark on every shop without a logo would be the "all places look the
   same" complaint committed by the platform itself. */
.logo {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: 0.6rem;
  object-fit: cover;
  overflow: hidden;
  background: var(--surface-2, #f2f2f2);
}

.logo--sm { width: 1.75rem; height: 1.75rem; border-radius: 0.45rem; }
.logo--lg { width: 4.5rem; height: 4.5rem; border-radius: 1rem; }

/* The letter tile. `--mark` is set inline from the shop's validated brand
   colour; the fallback here is for a mark rendered outside that helper. */
.logo--mono {
  background: var(--mark, #d62828);
  color: #fff;
  font-weight: 700;
  line-height: 1;
  /* The letter is the only thing in the box, so centring it optically beats
     centring it metrically — a cap sits high in its own em box. */
  padding-top: 0.05em;
  user-select: none;
  /* Sized per tile rather than inherited: the box is a fixed rem square, so the
     letter cannot be a share of a font-size it does not depend on. The two
     modifier rules below carry two classes and win on specificity, not order. */
  font-size: 1.35rem;
}

.logo--sm.logo--mono { font-size: 0.95rem; }
.logo--lg.logo--mono { font-size: 2.4rem; }

/* --- A shop's mark on the map ---------------------------------------------
   Slice Y-D, Y13. The owner's own reason for wanting logos at all: *"what would
   help out customers to pick company from the map"*. A default Leaflet pin
   makes every shop on the map the same shop.

   Leaflet positions the icon by its anchor, so this only has to be the right
   size and to look like a pin rather than a card. */
.mappin {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  overflow: hidden;
  border: 2px solid #fff;
  box-shadow: 0 1px 4px rgb(0 0 0 / 0.35);
  background: #fff;
}

/* The mark fills the pin. Round rather than the card's soft square: a circle
   reads as a point on a map, and a rounded rectangle reads as a label. */
.mappin .logo {
  width: 100%;
  height: 100%;
  border-radius: 50%;
  font-size: 1.1rem;
}

/* --- A shop's standing -----------------------------------------------------
   Slice Y-D, Y14. Shown only above REVIEW_MIN_COUNT, which `directory_card()`
   decides — the card never compares a count itself, so the two renderers cannot
   disagree about who is rated. */
.shopcard__rating { white-space: nowrap; font-variant-numeric: tabular-nums; }

/* The reviews on a shop's own page. */
.reviews { display: grid; gap: 1rem; }

.reviews__head {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  flex-wrap: wrap;
}

.reviews__score { font-size: var(--step-2); font-weight: 750; font-variant-numeric: tabular-nums; }

.review {
  display: grid;
  gap: 0.35rem;
  padding: 0.9rem 0;
  border-top: 1px solid var(--b-line, var(--line));
}

.review__head {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  font-size: var(--step--1);
  color: var(--ink-soft);
}

.review__stars { color: var(--brand, #d62828); letter-spacing: 0.05em; }

/* The shop's answer, set in from the review it answers so the thread reads as
   a thread rather than as two comments. */
.review__reply {
  margin: 0.35rem 0 0 1rem;
  padding-left: 0.85rem;
  border-left: 2px solid var(--b-line, var(--line));
  font-size: var(--step--1);
}

.review__reply-who { font-weight: 650; }
