/* The customer website — 07-website.md §2 and §4.
 *
 * MOBILE FIRST, LITERALLY. Everything below the first media query is the
 * phone layout: single column, full-width controls, nothing that needs a
 * hover. Larger screens are additions to it, in this order — large phone,
 * tablet, desktop — and each one adds space or a second column rather than
 * revealing anything that was hidden. Nothing here shrinks a desktop design
 * down; there is no desktop design to shrink.
 *
 * NO RAW PIXEL VALUES for anything a density profile owns: type, spacing,
 * radius, hit area and the content column all come from tokens.css, so this
 * whole sheet moves when a channel changes profile (§4.1). The handful of
 * pixel values that remain are things a profile does not own — a hairline
 * border, a thumbnail, the width of a sidebar — and each is a token declared
 * once at the top rather than a number typed into a rule.
 *
 * The palette is deliberately restrained: one primary, three status colours,
 * and greys. A restaurant's branding overrides --colour-primary and nothing
 * else, so a shop's colour cannot make a warning look like a success.
 */

:root {
  --site-rail: 200px;
  --site-wide: calc(var(--content-max-width) + var(--site-rail)
                    + var(--space-xl));
  --site-hairline: 1px;
  --site-thumb: 72px;
  --site-thumb-lg: 96px;
  --site-shadow: 0 1px 2px rgba(16, 16, 24, 0.06),
                 0 8px 24px rgba(16, 16, 24, 0.06);
  --site-shadow-bar: 0 -2px 12px rgba(16, 16, 24, 0.08);
  --colour-primary-soft: #eef0ff;
  --colour-success-soft: #e9f7f0;
  --colour-warning-soft: #fdf3e7;
  --colour-danger-soft: #fdeeed;
}

body {
  background: var(--colour-surface);
  /* Room for the sticky action bar, so the last line of a basket is never
   * hidden behind it on a short phone screen. */
  padding-bottom: env(safe-area-inset-bottom);
}

.site-main {
  max-width: var(--content-max-width);
  margin: 0 auto;
  padding: var(--space-md) var(--space-md) var(--space-xl);
}

.site-main--wide { max-width: var(--site-wide); }

/* The unavailable-ordering notice sits in its own band ABOVE <main>, so the
   page's full top-and-bottom padding was being spent twice before any food
   appeared. It keeps the side padding - the notice must not touch the edge -
   and gives back the rest. */
.site-main--notice {
  padding-top: var(--space-sm);
  padding-bottom: var(--space-sm);
}

/* Pages with a sticky bottom bar reserve its height, so nothing is trapped
 * underneath it. */
/* THE ROOM A FIXED ACTION BAR NEEDS BELONGS AT THE BOTTOM OF THE PAGE, NOT
 * THE BOTTOM OF <main>. The footer is a SIBLING of main, so reserving the
 * space on main put a void in the middle of the page and left the footer -
 * the opening hours and the allergen link - permanently underneath the bar
 * once there was nowhere left to scroll. Two reported symptoms, one cause.
 * The last element in the document carries the reservation instead. */
.has-actionbar { padding-bottom: var(--space-lg); }
.has-actionbar ~ .site-footer {
  padding-bottom: calc(var(--control-min-height) * 2.4);
}

/* ---------------------------------------------------------------- skip link */
.skip {
  position: absolute;
  left: var(--space-sm);
  top: calc(var(--space-sm) * -8);
  z-index: 40;
  /* A keyboard user's FIRST control, so it meets the same floor as every
     other one. Padding alone made it 40px - under the floor the tokens
     declare, and the sort of four-pixel shortfall only measurement finds. */
  display: inline-flex;
  align-items: center;
  min-height: var(--control-min-height);
  background: var(--colour-text);
  color: #fff;
  padding: var(--space-sm) var(--space-md);
  border-radius: var(--radius);
  transition: top 120ms ease;
}
.skip:focus { top: var(--space-sm); }

/* ------------------------------------------------------------------ app bar */
.appbar {
  position: sticky;
  top: 0;
  z-index: 20;
  background: var(--colour-bg);
  border-bottom: var(--site-hairline) solid var(--colour-border);
}

.appbar__inner {
  max-width: var(--site-wide);
  margin: 0 auto;
  padding: var(--space-sm) var(--space-md);
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}

.appbar__brand {
  display: flex;
  flex-direction: column;
  /* align-items is deliberately LEFT ALONE here: the default `stretch` is
     what gives `.appbar__name` a width to truncate against, and without it
     a long shop name overflows the header and collides with the nav at
     320px. Only the pill opts out, below. */
  gap: 2px;
  text-decoration: none;
  color: inherit;
  min-height: var(--tap-min);
  justify-content: center;
  flex: 1 1 auto;
  min-width: 0;
}

