/* =============================================================================
   Counter Warm — the Sourdoe design system.

   The single source of truth for this file is DESIGN.md. Every value below
   traces to a rule there. Components must use these custom properties and these
   classes: no hex values in a component, no hue that is not in the token table,
   no radius outside the three defined here, no drop shadows.
   ========================================================================== */

/* -----------------------------------------------------------------------------
   1. Tokens
   -------------------------------------------------------------------------- */

:root {
  /* Palette. DESIGN.md, "Colours". */
  --ink: #2a211c;
  --ink-60: #6b5a4e;
  --ink-40: #8f8073;
  --clay: #a8543a;
  --wheat: #e0b87a;
  --sand: #f0e6d6;
  --sand-shade: #e6dac6;
  --paper: #fbf7f0;
  --paper-dim: #c4b5a4;
  --moss: #6e7a4f;

  /* Type families. DESIGN.md, "Type". */
  --font-display: "Newsreader", Georgia, "Times New Roman", serif;
  --font-ui: "Public Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;

  /* Radii. Only these three. DESIGN.md, "Shape and space". */
  --radius-pill: 999px;
  --radius-card: 8px;
  --radius-row: 6px;

  /* Space. Section padding 48 desktop / 24 mobile; 20 between cards, 32 between columns. */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 20px;
  --space-6: 24px;
  --space-7: 32px;
  --space-8: 48px;
  --space-9: 64px;

  --gap-cards: 20px;
  --gap-columns: 32px;
  --section-pad: 48px;

  /* Measure caps. DESIGN.md, "Scale". */
  --measure-lead: 48ch;
  --measure-prose: 65ch;

  --hairline: 1px solid var(--ink);
  --page-max: 1180px;

  /* Height of the storefront masthead, which is sticky: the 76px min-height of
     .masthead__inner plus its hairline. Anything that pins underneath it reads
     this rather than guessing, so the two cannot drift apart. */
  --masthead-h: 77px;
}

@media (max-width: 720px) {
  :root {
    --section-pad: 24px;
  }
}

/* iPad portrait. 48px of gutter on an 820px screen is a quarter of the width
   given over to margin, and this is the back office's main target. */
@media (min-width: 721px) and (max-width: 1023px) {
  :root {
    --section-pad: 32px;
  }
}

/* -----------------------------------------------------------------------------
   2. Base
   -------------------------------------------------------------------------- */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: 16px;
  font-weight: 400;
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;
}

/* DESIGN.md asks for pretty wrapping on every paragraph and headline. */
p,
li,
dd,
figcaption,
blockquote {
  text-wrap: pretty;
}

h1,
h2,
h3,
h4 {
  text-wrap: balance;
}

img,
svg,
video {
  max-width: 100%;
  height: auto;
  display: block;
}

a {
  color: var(--clay);
  text-decoration: none;
  border-bottom: 1px solid color-mix(in srgb, var(--clay) 35%, transparent);
}

a:hover,
a:focus-visible {
  border-bottom-color: var(--clay);
}

/* Ink hairlines carry the structure, so the focus ring has to be visible
   against both paper and sand without a shadow. */
:focus-visible {
  outline: 2px solid var(--clay);
  outline-offset: 2px;
}

/* Except on a page heading. Routes.razor carries a FocusOnNavigate that moves
   focus to the first h1 on every navigation, so a screen reader announces the
   new page and the next Tab starts from it. That is the framework moving focus,
   not somebody arriving on a control — and a clay box drawn around the page
   title reads as an error in a palette whose only alarm colour is clay.

   Nothing reachable loses its indicator: an h1 is not tabbable, and the
   tabindex FocusOnNavigate adds is removed again the moment it has focused. */
h1:focus,
h1:focus-visible {
  outline: none;
}

hr {
  border: 0;
  border-top: var(--hairline);
  margin: var(--space-6) 0;
}

/* -----------------------------------------------------------------------------
   3. Type scale — DESIGN.md, "Scale"
   -------------------------------------------------------------------------- */

.display-hero,
.display-page,
.display-section,
.display-card {
  font-family: var(--font-display);
  font-weight: 400;
  margin: 0;
  /* Newsreader is never uppercase and never letterspaced. */
  text-transform: none;
  letter-spacing: normal;
}

.display-hero {
  font-size: clamp(40px, 7vw, 64px);
  line-height: 1.03;
  letter-spacing: -0.01em;
}

.display-page {
  font-size: clamp(30px, 4.4vw, 40px);
  line-height: 1.1;
}

.display-section {
  font-size: clamp(26px, 3.6vw, 34px);
  line-height: 1.15;
}

.display-card {
  font-size: clamp(22px, 2.4vw, 26px);
  line-height: 1.2;
}

.lead {
  font-size: 17px;
  line-height: 1.65;
  max-width: var(--measure-lead);
  margin: 0;
}

.prose {
  max-width: var(--measure-prose);
}

.body-sm {
  font-size: 15px;
  line-height: 1.7;
}

.ui-text {
  font-size: 14px;
  line-height: 1.5;
}

.eyebrow {
  font-family: var(--font-ui);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--clay);
  margin: 0;
}

/* Clay fails contrast on wheat and sand-shade, so an eyebrow on a dark tile
   switches to the paper tint instead.

   The ink board and the ink footer are dark tiles that never carried the
   marker class, so an eyebrow inside either was clay on ink — about 2.2:1,
   which fails outright. They read the same rule rather than remembering to
   add `.on-ink` to a wrapper. */
.on-ink .eyebrow,
.band--ink .eyebrow,
.board .eyebrow {
  color: var(--wheat);
}

.price {
  font-family: var(--font-ui);
  font-size: 16px;
  font-weight: 700;
}

.muted {
  color: var(--ink-60);
}

/* A headline stat on an overview card, and nothing else. It is Newsreader at
   30–40px, so inside a 14px table row it is four times the size of the text
   around it and drags the column out with it. Table cells want `.fig`. */
.numeral {
  font-family: var(--font-display);
  font-size: clamp(30px, 4vw, 40px);
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

/* Figures in a table or a summary row: the surrounding size, lined up in a
   column. This is what most of the back office actually wanted from .numeral. */
.fig {
  font-variant-numeric: tabular-nums;
}

/* -----------------------------------------------------------------------------
   4. Layout
   -------------------------------------------------------------------------- */

.page {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

.page > main {
  flex: 1;
}

.shell {
  width: 100%;
  max-width: var(--page-max);
  margin-inline: auto;
  padding-inline: var(--section-pad);
}

/* Structural blocks are separated by a full-width ink rule, not by a gap. */
.band {
  border-bottom: var(--hairline);
  padding-block: var(--section-pad);
}

.band--tight {
  padding-block: var(--space-6);
}

.band--flush {
  padding-block: 0;
}

.band--sand {
  background: var(--sand);
}

.band--ink {
  background: var(--ink);
  color: var(--paper-dim);
}

.band--ink .display-hero,
.band--ink .display-page,
.band--ink .display-section,
.band--ink .display-card,
.band--ink strong {
  color: var(--paper);
}

.band:last-child {
  border-bottom: 0;
}

.stack {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.stack--tight {
  gap: var(--space-2);
}

.stack--loose {
  gap: var(--space-6);
}

.row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
}

.row--between {
  justify-content: space-between;
}

.row--end {
  justify-content: flex-end;
}

.columns {
  display: grid;
  gap: var(--gap-columns);
}

/* A band or page whose picture does not exist yet: one column of words rather
   than a split with an empty half, held to the prose measure so a line never
   runs the width of the shell. No placeholder stands in for the picture; when
   it arrives the markup goes back to columns--split. */
.columns--bare {
  max-width: var(--measure-prose);
}

@media (min-width: 900px) {
  .columns--split {
    grid-template-columns: 1fr 1fr;
  }
}

/* Sidebar layouts turn on at 1024px, not lower, so an iPad in portrait always
   gets a single column. DESIGN.md, "Breakpoints".

   The row does not stretch: a sidebar that is already as tall as the column
   beside it has nowhere to stick to when it is asked to follow the scroll. */
@media (min-width: 1024px) {
  .columns--sidebar {
    grid-template-columns: minmax(0, 1fr) 320px;
    align-items: start;
  }
}

.card-grid {
  display: grid;
  gap: var(--gap-cards);
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
}

/* -----------------------------------------------------------------------------
   5. Buttons — one clay action per view group; the second is ink-outlined
   -------------------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 44px; /* touch target */
  padding: 14px 28px;
  border: var(--hairline);
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: 15px;
  font-weight: 500;
  line-height: 1;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  transition: background-color 120ms ease, color 120ms ease;
}

.btn:hover {
  border-bottom-color: var(--ink);
}

/* The one clay action. */
.btn--primary {
  background: var(--clay);
  border-color: var(--clay);
  color: var(--paper);
}

.btn--primary:hover:not(:disabled) {
  background: color-mix(in srgb, var(--clay) 88%, var(--ink));
  border-color: color-mix(in srgb, var(--clay) 88%, var(--ink));
}

/* Everything alongside it. */
.btn--outline {
  background: transparent;
  border-color: var(--ink);
  color: var(--ink);
}

.btn--outline:hover:not(:disabled) {
  background: var(--sand-shade);
}

.btn--quiet {
  border-color: transparent;
  padding-inline: var(--space-3);
  color: var(--ink-60);
}

.btn--quiet:hover:not(:disabled) {
  color: var(--ink);
  background: var(--sand-shade);
}

/* Inline "Add" scale from DESIGN.md: 9px vertical, 16–18px horizontal. The
   height floor is 44px, not the 32px that padding alone would give — this is the
   sign-in button in the storefront masthead and the row actions in the back
   office, and DESIGN.md sets 44px as the minimum anywhere a finger lands. */
.btn--inline {
  min-height: 44px;
  padding: 9px 18px;
  font-size: 14px;
}

.btn--block {
  width: 100%;
}

.btn:disabled,
.btn[aria-disabled="true"] {
  opacity: 0.45;
  cursor: not-allowed;
}

.btn-group {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
}

/* The group that leads somewhere — on the storefront, to the shed. Everything on
   the public pages is an argument for following that link, and at the ordinary
   15px the clay button sat at the same visual weight as the outlined one beside
   it, which reads as a pair of options rather than a way through.

   The size is on the GROUP, not on the one button, and that is the whole point.
   A 56px button standing next to a 44px one does not read as a hierarchy — it
   reads as two sizes of button, which is a mistake rather than an emphasis. What
   separates the two actions here is what separates them everywhere else in this
   system: the clay fill, the ink outline, and the arrow. Those work at any size,
   so the pair steps up together and the difference stays in the colour.

   Still one clay action per group. This makes that one louder; it does not
   license a second. 56px is also comfortably over the 48px DESIGN.md asks of a
   primary touch target, and 17px matches the paragraph the group follows. */
.btn-group--lead .btn {
  min-height: 56px;
  padding: 17px 32px;
  font-size: 17px;
}

/* The arrow only moves where somebody has asked for motion. */
.btn__arrow {
  transition: transform 140ms ease;
}

@media (prefers-reduced-motion: no-preference) {
  .btn-group--lead .btn:hover .btn__arrow {
    transform: translateX(3px);
  }
}

/* -----------------------------------------------------------------------------
   6. Forms
   -------------------------------------------------------------------------- */

.form {
  max-width: 420px;
}

.form--wide {
  max-width: 560px;
}

.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-bottom: var(--space-5);
}

/* The second selector is for a group of controls that is not one input — a
   segmented choice, a chip set — where a <label> would have nothing to point
   at and the group carries aria-labelledby instead. */
.field > label,
.field > .field__label {
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.01em;
}

.field__hint {
  font-size: 13px;
  line-height: 1.5;
  color: var(--ink-60);
}

.input,
.select,
.textarea {
  width: 100%;
  min-height: 44px;
  padding: 11px 14px;
  border: var(--hairline);
  border-radius: var(--radius-row);
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-ui);
  /* Never below 16px. iOS Safari zooms the page in on a smaller one, and the back
     office is used on an iPad. DESIGN.md, "Responsive and touch". */
  font-size: 16px;
  line-height: 1.5;
}

.textarea {
  min-height: 96px;
  resize: vertical;
}

.select {
  appearance: none;
  /* Chevron drawn in ink so no new hue enters the palette. */
  background-image: linear-gradient(45deg, transparent 50%, var(--ink) 50%),
    linear-gradient(135deg, var(--ink) 50%, transparent 50%);
  background-position: calc(100% - 19px) calc(50% + 2px), calc(100% - 14px) calc(50% + 2px);
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  padding-right: 38px;
}

.input::placeholder,
.textarea::placeholder {
  color: var(--ink-40);
}

.input:disabled,
.select:disabled {
  background: var(--sand);
  color: var(--ink-60);
}

.checkbox {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  font-size: 14px;
  min-height: 44px;
}

.checkbox input[type="checkbox"] {
  width: 18px;
  height: 18px;
  accent-color: var(--clay);
  margin: 0;
}

/* Validation. Clay carries the error, since the palette has no red. */
.field-error,
.validation-message {
  display: block;
  font-size: 13px;
  line-height: 1.5;
  color: var(--clay);
  font-weight: 500;
}

.input.invalid,
.select.invalid,
.textarea.invalid,
.input[aria-invalid="true"] {
  border-color: var(--clay);
  border-width: 2px;
}

