/* Rolina Grains Store: the whole visual system.
 *
 * Every value here references a token. No literal colour, size, spacing or
 * duration appears below this line (Constitution INV-26).
 *
 * The composition is deliberately operational rather than promotional: product
 * rows that read like a trader's price list, tabular figures for every price,
 * and hierarchy carried by weight and space. Doctrine 12 and Law VII-7 reject
 * the equal-padding card grid, so the catalogue is not one.
 *
 * ---------------------------------------------------------------------------
 * How this system separates things, which is the whole of its character
 * ---------------------------------------------------------------------------
 *
 * By GROUND, first and almost always. The page carries a soft neutral tone and
 * content sits on white. The step between the two is the boundary, so a card
 * takes no border, a list takes no divider between its rows, and a bar takes no
 * rule above it.
 *
 * This replaced a system that drew a hairline around, above or beneath very
 * nearly every element on every screen. That was not restraint. It was the same
 * clutter as a page of boxes, redrawn one edge at a time, and it made a form
 * field, a notice, a product row and a total look like four versions of one
 * thing. Ground costs nothing, reads instantly, and cannot accumulate.
 *
 * By SPACE, second. Sections are separated by real distance, not by a line
 * standing in for distance.
 *
 * By WEIGHT, third. A heading is a heading because of its size and its weight.
 *
 * A RULE is drawn in exactly three places, and each one earns it: along the
 * bottom of a form field, because a filled field on a white card cannot reach
 * the 3:1 boundary contrast WCAG 1.4.11 requires on fill alone; across the head
 * of a table of figures, because a column needs a head; and as the leading
 * accent on an alert, because an alert has to be findable at a glance.
 * Nothing else. If a fourth appears, it is the pattern coming back.
 *
 * Corners carry a small radius, an Operator amendment to Law VII-7. It is felt
 * rather than seen, and it is not a pill: nothing here takes a capsule radius.
 */

/* ===========================================================================
   Reset and base
   =========================================================================== */

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

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

body {
  margin: 0;
  /* The desk, not the paper. Everything a customer reads sits on a white card
     above this. */
  background: var(--colour-page);
  color: var(--colour-text);
  font-family: var(--font-body);
  font-size: var(--type-base);
  line-height: var(--leading-normal);
  font-weight: var(--weight-regular);
  -webkit-font-smoothing: antialiased;
  overflow-wrap: break-word;
}

h1, h2, h3, h4 {
  font-family: var(--font-display);
  line-height: var(--leading-tight);
  margin: 0;
  font-weight: var(--weight-semibold);
  letter-spacing: -0.01em;
}

h1 { font-size: var(--type-3xl); }
h2 { font-size: var(--type-xl); }
h3 { font-size: var(--type-md); }

p { margin: 0; }

a {
  color: var(--colour-primary-text);
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}
a:hover { color: var(--colour-primary-hover); }

img { max-inline-size: 100%; block-size: auto; display: block; }

ul, ol { margin: 0; padding: 0; }

/* Prices, quantities and any other figure meant to be compared down a column
 * are tabular, per Doctrine 7. */
.figure {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
}

:where(a, button, input, select, textarea, summary):focus-visible {
  outline: 3px solid var(--colour-focus);
  outline-offset: 2px;
}

.visually-hidden {
  position: absolute;
  inline-size: 1px; block-size: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.skip-link {
  position: absolute;
  inset-block-start: var(--space-8);
  inset-inline-start: var(--space-8);
  z-index: var(--z-overlay);
  padding: var(--space-12) var(--space-16);
  background: var(--colour-surface-deep);
  color: var(--colour-on-deep);
  font-weight: var(--weight-semibold);
  transform: translateY(-200%);
}
.skip-link:focus { transform: translateY(0); }

/* Following the skip link scrolls #main to the top of the window, and the top
   of the window is where the command bar and the category bar are. Without
   this, the one control on the page written for a keyboard lands the reader
   behind two opaque bars. The measurement is the same one the bars themselves
   use, so a taller header cannot leave this behind. */
#main { scroll-margin-block-start: calc(var(--header-actual, var(--header-height)) + 44px); }

/* ===========================================================================
   Layout primitives
   =========================================================================== */

.container {
  inline-size: min(100% - (var(--container-pad) * 2), var(--container-default));
  margin-inline: auto;
}
.stack > * + * { margin-block-start: var(--space-16); }
.stack-lg > * + * { margin-block-start: var(--space-32); }

/* -- Where the spacing used to live ----------------------------------------
 * Twenty-two style attributes were being built in JavaScript, in five files,
 * carrying spacing tokens and in four cases a colour. Nothing about them was
 * wrong on its own; together they meant a decision about how far apart two
 * things sit could be in the stylesheet, in the markup, or in a string inside
 * a render function, and which one it was depended on when it was written.
 * They are all named things now, and this is where they live.
 *
 * A supporting line under a heading or beside a figure. */
.lede { margin-block-start: var(--space-8); color: var(--colour-text-muted); }
.lede--spaced { margin-block-end: var(--space-24); }
.lede--roomy { margin-block-start: var(--space-24); }

/* A heading that opens a new block partway down a page that already has one. */
.block-heading { margin-block-start: var(--space-32); }

/* The body of a page that is one column of content rather than a composed
   layout: the order lookup, the privacy policy, the terms. It was written nine
   times as an inline style attribute holding the same two tokens, which is a
   layout decision taken in the markup and taken slightly differently each time.
   It is one class now, and it is the same on every page that uses it. */
.page-body { padding-block: var(--space-32) var(--space-64); }
.page-body--flush-top { padding-block-start: 0; }

/* A column held to a comfortable reading and form-filling width, wherever it
   sits. Not .container: these are blocks inside a container, not containers. */
.measure { max-inline-size: var(--container-narrow); }

main { min-block-size: 60vh; }

.section { padding-block: var(--space-64); }
.section--tight { padding-block: var(--space-32); }

/* -- The card ---------------------------------------------------------------
 * The one surface this system puts content on, and the reason it needs almost
 * no lines.
 *
 * White, on the page's ground, with a small radius and a whisper of shadow that
 * exists only to stop the edge reading as cut out rather than laid down. No
 * border: the ground is the border. A card must earn itself by being a
 * separable object, a product, a total, a notice, one block of contact detail,
 * never by being the next thing down the page.
 */
.panel {
  background: var(--colour-surface);
  border: 0;
  border-radius: var(--radius-md);
  padding: var(--space-24);
  box-shadow: var(--shadow-xs);
}
/* A stack of cards. The gap is the separator, and it is the only one. */
.cards { display: flex; flex-direction: column; gap: var(--space-12); }

/* A remembered order, in the list on the order page. It is a card that is also
   a control, so it is a real <button> rather than a div with a click handler:
   keyboard, focus ring and assistive technology all come free, and the milled
   accent edge on hover is the same answer every other tappable surface in this
   system gives. Matches _historyRow in order_screen.dart. */
.order-row {
  display: flex;
  align-items: center;
  gap: var(--space-16);
  inline-size: 100%;
  text-align: start;
  font: inherit;
  color: inherit;
  cursor: pointer;
  transition: box-shadow var(--motion-fast) var(--ease);
}
.order-row:hover { box-shadow: inset 0 -3px 0 var(--colour-primary), var(--shadow-sm); }
.order-row__main { display: flex; flex-direction: column; gap: var(--space-4); flex: 1; min-inline-size: 0; }
.order-row__number { font-weight: var(--weight-semibold); }
.order-row__summary {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.order-row__date { font-size: var(--type-sm); color: var(--colour-text-muted); }
.order-row__total { font-weight: var(--weight-semibold); white-space: nowrap; }

/* The words a page opens with, and how much of the screen they may take.
 *
 * On a phone this was 197px: a display-sized title over three lines of prose,
 * before a single product. The title steps down and the prose is set at the
 * size of the sentence it is, which is a note about how prices work rather
 * than an announcement. */
.page-head {
  padding-block: var(--space-32) var(--space-24);
}
.page-head h1 {
  font-size: var(--type-3xl);
  letter-spacing: -0.02em;
}
.page-head p {
  color: var(--colour-text-muted);
  margin-block-start: var(--space-8);
  font-size: var(--type-base);
  max-inline-size: 60ch;
}

.eyebrow {
  font-size: var(--type-xs);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--colour-primary-text);
}

/* ===========================================================================
   Header
   =========================================================================== */

/* The command bar is white, and holds the page by structure rather than by
 * colour: a taller bar and a wordmark set against everything around it.
 *
 * It needs no rule beneath it and no longer has one. It is white, the page is
 * not, and that step already draws the boundary from edge to edge. The accent
 * band that used to run under it was the loudest line on every page, repeated
 * on all eight of them, and an accent is not a frame. */
.site-header {
  position: sticky;
  inset-block-start: 0;
  z-index: var(--z-header);
  background: var(--colour-surface);
  border: 0;
  /* The bar is in front of the page, so it says so. A hairline along the base
     because white-on-ground alone stopped being enough once the bar grew tall
     enough to hold a search field, and a real shadow beneath it because content
     genuinely passes under a sticky bar (Doctrine 4). */
  box-shadow: 0 1px 0 var(--colour-border), var(--shadow-sm);
}

/* The bar is a real object, not a strip of text.
 *
 * Three columns with different jobs and different weights: identity on the
 * left, the search a shop is actually used through in the middle taking every
 * pixel the other two do not need, and the two destinations plus the basket on
 * the right. It reads as a shop because it is laid out like one.
 *
 * It is taller than it was, carries a hairline along its base and a real
 * shadow once content passes under it, so it sits in front of the page rather
 * than floating in the same plane as everything else. */
.site-header__inner {
  min-block-size: var(--header-height);
  display: flex;
  align-items: center;
  gap: var(--space-24);
}

/* Identity: the mark the application and the favicon already use, and the name
   beside it. A wordmark that is only text has no anchor, which is most of why
   this bar read as a document rather than a storefront. */
.wordmark {
  display: inline-flex;
  align-items: center;
  min-block-size: 44px;
  gap: var(--space-12);
  text-decoration: none;
  color: var(--colour-text);
  font-family: var(--font-display);
  /* Shrinkable, and allowed to go below its content width. The business name is
     a value the Operator types, so the header has to hold a name of any length
     rather than one that happens to fit; without this a long one pushes the
     basket off the row and the bar grows a third line. */
  flex: 0 1 auto;
  min-inline-size: 0;
}
.wordmark.wordmark:hover { color: var(--colour-ink); }

/* The Operator's own mark, the same file the favicon uses and the same artwork
   the application bundles. It was a charcoal tile with an R typed into it and
   an accent rule milled along the base, drawn in CSS because there was no logo
   to draw. There is one now.

   No ground, no radius and no milled edge. The artwork carries its own shape,
   it is transparent, and it reads on white and on charcoal alike, which is
   what lets the footer use the same asset the header does rather than a second
   inverted mark. */
.wordmark__mark {
  display: block;
  inline-size: 40px;
  block-size: 40px;
  flex: 0 0 auto;
  object-fit: contain;
}
.wordmark__lines { display: flex; flex-direction: column; gap: 1px; min-inline-size: 0; }
/* The business name, set in capitals.
 *
 * Done with text-transform rather than by typing it in capitals, for the same
 * reason the navigation is: a screen reader announcing "R O L I N A" letter by
 * letter is what typed capitals produce in several of them, while the
 * accessible name stays as the Operator wrote it and only the drawn glyphs
 * change. It is also a value the Operator sets, and they should be able to type
 * it in ordinary case.
 *
 * The tracking flips positive. Capitals set at a negative letter-spacing crowd
 * into one another, because the tightening was chosen for lower-case shapes
 * with ascenders and descenders to separate them, and capitals have neither. */
.wordmark__name {
  font-size: var(--type-md);
  font-weight: var(--weight-bold);
  text-transform: uppercase;
  letter-spacing: 0.03em;
  line-height: var(--leading-tight);
  /* The last resort, and for Rolina's own name it never fires. It exists so
     that a longer name shortens rather than breaking the bar. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.wordmark__suffix {
  font-size: var(--type-xs);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--colour-primary-text);
}

/* Search, in the bar, because that is how a shop is used.
 *
 * It submits as a plain GET form to the shop, so it works with JavaScript off
 * and a result is a real URL somebody can send to somebody else. */
.header-search {
  display: flex;
  flex: 1 1 auto;
  max-inline-size: 520px;
  min-inline-size: 0;
  margin-inline: auto;
  background: var(--colour-inset);
  border-radius: var(--radius-sm);
  box-shadow: inset 0 -2px 0 var(--edge-on-alt);
  overflow: hidden;
}
.header-search__field {
  flex: 1 1 auto;
  min-inline-size: 0;
  min-block-size: 44px;
  padding-inline: var(--space-16);
  border: 0;
  background: transparent;
  font: inherit;
  font-size: var(--type-sm);
  color: var(--colour-text);
}
.header-search__field::placeholder { color: var(--colour-text-faint); }
.header-search__field:focus { outline: none; }
.header-search:focus-within { box-shadow: inset 0 -2px 0 var(--colour-primary); }
.header-search__submit {
  flex: 0 0 auto;
  min-block-size: 44px;
  padding-inline: var(--space-16);
  border: 0;
  background: transparent;
  color: var(--colour-primary-text);
  font: inherit;
  font-size: var(--type-sm);
  font-weight: var(--weight-semibold);
  cursor: pointer;
}
.header-search__submit:hover { background: var(--colour-surface-sunk); }

.site-nav { display: flex; align-items: center; gap: var(--space-4); flex: 0 0 auto; }

/* The destinations and the basket are set in capitals.
 *
 * Done with text-transform rather than by typing them in capitals, for a
 * reason that matters: a screen reader announcing "S H O P" letter by letter is
 * what typed capitals produce in several of them, while the accessible name
 * here stays "Shop" and only the drawn glyphs change. Capitals also need
 * letter-spacing or they set too tight to read, which is the tracking below.
 *
 * It is scoped to the navigation deliberately. The search box keeps its own
 * wording exactly as written, because that is text a customer reads and types
 * into, not a label on a control. */
.site-nav-link,
.cart-button__text {
  text-transform: uppercase;
  letter-spacing: 0.07em;
  font-weight: var(--weight-semibold);
}

.site-nav-link {
  display: inline-flex;
  align-items: center;
  min-block-size: 44px;
  padding-inline: var(--space-12);
  /* "Track order" is one destination, not two words that may fall apart. Left
     to wrap it split across two lines on a narrow phone and read as two
     separate nav items stacked on top of each other. */
  white-space: nowrap;
  color: var(--colour-text-muted);
  text-decoration: none;
  font-size: var(--type-sm);
  border: 0;
  border-radius: var(--radius-sm);
  transition: background-color var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease);
}
.site-nav-link:hover { color: var(--colour-text); background: var(--colour-inset); }
.site-nav [aria-current='page'] {
  background: var(--colour-inset);
  color: var(--colour-text);
  font-weight: var(--weight-semibold);
}

/* The basket, and it is meant to have weight.
 *
 * It was a small filled pill in the corner, the same height as a nav link and
 * barely wider, which on an e-commerce site understates the one control the
 * whole page exists to feed. It is taller than the links beside it now, carries
 * its count in a real badge rather than as trailing text, and is the single
 * filled accent surface anywhere on the bar so nothing competes with it.
 *
 * Still not a capsule. The radius is the system's own, and the pill retirement
 * stands (Law VII-7). */
.cart-button {
  display: inline-flex;
  align-items: center;
  gap: var(--space-8);
  min-block-size: 48px;
  margin-inline-start: var(--space-8);
  padding-inline: var(--space-16);
  border-radius: var(--radius-sm);
  box-shadow: inset 0 -3px 0 var(--edge-on-primary);
  /* The deep tone carries white at 5.3:1; the anchor hue does not, and is not
     weakened to pretend otherwise. */
  background: var(--colour-primary-deep);
  color: var(--colour-on-primary);
  text-decoration: none;
  font-size: var(--type-sm);
  font-weight: var(--weight-semibold);
  white-space: nowrap;
  transition: background-color var(--motion-fast) var(--ease);
}
.cart-button:hover { color: var(--colour-on-primary); background: var(--colour-primary-hover); }
.cart-button__text { letter-spacing: 0.01em; }
/* On a phone the word "Basket" is not drawn, and without a mark the control was
   a green square with a nought in it. The glyph is what says basket when the
   word cannot be spared. It is authored into the markup rather than built by
   script, because a control that only means something once JavaScript has run
   is a control that means nothing to begin with. */
.cart-button__glyph { display: block; flex: 0 0 auto; }

/* The count as a badge on the button, not a number trailing the word. It keeps
   its own ground so a two-digit basket does not shift the label. */
.cart-button__count {
  display: inline-grid;
  place-items: center;
  min-inline-size: 24px;
  block-size: 24px;
  padding-inline: 6px;
  border-radius: var(--radius-xs);
  background: var(--veil-on-primary);
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--type-xs);
  line-height: 1;
}