.appbar__name {
  font-size: var(--font-lg);
  font-weight: 650;
  letter-spacing: -0.01em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.appbar__nav {
  display: flex;
  gap: var(--space-xs);
  align-items: center;
  flex: 0 0 auto;
}

/* --------------------------------------------------------------- status pill */
.pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  font-size: var(--font-sm);
  font-weight: 600;
  padding: 2px var(--space-sm);
  border-radius: 999px;
  white-space: nowrap;
}
/* A COLUMN flex container stretches its children to its full width, which
   turned the status pill into a full-width bar - a grey stripe when closed and
   a wide green one when open. The pill opts out on its OWN, rather than the
   parent turning stretching off for every child: the shop name NEEDS to be
   stretched, because that is what gives `text-overflow: ellipsis` a width to
   work against. Fixing this on the parent swapped one defect for another. */
.appbar__brand .pill { align-self: flex-start; max-width: 100%; }

.pill::before {
  content: "";
  width: 8px; height: 8px;
  border-radius: 50%;
  background: currentColor;
  flex: 0 0 auto;
}
.pill--open   { background: var(--colour-success-soft); color: var(--colour-success); }
.pill--closed { background: var(--colour-surface); color: var(--colour-text-muted); }
.pill--paused { background: var(--colour-warning-soft); color: var(--colour-warning); }

/* -------------------------------------------------------------------- buttons */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  min-height: var(--control-min-height);
  min-width: var(--tap-min);
  padding: var(--space-sm) var(--space-md);
  border: var(--site-hairline) solid transparent;
  border-radius: var(--radius);
  font-size: var(--font-base);
  font-weight: 600;
  line-height: 1.2;
  text-decoration: none;
  cursor: pointer;
  background: var(--colour-surface);
  color: var(--colour-text);
  transition: background 120ms ease, border-color 120ms ease;
}
.btn:hover { background: var(--colour-border); }