.input.modified.valid {
  border-color: var(--moss);
}

.validation-summary {
  margin: 0 0 var(--space-5);
  padding: var(--space-4) var(--space-5);
  border: 2px solid var(--clay);
  border-radius: var(--radius-card);
  background: var(--paper);
  color: var(--ink);
  font-size: 14px;
}

.validation-summary ul {
  margin: 0;
  padding-left: var(--space-5);
}

.validation-summary:empty {
  display: none;
}

/* -----------------------------------------------------------------------------
   7. Notices — no red, no green fills; wheat means "act soon"
   -------------------------------------------------------------------------- */

.notice {
  display: flex;
  gap: var(--space-3);
  align-items: flex-start;
  padding: var(--space-4) var(--space-5);
  border: var(--hairline);
  border-radius: var(--radius-card);
  background: var(--sand);
  font-size: 14px;
  line-height: 1.6;
  margin: 0 0 var(--space-5);
}

.notice--error {
  border-color: var(--clay);
  border-width: 2px;
  background: var(--paper);
}

.notice--success {
  border-left: 4px solid var(--moss);
}

.notice--attention {
  border-left: 4px solid var(--wheat);
}

/* -----------------------------------------------------------------------------
   8. Cards, pills, status dots
   -------------------------------------------------------------------------- */

.card {
  border: var(--hairline);
  border-radius: var(--radius-card);
  background: var(--sand);
  padding: var(--space-5);
  /* No drop shadows. DESIGN.md, "Shape and space". */
}

.card--paper {
  background: var(--paper);
}

.card__media {
  aspect-ratio: 4 / 3;
  border-radius: var(--radius-card);
  overflow: hidden;
  margin-bottom: var(--space-4);
}

.card__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 5px 12px;
  border: var(--hairline);
  border-radius: var(--radius-pill);
  background: var(--sand);
  font-size: 12px;
  font-weight: 500;
  line-height: 1.4;
  white-space: nowrap;
}

.pill--attention {
  background: var(--wheat);
  /* Clay on wheat fails contrast, so the text stays ink. */
  color: var(--ink);
}

.pill--quiet {
  background: transparent;
  border-color: var(--ink-40);
  color: var(--ink-60);
}

.chip {
  display: inline-flex;
  align-items: center;
  padding: 3px 9px;
  border: var(--hairline);
  border-radius: var(--radius-row);
  background: var(--paper);
  font-size: 12px;
  font-weight: 500;
}

/* Moss appears in the logo, in status dots, and in success text. Nowhere else. */
.dot {
  width: 8px;
  height: 8px;
  border-radius: var(--radius-pill);
  background: var(--ink-40);
  flex: none;
}

.dot--open {
  background: var(--moss);
}

.dot--attention {
  background: var(--wheat);
}

.dot--closed {
  background: var(--ink-40);
}

.text-success {
  color: var(--moss);
  font-weight: 500;
}

/* -----------------------------------------------------------------------------
   9. Tables — 6px row radius, ink hairlines, sand rows
   -------------------------------------------------------------------------- */

.table-scroll {
  overflow-x: auto;
  border: var(--hairline);
  border-radius: var(--radius-card);
}

.table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  font-size: 14px;
  line-height: 1.5;
  background: var(--paper);
}

.table th {
  text-align: left;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-60);
  padding: var(--space-3) var(--space-4);
  border-bottom: var(--hairline);
  white-space: nowrap;
}

.table td {
  padding: var(--space-4);
  border-bottom: 1px solid var(--sand-shade);
  vertical-align: middle;
}

.table tbody tr:last-child td {
  border-bottom: 0;
}

.table tbody tr:nth-child(even) {
  background: var(--sand);
}

.table tbody tr:hover {
  background: var(--sand-shade);
}

/* A row that goes somewhere when tapped. The title inside it is a real link and
   keeps the house underline, which is the affordance on an iPad, where there is
   no hover to give it away. */
.table tr.is-link {
  cursor: pointer;
}

.table tr.is-attention td {
  background: var(--wheat);
}

.table__actions {
  display: flex;
  gap: var(--space-2);
  justify-content: flex-end;
  flex-wrap: wrap;
}

.table__empty {
  padding: var(--space-8) var(--space-5);
  text-align: center;
  color: var(--ink-60);
}

/* -----------------------------------------------------------------------------
   10. Logo and brand marks
   -------------------------------------------------------------------------- */

/* The mark sits on an ink tile or on photography, never straight on paper. */
.mark-tile {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  min-width: 40px; /* DESIGN.md minimum */
  background: var(--ink);
  border-radius: var(--radius-pill);
  flex: none;
  overflow: hidden;
}

.mark-tile--square {
  border-radius: var(--radius-card);
}

.mark-tile img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.wordmark {
  font-family: var(--font-display);
  font-size: 20px;
  font-weight: 400;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1;
  color: var(--ink);
}

.on-ink .wordmark,
.band--ink .wordmark {
  color: var(--paper);
}

.lockup {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  border-bottom: 0;
}

/* -----------------------------------------------------------------------------
   11. Navigation
   -------------------------------------------------------------------------- */

/* Pinned: the shop's navigation stays reachable however far down a menu runs.
   Above the week strip in the stack, because the strip pins underneath it. */
.masthead {
  position: sticky;
  top: 0;
  z-index: 3;
  border-bottom: var(--hairline);
  background: var(--paper);
}

.masthead__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap-columns);
  min-height: 76px;
  padding-block: var(--space-3);
}

.nav {
  display: flex;
  align-items: center;
  gap: var(--space-6);
  flex-wrap: wrap;
}

.nav__link {
  position: relative;
  font-size: 14px;
  font-weight: 500;
  color: var(--ink);
  border-bottom: 1px solid transparent;
  padding-block: var(--space-1);
  touch-action: manipulation;
}

/* A 44px tap target without a 44px box: padding would drag the active rule away
   from the word it underlines, so the hit area is grown with a pseudo-element
   instead. Inset 4px each side keeps 8px between adjacent links at the phone
   gap, which is what DESIGN.md asks for between targets. */
.nav__link::after {
  content: "";
  position: absolute;
  inset-block: -12px;
  inset-inline: -4px;
}

.nav__link:hover {
  border-bottom-color: var(--ink);
}

.nav__link.active {
  color: var(--clay);
  border-bottom-color: var(--clay);
}

/* The account button, signed in: a circle of initials that opens onto the account
   pages and sign out.

   A <details> element and no script. The storefront is static SSR with no circuit
   behind it, and a disclosure is exactly what <details> is for — the browser gives
   the toggle, the keyboard handling and the expanded state, and everything below
   is presentation. What it does not give is closing when you click elsewhere on
   the page; the trigger closes it, and so does following any link in it, because
   every one of them is a full page load. */
.account-menu {
  position: relative;
}

/* 44px of target around a 40px circle, so the whole thing is one round tap and
   the focus ring is round with it. 2px of padding does the centring rather than
   flex: display:block is what actually drops the disclosure triangle in every
   engine — a summary is a list-item, and a marker belongs to no other display —
   and Safari has had trouble toggling a flex summary in the past. The webkit
   pseudo-element is the same job again for the versions that predate all that. */
.account-menu__trigger {
  display: block;
  width: 44px;
  height: 44px;
  padding: 2px;
  border-radius: var(--radius-pill);
  cursor: pointer;
  list-style: none;
  touch-action: manipulation;
}

.account-menu__trigger::-webkit-details-marker {
  display: none;
}

/* Sand rather than clay: this is chrome, and DESIGN.md keeps the one clay action
   for what the page is actually for. Bold and slightly tight, because two capitals
   at 14px in a 40px circle read as a gap otherwise. */
.avatar {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border: var(--hairline);
  border-radius: var(--radius-pill);
  background: var(--sand);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: 14px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.01em;
  user-select: none;
}

.account-menu__trigger:hover .avatar,
.account-menu[open] .avatar {
  background: var(--sand-shade);
}

/* Hangs off the masthead's own stacking context, which is already above the page
   and above the shed strip. Paper and a hairline, no shadow: DESIGN.md gets its
   depth from the border and the panel is opaque, which is all that is needed to
   read over what scrolls under it. */
.account-menu__panel {
  position: absolute;
  top: calc(100% + var(--space-2));
  right: 0;
  z-index: 1;
  min-width: 232px;
  padding: var(--space-2);
  border: var(--hairline);
  border-radius: var(--radius-card);
  background: var(--paper);
}

/* Whose account this is. Two letters in a circle are an initial, not an answer,
   and the address had to go somewhere when it came out of the row. */
.account-menu__who {
  padding: var(--space-2) var(--space-3) var(--space-3);
  margin-bottom: var(--space-2);
  border-bottom: var(--hairline);
}

.account-menu__name {
  margin: 0;
  font-size: 14px;
  font-weight: 500;
  color: var(--ink);
}

/* An address is long and is here to be checked, not read: it wraps at any
   character rather than pushing the panel wider than the phone it is on. */
.account-menu__email {
  margin: var(--space-1) 0 0;
  font-size: 13px;
  line-height: 1.4;
  color: var(--ink-60);
  overflow-wrap: anywhere;
}

/* A link and a form button side by side in one list, so both are reset to the
   same thing. 44px each, the full width of the panel, with the row radius. */
.account-menu__item {
  display: flex;
  align-items: center;
  width: 100%;
  min-height: 44px;
  padding: var(--space-2) var(--space-3);
  border: 0;
  border-radius: var(--radius-row);
  background: transparent;
  font-family: var(--font-ui);
  font-size: 14px;
  font-weight: 500;
  line-height: 1.4;
  color: var(--ink);
  text-align: left;
  cursor: pointer;
  touch-action: manipulation;
}

.account-menu__item:hover {
  background: var(--sand-shade);
}

.account-menu__form {
  display: block;
  width: 100%;
}

/* Phone. The masthead no longer changes shape here — the drawer takes it over
   at 900px, which is section 15 — but .nav is also the footer's site map and
   that still wraps at this width, and the wordmark steps down with the rest of
   the type.

   This block used to argue that a hamburger for two links was chrome for its
   own sake. That was right when the nav was two links. It became five, plus a
   basket and an account control. */
@media (max-width: 720px) {
  .nav {
    gap: var(--space-4);
  }

  .wordmark {
    font-size: 17px;
  }
}

/* Back office: an ink rail so admin never gets mistaken for the storefront. */
.admin-shell {
  display: grid;
  min-height: 100vh;
  grid-template-columns: 1fr;
}

/* 1024px, not 900px. DESIGN.md puts sidebar layouts at 1024 and gives
   everything below an off-canvas drawer, because a 12.9" iPad in portrait is
   1024 CSS px and is the case that decides it. Below this the rail is a
   drawer, not a stacked ink block above every page. */
@media (min-width: 1024px) {
  .admin-shell {
    grid-template-columns: 248px minmax(0, 1fr);
  }
}

.admin-rail {
  background: var(--ink);
  color: var(--paper-dim);
  padding: var(--space-6) var(--space-5);
  border-right: var(--hairline);
  display: flex;
  flex-direction: column;
  gap: var(--space-7);
}

/* The drawer. Off-canvas behind the 48px button in the page header, sliding
   over the page rather than pushing it, so nothing reflows on open. */
@media (max-width: 1023px) {
  .admin-rail {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    z-index: 30;
    width: 288px;
    max-width: 86vw;
    overflow-y: auto;
    transform: translateX(-100%);
    transition: transform 160ms ease;
    padding-bottom: calc(var(--space-6) + env(safe-area-inset-bottom));
  }

  .admin-shell.is-drawer-open .admin-rail {
    transform: translateX(0);
  }
}

.admin-scrim {
  position: fixed;
  inset: 0;
  z-index: 29;
  border: 0;
  padding: 0;
  background: color-mix(in srgb, var(--ink) 55%, transparent);
  cursor: pointer;
}

@media (min-width: 1024px) {
  .admin-scrim {
    display: none;
  }
}

.admin-rail__nav {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.admin-rail__link {
  display: block;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-row);
  border-bottom: 0;
  color: var(--paper-dim);
  font-size: 14px;
  font-weight: 500;
}

.admin-rail__link:hover {
  background: color-mix(in srgb, var(--paper) 10%, transparent);
  color: var(--paper);
}

.admin-rail__link.active {
  background: var(--clay);
  color: var(--paper);
}

.admin-rail__label {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-40);
  padding-inline: var(--space-4);
  margin-bottom: var(--space-2);
}

/* A column, so a page that ends in an action bar can grow its body to the foot
   of the screen — see .admin-body--barred. */
.admin-main {
  display: flex;
  flex-direction: column;
  background: var(--paper);
  min-width: 0;
}

.admin-header {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  border-bottom: var(--hairline);
  padding: var(--space-7) var(--section-pad);
  background: var(--paper);
}

/* Below the drawer breakpoint the header is the chrome: it carries the menu
   button and it stays put, so the title and its action never scroll away.
   One bar, not two — 1180px of iPad is not deep enough for a title strip
   under a separate nav strip. */
@media (max-width: 1023px) {
  .admin-header {
    position: sticky;
    top: 0;
    z-index: 4;
    padding-block: var(--space-3);
  }

  .admin-header .display-page {
    font-size: 24px;
  }
}