/* What is actually in the basket, not only how many lines are in it: a
 * count says "three things" and leaves whether that is worth stopping for
 * to a page not yet open. The remembered subtotal answers that here, on the
 * button itself. Display only, the same figure the basket page already
 * shows, and never the figure create_order is asked to charge (cart.js). */
.cart-button__value {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--type-xs);
  color: var(--colour-on-primary);
  opacity: 0.85;
}

/* ===========================================================================
   The category bar
   =========================================================================== */

/* Twenty-four products behind six categories, and until now those categories
 * existed on one page out of eight, inside a sidebar, with three of them also
 * listed at the bottom of the footer. A customer who wants beans does not go
 * looking in a footer.
 *
 * So the categories sit directly under the command bar on every page, drawn
 * from the live taxonomy rather than typed into eight files. It is a second
 * register of navigation and it is deliberately quieter than the first: the
 * bar above says where you can go, this says what there is to buy, and it is
 * set in sentence case rather than capitals so the two do not compete.
 *
 * It is one row that scrolls sideways when it will not fit, at every width, so
 * a seventh category never wraps the bar onto two lines or pushes the page
 * about. */
.category-bar {
  position: sticky;
  /* Measured, not assumed. The bar above wraps to two rows on a phone, so a
     fixed 72px offset would leave this sliding underneath it. main.js keeps
     --header-actual truthful; the token is the fallback before it does. */
  inset-block-start: var(--header-actual, var(--header-height));
  z-index: var(--z-sticky);
  background: var(--colour-surface);
  box-shadow: 0 1px 0 var(--colour-border);
}

.category-bar__strip {
  display: flex;
  align-items: stretch;
  gap: var(--space-4);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  /* The gutter belongs to the strip, not the container, so the first and last
     links can scroll past the edge instead of stopping short of it. */
  margin-inline: calc(var(--container-pad) * -1);
  padding-inline: var(--container-pad);
}
.category-bar__strip::-webkit-scrollbar { display: none; }

.category-bar__link {
  display: inline-flex;
  align-items: center;
  flex: 0 0 auto;
  min-block-size: 44px;
  padding-inline: var(--space-12);
  color: var(--colour-text-muted);
  text-decoration: none;
  font-size: var(--type-sm);
  font-weight: var(--weight-medium);
  white-space: nowrap;
  /* The selected one is marked by a milled edge along the base rather than by
     a filled surface, so the bar stays a bar and does not sprout a button in
     the middle of it. Transparent on the rest keeps every link the same height,
     which a border on one of them would not. */
  box-shadow: inset 0 -3px 0 transparent;
  transition: color var(--motion-fast) var(--ease),
              box-shadow var(--motion-fast) var(--ease);
}
.category-bar__link:hover {
  color: var(--colour-text);
  box-shadow: inset 0 -3px 0 var(--colour-border-strong);
}
.category-bar__link[aria-current='page'] {
  color: var(--colour-text);
  font-weight: var(--weight-semibold);
  box-shadow: inset 0 -3px 0 var(--colour-primary);
}
.category-bar__count {
  margin-inline-start: var(--space-8);
  font-family: var(--font-mono);
  font-size: var(--type-xs);
  color: var(--colour-text-faint);
}

/* ===========================================================================
   Buttons
   =========================================================================== */

/* Two forms exist here and only two.
 *
 * A FILLED button is a physical thing: a solid surface, weight in the label, a
 * small radius, and one darker edge milled along its lower boundary so it reads
 * as having thickness rather than being drawn on. Pressing it moves that edge to
 * the top, the way a real key behaves when the light stays put and the surface
 * goes down. That is the entire animation.
 *
 * The radius is small on purpose. It is not a capsule and must never become
 * one: a half-height radius reads playful and consumer-grade, and that form was
 * retired from this system deliberately. Six pixels is felt, not seen.
 *
 * A TEXT button has no container at all, colour and weight carry it.
 *
 * There is deliberately no outlined button in this system, and .btn--secondary
 * is a pale filled surface rather than the bordered rectangle it used to be. A
 * thin rectangle drawn around a word is neither substance nor restraint, and
 * repeated once per row down a list it is the single thing that made this
 * interface read as unfinished.
 *
 * Within the filled form there are three ranks, and the rank is the colour:
 *   --primary  teal-green  the one committing action on a screen
 *   --ink      charcoal    a repeated action, such as Add on every shelf row
 *   --secondary pale       a neutral alternative standing beside either
 * Twelve accent buttons down a list is a wall of one colour that also competes
 * with the prices for the same attention. A charcoal column reads as structure.
 */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-8);
  min-block-size: 44px;
  min-inline-size: 44px;
  padding-inline: var(--space-24);
  border: 0;
  border-radius: var(--radius-sm);
  font-family: var(--font-body);
  font-size: var(--type-base);
  font-weight: var(--weight-semibold);
  letter-spacing: 0.01em;
  line-height: 1;
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--motion-fast) var(--ease),
              box-shadow var(--motion-instant) var(--ease),
              color var(--motion-fast) var(--ease);
}

/* A disabled control is stated, not faded. Half-opacity accent on white is a
   washed mint that reads as a rendering fault rather than a decision. */
.btn:disabled, .btn[aria-disabled='true'] {
  background: var(--colour-inset);
  color: var(--colour-text-faint);
  box-shadow: none;
  cursor: not-allowed;
}

.btn--primary {
  background: var(--colour-primary-deep);
  color: var(--colour-on-primary);
  box-shadow: inset 0 -2px 0 var(--edge-on-primary);
}
.btn--primary:hover:not(:disabled) { background: var(--colour-primary-hover); color: var(--colour-on-primary); }
.btn--primary:active:not(:disabled) { box-shadow: inset 0 2px 0 var(--edge-on-primary); }

.btn--ink {
  background: var(--colour-ink);
  color: var(--colour-on-deep);
  box-shadow: inset 0 -2px 0 var(--edge-on-ink);
}
.btn--ink:hover:not(:disabled) { background: var(--colour-ink-mid); color: var(--colour-on-deep); }
.btn--ink:active:not(:disabled) { box-shadow: inset 0 2px 0 var(--edge-on-ink); }

.btn--secondary {
  background: var(--colour-inset);
  color: var(--colour-text);
  box-shadow: inset 0 -2px 0 var(--edge-on-alt);
}
.btn--secondary:hover:not(:disabled) { background: var(--colour-surface-sunk); color: var(--colour-text); }
.btn--secondary:active:not(:disabled) { box-shadow: inset 0 2px 0 var(--edge-on-alt); }

/* The text form. No surface, no edge, no container. */
.btn--ghost {
  background: transparent;
  color: var(--colour-text-muted);
  padding-inline: var(--space-12);
  font-weight: var(--weight-semibold);
  box-shadow: none;
}
.btn--ghost:hover:not(:disabled) { color: var(--colour-primary-text); background: transparent; }
.btn--ghost:disabled, .btn--ghost[aria-disabled='true'] { background: transparent; }

.btn--block { inline-size: 100%; }
.btn--sm { min-block-size: 44px; padding-inline: var(--space-16); font-size: var(--type-sm); }

/* ===========================================================================
   Hero
   =========================================================================== */

/* The hero is held by proportion and by air: a headline set large enough to be
 * the only thing on the page competing for attention, and a measured column
 * beneath it. Nothing is tinted, washed or shaded to make it feel designed.
 *
 * It is white, running the full width, and the page's ground picks up below it.
 * The two rules it used to carry, the accent above and a hairline below, are both
 * gone: the surface change already says where the hero ends, and a saturated
 * band across the top of every visit is a frame rather than an accent. */
/* The hero is the white paper the page opens on, and it has to END somewhere.
 *
 * It shared the header's white with nothing between them and then dissolved
 * into the ground below, so the top third of the site was one undifferentiated
 * field with no rank in it. It now closes with a hairline, the same way the bar
 * above it does, so header, hero and the shelves below are three surfaces
 * rather than one long blur. */
.hero {
  position: relative;
  background: var(--colour-surface);
  border: 0;
  border-block-end: var(--rule-hair, 1px) solid var(--colour-border);
  padding-block: var(--space-48);
}
/* Asymmetric by intent. An even split would read as a template.
 *
 * The copy is the narrower column now and the goods are the wider one, which
 * is the right rank for a shop: a sentence about grain is worth less of the
 * first screen than the grain. Aligned to the top rather than centred, because
 * two columns of different natural heights centred against one another is most
 * of what made this read as arbitrary. */
/* Centred against the shelf, not hung from the top of it.
   Two columns of unequal height aligned at the top put every pixel of the
   difference in one place: a hundred and fifty pixels of nothing under the
   copy, which is precisely the empty area this hero was criticised for.
   Centred, the difference is halved and sits above and below, where it reads
   as margin rather than as a hole. Below 900px the hero is one column and this
   has no effect at all. */
.hero__grid {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: var(--space-48);
  align-items: center;
}
.hero h1 {
  font-size: var(--type-3xl);
  font-weight: var(--weight-bold);
  letter-spacing: -0.03em;
  line-height: 1.06;
  margin-block-start: var(--space-12);
  text-wrap: balance;
}
.hero__lead {
  margin-block-start: var(--space-16);
  font-size: var(--type-md);
  line-height: var(--leading-normal);
  color: var(--colour-text-muted);
  max-inline-size: 42ch;
}
.hero__actions {
  margin-block-start: var(--space-24);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-12);
}