.btn--primary {
  background: var(--colour-primary);
  color: var(--colour-primary-text);
}
.btn--primary:hover { background: #2b2ea8; }

.btn--secondary {
  background: var(--colour-bg);
  border-color: var(--colour-border);
}
.btn--secondary:hover { background: var(--colour-surface); }

/* THE TWO BORDERLESS VARIANTS CARRY AN UNDERLINE AT REST.
   Both are transparent with no border, so before this the only thing marking
   them out from the paragraph around them was their colour - and their only
   other cue lived in the `:hover` rule below, which a touch screen can never
   produce. On a phone "Add something else" and "Remove" read as grey and red
   text. Colour alone is also exactly what the accessibility requirement rules
   out, so the underline does the work and the colour stays as emphasis. */
/* OUTLINED, not filled. The owner circled the old "back" link on an item
   page: 14px muted text, easy to look straight past. This is the same
   control at two steps up the shared type scale, in the primary colour, with
   a border thick enough to read as a control at rest - and deliberately NOT
   a filled button, because it must stay subordinate to Add to basket and
   Basket, which are the actions the page is for.

   No new colour: --colour-primary is the same token the filled buttons use.
   The border is the hairline doubled rather than a pixel value of its own. */
.btn--outline {
  background: var(--colour-bg);
  border-color: var(--colour-primary);
  border-width: calc(var(--site-hairline) * 2);
  color: var(--colour-primary);
  font-weight: 700;
}
.btn--outline:hover { background: var(--colour-surface); }

.btn--quiet {
  background: transparent;
  color: var(--colour-text-muted);
  font-weight: 500;
  padding-inline: var(--space-sm);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.btn--quiet:hover { background: var(--colour-surface); color: var(--colour-text); }

.btn--danger {
  background: transparent;
  color: var(--colour-danger);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.btn--danger:hover { background: var(--colour-danger-soft); }

.btn--block { width: 100%; }
.btn--lg { font-size: var(--font-lg); min-height: calc(var(--control-min-height) * 1.15); }

/* Disabled is stated, not only greyed: a customer must be able to find out
 * WHY, and colour alone must never carry the message. */
.btn[disabled], .btn[aria-disabled="true"] {
  background: var(--colour-surface);
  color: var(--colour-text-muted);
  border-color: var(--colour-border);
  cursor: not-allowed;
}

.btn__count {
  background: var(--colour-primary-text);
  color: var(--colour-primary);
  border-radius: 999px;
  font-size: var(--font-sm);
  min-width: 22px;
  padding: 0 6px;
  text-align: center;
}

/* ---------------------------------------------------------------------- cards */
.card {
  background: var(--colour-bg);
  border: var(--site-hairline) solid var(--colour-border);
  border-radius: var(--radius);
  padding: var(--space-md);
  margin-bottom: var(--space-md);
}
.card--flat { box-shadow: none; }
.card--raised { box-shadow: var(--site-shadow); border-color: transparent; }

.card__title {
  font-size: var(--font-lg);
  font-weight: 650;
  margin: 0 0 var(--space-xs);
  letter-spacing: -0.01em;
}

/* ------------------------------------------------------------------- headings */
.page-title {
  font-size: var(--font-display);
  font-weight: 700;
  letter-spacing: -0.02em;
  margin: var(--space-sm) 0 var(--space-xs);
}
.page-lede { color: var(--colour-text-muted); margin-bottom: var(--space-lg); }

.section-title {
  font-size: var(--font-lg);
  font-weight: 650;
  margin: var(--space-lg) 0 var(--space-sm);
  scroll-margin-top: calc(var(--control-min-height) * 2.2);
}

/* The section heading is the way into that section's own page. It declares
   its own hit area because a heading's line box is about 30px, under the
   floor - and it is underlined AT REST, because a control distinguished only
   by hovering is a control a touch screen cannot find. */
/* The shop's logo. Constrained in BOTH directions: the height so the header
   keeps the proportions it had when the name was words, and max-width so a
   wide logo cannot push the nav off a 320px screen - which is the defect the
   old `.appbar__name` ellipsis existed to prevent. `align-self` stops the
   flex column stretching it. */
.appbar__logo {
  align-self: flex-start;
  height: calc(var(--space-unit) * 4);
  width: auto;
  max-width: 100%;
  object-fit: contain;
}

/* The hero. ONE aspect at every width, matching the artwork's own 4.03:1,
   so `object-fit: cover` has nothing to crop and what was designed is what
   appears. The first version asked for 3:1 and let a max-height take over
   above 640px, which meant the container showed 3.00:1 on a phone and
   3.79:1 on a desktop while the file was 2:1 - so the picture was cropped by
   33% of its height at mobile and 47% at desktop, and the wordmark and the
   food, which sit at opposite ends, were both losing edges. */
.menu-hero {
  display: block;
  width: 100%;
  /* height:auto is LOAD-BEARING. The <img> carries width/height attributes
     so the browser can reserve space before it arrives, and those attributes
     are presentational hints - without this the used height is the
     attribute's 600px, the aspect-ratio below is ignored, and the banner
     renders 320x600 on a phone. Measured: it put the first food item 1060px
     down a 640px viewport. */
  height: auto;
  aspect-ratio: 4 / 1;
  object-fit: cover;
  border-radius: var(--radius);
  margin-bottom: var(--space-md);
  background: var(--colour-surface);
}

/* The home page's copy of the banner. Same file, same 4:1 - it differs only
   in how wide it is allowed to be, because it sits OUTSIDE <main> where
   nothing would otherwise constrain it. Measured full-bleed first: 1440x360
   at a 1440px window, 40% of the screen, with the first action pushed to
   y=624. Lining it up with the text beneath it fixes both. */
.home-hero {
  width: calc(100% - (var(--space-md) * 2));
  max-width: var(--content-max-width);
  margin-inline: auto;
}

/* THE IDENTITY BLOCK SITS UP AGAINST THE PICTURE (D-069 follow-up).
   Three separate spaces were stacking between the banner and the shop's
   name - the image's own bottom margin, the section's top padding and the
   title's margin - and three more between the name, "Order for collection."
   and the address. Nothing here changes what the section SAYS; it stops the
   page spending a third of a phone screen on gaps. */
.home-hero { margin-bottom: 0; }
.hero { padding-top: var(--space-md); }
.hero__title { margin: var(--space-xs) 0; }
.hero p.muted { margin: 0 0 var(--space-xs); }
.hero .hero__facts { margin-bottom: var(--space-sm); }

/* The deal's price rule: a note on the price above it, not a warning. */
.deal-rule { margin-top: calc(var(--space-xs) * -1); }

.section-title__link {
  display: inline-flex;
  align-items: center;
  min-height: var(--control-min-height);
  color: inherit;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}

/* -------------------------------------------------------------------- the hero */
.hero {
  background: var(--colour-bg);
  border-bottom: var(--site-hairline) solid var(--colour-border);
  padding: var(--space-lg) var(--space-md) var(--space-lg);
}
.hero__inner { max-width: var(--content-max-width); margin: 0 auto; }
.hero__title {
  font-size: var(--font-display);
  font-weight: 700;
  letter-spacing: -0.02em;
  margin: var(--space-sm) 0;
}
.hero__facts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm) var(--space-md);
  color: var(--colour-text-muted);
  font-size: var(--font-sm);
  margin-bottom: var(--space-md);
}

/* ------------------------------------------------------------- category strip */
/* A thumb-scrollable row of chips on a phone; a fixed rail on desktop. It is
 * the same markup, reflowed — not two navigations. */
.chipstrip {
  position: sticky;
  top: calc(var(--control-min-height) + var(--space-sm));
  z-index: 15;
  background: var(--colour-surface);
  margin: 0 calc(var(--space-md) * -1);
  padding: var(--space-sm) var(--space-md);
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  border-bottom: var(--site-hairline) solid var(--colour-border);
}
.chipstrip::-webkit-scrollbar { display: none; }
.chipstrip__list {
  display: flex;
  gap: var(--space-sm);
  list-style: none;
  margin: 0;
  padding: 0;
  width: max-content;
}
.chip {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  padding: 0 var(--space-md);
  border-radius: 999px;
  background: var(--colour-bg);
  border: var(--site-hairline) solid var(--colour-border);
  color: var(--colour-text);
  text-decoration: none;
  font-size: var(--font-sm);
  font-weight: 600;
  white-space: nowrap;
}
.chip:hover, .chip[aria-current="true"] {
  background: var(--colour-primary-soft);
  border-color: var(--colour-primary);
  color: var(--colour-primary);
}

/* -------------------------------------------------------------- menu item rows */
.items { list-style: none; margin: 0; padding: 0; }

.item-row {
  display: flex;
  gap: var(--space-md);
  align-items: flex-start;
  padding: var(--space-md);
  background: var(--colour-bg);
  border: var(--site-hairline) solid var(--colour-border);
  border-radius: var(--radius);
  margin-bottom: var(--space-sm);
  text-decoration: none;
  color: inherit;
  min-height: var(--tap-min);
}
.item-row:hover { border-color: var(--colour-primary); }
.item-row__body { flex: 1 1 auto; min-width: 0; }
.item-row__name { font-weight: 650; display: block; }
.item-row__desc {
  color: var(--colour-text-muted);
  font-size: var(--font-sm);
  margin: var(--space-xs) 0 0;
  /* Two lines on a phone: enough to tell dishes apart, not enough to turn a
   * menu into an essay. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.item-row__aside {
  flex: 0 0 auto;
  text-align: right;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--space-xs);
}
.item-row__price { font-weight: 650; white-space: nowrap; }
/* FOUR BY THREE, AND THE WHOLE PICTURE. This was a square filled with
 * `object-fit: cover`, which is a crop: a wide photograph lost its ends and a
 * tall one lost its top, and the shop had no way to know which part a
 * customer would see. `contain` fits the image inside the box instead, so
 * every photograph arrives whole and in its own proportions - nothing cut,
 * nothing stretched. A picture that is not 4:3 sits on the surface colour
 * rather than being trimmed to fit, which is why the editor now asks for 4:3.
 *
 * The WIDTH is what stays fixed and the height follows from the ratio. The
 * row's horizontal room is the scarce one at 320px, and widening the picture
 * would have taken it from the name and the price. */
.item-row__thumb {
  flex: 0 0 auto;
  width: var(--site-thumb);
  aspect-ratio: 4 / 3;
  border-radius: var(--radius);
  object-fit: contain;
  background: var(--colour-surface);
}

/* The product page's own photograph (D-060). Bounded by ASPECT RATIO rather
 * than a fixed height, so a portrait phone snap and a wide studio shot both
 * sit in the same space without either being stretched. `object-fit: cover`
 * fills that box; the stored file is never cropped, only this view of it. The
 * background is what shows while it loads, so the page does not jump. */
.item-photo {
  display: block;
  width: 100%;
  max-width: 480px;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: var(--radius);
  background: var(--colour-surface);
  margin: var(--space-md) 0;
}

/* Sold out and out-of-window items STAY on the menu (§2.3). Greyed, labelled,
 * and still clickable — a customer wants to know the shop still does it. */
.item-row--off { opacity: 0.72; }
.item-row--off .item-row__price { text-decoration: line-through; }

/* --------------------------------------------------------------------- badges */
.badge {
  display: inline-block;
  font-size: var(--font-sm);
  font-weight: 600;
  padding: 1px var(--space-sm);
  border-radius: var(--radius);
  background: var(--colour-surface);
  color: var(--colour-text-muted);
  border: var(--site-hairline) solid var(--colour-border);
}
.badge--off { background: var(--colour-surface); color: var(--colour-text-muted); }
/* The mandatory-field marker (D-072). A pill rather than the badge's usual
   corner radius, so it reads as a warning indicator and not as another
   label - and it is never the only thing saying so: the sentence beside it
   carries the meaning, and the badge itself is `aria-hidden`. */
.badge--error {
  background: var(--colour-danger);
  color: var(--colour-primary-text);
  border-color: transparent;
  border-radius: 999px;
  min-width: var(--font-base);
  text-align: center;
}
.badge--deal { background: var(--colour-primary-soft); color: var(--colour-primary); border-color: transparent; }
.badge--when { background: var(--colour-warning-soft); color: var(--colour-warning); border-color: transparent; }

/* --------------------------------------------------------------------- alerts */
.alert {
  border-radius: var(--radius);
  padding: var(--space-md);
  margin-bottom: var(--space-md);
  border: var(--site-hairline) solid transparent;
  font-size: var(--font-base);
}
.alert__title { font-weight: 650; margin: 0 0 var(--space-xs); }
.alert p:last-child { margin-bottom: 0; }
.alert--info    { background: var(--colour-primary-soft); color: #23256b; }
.alert--success { background: var(--colour-success-soft); color: #04503a; }
.alert--warning { background: var(--colour-warning-soft); color: #7a3d05; }
.alert--danger  { background: var(--colour-danger-soft);  color: #8a1c14; }
.alert--quiet {
  background: var(--colour-bg);
  border-color: var(--colour-border);
  color: var(--colour-text-muted);
  font-size: var(--font-sm);
}

/* ----------------------------------------------------------- modifier groups */
/* One component renders every kind of group (§2.4). What changes is the input
 * type and the rule sentence, both driven by the group's numbers. */
.group {
  border: var(--site-hairline) solid var(--colour-border);
  border-radius: var(--radius);
  padding: var(--space-md);
  margin: 0 0 var(--space-md);
  background: var(--colour-bg);
}
.group--invalid { border-color: var(--colour-danger); }

.group__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-xs) var(--space-sm);
  padding: 0;
  margin-bottom: var(--space-sm);
}
.group__name { font-weight: 650; font-size: var(--font-base); }
.group__rule { color: var(--colour-text-muted); font-size: var(--font-sm); }

.group__error {
  display: block;
  color: var(--colour-danger);
  font-size: var(--font-sm);
  font-weight: 600;
  margin-bottom: var(--space-sm);
}

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

.option {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  min-height: var(--tap-min);
  padding: var(--space-sm) 0;
  border-bottom: var(--site-hairline) solid var(--colour-border);
  cursor: pointer;
}
.option:last-child { border-bottom: 0; }
.option input { min-height: 0; width: 22px; height: 22px; flex: 0 0 auto; accent-color: var(--colour-primary); }
.option__name { flex: 1 1 auto; }
.option__price { color: var(--colour-text-muted); white-space: nowrap; }
.option--off { color: var(--colour-text-muted); cursor: not-allowed; }
.option--off .option__why { font-size: var(--font-sm); font-style: italic; }

/* A folded-away long list. The summary is the control, so it obeys the tap
 * floor like every other one.
 *
 * Named with a CLASS rather than reached as `.group summary`, because a
 * positional rule protects a control only while it stays where it was put:
 * the same summary moved out of `.group` would render at 19px and nothing
 * would say so. The guard in `tests/test_site_design.py` reads classes, so
 * carrying one is also what makes the guarantee checkable. */
.group__more {
  display: flex;
  align-items: center;
  min-height: var(--tap-min);
  cursor: pointer;
  font-weight: 600;
  color: var(--colour-primary);
  list-style-position: inside;
}
.group__more:hover { text-decoration: underline; }

/* Sizes read as a segmented control rather than a list: it is one question
 * with a small number of answers, and it changes every price below it. */
.sizes { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-sm); }
.size {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 2px;
  min-height: calc(var(--tap-min) * 1.15);
  padding: var(--space-sm) var(--space-md);
  border: var(--site-hairline) solid var(--colour-border);
  border-radius: var(--radius);
  background: var(--colour-bg);
  cursor: pointer;
}
.size input { position: absolute; opacity: 0; width: 0; height: 0; }
.size__label { font-weight: 600; }
.size__price { color: var(--colour-text-muted); font-size: var(--font-sm); }
.size:has(input:checked) {
  border-color: var(--colour-primary);
  background: var(--colour-primary-soft);
}
.size:has(input:focus-visible) { outline: 3px solid var(--colour-primary); outline-offset: 2px; }
/* Never colour alone: the chosen size is also stated in words to a screen
 * reader, and marked with a tick that does not depend on hue. */
.size:has(input:checked) .size__label::after { content: " ✓"; }

/* ----------------------------------------------------------------- deal slots */
.slot {
  border: var(--site-hairline) solid var(--colour-border);
  border-radius: var(--radius);
  padding: var(--space-md);
  margin-bottom: var(--space-md);
  background: var(--colour-bg);
}
.slot__label { font-weight: 650; display: block; margin-bottom: var(--space-sm); }
.included { list-style: none; margin: 0 0 var(--space-md); padding: 0; }
.included li {
  display: flex;
  gap: var(--space-sm);
  padding: var(--space-xs) 0;
  border-bottom: var(--site-hairline) solid var(--colour-border);
}
.included li:last-child { border-bottom: 0; }
.included__qty { color: var(--colour-text-muted); flex: 0 0 auto; }

/* ------------------------------------------------------------------- selects */
/* Every text-entry control the site uses. `tel` was missing, so the
   checkout's telephone field fell back to the browser default: narrower
   than the name field beside it, a different border, and no guarantee of
   the tap-target height everything else has. The right answer is to style
   the type, not to stop using it - `type="tel"` is what brings up a
   numeric keypad on a phone, which is the whole point of asking this way. */
select, input[type="text"], input[type="tel"], input[type="number"],
textarea {
  width: 100%;
  padding: var(--space-sm) var(--space-md);
  border: var(--site-hairline) solid var(--colour-border);
  border-radius: var(--radius);
  background: var(--colour-bg);
  color: var(--colour-text);
}
label.field { display: block; margin-bottom: var(--space-md); }
label.field > span { display: block; font-weight: 600; margin-bottom: var(--space-xs); }
.field__hint { color: var(--colour-text-muted); font-size: var(--font-sm); font-weight: 400; }

/* A validation message, beside the control it is about rather than in a list
   at the top of the page. Colour is not the only signal: the text says what is
   wrong, `aria-invalid` marks the input, and `role="alert"` announces it - a
   red border alone tells a colour-blind customer nothing. */
/* ONE treatment for "this still needs an answer", wherever it happens
   (D-072). It replaced `field__error` at checkout and the option group's own
   error line. `.group__error` remains for a different thing: a basket LINE
   that cannot be ordered, which is not a field anybody can fill in. */
.needs-answer {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  color: var(--colour-danger);
  font-size: var(--font-sm);
  font-weight: 600;
  margin: calc(var(--space-xs) * -1) 0 var(--space-sm);
}
.field input[aria-invalid="true"],
input[aria-invalid="true"] {
  border-color: var(--colour-danger);
  outline-color: var(--colour-danger);
}

/* --------------------------------------------------------------- basket lines */
.lines { list-style: none; margin: 0; padding: 0; }
.line {
  background: var(--colour-bg);
  border: var(--site-hairline) solid var(--colour-border);
  border-radius: var(--radius);
  padding: var(--space-md);
  margin-bottom: var(--space-sm);
}
.line--problem { border-color: var(--colour-danger); }
.line__head { display: flex; gap: var(--space-md); align-items: flex-start; }
/* Same size token and same treatment as the menu row's thumbnail, so a
 * customer sees the same picture at the same size in both places. */
/* The same 4:3 as the menu row, and for the same reason: a customer
 * checking their basket should recognise the picture they just chose from,
 * not a differently-cropped version of it. */
.line__thumb {
  flex: 0 0 auto;
  width: var(--site-thumb);
  aspect-ratio: 4 / 3;
  border-radius: var(--radius);
  object-fit: contain;
  background: var(--colour-surface);
}
.line__name { font-weight: 650; flex: 1 1 auto; }
.line__total { font-weight: 650; white-space: nowrap; }
.line__detail { color: var(--colour-text-muted); font-size: var(--font-sm); margin: var(--space-xs) 0 0; }
.line__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  margin-top: var(--space-md);
  flex-wrap: wrap;
}

.stepper {
  display: inline-flex;
  align-items: center;
  border: var(--site-hairline) solid var(--colour-border);
  border-radius: var(--radius);
  overflow: hidden;
}
.stepper button {
  min-width: var(--tap-min);
  min-height: var(--tap-min);
  border: 0;
  background: var(--colour-bg);
  /* Stated, not inherited - the minus and the plus were invisible on iOS. */
  color: var(--colour-text);
  font-size: var(--font-lg);
  font-weight: 600;
  cursor: pointer;
  border-radius: 0;
}
.stepper button:hover { background: var(--colour-surface); }
.stepper__value {
  min-width: calc(var(--tap-min) * 0.9);
  text-align: center;
  font-weight: 650;
  padding: 0 var(--space-sm);
}

/* --------------------------------------------------------------- order totals */
.totals { list-style: none; margin: 0; padding: 0; }
.totals li {
  display: flex;
  justify-content: space-between;
  gap: var(--space-md);
  padding: var(--space-sm) 0;
  border-bottom: var(--site-hairline) solid var(--colour-border);
}
.totals li:last-child { border-bottom: 0; }
/* ITEM 12. This is the number somebody opened the page to check, and bold
 * at the same size as the line above it is not emphasis - it is the same
 * text slightly darker. It gets the display size, the shop's colour, and a
 * rule above it so the eye stops there. */
.totals__grand {
  font-size: var(--font-display);
  font-weight: 700;
  line-height: 1.2;
  color: var(--colour-primary);
  border-top: 2px solid var(--colour-border);
  margin-top: var(--space-xs);
  padding-top: var(--space-sm);
}
.totals__grand > span:first-child {
  font-size: var(--font-base);
  font-weight: 600;
  color: var(--colour-text);
  align-self: center;
}

/* A folded block of reference information (item 7). */
/* The confirmation's head: number and time side by side (item 11). The
 * number keeps the display size - it is what gets called across a counter. */
.confirm-head { display: flex; gap: var(--space-md); align-items: center; }
.confirm-head__number { text-align: center; flex: 0 0 auto; }
.confirm-head__digits {
  font-size: calc(var(--font-display) * 1.6);
  font-weight: 800;
  line-height: 1;
  margin: 0;
  color: var(--colour-primary);
}
.confirm-head__when { flex: 1 1 auto; min-width: 0; }
.confirm-head__when .card__title { margin-top: 0; }

.fold { margin-top: var(--space-sm); }
.fold[open] > .group__more { margin-bottom: var(--space-xs); }

/* ------------------------------------------------------- collection/delivery */
.switch { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-sm); }
.switch button {
  min-height: var(--tap-min);
  border: var(--site-hairline) solid var(--colour-border);
  border-radius: var(--radius);
  background: var(--colour-bg);
  /* Stated, not inherited. The unselected half of this control was blank on
     iOS because only the SELECTED half named a colour. */
  color: var(--colour-text);
  font-weight: 600;
  cursor: pointer;
}
.switch button[aria-pressed="true"] {
  background: var(--colour-primary-soft);
  border-color: var(--colour-primary);
  color: var(--colour-primary);
}
.switch button[aria-pressed="true"]::after { content: " ✓"; }

/* --------------------------------------------------------------- action bar */
/* The primary action of a page, kept where a thumb is. It is a real element
 * in the flow on large screens, so a desktop visitor is not given a floating
 * bar they never asked for. */
.actionbar {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 25;
  background: var(--colour-bg);
  border-top: var(--site-hairline) solid var(--colour-border);
  box-shadow: var(--site-shadow-bar);
  padding: var(--space-sm) var(--space-md)
           calc(var(--space-sm) + env(safe-area-inset-bottom));
}
.actionbar__inner {
  max-width: var(--content-max-width);
  margin: 0 auto;
  display: flex;
  gap: var(--space-sm);
  align-items: center;
}
.actionbar__inner > .btn--primary { flex: 1 1 auto; }
.actionbar__note {
  max-width: var(--content-max-width);
  margin: 0 auto var(--space-xs);
  font-size: var(--font-sm);
  color: var(--colour-text-muted);
}

/* -------------------------------------------------------------- empty states */
.empty {
  text-align: center;
  padding: var(--space-xl) var(--space-md);
  background: var(--colour-bg);
  border: var(--site-hairline) dashed var(--colour-border);
  border-radius: var(--radius);
}
.empty__title { font-size: var(--font-lg); font-weight: 650; margin-bottom: var(--space-xs); }
.empty p { color: var(--colour-text-muted); }

/* -------------------------------------------------------------------- footer */
.site-footer {
  border-top: var(--site-hairline) solid var(--colour-border);
  background: var(--colour-bg);
  padding: var(--space-lg) var(--space-md) var(--space-xl);
  margin-top: var(--space-xl);
}
.site-footer__inner {
  max-width: var(--site-wide);
  margin: 0 auto;
  color: var(--colour-text-muted);
  font-size: var(--font-sm);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-md) var(--space-lg);
}
.site-footer a { color: inherit; }