.admin-header__menu {
  display: grid;
  place-items: center;
  width: 48px;
  height: 48px;
  flex: none;
  border: var(--hairline);
  border-radius: var(--radius-row);
  background: var(--paper);
  color: var(--ink);
  cursor: pointer;
  touch-action: manipulation;
}

.admin-header__menu:hover {
  background: var(--sand-shade);
}

@media (min-width: 1024px) {
  .admin-header__menu {
    display: none;
  }
}

.admin-body {
  padding: var(--space-7) var(--section-pad) var(--space-9);
}

/* -----------------------------------------------------------------------------
   12. Auth pages — one narrow column on paper
   -------------------------------------------------------------------------- */

.auth {
  display: grid;
  place-items: start center;
  min-height: 100vh;
  padding: var(--space-8) var(--section-pad) var(--space-9);
}

.auth__panel {
  width: 100%;
  max-width: 440px;
}

.auth__brand {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-8);
}

.auth__divider {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  margin-block: var(--space-6);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-40);
}

.auth__divider::before,
.auth__divider::after {
  content: "";
  flex: 1;
  border-top: var(--hairline);
}

.auth__aside {
  margin-top: var(--space-6);
  font-size: 14px;
  line-height: 1.6;
  color: var(--ink-60);
}

/* -----------------------------------------------------------------------------
   13. Image placeholders — used until the photography exists
   -------------------------------------------------------------------------- */

.placeholder {
  display: grid;
  place-items: center;
  background: repeating-linear-gradient(135deg, var(--sand) 0 8px, var(--sand-shade) 8px 16px);
  border-radius: var(--radius-card);
  min-height: 160px;
  padding: var(--space-5);
}

.placeholder--flush {
  border-radius: 0;
  border-block: var(--hairline);
}

.placeholder--hero {
  min-height: clamp(240px, 38vw, 420px);
}

.placeholder__label {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-60);
  text-align: center;
}

/* -----------------------------------------------------------------------------
   14. Utilities
   -------------------------------------------------------------------------- */

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

.mt-0 { margin-top: 0; }
.mb-0 { margin-bottom: 0; }
.mt-4 { margin-top: var(--space-4); }
.mt-6 { margin-top: var(--space-6); }
.mb-6 { margin-bottom: var(--space-6); }
.mt-7 { margin-top: var(--space-7); }

.text-right { text-align: right; }
.nowrap { white-space: nowrap; }
.grow { flex: 1; }

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    /* Delays as well as durations. The drawer's close is a transition-delay on
       visibility rather than a duration — zeroing only the duration would leave
       the sheet in the accessibility tree for 260ms after it had gone. */
    transition-delay: 0s !important;
  }
}

/* -----------------------------------------------------------------------------
   15. Editors — the ingredient and recipe forms in the back office

   Nothing new enters the palette here: these are layout rules over the tokens
   in section 1, the three radii, and ink hairlines instead of shadows.
   -------------------------------------------------------------------------- */

/* A list header that carries filters on the left and one action on the right. */
.toolbar {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-5);
  flex-wrap: wrap;
}

.toolbar__filters {
  display: flex;
  align-items: flex-end;
  gap: var(--space-4);
  flex-wrap: wrap;
}

/* A field sitting in a row of controls rather than stacked in a form. */
.field--inline {
  margin-bottom: 0;
}

.field--search {
  min-width: 240px;
}

.form--full {
  max-width: 780px;
}

/* Fields that belong together on one line: pack size next to its unit. */
.field-row {
  display: grid;
  gap: 0 var(--space-4);
  grid-template-columns: 1fr;
}

@media (min-width: 640px) {
  .field-row--2 {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .field-row--3 {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

/* The fourteen allergens. Wide enough for the longest label at 14px. */
.check-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(152px, 1fr));
  gap: 0 var(--space-4);
}

/* A bordered group inside a form, for the allergen set and the line list. */
.fieldset {
  border: var(--hairline);
  border-radius: var(--radius-card);
  padding: var(--space-5);
  margin: 0 0 var(--space-5);
}

.fieldset > legend {
  padding: 0 var(--space-2);
  font-size: 13px;
  font-weight: 500;
}

/* Totals under the line list: label left, tabular figure right. */
.summary {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding-top: var(--space-4);
  border-top: var(--hairline);
}

.summary__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
  font-size: 14px;
}

.summary__row > :last-child {
  font-variant-numeric: tabular-nums;
  font-weight: 500;
}

/* A cell holding several small facts, e.g. allergen chips in a table row. */
.chip-set {
  display: flex;
  gap: var(--space-2);
  flex-wrap: wrap;
}

/* -----------------------------------------------------------------------------
   16. Item picker — the list inside the add/edit dialog

   MudBlazor draws the dialog itself; these are the rows inside it, kept as plain
   buttons so the search filter stays trivial and the hit target stays honest.
   -------------------------------------------------------------------------- */

.picker {
  border: var(--hairline);
  border-radius: var(--radius-row);
  max-height: 280px;
  overflow-y: auto;
  background: var(--paper);
}

.picker__option {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
  width: 100%;
  min-height: 44px;
  padding: var(--space-3) var(--space-4);
  border: 0;
  border-bottom: 1px solid var(--sand-shade);
  background: transparent;
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: 15px;
  line-height: 1.4;
  text-align: left;
  cursor: pointer;
}

.picker__option:last-child {
  border-bottom: 0;
}

.picker__option:hover {
  background: var(--sand-shade);
}

/* Sand for the chosen row: the same "this one" treatment table rows use. */
.picker__option.is-selected {
  background: var(--sand);
  font-weight: 500;
}

.picker__name {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}

.picker__meta {
  color: var(--ink-60);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.picker__empty {
  margin: 0;
  padding: var(--space-6) var(--space-4);
  color: var(--ink-60);
  text-align: center;
  font-size: 14px;
}

/* -----------------------------------------------------------------------------
   17. Action bar — the sticky footer a long editor saves from

   DESIGN.md asks long forms for an action bar rather than a button the baker has
   to scroll to find. It carries a running figure on the left so the bar is worth
   its height on a screen where height is what you are short of.
   -------------------------------------------------------------------------- */

.action-bar {
  position: sticky;
  bottom: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
  margin-top: var(--space-7);
  padding: var(--space-4) var(--space-5);
  /* Padded for the home indicator on an iPad, per DESIGN.md's touch rules. */
  padding-bottom: calc(var(--space-4) + env(safe-area-inset-bottom));
  border: var(--hairline);
  border-radius: var(--radius-card);
  background: var(--sand);
}

.action-bar__figures {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.action-bar__total {
  font-size: 16px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* Row actions are tapped repeatedly on a floury iPad, so they get the 48px
   target DESIGN.md reserves for primary and destructive controls. */
.btn--touch {
  min-height: 48px;
  padding: 12px 22px;
}

/* A header row carrying a heading on the left and a control on the right. */
.section-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
  margin-bottom: var(--space-5);
}

/* -----------------------------------------------------------------------------
   18. Storefront menu — the published week

   The customer side of a bake week. Nothing new enters the palette: an ink strip
   for the deadline, the existing card and grid rules for the listings.
   -------------------------------------------------------------------------- */

/* Whether the shop is open and how long is left, pinned above the menu. Ink
   rather than sand because it is chrome, not another card, and because it has to
   stay legible over whatever scrolls under it. */
.shed-strip {
  position: sticky;
  /* Pins directly under the masthead rather than over it, so the nav and the
     closing time are both on screen the whole way down. */
  top: var(--masthead-h);
  z-index: 2;
  border-bottom: var(--hairline);
  /* Chrome, not a band. One line, so it costs about 48px rather than the 84px a
     stacked label beside a display numeral was taking — at that height it read as
     a second masthead and pushed the shed off the first screen. */
  padding-block: var(--space-3);
}

.shed-strip__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4) var(--gap-columns);
  flex-wrap: wrap;
}

/* The closing time, the one thing on the strip worth emphasis: the display
   face in full paper against the dimmed sans around it. 22px with a line-height
   of 1, so it reads as the figure it is without making the strip taller than the
   14px line beside it. DESIGN.md keeps Newsreader at 20px and up. */
.shed-strip__count {
  font-family: var(--font-display);
  font-size: 22px;
  font-weight: 400;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--paper);
  white-space: nowrap;
}

/* Order by, collect, where — the three facts, ruled apart. */
.dateline {
  padding: 0;
}

/* A dateline is as tall as the facts in it and no taller. As a direct child of
   a .columns grid it was stretching to match the prose column beside it, which
   on the home page meant a two-row card drawn at the height of a heading, three
   paragraphs and a button — a hairline box with most of a section of nothing
   inside it. Grid items stretch by default; this is the one shape here that must
   not, because its rows are ruled and the rule under the last one is removed. */
.columns > .dateline {
  align-self: start;
}

/* Label and value on one line, not stacked: three facts read as a short table,
   and the card costs about 170px rather than the 273px three stacked blocks
   took. Row padding matches .table td, because that is what this is. */
.dateline__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-4) var(--space-5);
  border-bottom: var(--hairline);
}

.dateline__row:last-child {
  border-bottom: 0;
}

.dateline__row > .eyebrow {
  flex: none;
  white-space: nowrap;
}

/* The row is flex, so nothing collapses and every paragraph in one charges the
   row the browser's default 1em above and below it. That is what quietly put
   this card back to the ~270px the row shape was invented to avoid: a single
   17px value line cost 88px rather than 56, and stacked address lines paid 16px
   between each pair on top of the stack's own gap. The row padding and the
   stack gap are the spacing here; a paragraph brings none of its own. */
.dateline__row p {
  margin-block: 0;
}

.dateline__value {
  font-size: 17px;
  font-weight: 500;
  line-height: 1.4;
  text-align: right;
  /* A what3words triple is one token with nothing to break at, and an address
     line is not much better. Same call as .reach__value, and for the same
     reason: without this the widest word in here becomes the floor under the
     whole page. */
  overflow-wrap: anywhere;
}

/* Label over value on a phone, the way .reach__row already does it.

   The row is label-beside-value because three facts read as a short table, and
   that holds right up until the label stops leaving the value room to wrap. The
   arithmetic, measured rather than guessed: the eyebrow is white-space: nowrap,
   so "Collection window" is an immovable 149px; the value beside it cannot go
   below its longest word; and 40px of row padding sits around both. That puts a
   331px floor under the card, the grid column it sits in, and therefore the
   page — which is 59px wider than a 320px phone has to give, and is what put a
   sideways scroll on /contact.

   Stacking removes the nowrap label from the same line as the value rather than
   letting the eyebrow wrap to "COLLECTION / WINDOW", which is the other way out
   and an uglier one. 560px because that is where the consent banner already
   breaks, not a new number. */
@media (max-width: 560px) {
  .dateline__row {
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-1);
  }

  /* Stacked, the value sits under its own label, so ranging it right would set
     it against the far edge of a card whose label starts at the near one. */
  .dateline__row .dateline__value,
  .dateline__row .text-right {
    text-align: left;
  }
}

/* The first listing in the reading order gets the wide treatment. The baker
   picks it by ordering the menu, so nothing has to be flagged as featured. */
.feature {
  display: grid;
  gap: var(--gap-columns);
  align-items: center;
}

@media (min-width: 900px) {
  .feature {
    grid-template-columns: 1fr 1fr;
  }
}

/* Every other row of the home page's range, with the picture on the right.

   In CSS rather than in the markup, and only where there are two columns to swap.
   The picture comes first in the document on every row, so a phone reads picture
   then words the whole way down; alternating the source order instead would make
   a narrow screen read words-then-picture on every second row for no reason
   anybody scrolling could see. */
@media (min-width: 900px) {
  .feature--flip .feature__media {
    order: 2;
  }
}

/* A feature with no picture yet. There is no placeholder standing in for the
   photograph, so there is no second column either: the words take the row on
   their own, held to the lead measure so a line never runs the width of the
   shell, and set off by a hairline above so the row still reads as one of the
   run rather than as stray prose between two pictures. Comes after the
   two-column rule above so it wins at every width. */
.feature--bare {
  grid-template-columns: minmax(0, 1fr);
  max-width: var(--measure-lead);
  padding-top: var(--space-5);
  border-top: var(--hairline);
}

.feature__media {
  aspect-ratio: 3 / 2;
  border-radius: var(--radius-card);
  overflow: hidden;
  min-height: 0;
}

.feature__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Price and what is left, under a light rule. The sand-shade line keeps this
   subordinate to the ink hairline round the card itself. */
.card__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
  margin-top: var(--space-4);
  padding-top: var(--space-4);
  border-top: 1px solid var(--sand-shade);
}

.feature .card__foot {
  border-top: var(--hairline);
}

/* -----------------------------------------------------------------------------
   19. Product page — one listing on its own

   Same tokens and the same card vocabulary as the menu; only the picture is
   bigger and the title is a heading rather than a card.
   -------------------------------------------------------------------------- */

/* Squarer than a card thumbnail: this is the only picture on the page. */
.product__media {
  aspect-ratio: 4 / 3;
  border-radius: var(--radius-card);
  overflow: hidden;
  min-height: 0;
}

.product__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The other pictures on a product's page: a row of small ones under the main
   picture, three across, each a link to the picture full size. */