/* The two live figures, as a line rather than as a panel. They are the only
 * checkable claim on this page, so they are said plainly and once. The figure
 * takes the accent and the mono face, which is what every other number in this
 * interface does. */
.hero__trust {
  margin-block-start: var(--space-24);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4) var(--space-24);
  font-size: var(--type-sm);
  color: var(--colour-text-muted);
}
.hero__trust-item strong {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-weight: var(--weight-semibold);
  color: var(--colour-primary-text);
}

/* The shop, inside the hero. Its heading is small because the goods beneath it
 * are doing the announcing, and a second large heading beside the h1 would be
 * two things talking at once. */
.hero__shelf-title {
  font-size: var(--type-xs);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--colour-text-faint);
  margin: 0 0 var(--space-12);
}
/* Four across, not three. Centring the grid against the copy (above) halves a
 * height mismatch rather than removing it, and three columns of eight real
 * categories is three rows with the last one only two-thirds full, tall
 * enough on its own to leave a real gap above the eyebrow line even
 * centred. Four columns is two full rows, shorter by a third, closer to the
 * copy's own height and with no dangling empty cell. */
.category-grid.category-grid--hero {
  margin-block-start: 0;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--space-12);
}

/* The panel that stood here held two numerals at display size inside a tinted
   box. It was 252px of an 844px phone screen, it was most of the reason the
   first thing a customer could buy sat a screen and a third down, and it was
   decoration added because a screen felt empty, which the visual system forbids
   by name. The figures survive as .hero__trust, which is a sentence. */

/* ===========================================================================
   Shop
   =========================================================================== */

/* There is no filter rail here any more, and no second search box.
 *
 * The shop used to carry a sidebar holding a search field and a category list,
 * with a horizontal category strip standing in for it on a phone. That put two
 * search boxes on one screen, doing almost the same job in two different ways:
 * the one in the bar reloaded the page, the one in the sidebar filtered in
 * place, and nothing told a customer which was which. It also put the shop's
 * categories somewhere they existed on exactly one page of eight.
 *
 * Both now live in the shell. The bar's search filters this page live instead
 * of reloading it, and the category bar under the header is the category
 * navigation for the whole site, this page included. One search, one category
 * control, present everywhere rather than here alone. */

.shop-count {
  padding-block: var(--space-24) var(--space-16);
  font-size: var(--type-sm);
  color: var(--colour-text-faint);
}

/* A price list, not a gallery./* A price list, not a gallery. Each product is its own surface, because a
 * product IS a separable object, and the ground between them is what separates
 * them: no rule above the list, no rule between the rows.
 *
 * They do not float. There is no lift, no hover shadow and no scale: the shadow
 * token here is a whisper that stops a white edge on a near-white ground reading
 * as cut out. A grid of equal padded tiles is still rejected (Doctrine 12); this
 * is a single column of rows that happen to have a surface. */
/* The shelf is one separable object, so it takes one surface, and the rows
 * inside it take none. That is the container rule read correctly: the failure
 * being corrected was twenty-four identical boxes, not the existence of a
 * surface. Removing the boxes and leaving the rows on the page's own grey put
 * the whole shop on the ground tone, and white is meant to be the dominant
 * surface of every screen.
 *
 * One white field, hairlines within it, and the page's grey left to do what it
 * is for: separating one section from the next. */
.product-list {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0;
  background: var(--colour-surface);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-xs);
  padding-inline: var(--space-16);
  /* Holds the shelf's space while it loads. Without this the footer starts
     near the top and is shoved down as rows arrive, which reads as the page
     jumping about. */
  min-block-size: 70vh;
}
.product-list:not(:empty) { min-block-size: 0; }
.product-list:empty { background: none; box-shadow: none; }

/* A shelf is a repeating rhythm, and the visual system is explicit about what
 * that gets: no container, a hairline between items, and hierarchy carried by
 * type weight, size and spacing. Every row here wore the same tinted card with
 * the same shadow and the same radius, twenty-four of them down the page, which
 * is the generic card grid Law VII-7 forbids by name and the single thing that
 * made this shelf read as a template.
 *
 * The rows sit on the page's own ground now, separated by one hairline, and the
 * white surface is kept for the things that genuinely are separable objects. */
.product-row {
  display: grid;
  grid-template-columns: 64px minmax(0, 1fr) auto auto;
  gap: var(--space-16);
  align-items: center;
  padding: var(--space-16) 0;
  background: none;
  border: 0;
  border-block-start: var(--rule-hair, 1px) solid var(--colour-border);
  border-radius: 0;
  box-shadow: none;
  transition: background-color var(--motion-fast) var(--ease);
}
/* The first row needs no rule above it: the heading already ended the section. */
.product-row:first-child { border-block-start: 0; }
.product-row:hover { background: var(--colour-surface-sunk); }

/* Law IV-7: where no real photograph exists, a brand panel carries the
 * product's initials over the brand's own colour. Never a broken image, never
 * a stock photo, never a generic placeholder graphic. */
/* Standing in for a photograph that is not on file yet (Law IV-7). It has to
 * look like a decision rather than a missing asset, and a grid of black squares
 * looks like neither.
 *
 * It previously used the palest surface tone with a hairline border, which
 * against a white row is very nearly white on white: all a customer actually
 * saw was a short red line under some letters, which reads as a rendering
 * fault. It now carries the sunk tone and no border, the fill is what draws
 * the square, the way every other surface in this system works, with charcoal
 * initials and an accent anchor rule. It will still not fight the photographs
 * when they arrive. */
.brand-panel {
  display: grid;
  place-items: center;
  aspect-ratio: 1;
  background: var(--colour-surface-sunk);
  color: var(--colour-ink);
  font-family: var(--font-display);
  font-weight: var(--weight-bold);
  font-size: var(--type-md);
  letter-spacing: 0.06em;
  border: 0;
  border-radius: var(--radius-sm);
  /* The accent is carried as a milled edge rather than as a rule ruled across
     the bottom, which is how every other chosen or marked thing in this system
     is signed.
     The fill stays light. It was briefly charcoal, which reads well at the size
     of a row thumbnail and turns a four-up featured shelf into a band of large
     dark blocks, and this system puts no dark band above the fold. */
  box-shadow: inset 0 -3px 0 var(--colour-primary);
}
.product-row .brand-panel, .product-row img { border-radius: var(--radius-sm); }
/* A wrapper around the photograph and nothing else: it carries the destination
   and takes no space of its own, at any width. */
.product-row__figure { display: block; flex: 0 0 auto; min-inline-size: 0; }

.product-detail__price-row {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-12);
}
.product-detail__stock { margin-block-start: var(--space-16); }
.product-detail__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-12);
  margin-block-start: var(--space-32);
}
.promo-tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-8);
  margin-block-start: var(--space-12);
}

.product-row__name {
  /* Inline block with a little vertical padding, because this is the largest
     and most-used target on the shelf and it was twenty-two pixels tall,
     under WCAG 2.5.8's twenty-four. */
  display: inline-block;
  padding-block: var(--space-4);
  font-weight: var(--weight-semibold);
  color: var(--colour-text);
  text-decoration: none;
  font-size: var(--type-md);
}
.product-row__name:hover { color: var(--colour-primary-text); }
.product-row__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-8);
  margin-block-start: var(--space-4);
  font-size: var(--type-sm);
  color: var(--colour-text-muted);
}

/* The grid cell itself: alignment and wrapping. .product-row__price is the
   typography, applied to the price text one level in, so the was/now pair
   priceAndPromo can return still lands in the same cell. */
.product-row__price-wrap { text-align: end; white-space: nowrap; }
.product-row__price {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--type-md);
  font-weight: var(--weight-medium);
}
.product-row__unit {
  display: block;
  font-family: var(--font-body);
  font-size: var(--type-xs);
  color: var(--colour-text-faint);
  font-weight: var(--weight-regular);
}

.product-row__action { display: flex; align-items: center; gap: var(--space-8); }

/* Low stock is the system's warning amber, and this is the one line that
   proves the amber was worth having.

   Under scarlet it could not be: scarlet was the accent, so a scarlet warning
   competed with every "Add" button on the same shelf for the same attention
   and a customer could not tell "buy this" from "hurry". The workaround was to
   set it in charcoal, which is honest but says nothing, it reads as one more
   line of specification. Now the accent is green and the warning is amber, the
   two cannot be confused, and this can finally look like what it is. Still
   carrying weight as well as colour, so it survives a colour-blind reader. */
.stock-note { font-size: var(--type-xs); display: inline-flex; align-items: center; gap: 5px; }
.stock-note--low {
  color: var(--colour-warn-text);
  font-weight: var(--weight-semibold);
}
.stock-note--out { color: var(--colour-text-faint); }

/* A positive confirmation: something was added, something succeeded.

   This used to carry its own forest green, a fourth hue held apart from a
   scarlet accent. With the accent itself now green, a second green a few
   degrees away would read as a mistake rather than a distinction, so the
   confirmation is simply set in the accent. That is also correct on its own
   terms: the accent marks the action, and this is that action reporting back.
   Empty until the customer actually adds something, so no page states a
   success that has not happened yet. */
.added-note {
  display: none;
  align-items: center;
  gap: 8px;
  font-size: var(--type-sm);
  font-weight: var(--weight-semibold);
  color: var(--colour-primary-text);
}
.added-note--shown { display: inline-flex; }

/* ===========================================================================
   Quantity stepper
   =========================================================================== */

/* One of the two or three places in this system where a contained surface is
   genuinely earned: minus, a number and plus are one instrument, and they have
   to read as joined rather than as three loose controls that happen to be
   adjacent. So it is filled and milled like a button, but as a single object,
   with the count sitting in a white well between the two keys, rather than
   three outlined rectangles in a row. */
.qty {
  display: inline-flex;
  align-items: stretch;
  background: var(--colour-inset);
  border-radius: var(--radius-sm);
  box-shadow: inset 0 -2px 0 var(--edge-on-alt);
  overflow: hidden;
}
.qty button {
  inline-size: 44px;
  min-block-size: 44px;
  background: transparent;
  border: 0;
  color: var(--colour-text);
  font-size: var(--type-md);
  font-weight: var(--weight-semibold);
  cursor: pointer;
  transition: background-color var(--motion-fast) var(--ease);
}
.qty button:hover:not(:disabled) { background: var(--colour-surface-sunk); }
.qty button:disabled { color: var(--colour-text-faint); cursor: not-allowed; }
.qty input {
  inline-size: 52px;
  min-block-size: 44px;
  border: 0;
  /* Transparent, so the three parts read as one instrument rather than as a
     white slot cut between two keys. */
  background: transparent;
  color: var(--colour-text);
  font-weight: var(--weight-semibold);
  text-align: center;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--type-base);
  -moz-appearance: textfield;
}
/* The fifth lost comma, and the one that was actually visible: Chromium draws
   its own up and down arrows inside a number input, and this rule was supposed
   to remove them so the quantity control shows only its own minus and plus.
   Compounded like that it selected nothing, so every quantity field on the shop
   and in the basket carried a second, smaller pair of arrows the design never
   asked for. Found by the check written an hour ago for the other four. */
.qty input::-webkit-outer-spin-button,
.qty input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

/* ===========================================================================
   Forms
   =========================================================================== */

.field { display: block; }
.field + .field { margin-block-start: var(--space-24); }
/* The first field under a heading needs the same air the ones under it get from
   each other. This was a style attribute on two of the four, so the checkout
   was very slightly uneven and nothing said why. */
.field:first-of-type { margin-block-start: var(--space-24); }

/* One section of a form to the next. Three of these were spaced by hand in the
   markup, in two different amounts. */
form > section + section { margin-block-start: var(--space-48); }
.form-action { margin-block-start: var(--space-32); }

.field__label {
  display: block;
  font-size: var(--type-sm);
  font-weight: var(--weight-semibold);
  margin-block-end: var(--space-8);
}
.field__hint {
  display: block;
  font-size: var(--type-xs);
  color: var(--colour-text-muted);
  margin-block-end: var(--space-8);
  font-weight: var(--weight-regular);
}

/* A field is filled, not outlined. The tinted surface is what says "you may
   type here", and it is the one place in this system that still carries a rule:
   a single line along the base, because a filled field on a white card cannot
   reach the 3:1 boundary contrast WCAG 1.4.11 asks for on fill alone. One line
   under a field is the oldest input convention there is, and it is not a box. */
/* The fill is WHITE, not the inset tone.
 *
 * An inset block sitting directly on the page ground is two neutrals about one
 * per cent apart, which is not a boundary, it is a smudge: the search field on
 * the shop all but disappeared. Anything placed straight onto the ground takes
 * the white surface. The inset tone is for blocks inside a card, where white is
 * the thing it has to stand apart from, and the two rules are the same rule
 * read from either side. */