/* A standalone link is a CONTROL, not prose: nothing around it is being read,
   it is there to be tapped. Inline text inside a sentence is deliberately
   exempt (WCAG 2.5.5) and is left alone.
   Carried as a CLASS on the link rather than as a descendant selector, so the
   template says which links are controls and the guard in
   `tests/test_site_design.py` can see it. */
.taplink {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
}

/* The way back, at the top of a category, a product, a deal and Review. It
   was a bare link 19px tall on all four - the commonest control on the site
   after the buttons, and the one nobody had styled. */
.backlink {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  min-height: var(--tap-min);
  color: var(--colour-text-muted);
  font-size: var(--font-sm);
  /* Pulled back by its own padding so the text still lines up with the
     heading beneath it rather than looking indented. */
  margin-left: calc(var(--space-sm) * -1);
  padding-inline: var(--space-sm);
  border-radius: var(--radius);
}
.backlink:hover { color: var(--colour-text); }

.muted { color: var(--colour-text-muted); font-size: var(--font-sm); }
.stack > * + * { margin-top: var(--space-md); }
.row { display: flex; gap: var(--space-sm); flex-wrap: wrap; align-items: center; }

/* =========================================================================
 * LARGE PHONE — a little more room, nothing structural.
 * ====================================================================== */
@media (min-width: 400px) {
  .sizes { grid-template-columns: repeat(3, 1fr); }
  /* Width only - `aspect-ratio` keeps it 4:3 (see the base rule). */
  .item-row__thumb { width: var(--site-thumb-lg); }
}