.product__gallery {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-3);
  margin: 0;
  padding: 0;
  list-style: none;
}

.product__gallery a {
  display: block;
  aspect-ratio: 4 / 3;
  border-radius: var(--radius-row);
  overflow: hidden;
}

.product__gallery img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Back to the menu. An arrow and a link, kept to a 44px target because it is
   the way off this page on a phone. */
.back-link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  align-self: flex-start;
  min-height: 44px;
  font-size: 14px;
  font-weight: 500;
  color: var(--clay);
  border-bottom: 0;
  touch-action: manipulation;
}

.back-link:hover {
  border-bottom: 0;
  text-decoration: underline;
}

/* A card title that is a link. The card is already the affordance, so the title
   stays ink and only picks up clay on hover — a grid of clay headings would put
   six accent colours where DESIGN.md allows one. */
.card__link {
  color: inherit;
  border-bottom: 0;
}

.card__link:hover {
  border-bottom: 0;
}

/* The whole card goes through to the item.

   The anchor stays on the title and is stretched over the card with a
   pseudo-element, rather than wrapping the picture and the copy in one <a>. That
   keeps the accessible name the product name instead of a run of card text, and
   it needs no JavaScript — which the storefront does not load. */
.card--link {
  position: relative;
  transition: border-color 120ms ease;
}

/* Hover does not touch the fill.

   It used to go to sand, which is the background of the very band the menu cards
   sit on — so hovering made a card dissolve into the page instead of standing out
   from it. There is no surface lighter than paper to go to either, so the signal
   moves to the edge: the border and the title both pick up clay, which is the
   colour everything clickable already uses. One change, in the action colour, and
   the card keeps its own ground. */
.card--link:hover {
  border-color: var(--clay);
}

.card--link:hover .card__link {
  color: var(--clay);
}

/* Scoped to a card that opted in. Unscoped, this overlay has no positioned
   ancestor wherever card__link is used outside a card — the basket page uses
   it for a plain title link — so it resolves against the initial containing
   block and covers the whole page, swallowing every click on it. */
.card--link .card__link::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: var(--radius-card);
}

/* The ring belongs on the card, since the card is what the link now covers. */
.card--link:focus-within {
  outline: 2px solid var(--clay);
  outline-offset: 2px;
}

.card--link .card__link:focus-visible {
  outline: none;
}

/* Anything with an action of its own inside a clickable card — an add button, a
   stepper — has to sit above the overlay or the card swallows the tap. */
.card__above {
  position: relative;
  z-index: 1;
}

/* The way through to a product's own page.

   The card titles are links as well, but .card__link above deliberately strips the
   house underline so a heading does not read as a link inside a card. That is right
   when the whole card is clickable and wrong when it is not — and the shed cards
   never opted into .card--link, so the range pages were reachable only by somebody
   who thought to click a heading that gave no sign of being one. This is the
   affordance that says so out loud.

   A plain link rather than a second button: the card already spends its one action
   on the basket, and DESIGN.md allows one clay action per view group. It keeps the
   house underline from the base `a` rule precisely because that is what makes it
   read as a link at a glance. */
.card__more {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 44px;
  font-family: var(--font-ui);
  font-size: 14px;
  font-weight: 500;
  white-space: nowrap;
}

/* The arrow leans out on hover. A pseudo-element so the markup stays a single
   anchor, and decoration only — the underline is the affordance, so nothing is
   lost where a pointer never hovers or where the character does not render. */
.card__more::after {
  content: "\2192";
  display: inline-block;
  transition: transform 120ms ease;
}

.card__more:hover::after,
.card__more:focus-visible::after {
  transform: translateX(2px);
}

/* -----------------------------------------------------------------------------
   20. Basket — the pinned bar and the basket page

   The bar is the one piece of storefront chrome that appears because of something
   the customer did, so it is ink like the masthead and the week strip rather than
   another sand surface.
   -------------------------------------------------------------------------- */

.basket-bar {
  position: sticky;
  bottom: 0;
  z-index: 2;
  border-top: var(--hairline);
  /* Ink meeting ink: the footer sits directly under this at full scroll, and an
     ink hairline between two ink surfaces is invisible. */
  border-bottom: 1px solid var(--ink-40);
  padding-block: var(--space-4);
  /* Clear of the home indicator on a phone. DESIGN.md, "Touch". */
  padding-bottom: calc(var(--space-4) + env(safe-area-inset-bottom));
}

.basket-bar__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4) var(--gap-columns);
  flex-wrap: wrap;
}

/* The lines on the basket page. The table idiom from section 9, warmed up: an ink
   hairline round the whole thing, sand-shade between the rows. */
.basket-list {
  border: var(--hairline);
  border-radius: var(--radius-card);
  background: var(--paper);
  overflow: hidden;
}

.basket-line {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--space-3) var(--space-5);
  align-items: center;
  padding: var(--space-5);
  border-bottom: 1px solid var(--sand-shade);
}

.basket-line:last-child {
  border-bottom: 0;
}

/* The line title is a link, and card__link deliberately carries no underline.
   Inside a card its hover colour comes from the card; there is no card here, so
   the line supplies it — otherwise the one link in the row reads as plain text. */
.basket-line .card__link {
  transition: color 120ms ease;
}

.basket-line .card__link:hover {
  color: var(--clay);
}

.basket-line__total {
  font-size: 18px;
}

/* Two columns on a phone: the name over the controls. At tablet width and up the
   line reads across — name, stepper, total, remove. */
@media (min-width: 721px) {
  .basket-line {
    grid-template-columns: minmax(0, 1fr) auto 92px auto;
  }
}

@media (max-width: 720px) {
  .basket-line__total {
    justify-self: end;
  }
}

/* -----------------------------------------------------------------------------
   21. Quantity stepper — minus, the number, plus

   One control rather than two buttons that happen to sit together, which is why
   both ends carry the clay fill: DESIGN.md's one-clay-per-group rule is about two
   competing actions, and these two are halves of the same one.
   -------------------------------------------------------------------------- */

.stepper {
  display: flex;
  align-items: center;
  /* 8px, the minimum DESIGN.md leaves between adjacent targets. */
  gap: var(--space-2);
}

.stepper__btn {
  display: grid;
  place-items: center;
  /* 48px, not 44: DESIGN.md reserves the larger target for repeatedly-tapped
     controls and names quantity steppers as one. */
  width: 48px;
  height: 48px;
  padding: 0;
  border: 1px solid var(--clay);
  border-radius: var(--radius-pill);
  background: var(--clay);
  color: var(--paper);
  cursor: pointer;
  touch-action: manipulation;
  transition: background-color 120ms ease, border-color 120ms ease;
}

/* The same darkening .btn--primary uses, so a clay circle and a clay pill behave
   identically under the finger. */
.stepper__btn:hover:not(:disabled) {
  background: color-mix(in srgb, var(--clay) 88%, var(--ink));
  border-color: color-mix(in srgb, var(--clay) 88%, var(--ink));
}

/* At the pre-order allowance. Outlined and quiet rather than a faded clay fill,
   so the ceiling reads as a ceiling instead of a button ignoring taps. */
.stepper__btn:disabled {
  background: transparent;
  border-color: var(--ink-40);
  color: var(--ink-40);
  cursor: not-allowed;
}

.stepper__count {
  min-width: 28px;
  text-align: center;
  font-size: 16px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* -----------------------------------------------------------------------------
   22. Native date and time inputs

   `InputDate` renders `<input type="date">` / `type="time"`, and the box is only
   half of what a browser draws: the calendar glyph, the segmented digits and the
   spin buttons are all browser chrome, in browser colours, and they are what makes
   an otherwise-themed form look unfinished.

   Only Chromium exposes these parts. Firefox draws its own and ignores all of it;
   the field still gets the `.input` box, which is the floor either way.
   -------------------------------------------------------------------------- */

/* The digits. Public Sans and tabular, so a column of dates does not shuffle
   sideways as the numbers change. */
.input::-webkit-datetime-edit {
  font-family: var(--font-ui);
  font-size: 16px;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  padding: 0;
}

/* The slashes and colons between the fields, which should not compete with the
   numbers either side of them. */
.input::-webkit-datetime-edit-text {
  color: var(--ink-40);
  padding: 0 2px;
}

/* An empty field shows its placeholder — dd, mm, yyyy — and it is a hint, not a
   value. */
.input::-webkit-datetime-edit-day-field,
.input::-webkit-datetime-edit-month-field,
.input::-webkit-datetime-edit-year-field,
.input::-webkit-datetime-edit-hour-field,
.input::-webkit-datetime-edit-minute-field,
.input::-webkit-datetime-edit-ampm-field {
  padding: 1px var(--space-1);
  border-radius: var(--radius-row);
}

/* The segment being typed into stays Chromium's blue, and there is no reaching it.
   Tried and measured, so nobody spends the afternoon again: a background on
   `::-webkit-datetime-edit-*-field:focus` never applies (focus is on the input,
   not the field), `input::selection` does not reach inside the control, and
   `accent-color` does not govern this highlight. It is drawn from the platform's
   active-selection colour inside the shadow tree.

   It is transient — visible only while a segment is being edited — so the field is
   themed and the highlight is left alone. If that blue ever has to go, the fix is
   to stop using a native input here and take MudBlazor's picker, which is themed
   further down in counter-warm-mud.css. */

/* Material's spinners and the little clear cross: both duplicate what the keyboard
   and the field already do, and neither can be made to look like anything here. */
.input::-webkit-inner-spin-button,
.input::-webkit-clear-button {
  display: none;
}

/* The picker button. A browser draws its own grey-blue glyph; this replaces it
   with the shape the rest of the app uses.

   The icon is a mask over a token fill rather than a coloured SVG, because a
   data-URI cannot read a custom property — this way the glyph still comes from
   `--ink-60` and no hex enters the file outside the token table. */
.input[type="date"]::-webkit-calendar-picker-indicator,
.input[type="time"]::-webkit-calendar-picker-indicator,
.input[type="datetime-local"]::-webkit-calendar-picker-indicator {
  width: 20px;
  height: 20px;
  /* A 44px reach on a 20px glyph: the back office is driven with a fingertip. */
  padding: 12px;
  margin: 0 calc(var(--space-1) * -1) 0 0;
  background-color: var(--ink-60);
  background-image: none;
  opacity: 1;
  cursor: pointer;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
  -webkit-mask-size: 20px 20px;
  mask-size: 20px 20px;
}

.input[type="date"]::-webkit-calendar-picker-indicator,
.input[type="datetime-local"]::-webkit-calendar-picker-indicator {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round'%3E%3Crect x='3.25' y='5.25' width='13.5' height='11.5' rx='1.5'/%3E%3Cpath d='M3.25 9.25h13.5M6.75 3.25v3M13.25 3.25v3'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round'%3E%3Crect x='3.25' y='5.25' width='13.5' height='11.5' rx='1.5'/%3E%3Cpath d='M3.25 9.25h13.5M6.75 3.25v3M13.25 3.25v3'/%3E%3C/svg%3E");
}

.input[type="time"]::-webkit-calendar-picker-indicator {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='10' cy='10' r='6.75'/%3E%3Cpath d='M10 5.75V10l2.75 1.75'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='10' cy='10' r='6.75'/%3E%3Cpath d='M10 5.75V10l2.75 1.75'/%3E%3C/svg%3E");
}

.input[type="date"]:hover::-webkit-calendar-picker-indicator,
.input[type="time"]:hover::-webkit-calendar-picker-indicator {
  background-color: var(--ink);
}

/* A week that has gone live has its commencing date fixed, and the glyph should
   say so rather than opening a calendar that refuses. */
.input:disabled::-webkit-calendar-picker-indicator {
  background-color: var(--ink-40);
  cursor: not-allowed;
}

.input:disabled::-webkit-datetime-edit {
  color: var(--ink-60);
}

/* -----------------------------------------------------------------------------
   23. Checkout, confirmation and the orders screen

   Paying is a task rather than a page to browse, so the column is capped and the
   summary rides alongside. The order number is the one thing a customer carries
   to the counter, so it is treated as an object rather than as a line of text.
   -------------------------------------------------------------------------- */

/* A receipt reads narrow, whatever the window is doing. */
.receipt {
  max-width: 560px;
}

/* The order number, as an object rather than a line of text: it is what somebody
   reads out at the counter and what a label will carry. Two-pixel border because
   this one thing may outrank a hairline. */
.numberplate {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-6);
  border: 2px solid var(--ink);
  border-radius: var(--radius-card);
  background: var(--paper);
}