.input, .select, .textarea {
  inline-size: 100%;
  min-block-size: 44px;
  padding: var(--space-12);
  background-color: var(--colour-surface);
  color: var(--colour-text);
  border: 0;
  border-block-end: var(--rule-firm) solid var(--colour-border-strong);
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
  font-family: var(--font-body);
  font-size: var(--type-base);
  transition: background-color var(--motion-fast) var(--ease),
              border-color var(--motion-fast) var(--ease);
}
/* Inside a card the relationship inverts: the card is white, so the field is
   the quieter tone. */
.panel .input, .panel .select, .panel .textarea,
.summary .input, .summary .select, .summary .textarea,
.notice .input, .notice .select, .notice .textarea {
  background-color: var(--colour-inset);
}
.input:hover, .select:hover, .textarea:hover {
  background-color: var(--colour-inset);
  border-block-end-color: var(--colour-text-faint);
}
.input:focus, .select:focus, .textarea:focus {
  border-block-end-color: var(--colour-primary);
  background-color: var(--colour-inset);
}
.textarea { min-block-size: 96px; resize: vertical; }
.select {
  appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: calc(100% - 20px) calc(50% + 2px), calc(100% - 14px) calc(50% + 2px);
  background-size: 6px 6px, 6px 6px;
  background-repeat: no-repeat;
  padding-inline-end: var(--space-48);
}

/* An invalid field is marked on two edges, not one, so it is distinguishable
   from a merely focused field at a glance and without relying on colour
   alone (WCAG 1.4.1). */
.input[aria-invalid='true'],
.select[aria-invalid='true'],
.textarea[aria-invalid='true'] {
  border-block-end-color: var(--colour-warn);
  border-inline-start: var(--rule-anchor) solid var(--colour-warn);
  padding-inline-start: var(--space-8);
  background-color: var(--colour-warn-soft);
}

.field__error {
  display: block;
  margin-block-start: var(--space-8);
  font-size: var(--type-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-warn-text);
}
.field__error:empty { display: none; }

/* Tender choice. A list of choices, so it follows the same rule the shelf and
   the filter strip follow: only the one that is actually chosen gets a surface.
   Three payment methods each in an identical outlined rectangle states nothing
   about which is selected. Unselected is a quiet inset block; selected takes
   the white surface and a milled accent edge, so the choice is legible by
   fill, by edge and by the radio itself, never by colour alone (WCAG 1.4.1).
   Matches _tenderTile in the application. */
.tender-list {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
  margin-block-start: var(--space-24);
}
/* The Till number, when the Operator has recorded one. */
.checkout-till { margin-block-start: var(--space-24); }
.tender {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--space-12);
  align-items: start;
  padding: var(--space-16);
  background: var(--colour-inset);
  border: 0;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: background-color var(--motion-fast) var(--ease),
              box-shadow var(--motion-fast) var(--ease);
}
.tender:hover { background: var(--colour-surface-sunk); }
.tender:has(input:checked) {
  background: var(--colour-surface);
  box-shadow: inset 0 -3px 0 var(--colour-primary), var(--shadow-xs);
}
.tender input { inline-size: 20px; block-size: 20px; margin-block-start: var(--space-2); accent-color: var(--colour-primary-deep); }
/* Both of these are spans, and a span is inline, so the margin below did
 * nothing at all and the three of them ran together on one line: a customer
 * choosing how to pay was reading "Lipa na M-PESAPay now" and "before
 * dispatch.Balance due within 14 days". On the one page where money moves.
 *
 * They are made blocks here rather than in the markup because the markup is
 * right: a label and its description are phrases, and a span is what a phrase
 * is. What was wrong was asking an inline box to take a block margin. */
.tender__label { display: block; font-weight: var(--weight-semibold); }
.tender__desc {
  display: block;
  font-size: var(--type-sm);
  color: var(--colour-text-muted);
  margin-block-start: var(--space-4);
}

/* ===========================================================================
   Tables (cart, order lines)
   =========================================================================== */

.table-scroll { overflow-x: auto; }

.line-table {
  inline-size: 100%;
  border-collapse: collapse;
  font-size: var(--type-base);
}
.line-table th {
  text-align: start;
  font-size: var(--type-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--colour-text-faint);
  font-weight: var(--weight-semibold);
  padding-block: var(--space-12);
  border-block-end: 1px solid var(--colour-border-strong);
  white-space: nowrap;
}
/* Rows are separated by their own height, not by a line each. The rule across
   the head stays, because a column of figures genuinely needs a head. */
.line-table td {
  padding-block: var(--space-16);
  vertical-align: middle;
}
/* A gutter between columns, which they did not have. PRICE is ranged right and
   QUANTITY is ranged left, so at the width the basket actually gets the two
   headings met in the middle and read as one word. */
.line-table th,
.line-table td { padding-inline-end: var(--space-24); }
.line-table th:last-child,
.line-table td:last-child { padding-inline-end: 0; }
.line-table tbody tr + tr td { padding-block-start: 0; }
.line-table .num { text-align: end; font-family: var(--font-mono); font-variant-numeric: tabular-nums; white-space: nowrap; }

/* A basket is five columns wide, and a phone is not.
 *
 * It was being handed to a sideways scroller, which meant the Remove control
 * sat off the right-hand edge of a card with nothing to say it was there. A
 * customer who wants a line gone has to discover a hidden scroll inside a box
 * to find the button that does it. The Price and Quantity headings were also
 * touching each other, because five columns of a fixed minimum width in 390
 * pixels leaves nothing between them.
 *
 * Below 620px each row becomes its own card and each cell becomes a labelled
 * line inside it. The table stays a real table, with a real head, so a screen
 * reader still announces "Price, KSh 480"; the head is simply not drawn, and
 * the label is drawn from the cell's own data-label instead. Nothing scrolls
 * sideways and nothing is off screen. */
@media (max-width: 620px) {
  .table-scroll { overflow-x: visible; }

  .line-table thead {
    position: absolute;
    inline-size: 1px; block-size: 1px;
    margin: -1px; padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  .line-table,
  .line-table tbody,
  .line-table tr,
  .line-table td { display: block; inline-size: 100%; }

  /* A basket line is a repeating rhythm, and the visual system names it in
     exactly those words alongside product rows and payment options: no
     container, a hairline between items. Each line wore its own card with its
     own shadow, so three items read as three floating objects rather than as
     one basket, and every line cost 32px of padding to say so.

     One surface holds the basket, given below on .line-table itself, and the
     lines inside it are separated by a rule. */
  .line-table {
    background: var(--colour-surface);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-xs);
    padding-inline: var(--space-16);
  }
  .line-table tr { padding-block: var(--space-16); }
  .line-table tr + tr { border-block-start: var(--rule-hair, 1px) solid var(--colour-border); }

  .line-table td {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-16);
    padding-block: var(--space-8);
  }
  .line-table tbody tr + tr td { padding-block-start: var(--space-8); }
  .line-table td:first-child { padding-block-start: 0; }

  /* Remove has no column heading to sit opposite, so space-between would
     leave it stranded at the left of the card while every value above it is
     ranged right. It goes where the values are. */
  .line-table__actions { justify-content: flex-end; }

  /* The column's own heading, put back beside its value now that the head
     above is not drawn. */
  .line-table td[data-label]::before {
    content: attr(data-label);
    font-size: var(--type-xs);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    font-weight: var(--weight-semibold);
    color: var(--colour-text-faint);
  }
}

/* ===========================================================================
   Summary panel
   =========================================================================== */

/* A genuine card, and one of the few. The total is a separable object, it does
   not belong to the rhythm of the lines above it, it is the conclusion drawn
   from them, so it gets a surface of its own. That surface is now white on the
   page's ground, which is separation enough; the charcoal rule it used to wear
   across the top was there only because the card and the page were the same
   colour. */
.summary {
  border: 0;
  border-radius: var(--radius-md);
  padding: var(--space-24);
  background: var(--colour-surface);
  box-shadow: var(--shadow-xs);
}
.summary h2 { font-size: var(--type-md); margin-block-end: var(--space-16); }
.summary__heading { margin-block-start: var(--space-24); }
.summary__note { margin-block-start: var(--space-12); }
/* The actions under a total. The first is separated from the note above it;
   the ones after it sit closer, because they are alternatives to each other
   rather than to what is above. */
.summary__action { margin-block-start: var(--space-16); }
.summary__action + .summary__action { margin-block-start: var(--space-8); }
.summary__action--quiet { color: var(--colour-text-faint); }
.summary__row {
  display: flex;
  justify-content: space-between;
  gap: var(--space-16);
  padding-block: var(--space-8);
  font-size: var(--type-base);
  color: var(--colour-text-muted);
}
.summary__row span:last-child {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--colour-text);
}
/* The conclusion of the column, marked by taking the inset ground and by
   weight, rather than by a rule ruled above it the way a paper ledger would. */
.summary__row--total {
  margin-block-start: var(--space-12);
  padding: var(--space-16);
  background: var(--colour-inset);
  border-radius: var(--radius-sm);
  font-size: var(--type-md);
  font-weight: var(--weight-semibold);
  color: var(--colour-text);
}
.summary__row--total span:last-child { font-size: var(--type-lg); font-weight: var(--weight-semibold); }

.checkout-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 360px;
  gap: var(--space-48);
  align-items: start;
  padding-block: var(--space-32) var(--space-64);
}
.checkout-grid__aside { position: sticky; inset-block-start: calc(var(--header-height) + var(--space-24)); }

.checkout-action {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-16);
  margin-block-start: var(--space-48);
  padding: var(--space-16);
  border: 0;
  border-radius: var(--radius-md);
  background: var(--colour-surface);
  box-shadow: var(--shadow-xs);
}
.checkout-action__total {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  font-size: var(--type-xs);
  color: var(--colour-text-muted);
}
.checkout-action__total strong {
  font-size: var(--type-xl);
  font-weight: var(--weight-semibold);
  color: var(--colour-text);
}

/* ===========================================================================
   Notices
   =========================================================================== */

/* Also a genuine card: a notice interrupts the page to say something happened,
   which is exactly the case a contained surface is for.
 *
 * An alert keeps ONE line, on its leading edge, and it is the third and last
 * rule in this system. It earns it: an alert has to be findable in a glance
 * down a page, and weight alone does not do that when the page is already full
 * of headings. A quiet notice, which is context rather than an interruption,
 * carries no line at all. */
.notice {
  border: 0;
  border-radius: var(--radius-md);
  background: var(--colour-surface);
  box-shadow: var(--shadow-xs);
  padding: var(--space-16) var(--space-24);
  font-size: var(--type-base);
}
/* A notice carries one line down its leading edge, and the colour of that line
   is the whole of what it says before a word is read.

   ALERT is something going wrong: amber, and the only tone that may use it.
   Under scarlet this edge was the same colour as the button beside it asking
   the customer to try again, which is the collision the amber exists to end.

   NOTE is something worth reading that is not a problem: an offer, a
   confirmation, a message from M-PESA. The accent.

   The distinction is not decoration. Both surfaces build offers, order
   confirmations and failures out of this one component, so a single default
   decides whether good news arrives wearing a warning. Matches NoticeTone in
   the application exactly. */
.notice--alert {
  border-inline-start: var(--rule-anchor) solid var(--colour-warn);
  padding-inline-start: calc(var(--space-24) - var(--rule-anchor));
}
.notice--note {
  border-inline-start: var(--rule-anchor) solid var(--colour-primary);
  padding-inline-start: calc(var(--space-24) - var(--rule-anchor));
}
/* Context rather than interruption, so it carries no leading rule. It keeps the
   white surface when it is standing on the page's ground, and drops to the
   inset tone only inside a card, for the reason given on the fields above. */
.notice--quiet { color: var(--colour-text-muted); }
.panel .notice--quiet,
.summary .notice--quiet,
.card .notice--quiet {
  background: var(--colour-inset);
  box-shadow: none;
}
.notice__title { font-weight: var(--weight-semibold); margin-block-end: var(--space-4); color: var(--colour-text); }
.notice .btn { margin-block-start: var(--space-16); }

.empty-state__action { margin-block-start: var(--space-24); }

.empty-state {
  padding-block: var(--space-64);
  text-align: center;
  color: var(--colour-text-muted);
}
.empty-state h2 { color: var(--colour-text); margin-block-end: var(--space-8); }

/* ===========================================================================
   Status
   =========================================================================== */

/* The facts of an order, on the charcoal surface. It is the one block on the
   page a customer comes back to find, so it is given the strongest value in the
   system rather than a rule to mark it out. */
.status-strip {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-32);
  padding: var(--space-24);
  background: var(--colour-surface-deep);
  color: var(--colour-on-deep);
  border: 0;
  border-radius: var(--radius-md);
}
.status-strip dt { color: var(--colour-on-deep-faint); }
.status-strip dd { color: var(--colour-on-deep); }
.status-strip dt {
  font-size: var(--type-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--colour-text-faint);
}
.status-strip dd {
  margin: var(--space-4) 0 0;
  font-size: var(--type-md);
  font-weight: var(--weight-semibold);
}

/* ===========================================================================
   Product detail
   =========================================================================== */

/* The photograph is the product. On a desk it was four columns of ten, which
   put a 461px picture of rice beside 700px of specification, and the
   specification is not what somebody came to look at. Six and six, so the
   picture is the equal of everything written about it. */