/* =========================================================================
 * TABLET — two columns where two columns help, and the action bar stops
 * floating because there is room for it in the page.
 * ====================================================================== */
@media (min-width: 640px) {

  .site-main { padding: var(--space-lg) var(--space-md) var(--space-xl); }
  .sizes { grid-template-columns: repeat(4, 1fr); }
  .options--wide { columns: 2; column-gap: var(--space-lg); }
  .options--wide .option { break-inside: avoid; }
  .hero { padding: var(--space-xl) var(--space-md); }

  .actionbar { position: static; box-shadow: none; border-top: 0; padding-inline: 0; }
  .has-actionbar { padding-bottom: var(--space-xl); }
  /* The bar is in the flow from here up, so the footer needs no clearance. */
  .has-actionbar ~ .site-footer { padding-bottom: var(--space-xl); }
  .actionbar__inner { padding: var(--space-md) 0 0; }
}

/* =========================================================================
 * DESKTOP — the category strip becomes a rail beside the menu, which is what
 * a mouse and a tall window are actually good for. Same markup.
 * ====================================================================== */
@media (min-width: 960px) {
  .site-main--wide {
    display: grid;
    grid-template-columns: var(--site-rail) minmax(0, 1fr);
    gap: var(--space-xl);
    align-items: start;
  }

  /* ONLY the rail and the menu beside it belong in the two columns.
     `<main>` also holds the flash messages, and a two-column grid given three
     children put the flash in the RAIL column, the rail in the CONTENT column
     and the menu itself into a 200px strip on the next row - measured, after
     adding an item started returning customers to the menu (D-058) and made
     it the state they see after the commonest action on the site.
     Stated as a rule about placement rather than about the flash, so the next
     thing added to the shell cannot break the page the same way. */
  .site-main--wide > * { grid-column: 1 / -1; }
  .site-main--wide > .chipstrip { grid-column: 1; }
  .site-main--wide > .chipstrip + * { grid-column: 2; }

  .chipstrip {
    position: sticky;
    top: calc(var(--control-min-height) + var(--space-lg));
    margin: 0;
    padding: 0;
    /* The rail is TALLER THAN THE WINDOW once a shop has a real menu - 980px
       for the pilot's 19 categories - and being sticky it never scrolls with
       the page, so the last six were unreachable on a laptop. It scrolls
       inside itself instead, with a scrollbar, because a scrolling region
       with no scrollbar gives no sign there is more. */
    max-height: calc(100vh - var(--control-min-height) - var(--space-lg) * 2);
    overflow-x: hidden;
    overflow-y: auto;
    scrollbar-width: thin;
    border-bottom: 0;
    background: transparent;
  }
  .chipstrip::-webkit-scrollbar { display: block; width: 8px; }
  .chipstrip__list { flex-direction: column; width: auto; align-items: stretch; }
  .chip { justify-content: flex-start; }
  .item-row:hover { box-shadow: var(--site-shadow); }
}

/* =========================================================================
 * PRINT — a customer showing the counter what they wanted.
 * ====================================================================== */
@media print {
  .appbar, .actionbar, .chipstrip, .site-footer, .skip { display: none; }
  .line, .card { border-color: #999; break-inside: avoid; }
}