.numberplate__code {
  font-family: var(--font-ui);
  font-size: 34px;
  font-weight: 700;
  letter-spacing: 0.08em;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

/* Moss is logo, status and success only. A confirmed payment is what it was being
   kept for — DESIGN.md, "Colours". */
.dot--paid {
  background: var(--moss);
}

.paid-mark {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: 14px;
  font-weight: 500;
  color: var(--moss);
}

/* The same line for a state that is not a success. A shut shed is a fact about
   a shop working as intended, not a failure, and moss is reserved for the logo,
   for "open now" and for success — so this reads in the surrounding ink tint,
   and in the board's own body colour when it is drawn on ink. */
.status-line {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  font-size: 14px;
  font-weight: 500;
  color: var(--ink-60);
}

.board .status-line,
.band--ink .status-line {
  color: var(--paper-dim);
}

/* The figures a baker acts on, before any table asks them to read. */
.figures {
  display: grid;
  gap: var(--gap-cards);
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
}

.figure {
  border: var(--hairline);
  border-radius: var(--radius-card);
  background: var(--paper);
  padding: var(--space-5);
}

.figure__value {
  display: block;
  margin-top: var(--space-2);
  font-family: var(--font-display);
  font-size: clamp(28px, 3.6vw, 34px);
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
   24. Form patterns — the back office on an iPad

   Six additions, all of them ways of making a long form shorter without
   removing anything from it. Nothing new enters the palette: no fourth radius,
   no fifth hue, no shadow, and clay stays spent on the one save action.
   -------------------------------------------------------------------------- */

/* `.fig` lives up with the type scale, beside the `.numeral` it exists to keep
   out of table cells. */

/* A value that is a prompt rather than an answer. */
.dim {
  color: var(--ink-40);
}

/* --- Fields inside a gapped container --------------------------------------

   A plain form gets its vertical rhythm from `.field`'s own bottom margin, and
   `.field-row` leans on that deliberately — see its comment. A flex container
   with a `gap` already owns that rhythm, so a field inside one would add 20px on
   top of the gap and read as a 44px hole. Zero the margin there and give the row
   the gap it stops getting from its children. */
.stack > .field,
.stack > .field-row > .field,
.sect__body > .field,
.sect__body > .field-row > .field,
.line-open__body > .field,
.line-open__body > .field-row > .field {
  margin-bottom: 0;
}

.stack > .field-row,
.sect__body > .field-row,
.line-open__body > .field-row {
  row-gap: var(--space-5);
}

/* --- Columns that only exist where there is room ---------------------------

   DESIGN.md lets a wide table drop columns below 1024px in a stated priority
   order. What survives is the name, the numbers being edited, the price and the
   row's action; what goes is carried as a meta line under the name instead, so
   the figure is still on the screen and only the column is gone. */
@media (max-width: 1023px) {
  .table .col-wide {
    display: none;
  }
}

/* --- A row of figures under a card's headline ------------------------------

   A flex row of clamp()-sized serif numbers wraps at a different place on every
   screen, and five of them do not fit across an iPad in portrait at all. A grid
   puts them in columns that stay where they are put, and the figures take a
   fixed size rather than .numeral's viewport-relative one. */
.stat-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: var(--space-5);
  margin-top: var(--space-5);
  padding-top: var(--space-5);
  border-top: 1px solid var(--sand-shade);
}

.stat {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}

.stat__value {
  font-family: var(--font-display);
  font-size: 28px;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
  margin: 0;
}

.row-meta {
  display: none;
}

@media (max-width: 1023px) {
  .row-meta {
    display: block;
    margin-top: var(--space-1);
    font-size: 13px;
    line-height: 1.4;
    color: var(--ink-60);
    font-variant-numeric: tabular-nums;
  }
}

/* --- The affix -------------------------------------------------------------

   Most hint paragraphs in the back office say only what a number is counted
   in: "Pence per kWh", "Kilowatts, off the plate on the back", "Counted in
   grams". The unit belongs in the control, where it is read at the moment it
   is needed and costs no vertical space. */
.control {
  display: flex;
  align-items: stretch;
  min-height: 44px;
  border: var(--hairline);
  border-radius: var(--radius-row);
  background: var(--paper);
  overflow: hidden;
}

.control > .input,
.control > .select,
.control > .textarea {
  border: 0;
  border-radius: 0;
  background: transparent;
  min-width: 0;
}

.control > .input:focus-visible,
.control > .select:focus-visible {
  outline-offset: -2px;
}

.control__affix {
  display: flex;
  align-items: center;
  padding: 0 14px;
  background: var(--sand);
  color: var(--ink-60);
  font-size: 14px;
  font-weight: 500;
  line-height: 1;
  white-space: nowrap;
  flex: none;
}

.control__affix--lead {
  border-right: var(--hairline);
}

.control__affix--trail {
  border-left: var(--hairline);
}

/* A unit picker as the affix, rather than a second control beside the first. */
.control > .select.control__picker {
  flex: none;
  width: auto;
  min-width: 92px;
  border-left: var(--hairline);
  background-color: var(--sand);
  color: var(--ink-60);
  font-size: 14px;
  font-weight: 500;
}

.control:has(> .input:disabled) {
  background: var(--sand);
}

/* Blazor stamps modified/valid/invalid straight onto the input, and those rules
   are more specific than `.control > .input`, so the border they set would come
   back inside a control that has already drawn one. Move the state to the
   control and keep the child flat. */
.control > .input.modified.valid,
.control > .input.invalid,
.control > .input[aria-invalid="true"] {
  border: 0;
}

.control:has(> .input.invalid),
.control:has(> .input[aria-invalid="true"]) {
  border-color: var(--clay);
  border-width: 2px;
}

.control:has(> .input.modified.valid) {
  border-color: var(--moss);
}

/* --- Segmented choice ------------------------------------------------------

   For two readings that are peers, and for tabs. Replaces a checkbox with a
   different explanatory paragraph under each state, which reflows the page
   under the finger that just tapped it. Ink when on: clay is spent on save. */
.seg {
  display: grid;
  border: var(--hairline);
  border-radius: var(--radius-pill);
  overflow: hidden;
  background: var(--paper);
}

.seg--2 {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.seg--3 {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

.seg__opt {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 48px;
  padding: 0 var(--space-4);
  border: 0;
  border-right: var(--hairline);
  background: transparent;
  color: var(--ink-60);
  font-family: var(--font-ui);
  font-size: 14px;
  font-weight: 500;
  line-height: 1.3;
  text-align: center;
  cursor: pointer;
  touch-action: manipulation;
}

.seg__opt:last-child {
  border-right: 0;
}

.seg__opt:hover:not(.is-on):not(:disabled) {
  background: var(--sand-shade);
  color: var(--ink);
}

.seg__opt.is-on {
  background: var(--ink);
  color: var(--paper);
  font-weight: 600;
}

.seg__opt:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* --- Toggle chips ----------------------------------------------------------

   The fourteen allergens, and the write-off reasons. Fourteen 44px checkbox
   rows is 176px of column and reads as a wall; two rows of chips is legible
   at arm's length and every one of them is a proper target. */
.toggles {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 44px;
  padding: 0 18px;
  border: var(--hairline);
  border-radius: var(--radius-pill);
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: 14px;
  font-weight: 500;
  line-height: 1.3;
  cursor: pointer;
  touch-action: manipulation;
}

.toggle:hover:not(.is-on) {
  background: var(--sand-shade);
}

.toggle.is-on {
  background: var(--ink);
  color: var(--paper);
}

/* --- The section panel -----------------------------------------------------

   A long editor is a handful of questions, not one column of thirty fields.
   Each panel carries its own answer on its head, so the whole thing can be
   read shut, and one is open at a time. */
.sect {
  border: var(--hairline);
  border-radius: var(--radius-card);
  background: var(--paper);
  overflow: hidden;
}

.sect__head {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  width: 100%;
  min-height: 60px;
  padding: var(--space-3) var(--space-5);
  border: 0;
  background: transparent;
  color: var(--ink);
  font-family: var(--font-ui);
  text-align: left;
  cursor: pointer;
  touch-action: manipulation;
}

.sect__head:hover {
  background: var(--sand-shade);
}

.sect.is-open > .sect__head {
  background: var(--sand);
  border-bottom: var(--hairline);
}

.sect.is-open > .sect__head:hover {
  background: var(--sand-shade);
}

.sect__label {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1;
  min-width: 0;
}

.sect__title {
  font-family: var(--font-display);
  font-size: 20px;
  line-height: 1.25;
}

.sect__answer {
  font-size: 13px;
  line-height: 1.4;
  color: var(--ink-60);
}

.sect__caret {
  flex: none;
  transition: transform 140ms ease;
}

.sect.is-open .sect__caret {
  transform: rotate(180deg);
}

.sect__body {
  padding: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

/* One note per section, in place of a hint under every field in it. */
.sect__note {
  font-size: 13px;
  line-height: 1.5;
  color: var(--ink-60);
  max-width: 62ch;
  margin: 0;
}

/* --- Rows that open --------------------------------------------------------

   DESIGN.md lets a table become stacked cards below 1024px. One 48px Edit per
   row rather than Edit beside Remove: a floury fingertip next to a Remove is
   how a line disappears. Remove lives inside the opened row, beside what it
   would delete. */
.line-list {
  border: var(--hairline);
  border-radius: var(--radius-card);
  background: var(--paper);
  overflow: hidden;
}

.line-card {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--sand-shade);
}

.line-card:last-child {
  border-bottom: 0;
}

.line-card:nth-of-type(even) {
  background: var(--sand);
}

.line-card__name {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  flex: 1;
  min-width: 0;
  font-size: 15px;
  font-weight: 500;
}

.line-card__meta {
  font-size: 13px;
  line-height: 1.4;
  color: var(--ink-60);
  font-variant-numeric: tabular-nums;
}

.line-card__figure {
  font-size: 15px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* The row, opened. It replaces itself in place, so nothing above or below it
   moves and what is being edited is under the finger that asked for it. */
.line-open {
  border-bottom: 1px solid var(--sand-shade);
  background: var(--paper);
}

.line-open:last-child {
  border-bottom: 0;
}

.line-open__head {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-3) var(--space-4);
  border-bottom: var(--hairline);
  background: var(--sand);
}

.line-open__title {
  flex: 1;
  min-width: 0;
  font-family: var(--font-display);
  font-size: 20px;
  line-height: 1.25;
}

.line-open__body {
  padding: var(--space-5) var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

/* --- Chrome ---------------------------------------------------------------- */

/* The action bar, run edge to edge. On a tablet the inset card spends the width
   it exists to save. Pulled out through .admin-body's own side padding, which is
   why it has to be a child of it and not of the form it saves — the submit
   button carries a `form` attribute instead. */
.action-bar--flush {
  border-radius: 0;
  border-left: 0;
  border-right: 0;
  border-bottom: 0;
  margin-top: 0;
  margin-inline: calc(var(--section-pad) * -1);
  padding-inline: var(--section-pad);
}

/* A page that ends in a flush bar. The bar is the bottom edge, so the body must
   not pad below it or the bar sticks to a gap. */
.admin-body--barred {
  flex-grow: 1;
  padding-bottom: 0;
}

/* Sticky only takes hold once the page scrolls. On a short one, a shed day with
   two lines in it, the bar sat straight under the content halfway up the screen.
   The body fills the height instead and the bar takes up the slack above itself,
   so it is on the bottom edge whether the page scrolls or not. */
.admin-body--barred > .action-bar--flush {
  margin-top: auto;
}

.icon-btn {
  display: grid;
  place-items: center;
  width: 48px;
  height: 48px;
  flex: none;
  border: var(--hairline);
  border-radius: var(--radius-pill);
  background: var(--paper);
  color: var(--ink);
  cursor: pointer;
  touch-action: manipulation;
}

.icon-btn:hover {
  background: var(--sand-shade);
}

/* The standing panel beside a long form: the running figures that would
   otherwise sit buried in three different fieldsets, in a box that follows the
   scroll. Hidden below 1024px, where there is no column to put it in and the
   section heads carry the same figures anyway. */
.form-aside {
  position: sticky;
  top: var(--space-7);
}

@media (max-width: 1023px) {
  .form-aside {
    display: none;
  }
}

/* -----------------------------------------------------------------------------
   14. The shed, and the back office's own shapes

   Added with the self-service redesign. Everything here is built out of the
   tokens above: no new hue, no radius outside the three, no shadows.
   -------------------------------------------------------------------------- */

/* The ink board at the head of the shed page. The shed itself is the hero —
   what day, what hours, how much is left — rather than a thin strip over a grid
   of cards, which is what made the page read as a list of nothing in
   particular. */
.board {
  background: var(--ink);
  color: var(--paper-dim);
  border-bottom: var(--hairline);
  padding-block: var(--space-7);
}

.board__inner {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--gap-columns);
  flex-wrap: wrap;
}

.board__title {
  color: var(--paper);
}

/* Three facts, ruled apart rather than gapped — the same hairline logic the
   rest of the system uses, in the one place the rule has to be lighter than
   ink because it is drawn on ink. */
.board__figures {
  display: flex;
  border: 1px solid var(--ink-60);
  border-radius: var(--radius-card);
  overflow: hidden;
}

/* Bottom-aligned, so the figures line up on their numerals rather than on their
   labels. On a phone a two-word label wraps and a one-word label does not, and
   reading a row of figures that sit at three different heights is the one thing
   a board like this must not do. */
.board__figure {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: var(--space-2);
  padding: var(--space-4) var(--space-6);
  border-right: 1px solid var(--ink-60);
}

.board__figure:last-child {
  border-right: 0;
}

.board__label {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-40);
  margin: 0;
}

/* Newsreader at size, which is what DESIGN.md reserves the display face for in
   an ops view. Tabular so the figures do not jiggle as they count down. */
.board__value {
  font-family: var(--font-display);
  font-size: clamp(30px, 4vw, 40px);
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--paper);
  white-space: nowrap;
}

.board__value--dim {
  color: var(--paper-dim);
}

/* Wheat means "act soon", and a shed about to shut is exactly that. */
.board__value--soon {
  color: var(--wheat);
}

@media (max-width: 720px) {
  .board__figures {
    width: 100%;
  }

  .board__figure {
    flex: 1 1 0;
    min-width: 0;
    padding: var(--space-3) var(--space-4);
  }
}

/* What the baker wants to say about today, above the list. Wheat, because it is
   almost always a "not yet" — focaccia still proving, buns out at eleven. */
.shed-note {
  background: var(--wheat);
  border-bottom: var(--hairline);
  padding-block: var(--space-3);
}

/* Sold-out things leave the grid for this. DESIGN.md says a sold-out item is
   ink-60 text rather than a wheat badge, and a quiet list of what has gone
   reads as a shop that sells out instead of a page of greyed-out cards. */
.gone {
  border: 1px solid var(--sand-shade);
  border-radius: var(--radius-card);
  overflow: hidden;
}

.gone__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-3) var(--space-5);
  border-bottom: 1px solid var(--sand-shade);
  color: var(--ink-60);
  font-size: 15px;
}