.product-detail {
  display: grid;
  grid-template-columns: minmax(0, 6fr) minmax(0, 6fr);
  gap: var(--space-48);
  align-items: start;
  padding-block: var(--space-24) var(--space-64);
}
.product-detail__price {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--type-3xl);
  font-weight: var(--weight-medium);
  margin-block: var(--space-16) var(--space-4);
}
/* A specification is a set of pairs, and pairs read as pairs when they are
   spaced. Alternating ground separates them at a glance without ruling twelve
   lines across the page. */
.spec-list {
  list-style: none;
  margin-block-start: var(--space-32);
  border-radius: var(--radius-sm);
  overflow: hidden;
}
.spec-list li {
  display: flex;
  justify-content: space-between;
  gap: var(--space-16);
  padding: var(--space-12) var(--space-16);
  font-size: var(--type-base);
}
.spec-list li:nth-child(odd) { background: var(--colour-inset); }
.spec-list dt, .spec-list .spec-key { color: var(--colour-text-muted); }
.spec-list .spec-val { font-weight: var(--weight-medium); }

/* ===========================================================================
   Steps strip
   =========================================================================== */

.steps {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-16);
  margin-block-start: var(--space-32);
}
/* The delivery promise, under the three steps on the charcoal band. */
.band__promise { margin-block-start: var(--space-32); }
/* Three cards, and the number is what marks each one rather than an accent rule
   ruled over the top of it. */
.steps li {
  background: var(--colour-surface);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-xs);
  padding: var(--space-24);
}
.steps__n {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: 28px;
  min-block-size: 28px;
  border-radius: var(--radius-xs);
  background: var(--colour-primary-deep);
  color: var(--colour-on-primary);
  font-family: var(--font-mono);
  font-size: var(--type-xs);
  font-weight: var(--weight-medium);
}
.steps h3 { margin-block: var(--space-8) var(--space-4); }
.steps p { color: var(--colour-text-muted); font-size: var(--type-sm); }
/* A second, Operator-set line under the second step: what "before you
   confirm, never after" is actually the basis for. Its own top margin, since
   the reset above puts every paragraph flush against the one before it. */
.steps__note { margin-block-start: var(--space-8); }

/* ===========================================================================
   Prose (legal pages)
   =========================================================================== */

.prose { max-inline-size: 68ch; }
.prose h2 { margin-block: var(--space-48) var(--space-12); font-size: var(--type-lg); }
.prose h3 { margin-block: var(--space-32) var(--space-8); }
/* Commas, not compounds. These read ".prose li inside a .prose p" and
   ".prose ol inside a .prose ul", neither of which exists on either legal page,
   so for as long as they have been written the privacy policy and the terms
   have had unstyled body text and, worse, lists with no leading indent at all:
   list-style is set below, so the markers were being drawn outside the content
   box. Third instance of this exact fault, after five button and field
   selectors and the two figure-box sizes. */
.prose p,
.prose li { color: var(--colour-text-muted); line-height: var(--leading-relaxed); }
.prose p + p { margin-block-start: var(--space-16); }
.prose ul,
.prose ol { margin-block-start: var(--space-12); padding-inline-start: var(--space-24); }
.prose li + li { margin-block-start: var(--space-8); }
.prose ul { list-style: disc; }
.prose ol { list-style: decimal; }

/* ===========================================================================
   Footer
   =========================================================================== */

.site-footer {
  /* No margin. This carried 96px of page ground above it, which is right when
     the section above is white but reads as a forgotten gap when it is the
     charcoal Ordering band, since the effect is a pale stripe between two dark
     blocks. The footer's own top padding is the separation. */
  margin-block-start: 0;
  background: var(--colour-surface-deep);
  color: var(--colour-on-deep);
  /* No bottom padding. The legal band below supplies its own, and it runs edge
     to edge rather than sitting inside this block's gutter. */
  padding-block: var(--space-64) 0;
}
.site-footer > .container { padding-block-end: var(--space-48); }
/* Every link in the footer's directories is a target, not a word in a sentence,
   and each was nineteen pixels tall. WCAG 2.5.8 puts the floor at 24; padding
   takes them to twenty-seven, which with the gap between rows gives a thumb
   something real to land on without making the footer twice as tall. Inline
   block rather than block, so the hit area is the width of the words rather
   than the width of the column. */
/* The contact column holds four things of equal standing: a phone number, an
   email address, a street address and opening hours. Two of them are links and
   two are not, and only the links were being softened, so the address and the
   hours read LOUDER than the phone number somebody came to the footer to find.
   The tone belongs to the list, not to the anchor. */
.site-footer li { color: var(--colour-on-deep-soft); }
.site-footer a {
  display: inline-block;
  padding-block: var(--space-4);
  color: inherit;
  text-decoration: none;
}
.site-footer a:hover { color: var(--colour-on-deep); text-decoration: underline; }
.site-footer li + li { margin-block-start: var(--space-4); }

/* Two real locations, each with its own name, address and number -- not one
 * address and a footnote about the other, which is what this used to be
 * before both branches were real rows a footer could list (20260827110000).
 * Rendered by main.js from public.branches; empty until that resolves. */
.footer-branches { margin-block-start: var(--space-16); }
.footer-branch + .footer-branch { margin-block-start: var(--space-16); }
.footer-branch__name {
  color: var(--colour-on-deep);
  font-weight: var(--weight-semibold);
  margin-block-end: var(--space-4);
}
.footer-branch__address {
  color: var(--colour-on-deep-soft);
  margin-block-end: var(--space-4);
}
.footer-branch__phone {
  display: inline-flex;
  align-items: center;
  min-block-size: 44px;
  color: var(--colour-on-deep-soft);
  text-decoration: none;
}
.footer-branch__phone:hover { color: var(--colour-on-deep); text-decoration: underline; }
/* Four columns, and the first one is the business rather than a column heading.
 *
 * The footer used to be three thin lists that read as an afterthought stuck to
 * the bottom of the page. A shop's footer is where somebody goes when the page
 * did not answer them, so it carries the mark, what the business is, what it
 * promises about delivery, and three real groups: what there is to buy, what to
 * do about an order already placed, and how to reach a person. */
.site-footer__grid {
  display: grid;
  grid-template-columns: minmax(0, 5fr) repeat(3, minmax(0, 3fr));
  gap: var(--space-48) var(--space-32);
}
.site-footer__brand h2 {
  font-size: var(--type-md);
  font-weight: var(--weight-bold);
  letter-spacing: -0.01em;
  text-transform: none;
  opacity: 1;
  margin-block: var(--space-12) var(--space-4);
}
.site-footer__brand p { color: var(--colour-on-deep-soft); font-size: var(--type-sm); }
/* The delivery promise, repeated here on purpose: it is the one fact somebody
   scrolling to the bottom to find a phone number most often actually wants. */
.site-footer__promise {
  margin-block-start: var(--space-16);
  max-inline-size: 34ch;
}
/* The footer used to invert the tile, because a charcoal tile on a charcoal
   ground is an invisible box holding a floating letter. The mark has no ground
   of its own now, so there is nothing to invert: the same artwork is drawn
   larger here, because the footer is where somebody has arrived looking for who
   this is. */
.site-footer__brand .wordmark__mark {
  inline-size: 52px;
  block-size: 52px;
}

.site-footer h3 {
  font-size: var(--type-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--colour-on-deep-faint);
  margin-block-end: var(--space-16);
  font-weight: var(--weight-semibold);
}
.site-footer ul { list-style: none; }

/* The bottom line is a band, not a line.
 *
 * Copyright, Privacy, Terms and who built the thing are a different class of
 * information from the rest of the footer: nobody scrolls down looking for
 * them, they are simply owed. Marking that with a smaller font would have said
 * "less important" and nothing else. A genuinely darker ground, running the
 * full width of the window rather than stopping at the gutter, says "this is a
 * different part of the page", which is what is actually true.
 *
 * The tone is a real step down from the footer above it, not a shade: #23272b
 * to #16191c is visible on a phone in daylight. White sits on it at 17.6:1 and
 * the softened text at 9.5:1, so nothing is traded for the separation. */
.site-footer__legal {
  background: var(--colour-surface-deepest);
  padding-block: var(--space-24);
  font-size: var(--type-sm);
  color: var(--colour-on-deep-soft);
}
.site-footer__legal-inner {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-12) var(--space-32);
  justify-content: space-between;
  align-items: center;
}
/* The three links were separated by two non-breaking spaces each, which is
   layout done with punctuation: it cannot wrap sensibly and it collapses to
   nothing when the line breaks. A gap does the same job and survives a narrow
   window. */
.site-footer__legal-links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-8) var(--space-24);
}

/* ===========================================================================
   Responsive. No horizontal scroll at any width (Law VII-5, INV-12).
   =========================================================================== */

@media (max-width: 900px) {
  .hero__grid,
  .checkout-grid,
  .product-detail { grid-template-columns: minmax(0, 1fr); gap: var(--space-32); }

  /* Stacked, the hero is copy and then goods, and the gap between the two is
     what decides whether the goods reach the first screen. */
  .hero__grid { gap: var(--space-24); }

  /* The summary genuinely does become a normal block at this width: it is a
     total to read on the way past, not something to keep in view. */
  .checkout-grid__aside { position: static; }

  /* Two columns, not one. The footer holds four groups and at 900px there is
     room for two of them side by side; collapsing straight to a single column
     turned it into eight hundred pixels of scrolling on a tablet, which is a
     footer nobody reaches the bottom of. The business block spans, because it
     is a paragraph rather than a list. */
  .site-footer__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-32); }
  .site-footer__brand { grid-column: 1 / -1; }
  .steps { grid-template-columns: minmax(0, 1fr); gap: var(--space-24); }
  .section { padding-block: var(--space-48); }
}

@media (max-width: 620px) {
  :root { --container-pad: var(--space-16); }

  /* The footer holds four groups, and a phone has room for two of them side by
     side. In one column it measured 1,040px, which is a quarter of the whole
     home page spent on a directory nobody scrolls to the bottom of. The
     business block still spans both, because it is a paragraph and not a list,
     and the promise under it stops being held to 34 characters when it has the
     full width to sit in. */
  .site-footer { padding-block: var(--space-48) 0; }
  .site-footer > .container { padding-block-end: var(--space-32); }
  .site-footer__grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-32) var(--space-24);
  }
  .site-footer__brand { grid-column: 1 / -1; }
  .site-footer__promise { max-inline-size: none; }

  /* The shelf, composed for a phone rather than folded down from a desk.
     ------------------------------------------------------------------------
     It was a 64px thumbnail with the name beside it, the price underneath the
     name, and the controls on a third line, so everything a customer looks at
     sat in the left two thirds of the screen and the right third was
     permanently empty. Measured: a 326px row using 64px of photograph.

     Two products across, each one a photograph the full width of its column
     with its own words underneath. The picture goes from 64px to 171px, which
     is nearly three times across and seven times the area, and the width is
     used from edge to edge because there is no longer a column with nothing in
     it. This is not a grid of padded boxes: the tiles have no surface, no
     border and no shadow of their own, they sit on the one white field the
     shelf already had, and the photograph is the object (Doctrine 12,
     Law VII-7).

     The quantity stepper is not drawn here. Three buttons and a number field
     beside an Add button, inside 171px, is four targets fighting for a thumb's
     width. Add puts one in the basket, which is what Add means, and the basket
     is where a quantity is changed, which is where a customer expects to change
     it. Nothing is unreachable and nothing is hidden that has no other door. */
  .product-list {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-24) var(--space-16);
    padding: var(--space-16);
  }
  .product-row {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-8);
    padding: 0;
    background: none;
    border-block-start: 0;
  }
  .product-row:hover { background: none; }
  .product-row .figure-box--sm {
    inline-size: 100%;
    border-radius: var(--radius-md);
  }
  /* The padding stays. It was taken off to tighten the tile and the Layout
     Gate refused it at 320px: the name is the largest target on the shelf and
     without it the link measures 20px against WCAG 2.5.8's floor of 24. Eight
     pixels a tile is what a thumb costs. */
  .product-row__name {
    padding-block: var(--space-4);
    font-size: var(--type-base);
    line-height: var(--leading-snug);
  }
  .product-row__meta {
    margin-block-start: var(--space-2);
    gap: var(--space-2) var(--space-8);
    font-size: var(--type-xs);
  }
  .product-row__price-wrap { text-align: start; white-space: normal; }
  /* Pushed to the foot of the tile, so two tiles of different name lengths
     still line their buttons up with each other. */
  .product-row__action { margin-block-start: auto; }
  .product-row__action .qty { display: none; }
  .product-row__action .btn { inline-size: 100%; justify-content: center; }

  .shelf { margin-block-start: var(--space-32); }
  .category-card__name { font-size: var(--type-base); }

  /* The action follows the customer down the form rather than waiting at the
     bottom of it. */
  .checkout-action {
    position: sticky;
    inset-block-end: 0;
    z-index: var(--z-header);
    margin-inline: calc(var(--container-pad) * -1);
    padding-inline: var(--container-pad);
    border-radius: 0;
    /* The one place a real shadow is earned: content genuinely passes under
       this bar, and the shadow is what says so (Doctrine 4). */
    box-shadow: var(--shadow-md);
  }
  .checkout-action .btn { flex: 1 1 auto; }

  .status-strip { gap: var(--space-16); }

  .page-head { padding-block: var(--space-16) var(--space-16); }
  .page-head h1 { font-size: var(--type-2xl); }
  .page-head p { font-size: var(--type-sm); }
  /* The hero on a phone. Every value here was chosen against a measurement:
     the headline was running to four lines and the first purchasable thing sat
     at 1149px on an 844px screen. The headline steps down one place in the
     scale, the air around it is halved, and the categories come up to meet it. */
  .hero { padding-block: var(--space-24) var(--space-32); }
  .hero__grid { gap: var(--space-24); }
  .hero h1 { font-size: var(--type-2xl); }
  .hero__lead { font-size: var(--type-base); margin-block-start: var(--space-12); }
  /* Two doors, equal in width and unequal in weight, which is what a pair of
     actions should look like. Left to wrap, they set at their own text widths
     and the row read as two buttons that had landed where they landed. */
  .hero__actions {
    margin-block-start: var(--space-16);
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-8);
  }
  .hero__actions .btn { inline-size: 100%; justify-content: center; }
  /* The two live figures, ranged as a pair across the width rather than
     trailing off after the first one. */
  .hero__trust {
    margin-block-start: var(--space-16);
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-8);
    padding-block-start: var(--space-16);
    border-block-start: var(--rule-hair, 1px) solid var(--colour-border);
  }
  .hero__trust-item { display: flex; flex-direction: column; gap: var(--space-2); }
  .hero__shelf-title { margin-block-end: var(--space-8); }
  .category-grid.category-grid--hero { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-8); }
}

/* ===========================================================================
   The command bar below a desktop width
   =========================================================================== */

/* Two rows, and it has to be exactly two at every width, for any business name.
 *
 * It was three at 320px and three again between about 440 and 500, and both
 * cases were the same fault: the first row is asked to hold identity AND the
 * destinations AND the basket, and when they will not fit, flex-wrap does the
 * only thing it can and starts a third line. On a 320x568 phone that left two
 * hundred and twelve pixels of the screen permanently spent on chrome, which is
 * thirty-seven per cent of it.
 *
 * Three changes make the first row fit rather than wrap:
 *
 *   The search moves to its own row further up than it used to. It used to
 *   happen at 620px, which left the range from 621 to 820 sharing one line
 *   between the name, an eighty-pixel search field and the navigation. Eighty
 *   pixels is not a search field, it is a slot. It now happens below 820, where
 *   a real one no longer fits.
 *
 *   SHOP steps out below 820. The category bar sits forty-four pixels beneath
 *   this one and its first entry, "All products", is the same destination. Two
 *   links to one page, one directly above the other, is a duplicate anywhere;
 *   it is a duplicate that costs a row where the room is scarce. The Operator
 *   may want it gone at every width, which is their call rather than this
 *   file's.
 *
 *   The name steps back to its mark below 460 rather than below 420. Between
 *   those two figures the name, TRACK ORDER and the basket were together wider
 *   than the row. The name is still announced, just not drawn: hiding it
 *   outright would leave the link home with no accessible name at all. */
/* One rhythm across the bar, below a desk width.
 *
 * Every control on this row was a different height: the mark 36, the links 44,
 * the basket 48. Centred, that is three different top edges and three different
 * bottom edges within twelve pixels of each other, which reads as things that
 * have landed near one another rather than as a bar that was set. They are all
 * forty-four now, which is also the floor Law VII-2 puts under a touch target,
 * so nothing is traded for the alignment.
 *
 * The basket keeps its weight without keeping the extra four pixels: it is the
 * only filled surface on the bar and it now carries a mark as well as a count,
 * which is a stronger claim on the eye than being marginally taller was. */
@media (max-width: 820px) {
  .site-header__inner {
    flex-wrap: wrap;
    gap: var(--space-8) var(--space-12);
    padding-block: var(--space-8);
    min-block-size: 60px;
  }
  .header-search { order: 3; flex: 1 0 100%; max-inline-size: none; margin-inline: 0; }
  .site-nav { margin-inline-start: auto; }
  .site-nav-link[href="/shop.html"] { display: none; }
  .wordmark__suffix { display: none; }
  .wordmark__name { font-size: var(--type-md); }
  .wordmark__mark { inline-size: 36px; block-size: 36px; font-size: var(--type-md); }
  .cart-button {
    min-block-size: 44px;
    padding-inline: var(--space-12);
    margin-inline-start: var(--space-4);
    gap: var(--space-8);
  }
  .cart-button__count { min-inline-size: 22px; block-size: 22px; }
  .site-nav-link { padding-inline: var(--space-8); }
}

/* The name stays drawn on a phone.
 *
 * It used to be hidden below 460px, leaving a stranger looking at a single
 * letter in a dark square and no way to know whose shop this is. That is the
 * one thing a shop front may not do, and the room for the name was found by
 * asking which of the things beside it had to be there.
 *
 * Track order goes, on a phone only. It is in the footer of every page and it
 * is a button in the hero, so nothing becomes unreachable, and of the three
 * destinations it is the one a customer arrives needing least often. Identity
 * outranks it.
 *
 * The name then takes whatever room is left and truncates if it runs out,
 * rather than pushing the bar to a third row. That holds for a name of any
 * length, which is the rule this bar has already been corrected into once. */
/* The search button, and the field it stands for.
 *
 * The button exists only where the field is collapsed, which is only on a
 * phone and only when JavaScript is present to open it again. Above that width
 * the field is simply there and the button is not. */
.header-search-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  min-inline-size: 44px;
  min-block-size: 44px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--colour-text);
  cursor: pointer;
}
.header-search-toggle:hover { color: var(--colour-primary-text); }

@media (max-width: 820px) {
  .site-header.search-collapsed .header-search { display: none; }
  .site-header.search-collapsed.search-open .header-search { display: flex; }
  .site-header.search-collapsed .header-search-toggle { display: inline-flex; }
}

@media (max-width: 560px) {
  /* Track order and Account both step out on a phone, and the reasoning is
     the one already written above for Track order: each is in the footer of
     every page, so nothing becomes unreachable, and identity outranks a
     destination a customer arrives needing less often. Account earns its place
     back the moment somebody has one, because the footer link is where they
     will look for it.

     What the room buys: the business name set in capitals at its full length
     rather than truncated to ROLINA GRAI... A shop front that cannot say its
     own name is the one thing this bar may not do. */
  .site-nav-link[href="/order.html"],
  .site-nav-link[href="/account.html"] { display: none; }
  /* A zero flex-basis, not auto.
     ------------------------------------------------------------------------
     In a wrapping flex row the browser places items onto lines using their
     content width FIRST and only shrinks them afterwards, so a wordmark wide
     enough to claim the row pushes the navigation onto a line of its own
     before it ever considers truncating. Setting the name in capitals made it
     wide enough, and the bar went to three rows on a phone, which is the exact
     fault this breakpoint was written to prevent.

     A zero basis means it never claims a line. It still grows into whatever
     the mark and the controls leave, and still truncates rather than wrapping,
     so a business name of any length holds the bar at two rows. */
  .wordmark { min-inline-size: 0; flex: 1 1 0; }
  .wordmark__lines { min-inline-size: 0; }
  .wordmark__name {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: var(--type-base);
  }
  .cart-button__text {
    position: absolute;
    inline-size: 1px; block-size: 1px;
    margin: -1px; padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  /* With the word "Basket" already gone at this width, a value beside the
     glyph and the count badge alone would be a third thing to parse in a
     header already this tight. The count stays; the value waits for the
     basket page it already appears on. */
  .cart-button__value { display: none; }
}

/* A short window is the one case where the name genuinely has nowhere to go:
   the bar returns to a single row below, and on one row the name competes with
   the search field rather than with the navigation. It is still announced. */
@media (max-height: 600px) {
  .wordmark__lines {
    position: absolute;
    inline-size: 1px; block-size: 1px;
    margin: -1px; padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}

@media (max-width: 460px) {
  .site-nav { gap: 0; }
  .site-nav-link { padding-inline: var(--space-4); }
}

/* A short window: a phone held sideways, the smallest phones held upright, or
 * a browser window somebody has squashed.
 *
 * The command bar and the category bar are both sticky, and together they are
 * a hundred and sixty pixels. On a 568px-tall screen that is twenty-eight per
 * cent of it and on a 320px-tall one it is half, taken permanently, before a
 * single product is shown. Somebody turning a phone sideways is usually doing
 * it to see MORE of something.
 *
 * So below this height the category bar scrolls away with the page. It is
 * still the first thing under the header and still one flick from anywhere; it
 * simply stops occupying a screen with none to spare. The command bar keeps
 * its position, because the basket and the search are worth the room and
 * losing them would cost more than the bar does.
 *
 * 600px rather than 480px, because 320x568 is a real phone that is still in
 * use and it failed the same way a landscape one does. */
@media (max-height: 600px) {
  .category-bar { position: static; }
  .hero { padding-block: var(--space-32); }
  .page-head { padding-block: var(--space-24) var(--space-16); }
  .section { padding-block: var(--space-32); }
}

/* Short but wide, which is a phone held sideways.
 *
 * The two-row bar exists because below 820px the name, the search and the
 * navigation will not share a line. Turn the phone and that stops being true:
 * there are now 568 or 740 pixels across and only 320 or 360 down, so the
 * scarce direction has swapped. The bar goes back to one row, which halves it
 * from 116px to 64px, and the search still gets 150px or more.
 *
 * Below 460px across, this does not apply and the two-row bar stays: at that
 * width one row genuinely does not fit, and 116px of a 568px screen is a fifth
 * rather than a third. */
/* Shorter still, which in practice means a reader at 400% zoom. WCAG asks
 * that the page still work when 1280 by 800 has become 320 by 200, and at that
 * size a bar parked at the top is more than a third of everything they have.
 * So it stops being parked. Nothing is lost: the bar is still the first thing
 * on the page, and scrolling to the top is one gesture.
 *
 * 300px, because that is where even the short-window bar at 72px reaches a
 * quarter of the screen, which is this system's ceiling for chrome. */
@media (max-height: 300px) {
  .site-header { position: static; box-shadow: 0 1px 0 var(--colour-border); }
}

@media (max-height: 600px) and (min-width: 460px) {
  .site-header__inner {
    flex-wrap: nowrap;
    padding-block: var(--space-4);
    gap: var(--space-12);
  }
  .header-search {
    order: 0;
    flex: 1 1 auto;
    max-inline-size: 420px;
    margin-inline: 0;
  }
  .site-nav { margin-inline-start: 0; }
}

/* Motion is state-transition logic, and a reader who asks for less gets none
 * (Doctrine 5, Constitution 13.7). */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* ===========================================================================
   Product imagery: a fixed box, always
   =========================================================================== */
/* Every photograph drops into a container of known size and known ratio. A tall
   photograph and a wide one occupy exactly the same space, so nothing stretches,
   nothing deforms, and nothing below moves while an image loads. A product with
   no photograph gets the same box, which is why a shelf stays level whether or
   not the Operator has uploaded imagery yet. */

.figure-box {
  position: relative;
  flex: 0 0 auto;
  aspect-ratio: 1;
  overflow: hidden;
  border-radius: var(--radius-sm);
  background: var(--colour-inset);
}
/* A comma, not a compound. These read ".figure-box--md AND .figure-box--lg",
   which no element is: productFigure writes exactly one of sm, md or lg. So
   neither of the two larger sizes had a width or the card radius, and both
   were falling back to whatever the flex context happened to give them. The
   same fault, written the same way, was found and fixed in five button and
   field selectors in an earlier sweep; this pair survived it. */
.figure-box--md,
.figure-box--lg { border-radius: var(--radius-md); inline-size: 100%; }
.figure-box--sm { inline-size: 64px; }

.figure-box > img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  display: block;
}
.figure-box > .brand-panel {
  inline-size: 100%;
  block-size: 100%;
  aspect-ratio: auto;
}
.figure-box--md > .brand-panel { font-size: var(--type-3xl); }
.figure-box--lg > .brand-panel { font-size: var(--type-4xl); }

/* The product page's main frame: a real photograph worth a closer look, and
 * a doorway to one. Pointer-only, because a phone's own pinch gesture is
 * already the real zoom there and a cursor hint would be lying to it. */
.figure-box--zoomable { cursor: default; }
@media (hover: hover) and (pointer: fine) {
  .figure-box--zoomable { cursor: zoom-in; }
}
.figure-box--zoomable:focus-visible { outline: 2px solid var(--colour-primary); outline-offset: 2px; }
.figure-box__zoom-hint {
  position: absolute;
  inset-block-end: var(--space-8);
  inset-inline-end: var(--space-8);
  padding: var(--space-4) var(--space-8);
  border-radius: var(--radius-sm);
  background: rgb(16 19 21 / 55%);
  color: var(--colour-on-deep);
  font-size: var(--type-xs);
  pointer-events: none;
}

/* The full-screen viewer. One instance, reused for every product and every
 * photograph, appended to the body rather than nested in the page so it sits
 * above everything regardless of where the click that opened it came from. */
.viewer {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-24);
  background: rgb(16 19 21 / 92%);
}
.viewer[hidden] { display: none; }
.viewer__image {
  max-inline-size: 100%;
  max-block-size: 100%;
  object-fit: contain;
  cursor: zoom-in;
  transition: transform var(--motion-fast) var(--ease);
}
.viewer__image--zoomed { transform: scale(1.8); cursor: zoom-out; }
.viewer__close {
  position: absolute;
  inset-block-start: var(--space-16);
  inset-inline-end: var(--space-16);
  inline-size: 44px;
  block-size: 44px;
  border-radius: 50%;
  border: none;
  background: rgb(255 255 255 / 12%);
  color: var(--colour-on-deep);
  font-size: var(--type-2xl);
  line-height: 1;
  cursor: pointer;
}
.viewer__close:hover { background: rgb(255 255 255 / 22%); }