.gone__row:last-child {
  border-bottom: 0;
}

.gone__price {
  color: var(--ink-40);
  text-decoration: line-through;
}

/* The shelf strip: what is left per line, at a glance. Nothing else on the
   overview answers "which one is about to go". */
.shelf {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.shelf__row {
  display: flex;
  align-items: center;
  gap: var(--space-4);
}

.shelf__name {
  width: 150px;
  flex: none;
  font-size: 14px;
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.shelf__track {
  flex-grow: 1;
  height: 10px;
  border: var(--hairline);
  border-radius: var(--radius-pill);
  background: var(--paper);
  overflow: hidden;
}

.shelf__fill {
  height: 100%;
  background: var(--clay);
}

.shelf__fill--soon {
  background: var(--wheat);
}

/* Gone entirely: ink-40, not wheat. There is nothing left to act on. */
.shelf__fill--gone {
  background: var(--ink-40);
}

.shelf__count {
  width: 78px;
  flex: none;
  text-align: right;
  font-size: 14px;
  color: var(--ink-60);
  font-variant-numeric: tabular-nums;
}

@media (max-width: 720px) {
  .shelf__name {
    width: 110px;
  }

  .shelf__count {
    width: 62px;
  }
}

/* A panel whose head is the live thing and whose body is the detail. Used for
   today's shed on the overview and the open day on the shed-days list. */
.panel {
  border: var(--hairline);
  border-radius: var(--radius-card);
  overflow: hidden;
}

.panel__head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-5);
  flex-wrap: wrap;
  padding: var(--space-5) var(--space-6);
  background: var(--ink);
  color: var(--paper-dim);
}

.panel__title {
  color: var(--paper);
}

.panel__body {
  padding: var(--space-5) var(--space-6);
  background: var(--sand);
  border-top: 1px solid var(--sand-shade);
}

/* Figures inside a panel, ruled rather than gapped so they read as one object
   with the head above them. */
.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  background: var(--paper);
}

.tile {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-5) var(--space-6);
  border-right: 1px solid var(--sand-shade);
  min-width: 0;
}

.tile:last-child {
  border-right: 0;
}

.tile__value {
  font-family: var(--font-display);
  font-size: clamp(26px, 3.4vw, 40px);
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

/* The one figure on a panel that is acted on. One clay per view group. */
.tile__value--lead {
  color: var(--clay);
}

/* A customer's own orders: one card per order, stacked. Not .line-list, which
   is one bordered box with rows inside it — putting a bordered card in each row
   of that drew a box inside a box, with the browser's list indent between them. */
.order-list,
.order-list__lines {
  list-style: none;
  margin: 0;
  padding: 0;
}

.order-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

/* The back office's list row: a picture, a name, and controls that are always
   present rather than revealed on hover — there is no hover on an iPad. */
.row-card {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-3) var(--space-4);
  border: var(--hairline);
  border-radius: var(--radius-card);
  background: var(--paper);
}

/* The same for a row card: the whole card is the target, and the title link
   says so without needing a hover. */
.row-card--link {
  cursor: pointer;
  transition: border-color 120ms ease;
}

.row-card--link:hover {
  border-color: var(--clay);
}

.row-link {
  color: var(--ink);
}

/* The base link hover goes to paper, which is the ground these rows sit on. */
.row-link:hover,
.row-link:focus-visible {
  color: var(--clay);
}

.row-card--attention {
  background: var(--wheat);
}

.row-card--muted {
  background: var(--sand);
  border-color: var(--sand-shade);
}

.row-card__body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex-grow: 1;
  min-width: 0;
}

.row-card__actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: none;
}

/* Below iPad portrait a row cannot hold a picture, a name and three controls on
   one line, so the controls drop under the name rather than the row scrolling
   sideways. DESIGN.md, "Admin layout patterns". */
@media (max-width: 720px) {
  .row-card {
    flex-wrap: wrap;
  }

  .row-card__actions {
    width: 100%;
    justify-content: flex-end;
  }
}

.thumb {
  width: 72px;
  height: 72px;
  flex: none;
  border: var(--hairline);
  border-radius: var(--radius-card);
  object-fit: cover;
  background: repeating-linear-gradient(135deg, var(--sand) 0 8px, var(--sand-shade) 8px 16px);
}

.thumb--sm {
  width: 56px;
  height: 56px;
}

/* A honeypot: a field a person never sees and a bot fills in. Positioned off
   screen rather than display:none, because some form-fillers skip hidden
   fields — and this catches the indiscriminate ones, which is most of them. */
.trap {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* The Turnstile widget on sign-up and password reset. Its height is reserved
   before the script fills it in, so the submit button does not jump under a
   finger that was already on its way to it. */
.human-check {
  min-height: 65px;
  margin-top: var(--space-4);
}

/* -----------------------------------------------------------------------------
   15. Navigation corrections

   The masthead grew from two links to five plus a basket when About, Contact and
   the shed replaced the old single Menu link, and the footer gained a nav of its
   own. Both needed rules the earlier two-link row never did.
   -------------------------------------------------------------------------- */

/* A link in the ink footer inherits .nav__link's ink colour and disappears into
   the background. Everything else on ink already has a rule; these did not
   exist when the footer had no links in it.

   Not buttons: (0,1,1) outranks .btn--primary's (0,1,0), and the clay buttons on
   ink (Go to basket on the basket bar, Create an account on the home page) came
   out with paper-dim text, which read as disabled. A button owns its colours. */
.band--ink a:not(.btn) {
  color: var(--paper-dim);
}

.band--ink a:not(.btn):hover {
  color: var(--paper);
}

/* Those two fixed the text and left the borders on the base rule, which draws
   them in clay: 35% clay over ink at rest, full clay on hover, and a clay focus
   ring. Clay on ink is about 2.3:1 — DESIGN.md puts clay text on paper or sand
   only, and 2.3:1 is not a visible focus indicator either. Wheat on ink is
   8.5:1, which is what the reach card on /contact already uses.

   Buttons and nav links are excluded: .btn owns its whole border and .nav__link
   owns its underline, including the clay-free active state below. */
.band--ink a:not(.btn):not(.nav__link) {
  border-bottom-color: color-mix(in srgb, var(--wheat) 35%, transparent);
}

.band--ink a:not(.btn):not(.nav__link):hover,
.band--ink a:not(.btn):not(.nav__link):focus-visible {
  border-bottom-color: var(--wheat);
}

/* Every focusable thing on an ink ground, links and nav alike. */
.band--ink :focus-visible {
  outline-color: var(--wheat);
}

.band--ink .nav__link {
  color: var(--paper-dim);
  border-bottom-color: transparent;
}

.band--ink .nav__link:hover {
  color: var(--paper);
  border-bottom-color: var(--paper);
}

/* Where you are, in the footer as in the masthead. */
.band--ink .nav__link.active {
  color: var(--paper);
  border-bottom-color: var(--paper);
}

/* The storefront masthead reads at the body size, not the UI size. 14px is the
   right weight for a table cell or a row action in the back office, and it is
   what .nav__link is built for — but this row is the shop's own signage, sitting
   above 17px prose, and at 14px it read as a caption over the page rather than
   the way around it. The footer nav stays at 14: it is a site map, not signage.

   The hit area is unchanged. .nav__link::after already draws a 44px box around
   each link independently of the type, which is exactly why the size can move
   without anything else having to. */
.masthead .nav__link {
  font-size: 16px;
}

/* The basket beside them, so the row sits at one size rather than two. */
.masthead .btn--inline {
  font-size: 15px;
}

/* Below 900px the masthead is the lockup, the basket and the menu button, and
   nothing else on the line. The links and the account are in the drawer —
   section 15b below.

   900 rather than DESIGN.md's 720px phone breakpoint, because 720 is not where
   the row stops fitting. Measured in a browser rather than guessed: the full
   row wants about 790px signed out and about 880 with an admin's fifth link, so
   wrapping at 1023 and again at 720 left a two-line masthead across the whole
   tablet range, and a three-line one — 180px of sticky chrome on every page,
   all the way down — on a phone. 900 is not a new number either: it is where
   .columns--split turns on.

   The bar stays 76px on both sides of the switch. --masthead-h is published as
   77px and .shed-strip pins itself directly under that; a shorter phone bar
   would leave the strip floating clear of it. */
@media (max-width: 899px) {
  .masthead__nav,
  .masthead__account {
    display: none;
  }

  /* --gap-columns is 32, which is the space between two columns of a layout and
     far too much between the two things left on this line. At 320px it was the
     five pixels that pushed the basket and the menu apart onto two rows and put
     the bar back to 124px — the exact shape this whole breakpoint exists to get
     rid of. */
  .masthead__inner {
    gap: var(--space-4);
  }

  /* The basket and the menu are a pair and stay one. .row wraps by default and
     a flex item shrinks by default, which together is how they came to be
     stacked rather than side by side. */
  .masthead__actions {
    flex: none;
    flex-wrap: nowrap;
  }

  /* Icon only, so the basket and the menu read as a matched pair of 44px
     circles rather than a pill beside a circle. The accessible name is the
     aria-label on the anchor, and a shopping basket is one of the few icons
     that genuinely needs no word. This used to happen at 400px, where it was
     the last thing making the row fit; now it is a choice about the pair. */
  .masthead .btn--inline .basket-label {
    display: none;
  }

  .masthead__actions .btn--inline {
    width: 44px;
    padding-inline: 0;
  }
}

/* Above it the row is the navigation and the drawer is not drawn at all. */
@media (min-width: 900px) {
  .nav-drawer {
    display: none;
  }
}

/* Narrower than a phone. The lockup cannot shrink — the mark is 44px and
   "SOURDOE" is one unbreakable word — so the line needs 145 + 16 + 100 + 48 of
   gutter, which is 309px. A 320px phone clears that with eleven to spare; a
   folded Galaxy at 280 does not, and what gives is the wordmark rather than a
   sideways scroll on every page. The mark still goes home and still says whose
   shop it is. */
@media (max-width: 319px) {
  .masthead .wordmark {
    display: none;
  }
}

/* -----------------------------------------------------------------------------
   15b. The storefront drawer — the navigation below 900px

   A <details> and no script. These pages are static SSR with no circuit behind
   them and ship no JavaScript to open a panel with; a disclosure is exactly
   what the element is for, and AccountMenu above is already the same decision.
   -------------------------------------------------------------------------- */

.nav-drawer {
  /* Deliberately not position: relative, which is what .account-menu does and
     the obvious thing to copy across. The panel is full bleed, so it has to
     resolve against .masthead — which is sticky, and therefore already the
     nearest positioned ancestor. Positioning this would shrink the panel to the
     width of a 44px button. */
  flex: none;
}

/* A 44px round control, matched to the basket beside it. display: block is what
   actually drops the disclosure triangle in every engine — a summary is a
   list-item and a marker belongs to no other display — and the webkit
   pseudo-element is the same job again for the versions that predate that. */
.nav-drawer__trigger {
  display: block;
  width: 44px;
  height: 44px;
  border: var(--hairline);
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--ink);
  cursor: pointer;
  list-style: none;
  touch-action: manipulation;
}

.nav-drawer__trigger::-webkit-details-marker {
  display: none;
}

.nav-drawer__trigger:hover,
.nav-drawer[open] .nav-drawer__trigger {
  background: var(--sand-shade);
}

/* The glyph is a box the three bars are positioned inside, rather than a grid
   that centres an icon: the bars have to know where the middle is to travel to
   it. 18x12 at 1.6px matches the stroke weight of the basket beside it. */
.nav-drawer__glyph {
  position: relative;
  display: block;
  width: 18px;
  height: 12px;
  margin: 15px auto;
}

/* The pill radius, which is one of the three DESIGN.md allows, and it is what
   gives the bars the round cap the basket's stroke-linecap gives that icon. */
.nav-drawer__bar {
  position: absolute;
  left: 0;
  width: 100%;
  height: 1.6px;
  border-radius: var(--radius-pill);
  background: currentColor;
  transition: transform 240ms cubic-bezier(0.32, 0.72, 0, 1), opacity 120ms ease;
}

.nav-drawer__bar:nth-child(1) {
  top: 0;
}

.nav-drawer__bar:nth-child(2) {
  top: 50%;
  margin-top: -0.8px;
}

.nav-drawer__bar:nth-child(3) {
  bottom: 0;
}

/* Shut it is three lines; open it is a cross. 5.2px is the distance from a bar
   to the middle of a 12px box — (12 / 2) - (1.6 / 2). The middle bar goes out
   faster than the other two arrive, so there is never a moment with a stroke
   through the middle of the cross. */
.nav-drawer[open] .nav-drawer__bar:nth-child(1) {
  transform: translateY(5.2px) rotate(45deg);
}

.nav-drawer[open] .nav-drawer__bar:nth-child(2) {
  opacity: 0;
  transform: scaleX(0.4);
}

.nav-drawer[open] .nav-drawer__bar:nth-child(3) {
  transform: translateY(-5.2px) rotate(-45deg);
}

/* A sheet that comes in from the right and fills everything under the bar.

   Fixed rather than absolute, and to the viewport rather than to the masthead:
   it is the page for as long as it is open, so it should not be a panel hanging
   off a header that happens to be sticky. .masthead is sticky and has no
   transform, so a fixed descendant still resolves against the viewport.

   It stops at the bar rather than covering it. A <details> has exactly one
   <summary>, so there is no second control to put inside the sheet — the way
   out has to stay where the way in was. Keeping the bar also keeps the lockup
   and the basket reachable with the menu open, which is the whole reason the
   basket was left out of the sheet in the first place.

   Right, because that is the side the button is on. A sheet that arrives from
   the opposite side to the control that opened it reads as a second thing.

   No shadow and no scrim. DESIGN.md takes its depth from hairlines, and a sheet
   that covers the whole width has nothing behind it left to dim. */
.nav-drawer__panel {
  position: fixed;
  top: var(--masthead-h);
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 1;
  display: flex;
  flex-direction: column;
  background: var(--paper);
  overflow-y: auto;
  /* The sheet is its own scroll container even when the content fits, so a
     touch that runs out of sheet does not carry on scrolling the page behind
     it. This is the no-JavaScript half of a scroll lock: html { overflow:
     hidden } is the other half and is deliberately not here — it does not
     reliably lock iOS Safari anyway, and on a desktop window under 900px it
     jumps the whole page by the width of the scrollbar it removes. */
  overscroll-behavior: contain;
  padding-bottom: env(safe-area-inset-bottom);

  /* Off to the right and out of the tree until it is asked for. visibility
     rather than display or content-visibility: it is what takes the sheet out
     of the tab order and out of the accessibility tree while still leaving a
     box that transform can move, and unlike the other two it introduces no
     containment. That last part is not a detail — see the note below. */
  visibility: hidden;
  transform: translateX(100%);
  /* The delay is the close animation. visibility does not interpolate, so it is
     held at visible for exactly as long as the slide takes and flipped at the
     end; the [open] rule below cancels the delay so opening is immediate. */
  transition: transform 260ms cubic-bezier(0.32, 0.72, 0, 1), visibility 0s linear 260ms;
}

.nav-drawer[open] .nav-drawer__panel {
  visibility: visible;
  transform: translateX(0);
  transition: transform 260ms cubic-bezier(0.32, 0.72, 0, 1), visibility 0s;
}

/* Why the sheet is kept rendered rather than left to the browser.

   A <details> stops rendering its content the moment [open] comes off, so by
   the time a close transition would run there is nothing left to run it on.
   ::details-content is the handle on that content, and this hands the rendering
   back to the rules above, which do the hiding themselves with visibility.

   The obvious alternative — content-visibility: hidden here plus
   transition-behavior: allow-discrete — was tried first and is wrong, in a way
   worth writing down. content-visibility: hidden implies contain: paint, and a
   paint-contained element is a containing block for fixed-position descendants.
   The sheet stopped resolving against the viewport and started resolving against
   the 44px button, so it laid out 44px wide and zero high. Anything that
   contains — content-visibility, a transform, a filter, will-change on either —
   must stay off every ancestor of this panel.

   Behind @supports because the fallback has to be right: an engine without
   ::details-content skips this block, the browser keeps hiding the content
   itself, and the sheet opens and shuts with no animation. Nothing is broken —
   only unanimated. */
@supports selector(::details-content) {
  .nav-drawer::details-content {
    content-visibility: visible;
  }
}

/* A stack of ruled rows. DESIGN.md: structure comes from hairlines, not gaps. */
.nav-drawer__nav {
  display: flex;
  flex-direction: column;
}

/* 52px rather than the 44px minimum: this is the whole navigation, on the
   device the storefront is mostly used on, read standing up. The inline padding
   is --section-pad, so a row starts where the page's own gutter does. */
.nav-drawer__link {
  display: flex;
  align-items: center;
  min-height: 52px;
  padding: var(--space-2) var(--section-pad);
  border-bottom: var(--hairline);
  color: var(--ink);
  font-size: 17px;
  font-weight: 500;
  line-height: 1.3;
  touch-action: manipulation;
}

/* The base anchor rule turns a border-bottom clay on hover, which would pick
   one row's rule out in a colour none of the others are. */
.nav-drawer__link:hover,
.nav-drawer__link:focus-visible {
  background: var(--sand-shade);
  border-bottom-color: var(--ink);
}

/* Where you are. Clay on paper, which is where DESIGN.md allows clay text, and
   no underline — a ruled row does not need a second line under the word. */
.nav-drawer__link.active {
  color: var(--clay);
}

/* The last link drops its own rule: the account block below draws a border-top,
   and two ink hairlines stacked read as one 2px rule, which is a weight the
   palette has not got. */
.nav-drawer__nav > :last-child {
  border-bottom: 0;
}

/* What the shop is doing, under the links. A fact rather than a link, so it is
   set at the body size in ink-60 with a status dot and no row hover.

   It carries the rule between itself and the nav, because the last link drops
   its own — see above. Its foot is left bare and the account block below draws
   the next one, so there is never a 2px rule where two 1px ones meet. */
.nav-drawer__status {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  margin: 0;
  padding: var(--space-4) var(--section-pad);
  border-top: var(--hairline);
  font-size: 15px;
  line-height: 1.5;
  color: var(--ink-60);
}

/* The dot is 8px of nothing, so it has no baseline to sit on. This lands it on
   the middle of the first line rather than the top of the box, and it is why
   the row is flex-start and not baseline — baseline would drop it to the bottom
   of a sentence that wraps to two lines. */
.nav-drawer__status .dot {
  margin-top: 0.45em;
}

/* Pushed to the foot of the sheet rather than sitting straight under the last
   link. The sheet is the height of the screen and the nav is five rows, so the
   alternative is four hundred pixels of nothing under the account block. An
   auto margin only pushes when there is space going spare, so on a phone held
   sideways — where there is not — the rows simply carry on and the sheet
   scrolls. */
.nav-drawer__account {
  margin-top: auto;
  border-top: var(--hairline);
}

/* Who is signed in. Two letters in a circle are an initial, not an
   identification — the same reasoning as AccountMenu's panel. */
.nav-drawer__who {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4) var(--section-pad) var(--space-3);
}

.nav-drawer__identity {
  display: block;
  min-width: 0;
}

.nav-drawer__name {
  display: block;
  font-size: 14px;
  font-weight: 500;
  line-height: 1.3;
  color: var(--ink);
}

/* An address is long, has no spaces to break at, and is here to be checked
   rather than read. Same call as .account-menu__email. */
.nav-drawer__email {
  display: block;
  font-size: 13px;
  line-height: 1.4;
  color: var(--ink-60);
  overflow-wrap: anywhere;
}

/* Sign out is a form button sitting in a list of links, so it is reset to the
   row the links are. */
.nav-drawer__form {
  display: block;
  width: 100%;
}

.nav-drawer__signout {
  width: 100%;
  border: 0;
  border-bottom: var(--hairline);
  border-radius: 0;
  background: transparent;
  font-family: var(--font-ui);
  text-align: left;
  cursor: pointer;
}

.nav-drawer__form:last-child .nav-drawer__signout {
  border-bottom: 0;
}

/* Signed out, the same slot is one full-width ink-outlined button and the line
   that says it is optional. Never clay: DESIGN.md keeps the one clay action for
   what the page is actually for, and an account is not how you buy bread here. */
.nav-drawer__signin {
  padding: var(--space-4) var(--section-pad) var(--space-5);
}

.nav-drawer__note {
  margin: var(--space-3) 0 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--ink-60);
}

/* -----------------------------------------------------------------------------
   16. The contact page — the address, set as the object on the page

   Everywhere else on the storefront the address is one row of a dateline beside
   three others, which is the right weight for a page that is mostly about
   something else. On /contact it is what the visitor came for, so it gets a
   surface of its own.

   Ink is not a new move here: the footer is already an ink band with a wheat
   eyebrow over paper-dim body copy. This is that, in a card.
   -------------------------------------------------------------------------- */

.reach {
  background: var(--ink);
  border-radius: var(--radius-card);
  /* The rows square off against the radius without it. */
  overflow: hidden;
}

/* The same call as .columns > .dateline above, and for the same reason: the card
   is as tall as the facts in it and no taller. On /contact it is one half of a
   split, and a grid item stretches by default — which on a page whose other
   column runs heading, prose, button and a photograph would draw a slab of ink
   below the last row with nothing in it. */
.columns > .reach {
  align-self: start;
}

/* Ink on ink needs a lighter rule than the structural hairline. --ink-40 is the
   value DESIGN.md gives for anything drawn on an ink ground. It separates the
   heads from the rows as well as the rows from each other, which is what lets
   the card hold "where to come" and "how to reach" without a second weight of
   rule or a hue that is not in the table. */
.reach > * + * {
  border-top: 1px solid var(--ink-40);
}

.reach__head,
.reach__row,
.reach__note {
  padding: var(--space-4) var(--space-6);
}

.reach__head {
  padding-block: var(--space-5);
}

/* Self-contained rather than relying on .on-ink being remembered at the call
   site: clay on ink fails contrast, so an eyebrow in here is never clay. */
.reach .eyebrow {
  color: var(--wheat);
}

.reach__row {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  /* Label over value rather than the dateline's label-beside-value: an email
     address squeezed into the right half of a phone-width row wraps mid-domain.
     76px is the 44px target plus the row's own padding — this is a mailto and a
     tel, which are the two things on the page most likely to be tapped. */
  min-height: 76px;
  justify-content: center;
}

.reach__label {
  font-size: 13px;
  font-weight: 500;
  line-height: 1.5;
  color: var(--ink-40);
  margin: 0;
}

.reach__value {
  font-size: 17px;
  font-weight: 500;
  line-height: 1.4;
  color: var(--paper);
  margin: 0;
  /* An address is long and has no spaces to break at. */
  overflow-wrap: anywhere;
}

/* DESIGN.md: clay text goes on paper or sand only. On ink the link is paper and
   the underline carries the affordance instead of the colour. */
.reach__value a {
  color: var(--paper);
  border-bottom-color: var(--wheat);
}

.reach__value a:hover,
.reach__value a:focus-visible {
  color: var(--wheat);
  border-bottom-color: var(--wheat);
}

/* The focus ring is clay everywhere else, which comes out at about 2.3:1 on ink
   and is not a visible indicator. Wheat on ink is 8.5:1. */
.reach :focus-visible {
  outline-color: var(--wheat);
}

.reach__note {
  font-size: 14px;
  line-height: 1.6;
  color: var(--paper-dim);
  margin: 0;
}

/* A placeholder that sits in a column of cards rather than across the page, so
   it takes a card's proportions and a card's hairline. Section 13 has the flush
   and hero variants this sits beside. */
.placeholder--media {
  aspect-ratio: 4 / 3;
  min-height: 0;
  border: var(--hairline);
  /* 4:3 of a full-width column is 579px of stripe on an iPad in portrait, which
     is more of a missing photograph than anyone needs to see. The cap only bites
     below 1024px, where the column stops being half the page: at the two-column
     width the box is 526px across and 394px tall, under the cap already. */
  max-height: 420px;
}

/* -----------------------------------------------------------------------------
   17. The hero photograph — the one full-bleed picture on the site
   -------------------------------------------------------------------------- */

/* Edge to edge, no radius. DESIGN.md, "Imagery".

   No border of its own: the sticky masthead above it already draws a bottom
   hairline and the .band around it draws another underneath, so adding
   border-block here would double both of them. The sand-stripe placeholder this
   replaced did exactly that.

   Both heroes (loaves on /, the kitchen on /about) are 4:3, so up to about 745px wide it lands shorter than
   the cap and nothing is cropped — a phone gets the whole picture. Wider than
   that the cap bites and cover takes the top and bottom off; in both, the
   subject sits a little above the middle of the frame, hence the object-position. The cap is
   what stops a wide monitor turning it into a thousand pixels of bread. Same
   reasoning as .placeholder--media. */
.hero {
  display: block;
  width: 100%;
  background: var(--sand);
}

/* The image sizes itself: full width, natural height, capped. No aspect-ratio on
   the wrapper and no percentage height on the picture — a percentage height has
   to resolve against a definite parent height, and a parent whose height came
   from an aspect-ratio that a max-height then clamped is exactly the case that
   goes wrong. Width first, height follows, cover crops only once the cap bites. */