/* ===========================================================================
   Badges
   =========================================================================== */

/* ===========================================================================
   Promotion markers
   =========================================================================== */
/* One visual language, two honest uses.
 *
 * A percent taken off scales the same way at any quantity, so a percent_off
 * offer naming a product or its category is shown as a real was/now price:
 * the figure is exactly what create_order will charge. Every other kind, a
 * flat amount off a whole order, a free item, a shop-wide threshold, is a
 * fact about the basket or the order, not about one unit of a product, and is
 * stated in the Operator's own words as a marker instead of a computed price
 * this component would otherwise have invented.
 *
 * The marker itself is a precise, outlined tag, never a filled block: it sits
 * beside the price it explains rather than as a separate sticker in its own
 * zone, and it never crosses onto the product's photograph. */

.price-was {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 0.8em;
  font-weight: var(--weight-regular);
  color: var(--colour-text-faint);
  text-decoration: line-through;
  text-decoration-color: var(--colour-border-strong);
}
.price-now {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-weight: var(--weight-semibold);
  color: var(--colour-primary-text);
}
.card__price, .product-row__price, .product-detail__price { display: inline-flex; align-items: baseline; gap: var(--space-8); }

/* A saving is a claim about money, and it belongs beside the price as a
   figure, not inside a badge. The bordered pill this used to be repeated once
   per row and read as a sticker applied to the shelf rather than as part of
   the price. Weight and the accent carry it now. */
.promo-tag {
  display: inline-flex;
  align-items: center;
  font-size: 0.6875rem;
  font-weight: var(--weight-bold);
  letter-spacing: 0.02em;
  color: var(--colour-primary-text);
  background: transparent;
}
.promo-tag--save { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

/* ===========================================================================
   Product card and shelf
   =========================================================================== */
/* A shelf is a row the thumb pushes sideways on a phone and a grid where there
   is room. It is not a wall of equal boxes: the card carries a real hierarchy,
   with the name reading first, the price second, and everything else quiet. */

/* ===========================================================================
   Categories on the home page
   =========================================================================== */

/* Six white cards on the ground, each a category and the count behind it.
 *
 * No photograph and no icon. There is no honest picture of "Lentils and Peas"
 * that this shop owns, and a bought one would be a photograph of somebody
 * else's lentils presented as ours (Law II-6). The name and the number are
 * the whole of what is true, so they are the whole of the card.
 *
 * They auto-fit rather than sitting on a fixed count, so adding a seventh
 * category reflows instead of leaving a hole. */
.category-grid {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: var(--space-12);
  margin-block-start: var(--space-24);
}

.category-card {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: var(--space-4);
  min-block-size: 92px;
  block-size: 100%;
  padding: var(--space-16);
  background: var(--colour-surface);
  border-radius: var(--radius-md);
  box-shadow: inset 0 -3px 0 transparent, var(--shadow-xs);
  color: var(--colour-text);
  text-decoration: none;
  transition: box-shadow var(--motion-fast) var(--ease);
}
.category-card:hover {
  color: var(--colour-text);
  box-shadow: inset 0 -3px 0 var(--colour-primary), var(--shadow-sm);
}
.category-card__name {
  font-size: var(--type-md);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-snug);
}
.category-card__count {
  font-size: var(--type-xs);
  font-family: var(--font-mono);
  color: var(--colour-text-faint);
}

.shelf { margin-block-start: var(--space-48); }
/* The first shelf under a section heading needs no space of its own above it.
   The section's padding is already the separation, and adding to it produced
   112px of nothing between the offer strip and the word "Featured". */
.shelf:first-child { margin-block-start: 0; }

.shelf__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-16);
  padding-block-end: var(--space-16);
}
.shelf__head h2 { font-size: var(--type-lg); }
/* It was forty pixels by twenty, which is a link a thumb misses. The words are
   the same size; what changed is that it now has a hit area around them, and
   the row aligns on centres rather than baselines so the taller target does not
   drag the heading up with it. */
.shelf__more {
  display: inline-flex;
  align-items: center;
  min-block-size: 44px;
  padding-inline: var(--space-8);
  margin-inline-end: calc(var(--space-8) * -1);
  font-size: var(--type-sm);
  font-weight: var(--weight-semibold);
  white-space: nowrap;
}

.shelf__rail {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--space-16);
  margin-block-start: var(--space-24);
}

/* A shelf card, and the word is now literal: white, on the page's ground, with
   the small radius every surface here carries. It needs no rule at all. It used
   to stand on a charcoal base rule purely because the card and the page behind
   it were both white and nothing else would separate them; the ground does that
   now, so the rule goes.
   The pointer is answered by a milled accent edge rather than by moving,
   lifting or growing a shadow: the card keeps its footprint exactly. */
.card {
  display: flex;
  flex-direction: column;
  background: var(--colour-surface);
  border: 0;
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-xs);
  overflow: hidden;
  transition: box-shadow var(--motion-fast) var(--ease);
}
.card:hover { box-shadow: inset 0 -3px 0 var(--colour-primary), var(--shadow-sm); }

.card__link {
  display: flex;
  flex-direction: column;
  text-decoration: none;
  color: inherit;
}
.card__link:hover { color: inherit; }

.card__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-16) var(--space-16) var(--space-8);
}
.card__category {
  font-size: var(--type-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--colour-text-faint);
}
.card__name {
  font-weight: var(--weight-semibold);
  font-size: var(--type-base);
  line-height: var(--leading-snug);
}
.card__price {
  margin-block-start: var(--space-8);
  font-size: var(--type-md);
  font-weight: var(--weight-medium);
}
.card__unit { font-size: var(--type-xs); color: var(--colour-text-faint); }

.card__foot {
  margin-block-start: auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-8);
  padding: var(--space-8) var(--space-16) var(--space-16);
}

/* ===========================================================================
   Depth
   =========================================================================== */
/* A page of white boxes on white has no hierarchy to read. Charcoal carries the
   weight, the accent marks the boundary, and the tinted surface separates one band
   of the page from the next. No gradient, no glass, no shadow doing the work
   that structure should do (Doctrine 2, 4). */

.band {
  background: var(--colour-surface-deep);
  color: var(--colour-on-deep);
  padding-block: var(--space-64);
}
/* The same fault a fourth time, and this one has been harmless: headings carry
   no colour of their own, so they already inherit the band's white. The rule is
   deleted rather than repaired, because a rule that restates what inheritance
   already does is one more thing to keep true. */
.band p { color: var(--colour-on-deep-soft); }
/* On the charcoal band the step cards drop their white surface: a white card on
   charcoal is a hole in the band, not a card on a desk. Weight carries them. */
.band .steps li { background: transparent; box-shadow: none; padding: 0; }
.band .steps p { color: var(--colour-on-deep-soft); }

/* What is on offer today is a standing announcement, not a row in a list, so
   this is the third and last place on the shop that earns a surface of its
   own. Filled, anchored in the accent on the leading edge, and stated once, 
   which is also why no product row repeats a shop-wide offer beneath it. */
.offer-strip {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-8) var(--space-24);
  align-items: center;
  margin-block: var(--space-16);
  padding: var(--space-16) var(--space-24);
  background: var(--colour-surface);
  border: 0;
  border-inline-start: var(--rule-anchor) solid var(--colour-primary);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-xs);
  font-size: var(--type-sm);
}
/* The strip belongs to the shelves below it, not to the band above, so the
   space closes up behind it rather than leaving it stranded.

   There used to be a second selector here, ".offer-strip + * .section:first-child",
   which reads "a first-child section inside the strip's next sibling". The
   strip sits inside a container, so its next sibling is inside that container
   too and can never hold a page section. It matched nothing, on any page, ever. */
.hero + .container + .section { padding-block-start: var(--space-48); }
.offer-strip__item {
  display: inline-flex;
  align-items: center;
  gap: var(--space-8);
  color: var(--colour-text);
  font-weight: var(--weight-medium);
}

/* ===========================================================================
   Specification list, for product attributes
   =========================================================================== */

.spec-list .spec-key { color: var(--colour-text-muted); }

.gallery {
  display: flex;
  gap: var(--space-8);
  margin-block-start: var(--space-8);
  overflow-x: auto;
  scrollbar-width: none;
}
.gallery::-webkit-scrollbar { display: none; }
/* A thumbnail needs no frame: the photograph is its own edge, and on the page's
   ground it already reads as a separate tile. The one that is being shown is
   marked by a milled accent edge, the same signal every other chosen thing in
   this system carries. */
.gallery button {
  flex: 0 0 auto;
  inline-size: 64px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: none;
  cursor: pointer;
  transition: box-shadow var(--motion-fast) var(--ease);
}
.gallery button[aria-current='true'] { box-shadow: inset 0 -3px 0 var(--colour-primary); }

@media (max-width: 900px) {
  /* Two and a bit across rather than three and a bit. At 45% a card was 150px
     wide on a phone and 320px on a tablet, and 150px is a thumbnail with a
     price under it. The rail itself only survives above the phone width now;
     below it the shelf becomes a real grid, further down this file. */
  .shelf__rail {
    grid-auto-flow: column;
    grid-auto-columns: minmax(220px, 34%);
    grid-template-columns: none;
    overflow-x: auto;
    scroll-snap-type: x proximity;
    scrollbar-width: none;
    margin-inline: calc(var(--container-pad) * -1);
    padding-inline: var(--container-pad);
    padding-block-end: var(--space-8);
  }
  .shelf__rail::-webkit-scrollbar { display: none; }
  .shelf__rail > li { scroll-snap-align: start; }
  .band { padding-block: var(--space-48); }
}

/* ===========================================================================
   The account page
   ===========================================================================
   Three states share one page. Signed out shows two authored forms and nothing
   else; signed in shows four sections drawn from the backend; and before the
   page knows which, it shows the signed-out half, because that is what a
   browser with no session and a browser with no JavaScript both are.

   The switch is a data attribute on <main> rather than a class toggled on each
   block, so there is exactly one place that decides, and a browser that never
   runs the script lands in the correct state by default rather than by
   accident. */

[data-account] [data-signed-in] { display: none; }
[data-account][data-account-state='in'] [data-signed-in] { display: block; }
[data-account][data-account-state='in'] [data-signed-out] { display: none; }

/* Two doors, side by side where there is room and stacked where there is not.
   Signing in comes first in the source and stays first on a phone, because the
   returning customer is the one who came here on purpose. */
.account-doors {
  display: grid;
  gap: var(--space-24);
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  align-items: start;
}
.account-door h2 { margin-block-end: var(--space-4); }
.account-door .lede { color: var(--colour-text-muted); }

/* A sentence under a form, quieter than the form and not a hint on any one
   field. */
.note-line {
  margin-block-start: var(--space-16);
  font-size: var(--type-sm);
  color: var(--colour-text-muted);
}

/* A control that belongs in a sentence rather than on a row of its own. It is a
   real <button>, because it does something rather than going somewhere, and it
   carries the underline a link carries so it is recognisable as pressable
   without colour being the only signal. */
.linkish {
  /* A control, so it carries a control's hit area rather than a link's.
     Written as a sentence it measured 173 by 20 and the Layout Gate was right
     to refuse it: a thumb is not a mouse pointer. The words stay the same
     size; the target around them does not. */
  display: inline-flex;
  align-items: center;
  min-block-size: 44px;
  font: inherit;
  color: var(--colour-primary-text);
  background: none;
  border: 0;
  padding: 0;
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}
.linkish:hover { color: var(--colour-primary-hover); }

/* Name and value, in two columns where there is room. */
.detail-list {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: var(--space-8) var(--space-24);
  margin-block-start: var(--space-16);
}
.detail-list dt { color: var(--colour-text-muted); }
.detail-list dd { margin: 0; font-weight: var(--weight-medium); }

/* Orders bought before there was an account to put them on. */
.claim-list { list-style: none; margin: var(--space-16) 0 0; padding: 0; }
.claim-list__item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-16);
  flex-wrap: wrap;
  padding-block: var(--space-12);
  border-block-start: 1px solid var(--colour-border);
}
.claim-list__item:first-child { border-block-start: 0; }