.hero img {
  display: block;
  width: 100%;
  max-width: 100%;
  max-height: 560px;
  object-fit: cover;
  object-position: 50% 45%;
}

/* Where the photograph has not been taken yet, the same slot holds the sand
   stripe instead. Nothing uses that now; it is kept for the next page that
   needs a hero before its photograph exists. Only the radius needs squaring: edge to edge means no
   radius, and the stripe keeps the bare .placeholder's no border for the reason
   above, so swapping it for an <img> later changes nothing around it. */
.hero .placeholder {
  border-radius: 0;
}

/* The millstone on /about, beside the flour copy.

   Square rather than the 4:3 every other column picture uses, because this
   subject fills its frame: the wheel runs edge to edge in a square photograph,
   so a 4:3 crop takes the top and bottom off the circle and leaves a stone with
   no rim. DESIGN.md allows either proportion for a picture in a column, and
   this is the one that has to be the square.

   Capped like .placeholder--media, and for the same reason: a square of a
   full-width column is 772px tall on an iPad in portrait, which is more
   millstone than the page can spare. The cap only bites below 1024px — at the
   two-column width the box is 526px and already under it. */
.about__media {
  aspect-ratio: 4 / 3;
  min-height: 0;
  border-radius: var(--radius-card);
  overflow: hidden;
}

/* The cap belongs to the single-column layout and only to it. A square of the
   full page width is 772px of millstone on an iPad in portrait, which is more
   than the page can spare — but at the two-column width the box is 526px across
   and an unconditional cap shrinks the whole frame, aspect ratio and all, so the
   picture stops reaching the edges of the column it is supposed to fill. That is
   what it was quietly doing before this section became picture-led. */
@media (max-width: 1023px) {
  .about__media {
    max-height: 460px;
  }
}

.about__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The millstone is the one that has to be square, for the reason above. */
.about__media--square {
  aspect-ratio: 1 / 1;
}

/* The shed on /contact is tall and narrow, so it is the one picture that
   stands up. 4:5 rather than the photograph's 3:4 so it does not tower over
   the two cards beside it; the crop is a sliver of brick top and bottom.

   The single-column height cap would cut it to the door on an iPad, so a
   standing picture is narrowed there instead of cropped: the whole shed and
   the doormat, at a width the page can spare. */
.about__media--portrait {
  aspect-ratio: 4 / 5;
}

@media (max-width: 1023px) {
  .about__media--portrait {
    max-height: none;
    max-width: 420px;
  }
}

/* A picture in a column sits level with the prose beside it rather than
   stretching to the height of it: these sections are a photograph and a few
   paragraphs, and a 4:3 frame pulled to the height of a heading and three
   paragraphs crops the subject to a letterbox. Same call as the dateline. */
.columns > .about__media,
.columns > .placeholder {
  align-self: start;
}

/* Puts a section's picture in the first column and its words in the second,
   which is what makes the pictures down the About page alternate sides instead
   of stacking against one edge. On a phone the grid collapses to one column in
   source order, and source order there has to be the heading first — nobody
   scrolls past an unlabelled photograph to find out what it is of. So the markup
   always keeps the words first and the picture is moved into column one only
   once there are two columns to move it between.

   Both kinds of media slot, because half the pictures on that page are still
   placeholders and a stripe has to stagger the same way the photograph that
   replaces it will. Matching only .about__media is what made adding this class
   to a placeholder section silently do nothing. */
@media (min-width: 900px) {
  .columns--media-first > .about__media,
  .columns--media-first > .placeholder--media {
    grid-column: 1;
    grid-row: 1;
  }
}

/* -----------------------------------------------------------------------------
   18. The cookie banner

   Pinned to the foot of the storefront, shown once, and then never again. It is
   the only overlay on the public site.
   -------------------------------------------------------------------------- */

/* Fixed rather than sticky, unlike .basket-bar: this has to be answerable
   without scrolling to the bottom of a long shed page first. Paper on an ink
   hairline, which is how everything else here separates — no shadow, no scrim,
   and no dimming of the page behind it. Nothing is blocked while it is up; a
   customer who wants to ignore it can still buy bread. */
.consent {
  position: fixed;
  inset-inline: 0;
  bottom: 0;
  /* Above .basket-bar's 2, which it will sit on top of on the shed page. That
     overlap lasts one tap and then the bar has the corner back for good. */
  z-index: 20;
  border-top: var(--hairline);
  background: var(--paper);
  padding-block: var(--space-5);
  /* Clear of the home indicator on a phone. DESIGN.md, "Touch". */
  padding-bottom: calc(var(--space-5) + env(safe-area-inset-bottom));
}

.consent[hidden] {
  display: none;
}

.consent__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4) var(--gap-columns);
  flex-wrap: wrap;
}

/* Capped at the lead measure. The banner runs the full width of the page and a
   sentence that long is unreadable in one line. */
.consent__text {
  max-width: var(--measure-lead);
  margin: 0;
}

/* Allow and no thanks are the same size and sit side by side, so refusing is
   exactly as easy as accepting. Do not shrink the second one. */
.consent__actions {
  flex: 0 0 auto;
}

/* On a phone the row becomes two, and the buttons share the width rather than
   leaving one stranded. */
@media (max-width: 560px) {
  .consent__actions {
    width: 100%;
  }

  .consent__actions .btn {
    flex: 1 1 0;
  }
}

/* -----------------------------------------------------------------------------
   19. Storefront body copy — one size on the public pages

   The storefront had three sizes of running text on a page and two shades of
   it: a 17px lead, a 15px .body-sm under it, and .muted turning some of those
   ink-60. On a section that is a heading and three paragraphs that reads as the
   first paragraph mattering and the rest being a footnote, which is not what any
   of them are saying — they are one piece of prose, written to be read in order.

   So on the public pages the two prose classes are one thing: 17px, the lead
   size, at the ordinary weight. .lead keeps its name because it is still the
   paragraph a section leads with, and it still gets the first position; it no
   longer gets a different size for it.

   Scoped to `.page > main`, which is the storefront and nothing else — the back
   office is `.admin-shell` and the sign-in pages are `.auth`, and neither wants
   this. A 17px .body-sm inside an admin table row would be four kinds of
   trouble.
   -------------------------------------------------------------------------- */

.page > main :is(.lead, .body-sm) {
  font-size: 17px;
  line-height: 1.65;
  font-weight: 400;
  /* The measure goes with the size. .lead is capped at 48ch and .body-sm at
     nothing, so before this the lead wrapped several words earlier than the
     paragraph beneath it and the two blocks had visibly different right edges
     in one column. One measure, the prose one; in a half-width column the cap
     does not bite at all, and it is there for the full-width bands. */
  max-width: var(--measure-prose);
}

/* Prose is ink. .muted is for a caption doing a different job from the sentence
   above it — an availability line beside a price, the count next to a section
   heading — and those keep it, because they are labels rather than text. A
   paragraph that has been dimmed is only a paragraph somebody has to work
   harder to read. This catches the ones the markup no longer asks for, and
   stops a new page quietly reintroducing the pattern. */
.page > main p.lead.muted,
.page > main p.body-sm.muted {
  color: var(--ink);
}

/* The ink board and any ink band set their own body colour and keep it:
   paper-dim on ink is the contrast pair those were built on, and --ink there
   would be unreadable. */
.page > main .board :is(.lead, .body-sm),
.page > main .band--ink :is(.lead, .body-sm) {
  color: var(--paper-dim);
}

/* -----------------------------------------------------------------------------
   20. Facebook and Instagram — the two links the shop publishes

   Icon and handle together, deliberately: an unlabelled glyph is a guess, and
   the handle is what somebody searches for when they would rather find the page
   themselves than follow a link. Both places this appears are ink — the footer
   and the reach card — so the colours are the ink pair and the focus ring is
   the wheat one those grounds already use.
   -------------------------------------------------------------------------- */

.social {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-5);
  list-style: none;
  margin: 0;
  padding: 0;
}

.social__link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  /* 44px of target without a 44px box, the way .nav__link does it: padding here
     would push the two links further apart than the row wants. */
  min-height: 44px;
  font-size: 15px;
  font-weight: 500;
  border-bottom: 1px solid transparent;
}

.social__glyph {
  display: inline-flex;
  flex: none;
}

/* Stacked rather than in a row: inside the reach card each network is a line of
   its own, matching the email and phone rows above it. */
.social--stacked {
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-2);
}

/* The reach card is ink, and section 16 already sets the pair: paper at rest,
   wheat on hover, with the underline carrying the affordance rather than a
   colour DESIGN.md keeps off ink. These links are anchors without .reach__value
   around them, so they say it for themselves. */
.reach .social__link {
  color: var(--paper);
  border-bottom-color: transparent;
}

.reach .social__link:hover,
.reach .social__link:focus-visible {
  color: var(--wheat);
  border-bottom-color: var(--wheat);
}

/* Four rows of label-and-value, drawn at the width of a page, is a card that has
   become a table. It holds an email address, so this is the measure it needs. */
.reach {
  max-width: 620px;
}

/* -----------------------------------------------------------------------------
   21. The baker's dozen card, and the account pages that go with it

   The baker's dozen drawn as the paper card it stands in for: twelve holes and a
   prize at the end. Tokens only, three radii, ink hairlines — a punched hole is
   ink filled and an empty one is sand, which is the strongest contrast the
   palette allows without reaching for a colour that is not in it.
   -------------------------------------------------------------------------- */

.punchcard {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding: var(--space-6);
  border: var(--hairline);
  border-radius: var(--radius-card);
  background: var(--paper);
}

/* The example card on the home page sits on an ink band, and three separate
   rules colour text for ink the moment it does: .band--ink sets paper-dim on
   everything inside it, .band--ink strong sets paper, and section 19's
   `.page > main .band--ink :is(.lead, .body-sm)` sets paper-dim again on the
   two prose classes. All three land on a paper card, where paper-dim on paper
   is 1.9:1 and paper-dim on the wheat prize row is 1.4:1 — which is how "comes
   off the next order at checkout" arrived as a line nobody could read.

   So the card takes its own colours back and reads exactly as it does in an
   account. Scoped to .punchcard rather than loosened on .band--ink, because the
   rest of that band genuinely is text on ink and wants the rule it has — the
   lead paragraph and the caption under this card included.

   The third selector carries `.page > main` for one reason: section 19's rule
   is (0,3,1) and a .punchcard selector alone does not outrank it. Specificity
   here, not source order, because a rule that only works while it sits below
   another one is a rule that breaks the next time the file is reordered. */
.band--ink .punchcard,
.band--ink .punchcard strong,
.page > main .band--ink .punchcard :is(.lead, .body-sm) {
  color: var(--ink);
}

.punchcard__count {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  margin: 0;
}

.punchcard__of {
  font-family: var(--font-display);
  font-size: 26px;
  line-height: 1;
  color: var(--ink-60);
}

/* Six across on a phone, twelve on anything wider — at 1024px the whole card
   reads as one strip, which is what a paper one looks like. */
.punchcard__grid {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: var(--space-2);
}

@media (min-width: 1024px) {
  .punchcard__grid {
    grid-template-columns: repeat(12, minmax(0, 1fr));
  }
}

.punch {
  display: grid;
  place-items: center;
  aspect-ratio: 1;
  border-radius: var(--radius-pill);
  background: var(--sand);
  border: 1px solid var(--sand-shade);
}

.punch--done {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--paper);
}

/* The one about to be punched. A dashed hairline rather than a colour, so the
   card still reads at a glance in one ink. */
.punch--next {
  background: var(--paper);
  border: 1px dashed var(--ink);
}

.punchcard__prize {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-4);
  border: 1px dashed var(--ink);
  border-radius: var(--radius-card);
  background: var(--sand);
}

/* Won. Wheat means act soon, which is exactly what a loaf waiting to be spent
   is — and the hairline goes solid because it is no longer a promise. */
.punchcard__prize--won {
  border-style: solid;
  background: var(--wheat);
}

.punchcard__seal {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  flex: none;
  border-radius: var(--radius-pill);
  border: 1px dashed var(--ink);
  background: var(--paper);
  font-weight: 700;
}

.punchcard__prize--won .punchcard__seal {
  border-style: solid;
  background: var(--ink);
  color: var(--wheat);
}

/* ---- recovery codes ------------------------------------------------------ */

/* Not .numeral: these are read character by character and typed back, so they
   want tabular sans at a size that fits ten of them, not the display face. */
.code-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: var(--space-2) var(--space-5);
}

.code {
  font-family: var(--font-ui);
  font-size: 15px;
  font-weight: 700;
  letter-spacing: 0.06em;
  font-variant-numeric: tabular-nums;
}

/* ---- the two-factor QR --------------------------------------------------- */

/* Square, on paper, and big enough for a phone camera to find across a kitchen
   table. The SVG inside is generated server-side — see AuthenticatorSetup. */
.thumb--qr {
  width: 200px;
  height: 200px;
  padding: var(--space-2);
  background: var(--paper);
}

.thumb--qr svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* The generator emits a white ground and black modules; both are recoloured here
   so the code sits in the palette rather than punching a white hole in the page.
   Contrast stays far above what any scanner needs. */
.thumb--qr svg rect {
  fill: var(--paper);
}

.thumb--qr svg path {
  fill: var(--ink);
}