/* Saved addresses. A card per address on the page's ground, so the step from
   ground to white is the boundary and no border is needed. */
.address-list {
  list-style: none;
  margin: var(--space-16) 0 0;
  padding: 0;
  display: grid;
  gap: var(--space-16);
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
}
.address-card {
  background: var(--colour-surface);
  border-radius: var(--radius-md);
  padding: var(--space-24);
  box-shadow: var(--shadow-sm);
}
/* The default one is the one that will be used, so it is marked by the same
   milled accent edge every other chosen thing in this system carries. */
.address-card--default { box-shadow: inset 3px 0 0 var(--colour-primary), var(--shadow-sm); }
.address-card__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-8);
}
.address-card__head h3 { font-size: var(--type-md); }
.address-card__flag {
  font-size: var(--type-xs);
  font-weight: var(--weight-semibold);
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--colour-primary-text);
}
.address-card__line { margin-block-start: var(--space-8); white-space: pre-line; }
.address-card__zone { color: var(--colour-text-muted); font-size: var(--type-sm); }
.address-card__notes {
  margin-block-start: var(--space-8);
  color: var(--colour-text-muted);
  font-size: var(--type-sm);
}
.address-card__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-8);
  margin-block-start: var(--space-16);
}

.address-form { margin-block-start: var(--space-16); }

/* A checkbox and its words, on one line, with the whole line as the target
   rather than the box alone. */
.checkline {
  display: flex;
  align-items: center;
  gap: var(--space-12);
  min-block-size: 44px;
  margin-block-start: var(--space-24);
  cursor: pointer;
}
.checkline input { inline-size: 20px; block-size: 20px; flex: 0 0 auto; }

/* Two buttons that belong together, wrapping rather than crushing. */
.form-action--pair { display: flex; flex-wrap: wrap; gap: var(--space-12); }

/* The order rows on this page are links rather than buttons, so they need the
   card surface the button form already carries and none of the link colour. */
a.order-row { color: inherit; text-decoration: none; }
a.order-row:hover { text-decoration: none; }

/* Saved addresses at checkout. A row of shortcuts, not a form control: pressing
   one fills the boxes below, which stay the thing the customer confirms. */
.saved-addresses { margin-block-end: var(--space-24); }
.saved-addresses__row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-8);
  margin-block: var(--space-8);
}

/* A phone's own header, rather than the desktop one folded up.
   ===========================================================================
   Adding a fourth destination pushed this to three rows at 320 and again
   between 460 and 520, and three rows of chrome on a 568px screen is 30% of
   the window gone before a word of the shop is drawn. The Layout Gate refused
   it and was right to.

   Two rows at every phone width, and the levers are the ones that cost a
   customer nothing: the tracking that makes capitals easy to read at 13px is not
   needed at 12px, and the padding around a link is not the link. Every label
   the Operator specified stays written out in full. */
@media (max-width: 520px) {
  .site-nav-link,
  .cart-button__text {
    letter-spacing: 0.02em;
  }
  .site-nav-link {
    font-size: var(--type-xs);
    padding-inline: var(--space-4);
  }
  .cart-button {
    padding-inline: var(--space-8);
    gap: var(--space-4);
  }
  .cart-button__text { font-size: var(--type-xs); }
}

/* The narrowest phone still sold, and the width WCAG's reflow rule is written
   at. Eight pixels was the whole difference between two rows and three here,
   so it is taken off the space around the words rather than off the words. */
@media (max-width: 360px) {
  .site-header__inner { gap: var(--space-8); }
  .site-nav-link { padding-inline: var(--space-2); }
}

/* A category tile with a photograph on it.
   ---------------------------------------------------------------------------
   Two finished designs, not one degraded into the other. A category the
   Operator adds tomorrow has no photograph until somebody takes one, and it
   has to look deliberate in the meantime, so the plain tile above stays
   exactly as it was and this is a modifier on top of it.

   The name sits UNDER the photograph on white rather than over it. Type on a
   photograph needs a scrim to guarantee its contrast, and a scrim is a wash,
   which this system does not have anywhere (Brantenna One, "How this is
   enforced in the code"). Putting the words on the surface below is not a
   compromise around that rule: it is the same ground-and-surface step every
   other card in this interface uses, and the contrast is a fact rather than a
   hope about whatever photograph is uploaded next. */
.category-card--photo {
  padding: 0;
  overflow: hidden;
  min-block-size: 0;
  justify-content: flex-start;
  gap: 0;
}
.category-card__photo {
  display: block;
  inline-size: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
  background: var(--colour-inset);
}
/* The name and the count, stacked, with the air taken out from around them
   rather than out from between them.
   This was tried on one line, the count ranged right against the name, and the
   real strings refused it: "Millet and Sorghum" beside "4 products" needs more
   than a 216px tile has, so the name wrapped and the tile got taller rather
   than shorter. The measurement decided it, not the drawing. */
.category-card--photo .category-card__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-12) var(--space-16);
}

/* The tablet, given a composition of its own.
   ===========================================================================
   A tablet is not a large phone and it is not a small desktop, and until now
   it was being served the desktop shelf stretched: one column of very wide
   rows, with a product name occupying four hundred pixels of empty space and
   the price marooned at the far edge. Nothing overflowed and nothing collided,
   which is why the Gates were content, and it still read as a layout nobody
   had thought about.

   Two columns instead, from the width where a row would otherwise be too wide
   to scan up to the width where the desktop's own density starts earning its
   keep. Each row keeps its photograph and its name on one line and drops the
   price and the Add control onto a second, which is the same compact shape the
   phone already uses. It is not the phone's layout: the phone shows one of
   these and a tablet shows two, and at this size that is the difference
   between reading a shelf and scrolling a list.

   The category tiles and the shelves get the same treatment, so the whole page
   is composed for the width rather than one section of it. */
@media (min-width: 700px) and (max-width: 1023px) {
  .product-list {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-12);
    align-content: start;
  }

  .product-row {
    grid-template-columns: 56px minmax(0, 1fr);
    row-gap: var(--space-12);
    align-content: start;
  }
  /* Two columns means two rows are the top row, and both of them open their
     own column rather than only the first in source order. */
  .product-row:nth-child(-n+2) { border-block-start: 0; }
  .product-row__price-wrap { grid-column: 2; text-align: start; }
  .product-row__action { grid-column: 2; flex-wrap: wrap; }

  /* Three tiles across reads as a shelf; two reads as a pair of banners and
     six reads as a strip. */
  .category-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }

  /* The two doors on the account page sit side by side from here, because
     there is genuinely room for both and stacking them puts creating an
     account below the fold on a screen that has space for it. */
  .account-doors { grid-template-columns: repeat(2, minmax(0, 1fr)); }

  /* A saved address is a card, and three across at this width leaves each one
     too narrow to hold a street name without wrapping twice. */
  .address-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ===========================================================================
   The desktop shelf, as a shelf rather than a ledger
   ===========================================================================
   A 64px photograph, a name, then several hundred pixels of nothing before
   the price reached the far edge of a 1200px row: measured at that width,
   the name column ended near 420px and the price began past 1040px, a gap
   wider than everything either side of it put together. Nothing overflowed
   and every row was individually correct, and the whole shelf still read as
   a spreadsheet, because a single dense column of thin rows is what a
   spreadsheet is.
   Above the tablet width, the shelf becomes a grid of tiles instead, in the
   same borderless style the phone already earned: no card, no shadow, no
   border, the photograph the size it deserves rather than a 64px icon
   (Doctrine 12, Law VII-7's ban on the generic equal-padding card grid does
   not reach a tile with no card drawn around it). Four across is close to a
   1200px container's own arithmetic: a photograph near 260px on a side,
   large enough to be worth looking at, without so few columns that fifty-one
   products reads as an inventory printout the other way. */
@media (min-width: 1024px) {
  .product-list {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: var(--space-32) var(--space-24);
    padding: var(--space-24);
  }
  .product-row {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-8);
    padding: 0;
    background: none;
    border-block-start: 0;
  }
  .product-row:hover { background: none; }
  .product-row .figure-box--sm {
    inline-size: 100%;
    border-radius: var(--radius-md);
  }
  .product-row__name { padding-block: var(--space-4); }
  .product-row__price-wrap { text-align: start; white-space: normal; }
  /* Pushed to the foot of the tile, so tiles of different name lengths still
     line their controls up with each other, the same reasoning the phone
     tile already uses. Unlike the phone, there is room for the quantity
     stepper beside Add rather than sending everyone who wants more than one
     into the basket to change it, so it stays. */
  .product-row__action {
    margin-block-start: auto;
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-8);
  }
  .product-row__action .btn { flex: 1 1 auto; justify-content: center; }
}

/* ===========================================================================
   The phone, composed last
   ===========================================================================
   These rules have to be written here rather than with the rest of the phone's
   rules further up the file. Every one of them answers a selector that appears
   later in this file at equal or greater weight: the shelf rail below 900px,
   the section rhythm below 900px, the compound that opens the first section
   under the hero, and the category tile body. Written any earlier they are
   silently overruled and nothing says so, which is the same class of fault as
   the lost-comma selectors and the two figure-box sizes. Source order is the
   fix, not more weight.
   =========================================================================== */

@media (max-width: 620px) {
  /* Two products across, whole, rather than three across with the third cut
     off by the edge of the screen. The rail exists for a tablet, where there
     genuinely is somewhere for it to scroll to. */
  .shelf__rail {
    grid-auto-flow: row;
    grid-auto-columns: auto;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    overflow-x: visible;
    margin-inline: 0;
    padding-inline: 0;
    padding-block-end: 0;
    gap: var(--space-24) var(--space-16);
    margin-block-start: var(--space-16);
  }

  /* Sixty-four pixels of air between one band of the page and the next is
     right on a desk and is an eighth of a phone screen showing neither of
     them. Measured before this: 112px between the offer strip and the first
     shelf heading, on an 844px window. */
  .section { padding-block: var(--space-32); }
  .band { padding-block: var(--space-32); }
  .hero + .container + .section { padding-block-start: var(--space-24); }

  /* The photograph takes the room, and the two facts under it stack rather
     than sharing a line. A category name here is two or three words and does
     not fit beside its count at this width; the single line above stands from
     the tablet up, where it does. */
  .category-card--photo .category-card__photo { aspect-ratio: 4 / 3; }
  .category-card--photo .category-card__body {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-2);
    padding: var(--space-12);
  }
}

@media (max-width: 620px) {
  /* A shelf card at 171px carries the same words a 288px one does, and the
     padding written for the wider one leaves the narrow one two thirds air.
     The words do not shrink; the space around them does. */
  .card__body { padding: var(--space-12) var(--space-12) var(--space-4); }
  .card__price { margin-block-start: var(--space-4); font-size: var(--type-base); }
  .card__foot { padding: var(--space-4) var(--space-12) var(--space-12); gap: var(--space-4); }
  .card__name { font-size: var(--type-base); }
  .shelf__head { padding-block-end: var(--space-8); }
  .shelf { margin-block-start: var(--space-24); }
}

@media (max-width: 620px) {
  /* Twenty-four pixels either side of a label is right for a button that sizes
     itself and wrong for one holding half a phone's width: "See what is in
     stock" broke onto two lines while the button beside it sat on one, and the
     pair read as a mistake rather than as a pair. The words are untouched; the
     room around them is what gives way, which is the same call made for the
     command bar. */
  .hero__actions .btn { padding-inline: var(--space-12); }

  /* The announcement above the shelf, and the count below it. Both were spaced
     for a page with room to spare, and together they put the first product
     three quarters of a screen down. */
  .offer-strip { margin-block: var(--space-12); padding: var(--space-12) var(--space-16); }
  .shop-count { padding-block: var(--space-16) var(--space-8); }
}

@media (max-width: 620px) {
  /* The footer's three directories, placed rather than left to flow.
     ------------------------------------------------------------------------
     Flowing into two columns put Shop and Your order side by side and then
     dropped Reach us onto a row of its own with half the width empty beside
     it, which is the same left-heavy shape the shop page had. Placed, the
     seven shop links take the left column for both rows and the two shorter
     lists stack down the right, so neither side is a column of air.

     This leans on source order, which is fair here: the footer is authored
     identically in every page of the site and its three groups have been in
     this order since it was written. If a fourth is ever added it lands in the
     flow after these and is placed by the grid, not hidden. */
  .site-footer__grid > :nth-child(2) { grid-column: 1; grid-row: 2 / span 2; }
  .site-footer__grid > :nth-child(3) { grid-column: 2; grid-row: 2; }
  .site-footer__grid > :nth-child(4) { grid-column: 2; grid-row: 3; }
  .site-footer__grid { gap: var(--space-24); align-items: start; }
}

@media (max-width: 620px) {
  /* The price and the offer that touches it, one under the other.
     Side by side, a 36px figure and a 13px sentence share a baseline and the
     sentence reads as something that has been squeezed in beside the price
     rather than as a fact about it. There is no room to set them as a pair at
     this width, so they are set as two lines. */
  .product-detail__price-row {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-4);
  }
  .product-detail__price { margin-block: var(--space-12) var(--space-4); }
}
