/* Layout and components. Phone-first at 390px (§6.1, §14).
 *
 * Ergonomics that shape this file: two or three items per screen, one very large
 * element per screen, controls in the lower two-thirds, nothing important in the
 * top corners, large touch targets, high contrast.
 */

* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  height: 100%;
}

body {
  font-family: var(--font);
  color: var(--ink);
  /* PAPER, NOT #111, AND THIS IS THE STATUS BAR FIX.
   *
   * With `viewport-fit=cover` the status bar and the home-indicator strip are
   * part of the viewport, and whatever the BODY is painted with shows through
   * them wherever `#app` does not reach. `#111` made that black in standalone.
   * The surround is white too, by ruling — there is no dark ground anywhere on
   * any surface now, so this is the only background the page ever paints. */
  background: var(--paper);
  -webkit-font-smoothing: antialiased;
  overscroll-behavior: none;
}

/* The phone. On a real phone this fills the viewport; on a desktop it renders as
 * the design's 390x844 frame so the layout can be checked at its true size. */
#app {
  position: relative;
  width: 100%;
  max-width: 390px;
  height: 100dvh;
  max-height: 844px;
  margin: 0 auto;
  background: linear-gradient(180deg, var(--paper) 0%, var(--paper-warm) 100%);
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

/* THE DESKTOP PREVIEW FRAME, and the pointer query is the whole of the bug fix.
 *
 * This was gated on size alone — `min-width: 430px and min-height: 880px` — which
 * is a description of a large PHONE as accurately as of a desktop window. An
 * iPhone 15 Pro Max in standalone is 430 x 932, so it MATCHED: the shopper got
 * the desktop mock-up's centred, rounded, drop-shadowed frame on the device the
 * frame exists to imitate, with `#111` showing through the status bar around it.
 *
 * `(hover: hover) and (pointer: fine)` is the honest discriminator — a mouse,
 * which no handset has — rather than a size that a phone can grow into. */
/* NO DARK SURROUND ANYWHERE. Product ruling 2026-08-15: the surround is white as
   well as the status bar. The mock-up's dark ground is gone from every surface —
   the body is `--paper` on desktop and on device alike, so no viewport,
   orientation or safe-area combination can put black at an edge. The simulated
   phone's own BEZEL survives below as a box-shadow ring on `#app`: that is the
   device's edge rather than the page's background, and the pointer query keeps it
   unreachable from a handset. */
@media (hover: hover) and (pointer: fine) and (min-width: 430px) and (min-height: 880px) {
  body {
    display: grid;
    place-items: center;
  }
  #app {
    border-radius: 46px;
    box-shadow: 0 0 0 11px #131316, 0 0 0 13px #3a3a3e, 0 22px 50px rgba(0, 0, 0, 0.28);
  }
}

/* On a real touch device the app fills the screen. `max-height: 844px` is the
   design's canvas and a preview constraint; on a 932pt phone it left 88px of
   body showing below the app, which is the same defect as the status bar at the
   other end. */
@media (pointer: coarse) {
  #app {
    max-height: none;
    max-width: none;
  }
}

/* ------------------------------------------------------------------ screens */

.screen {
  position: absolute;
  inset: 0;
  display: none;
  flex-direction: column;
  padding-top: env(safe-area-inset-top, 0);
}

.screen[data-active='true'] {
  display: flex;
}

.screen__body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

/* Screens that carry the persistent surface leave room for it. */
.screen--with-surface .screen__body {
  padding-bottom: calc(var(--surface-h) + 16px);
}

/* --------------------------------------------------------------- typography */

.kicker {
  font: 600 15px/1.4 var(--font);
  color: var(--ink-kicker);
  letter-spacing: 0.4px;
  text-align: center;
}

.display {
  font: 800 40px/1.1 var(--font);
  color: var(--ink);
  letter-spacing: -1.4px;
  text-align: center;
  text-wrap: balance;
  margin: 0;
}

.answer {
  font: 700 27px/1.3 var(--font);
  color: var(--ink);
  letter-spacing: -0.4px;
  text-wrap: pretty;
  margin: 12px 0 0;
}

/* §7.1 rule 3 wants one or two sentences — a caption for a structured result,
 * not the result. The service sometimes writes four. Clamping keeps §8.4's "two
 * results visible without scrolling" true; the full text is one tap away, so
 * nothing is hidden, only folded. */
.answer--clamped {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.answer-more {
  appearance: none;
  border: 0;
  background: none;
  padding: 4px 0 0;
  font: 600 15px var(--font);
  color: var(--accent);
  cursor: pointer;
}

.answer-more[hidden] {
  display: none;
}

.echo {
  font: 500 17px/1.4 var(--font);
  color: var(--ink-faint);
  margin: 0;
}

.label {
  font: 700 12px var(--font);
  color: var(--ink-faint);
  letter-spacing: 0.7px;
}

.muted {
  font: 500 15px var(--font);
  color: var(--ink-faint);
}

/* ------------------------------------------------------------------- the orb */

.orb {
  position: relative;
  width: 190px;
  height: 190px;
  border-radius: 50%;
  overflow: hidden;
  flex: none;
  background: radial-gradient(
    circle at 50% 62%,
    var(--accent-hot) 0%,
    #ff7a1a 26%,
    #ffb25e 48%,
    #ffdca8 66%,
    #fff3e2 82%,
    #fff 100%
  );
  box-shadow: 0 0 60px rgba(255, 120, 20, 0.35), inset 0 -10px 30px rgba(255, 255, 255, 0.6);
  filter: blur(0.4px);
}

.orb--lg {
  width: 224px;
  height: 224px;
}

.orb__wave {
  position: absolute;
  border-radius: 46% 54% 58% 42%;
}

.orb__wave--a {
  width: 120%;
  height: 120%;
  left: -10%;
  top: -4%;
  background: radial-gradient(
    circle at 50% 50%,
    rgba(255, 58, 0, 0.9) 0%,
    rgba(255, 72, 4, 0.45) 40%,
    rgba(255, 110, 20, 0) 70%
  );
  animation: waveA 7.5s ease-in-out infinite;
}

.orb__wave--b {
  width: 104%;
  height: 104%;
  left: -2%;
  top: -6%;
  border-radius: 58% 42% 45% 55%;
  background: radial-gradient(
    circle at 50% 50%,
    rgba(255, 138, 31, 0.6) 0%,
    rgba(255, 150, 45, 0.28) 42%,
    rgba(255, 170, 70, 0) 70%
  );
  animation: waveB 9.5s ease-in-out infinite;
}

.orb__wave--c {
  width: 76%;
  height: 76%;
  left: 6%;
  top: 52%;
  border-radius: 52% 48% 40% 60%;
  background: radial-gradient(
    circle at 50% 50%,
    rgba(255, 196, 120, 0.45) 0%,
    rgba(255, 205, 140, 0.16) 46%,
    rgba(255, 215, 160, 0) 72%
  );
  animation: waveC 11s ease-in-out infinite;
}

/* Listening speeds the orb up — motion responds to state (§8.3). */
.orb--live .orb__wave--a {
  animation-duration: 4.6s;
}
.orb--live .orb__wave--b {
  animation-duration: 6s;
}
.orb--live .orb__wave--c {
  animation-duration: 7.4s;
}

@keyframes waveA {
  0%,
  100% {
    transform: scale(1) rotate(0deg);
  }
  50% {
    transform: scale(1.06) rotate(8deg);
  }
}
@keyframes waveB {
  0%,
  100% {
    transform: scale(1.04) rotate(0deg);
  }
  50% {
    transform: scale(0.96) rotate(-10deg);
  }
}
@keyframes waveC {
  0%,
  100% {
    transform: scale(0.98);
  }
  50% {
    transform: scale(1.08);
  }
}

@keyframes caret {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0;
  }
}

.caret {
  display: inline-block;
  width: 3px;
  height: 0.72em;
  background: var(--accent);
  vertical-align: -0.1em;
  margin-left: 3px;
  animation: caret 1s step-end infinite;
}

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

/* An element with `hidden` must actually be hidden.
 *
 * `[hidden]` is only a UA `display: none`, so ANY component rule that sets its
 * own `display` silently outranks it — and this file already patches that
 * per-component three times (`.surface[hidden]`, `.toast[hidden]`,
 * `.answer-more[hidden]`). The failure is invisible: the element is "hidden" in
 * the DOM and painted on the screen, which is how the read-back's two action
 * buttons rendered as empty black and white slabs during parsing.
 *
 * One rule instead of a fourth patch, so the next component cannot inherit it. */
[hidden] {
  display: none !important;
}

.btn {
  appearance: none;
  border: 0;
  font-family: var(--font);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
}

.btn--primary {
  width: 100%;
  height: 66px;
  border-radius: var(--r-tile);
  background: #0a0a0c;
  color: #fff;
  font: 700 20px var(--font);
}

.btn--accent {
  width: 100%;
  height: 68px;
  border-radius: var(--r-card);
  background: var(--accent);
  color: #fff;
  font: 800 20px var(--font);
  box-shadow: var(--shadow-accent);
}

.btn--ghost {
  min-height: var(--touch-min);
  padding: 0 20px;
  border-radius: var(--r-control);
  border: 1px solid var(--line-strong);
  background: var(--paper);
  color: var(--ink);
  font: 600 16px var(--font);
}

.btn--chip {
  min-height: 48px;
  padding: 13px 18px;
  border-radius: var(--r-chip);
  border: 1px solid var(--line-strong);
  background: var(--paper);
  color: var(--ink);
  font: 600 15px var(--font);
  text-align: left;
}

.btn:disabled,
.tile[aria-disabled='true'] {
  opacity: 0.45;
  cursor: default;
}

/* ------------------------------------------------------------------- tiles */

.tiles {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--gap-m);
  padding: 26px 24px 0;
}

.tile {
  min-height: 104px;
  border-radius: var(--r-tile);
  background: var(--paper);
  border: 1px solid var(--line);
  box-shadow: var(--shadow-tile);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--gap-s);
  padding: 12px 16px;
  font: 700 19px/1.15 var(--font);
  color: var(--ink);
  text-align: center;
  cursor: pointer;
}

.tile__why {
  font: 500 12px/1.3 var(--font);
  color: var(--ink-faint);
}

/* ------------------------------------------------------------ product cards */

.cards {
  display: flex;
  flex-direction: column;
  gap: var(--gap-m);
  padding: 18px 22px 0;
}

.card {
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  padding: var(--pad-card);
  display: flex;
  gap: var(--gap-l);
  align-items: center;
  box-shadow: var(--shadow-card);
  background: var(--paper);
  text-align: left;
  width: 100%;
  font-family: var(--font);
  cursor: pointer;
}

.card__shot {
  width: 96px;
  height: 116px;
  flex: none;
  border-radius: var(--r-thumb);
  object-fit: contain;
  background: var(--paper);
  border: 1px solid var(--line-faint);
}

.card__shot--empty {
  background: repeating-linear-gradient(
    45deg,
    var(--skeleton-a) 0 7px,
    var(--skeleton-b) 7px 14px
  );
}

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

.card__brand {
  font: 600 13px var(--font);
  color: var(--ink-faint);
  letter-spacing: 0.3px;
  text-transform: uppercase;
}

.card__name {
  font: 800 22px/1.15 var(--font);
  color: var(--ink);
  margin-top: 6px;
  letter-spacing: -0.5px;
  /* Catalog names run to 90+ characters ("Pictsweet Farms - Seasoned Vegetables
   * For The Air Fryer - ... - 11 Ounce"). Two lines on a card keeps the price and
   * the destination in the hierarchy §14 asks for; the product screen shows the
   * name in full. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.card__meta {
  font: 500 15px var(--font);
  color: var(--ink-muted);
  margin-top: 5px;
}

.card__price {
  font: 800 26px var(--font);
  color: var(--ink);
  margin-top: 9px;
  display: flex;
  align-items: baseline;
  gap: 10px;
}

.card__was {
  font: 500 16px var(--font);
  color: var(--ink-faint);
  text-decoration: line-through;
}

.card__off {
  font: 700 13px var(--font);
  color: var(--accent);
}

.badges {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.badge {
  font: 700 11px var(--font);
  padding: 5px 9px;
  border-radius: 7px;
  background: var(--chip-bg);
  color: var(--ink-muted);
}

.badge--sale {
  background: var(--accent-tint);
  color: var(--accent);
}

.badge--sponsored {
  background: var(--chip-bg);
  color: var(--ink-muted);
}

.tags {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 8px;
}

.tag {
  font: 600 12px var(--font);
  padding: 4px 9px;
  border-radius: 7px;
  background: var(--good-tint);
  color: var(--good);
}

/* Skeletons paint on `result`, before the prose has streamed (§6.3). */
@keyframes shimmer {
  0% {
    background-position: -200px 0;
  }
  100% {
    background-position: 200px 0;
  }
}

.skeleton {
  border-radius: 8px;
  background: linear-gradient(
    90deg,
    var(--skeleton-a) 0px,
    var(--skeleton-b) 80px,
    var(--skeleton-a) 160px
  );
  background-size: 400px 100%;
  animation: shimmer 1.2s linear infinite;
}

.card--skeleton {
  pointer-events: none;
}

/* ---------------------------------------------------------------- the stage */

.stage {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 14px;
  font: 600 15px var(--font);
  color: var(--ink-faint);
}

.stage__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--accent);
  animation: pulse 1s ease-in-out infinite;
}

@keyframes pulse {
  0%,
  100% {
    opacity: 0.35;
    transform: scale(0.8);
  }
  50% {
    opacity: 1;
    transform: scale(1.15);
  }
}

/* ------------------------------------------------------------------ the map */

/* THE PLAN IS THE DESIGNER'S DRAWING, LIFTED. Almost nothing here styles it —
 * `assets/store-plan.html` carries its own fills, its own type and its own
 * labels. What this file owns is the frame the plan sits in and the one mark we
 * add to it. Restyling the plan from here would be the same mistake as redrawing
 * it: two sources for one appearance, and the drift is silent. */

/* The plan's own pixel box. Fixed, because the drawing is fixed; the stage
   scales it. `transform-origin: center` so the base fit-scale and a pinch
   compose around the same point rather than fighting for the top-left. */
.plan-map {
  position: relative;
  width: 1385px;
  height: 900px;
  flex: none;
  transform-origin: center center;
  background: #fff;
}

/* The marks ride over the plan in the plan's own coordinates. `pointer-events:
   none` so the pinch handler sees the stage, not the overlay. */
.plan-map__marks {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  /* ABOVE THE DRAWING, BELOW THE NAMES. The marks are appended last, so without
     this they paint over the labels and the destination hides the name of the
     place it is in — measured: "Dairy Aisle" was covered by its own highlight,
     which is the map answering a question by deleting the answer. The label is
     what the shopper reads and the mark is what they look for; both, in that
     order. */
  z-index: 1;
}

.plan-map [data-label] {
  z-index: 2;
}

/* THE DESTINATION. The only thing on this map we draw, and the only thing that
 * is allowed to be this loud: the plan is grey, its two landmarks are red and
 * green, and this is the one shelf the shopper is walking to. Sized in plan
 * units so it scales with the drawing. */
.plan-map__mark {
  fill: var(--accent);
  stroke: #7a2b00;
  stroke-width: 3;
  stroke-linejoin: round;
}

/* Same concept in several places: one solid, the others outlined (§9.2 rule 7) */
.plan-map__mark--also {
  fill: rgba(255, 90, 0, 0.18);
  stroke: var(--accent-hot);
  stroke-width: 2.5;
  stroke-dasharray: 7 5;
}

/* Zone-level, when the shelf is not known: the whole zone, and the copy says so
   rather than the drawing implying shelf precision. */
.plan-map__zone-on {
  fill: rgba(255, 90, 0, 0.16);
  stroke: var(--accent-hot);
  stroke-width: 3;
  stroke-linejoin: round;
}

/* full screen (§9.3) */
.map-full {
  position: absolute;
  inset: 0;
  background: var(--paper);
  z-index: 40;
  display: none;
  flex-direction: column;
  padding: 20px;
}

.map-full[data-open='true'] {
  display: flex;
}

.map-full__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 8px;
}

.map-full__title {
  font: 800 24px var(--font);
  color: var(--ink);
  letter-spacing: -0.5px;
}

.map-full__stage {
  flex: 1;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  /* PINCH TO ZOOM, so the browser must not claim the second finger for its own
     page zoom — with the default the handler sees one pointer that jumps and the
     gesture reads as broken. Same reason the walking screen sets it. */
  touch-action: none;
  /* The zoomed map is clipped to its stage rather than painting over the title
     and the close button. */
  overflow: hidden;
}

/* No transition on the transform: a pinch must track the fingers, and an eased
   scale during a live gesture is what makes a map feel like it is arguing. */
.map-full__stage .plan-map {
  will-change: transform;
}

/* Zoomed in, a one-finger drag pans. Grab is the honest affordance for that and
   costs nothing on a touch screen. */
.map-full__stage[data-zoomed='true'] {
  cursor: grab;
}

/* THE PLAN IS SCALED BY A TRANSFORM, NOT BY A VIEWBOX, and that is forced rather
 * than chosen: its labels are HTML nodes positioned over an SVG, so there is no
 * single viewport that owns both layers. The stage measures itself and sets
 * `--plan-fit` to whichever of the two axis ratios is smaller — the whole store,
 * uncropped (§9.2 rule 1) — and the pinch multiplies that same variable. One
 * number, two multipliers, so a gesture cannot fight the fit.
 *
 * NO `width`/`height` RULE HERE. A transform does not change layout size, so the
 * plan still occupies its own 1385x900 in the flex centring; that is why the
 * stage clips (`overflow: hidden`) and why `transform-origin` is the centre. */
.map-full__stage {
  --plan-fit: 1;
}

/* The resting state, as a stylesheet rule rather than only as something JS does.
   The inline transform the pinch writes wins over this, so it is a fallback and
   not a second implementation: if `apply()` never runs the map is still fitted
   rather than showing its top-left corner at 1:1. */
.map-full__stage .plan-map {
  transform: scale(var(--plan-fit));
}

.iconbtn {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: var(--chip-bg);
  border: 0;
  color: var(--ink-muted);
  font: 600 18px var(--font);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  flex: none;
}

/* ------------------------------------------------- the persistent surface (§6) */

.surface {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 18px 12px calc(20px + env(safe-area-inset-bottom, 0));
  display: flex;
  align-items: center;
  gap: 8px;
  z-index: 20;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, var(--paper-warm) 38%);
}

.surface[hidden] {
  display: none;
}

/* Touch targets stay at 56px+ even though the boxes are narrower — the ceiling
 * is four controls, and squeezing a fifth in is what §6 warns against. */
.surface__btn {
  width: 52px;
  height: 60px;
  border-radius: var(--r-control);
  background: var(--paper);
  border: 1px solid var(--line);
  box-shadow: var(--shadow-tile);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  flex: none;
}

.surface__ask {
  flex: 1;
  min-width: 0;
  height: 60px;
  border-radius: var(--r-card);
  background: var(--paper);
  border: 1px solid var(--line);
  box-shadow: var(--shadow-float);
  display: flex;
  align-items: center;
  padding: 0 6px 0 14px;
  gap: 8px;
  cursor: pointer;
}

.surface__example {
  flex: 1;
  min-width: 0;
  font: 600 15px var(--font);
  color: var(--ink-faint);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  text-align: left;
  border: 0;
  background: none;
  padding: 0;
  cursor: pointer;
}

/* The keyboard glyph on the pill. A 44px touch target (the iOS minimum) inside the
 * pill's right edge, so typing has a visible affordance now that the standalone
 * keyboard box is gone. It shares `data-action="keyboard"` with the pill itself —
 * one route, two ways to hit it, not two routes. */
.surface__keyglyph {
  width: 44px;
  height: 44px;
  flex: none;
  border: 0;
  background: none;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}

.surface__mic {
  width: 48px;
  height: 48px;
  border-radius: 50%;
  border: 0;
  background: radial-gradient(circle at 50% 60%, var(--accent-hot), var(--accent-warm));
  box-shadow: 0 4px 12px rgba(255, 90, 0, 0.35);
  cursor: pointer;
  flex: none;
}

/* ---------------------------------------------------------------- waveform */

.wave {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 6px;
  height: 52px;
  margin-top: 26px;
}

.wave__bar {
  width: 5px;
  height: 8px;
  background: var(--accent);
  border-radius: 3px;
  /* Height is set from the real input level in voice.js — never a fake
   * animation. "Is it hearing me?" must never be a question (§8.3 rule 5). */
  transition: height 60ms linear;
}

/* ------------------------------------------------------------------ notices */

.notice {
  margin: 16px 22px 0;
  padding: 14px 16px;
  border-radius: var(--r-control);
  background: var(--accent-bg);
  border: 1px solid var(--accent-line);
  font: 600 15px/1.4 var(--font);
  color: var(--ink);
}

.notice--plain {
  background: var(--paper);
  border-color: var(--line);
  color: var(--ink-muted);
}

/* A REFUSED MICROPHONE HIDES EVERYTHING THAT MEANS "LISTENING".
   The orb, the level bars and the example chips are all claims that capture is
   running, and none of them is true behind a permission panel. Hiding them is also
   what stops a four-step path pushing the examples out of `.screen__body`, which
   CLIPS rather than scrolls — measured at 375x812: 988px of content in an 812px box,
   with the chips entirely below the fold and unreachable.

   SPECIFICITY IS DELIBERATE, NOT INCIDENTAL. `[data-blocked]` on the section plus a
   descendant is (0,3,1); the rules being overridden are single classes and inline
   `display: flex` on the orb frame, which is why the frame carries `display: none`
   with `!important` and the others do not need it. Three of the five defects the
   recipe screens shipped were a correctly written rule that lost on specificity, so
   this is verified with `getComputedStyle`, not by reading it back. */
[data-screen='voice'][data-blocked='true'] [data-region='voice-orb-frame'] {
  display: none !important;
}

[data-screen='voice'][data-blocked='true'] [data-region='wave'],
[data-screen='voice'][data-blocked='true'] [data-region='voice-examples'] {
  display: none;
}

/* The microphone-permission recovery panel. A numbered path the shopper has to
   follow in another app, so it is set as a real list rather than a run-on sentence:
   they will be looking away from this screen while they do it.

   The panel lives inside `[data-region="voice-error"]`, which sits above a
   `flex: 1` spacer on the voice screen, so it can grow to four steps without
   pushing the example chips off the bottom. */
.notice__title {
  font: 700 17px/1.3 var(--font);
  margin-bottom: 6px;
}

.notice__lead {
  font: 600 14.5px/1.45 var(--font);
  margin-top: 6px;
}

.notice__alt {
  font: 500 13.5px/1.45 var(--font);
  color: var(--ink-muted);
  margin-top: 8px;
}

.mic-steps {
  margin: 10px 0 0;
  padding-left: 22px;
  font: 600 14.5px/1.6 var(--font);
}

.mic-actions {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  margin-top: 14px;
}

.toast {
  position: absolute;
  left: 16px;
  right: 16px;
  bottom: calc(var(--surface-h) + 16px);
  padding: 14px 16px;
  border-radius: var(--r-control);
  background: var(--ink);
  color: #fff;
  font: 600 15px/1.4 var(--font);
  z-index: 60;
  box-shadow: var(--shadow-float);
}

/* Clear the walking screen's primary action. A confirmation that covers "Got it"
 * arrives exactly when the shopper is reaching for it. */
[data-screen='walking'][data-active='true'] ~ .toast {
  bottom: calc(var(--surface-h) + 96px);
}

.toast[hidden] {
  display: none;
}

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  padding: 16px 22px 0;
}

/* NOTE: Phase 1's `.pager` was the page-dot row. Phase 2 makes `.pager` the
 * horizontal card pager and moves the dots to `.pager__dots`. The old rule is
 * gone rather than left to cascade — its `justify-content: center` centred the
 * overflowing pager, which puts half the content at negative x where no amount
 * of scrolling can reach it. */

/* ------------------------------------------------------------------ product */

.metrics {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--gap-m);
  padding: 14px 22px 0;
}

.metric {
  border: 1px solid var(--line);
  border-radius: var(--r-control);
  padding: 16px 6px;
  text-align: center;
}

.metric__value {
  font: 800 24px var(--font);
  color: var(--ink);
}

.metric__name {
  font: 500 14px var(--font);
  color: var(--ink-muted);
  margin-top: 2px;
}

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

/* The debug panel is an OVERLAY, never a flex child of the layout.
 *
 * REGRESSION IT CAUSED: as an ordinary child of `.screen__body--fixed` it grew to
 * 585px of JSON, and since `.pager` is `flex: 1 1 auto; min-height: 0` the pager
 * collapsed to ZERO height. Ten pages of cards existed, each 154px tall, and none
 * of them were visible — while the dots and the swipe count still rendered,
 * because those are `flex: none`. A diagnostic must never be able to displace the
 * thing it is there to help diagnose. */
.debug {
  position: absolute;
  left: 12px;
  right: 12px;
  bottom: calc(var(--surface-h) + 8px);
  max-height: 26%;
  overflow: auto;
  z-index: 30;
  padding: 12px;
  border-radius: 10px;
  background: rgba(247, 247, 248, 0.97);
  border: 1px solid var(--line-strong);
  font: 400 11px/1.5 var(--font-mono);
  color: var(--ink-muted);
  white-space: pre-wrap;
  word-break: break-all;
}

.debug[hidden] {
  display: none;
}

/* ============================================================ Phase 2 ==== */

/* Screens whose content must fit without scrolling: the pager and the two
 * actions below it are the point of two-results-at-a-time (§8.4, §A2). The
 * answer text and the map give up space first; the actions never do. */
.screen__body--fixed {
  overflow-x: hidden;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.answer-head {
  flex: 0 0 auto;
  padding: 22px var(--pad-screen) 0;
}

.answer-head__row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
}

.answer-head__row .echo {
  flex: 1;
  min-width: 0;
}

/* One sentence now, so it needs less type than the design's two-line block. */
.answer {
  font-size: 24px;
}

/* A locate answer also carries the map, and the destination is already the
 * headline of the map block — so the caption gives up a line rather than the
 * cards or the two actions giving up their space. */
.answer--with-map.answer--clamped {
  -webkit-line-clamp: 2;
}

/* Fixed height, so the pager's budget is known and the map block's footer can
 * never be clipped. The SVG inside absorbs the remainder and scales to fit — it
 * is never cropped, only drawn smaller. */
/* The screen's own horizontal padding, which the region did NOT have.
 *
 * It never needed it: the retired `.map-thumb` was an inline-ish button carrying
 * its own margins, so the region could sit full-bleed and the thumbnail still
 * lined up. The strip is `width: 100%`, so it ran edge to edge while the headline
 * above it sat at 26 px — measured in the browser, region left 0 / width 375
 * against text left 26 / width 323. Nothing failed; it just looked wrong, which
 * is the only way this class of defect ever shows up. */
.answer-map {
  flex: none;
  padding: 0 var(--pad-screen);
}

/* ------------------------------------------------------------------- pager */

.pager {
  flex: 1 1 auto;
  /* A floor, not zero: defence in depth against the class of bug above. Whatever
   * else appears in this column, the cards keep enough room to be seen. */
  min-height: 168px;
  display: flex;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}

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

.pager__page {
  flex: 0 0 100%;
  /* `start`, not `center`: with a page exactly as wide as the scrollport,
   * centre-alignment leaves Chrome able to pick a snap target that desyncs from
   * scrollLeft, which strands the pager between two pages. */
  scroll-snap-align: start;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 10px;
  padding: 12px 22px;
  min-width: 0;
}

.pager-foot {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 2px 0 6px;
}

.pager__dots {
  display: flex;
  gap: 6px;
  align-items: center;
}

.pager__dot {
  width: 6px;
  height: 6px;
  border-radius: 3px;
  background: #e1e4e8;
  border: 0;
  padding: 0;
  transition: width 140ms ease;
}

.pager__dot--on {
  width: 24px;
  background: var(--accent);
}

/* Cards are more compact than Phase 1's so two fit above the actions. */
.pager .card {
  padding: 12px;
  gap: 14px;
}

.pager .card__shot {
  width: 72px;
  height: 86px;
}

.pager .card__name {
  font-size: 19px;
  margin-top: 4px;
}

.pager .card__price {
  font-size: 22px;
  margin-top: 6px;
}

.pager .card__meta {
  font-size: 14px;
  margin-top: 3px;
}

/* ---------------------------------------------------------- answer actions */

.answer-actions {
  flex: none;
  display: flex;
  gap: 10px;
  padding: 0 20px 10px;
}

.answer-actions .btn {
  flex: 1;
  height: 54px;
}

/* -------------------------------------------------------------- list count */

.list-count {
  position: absolute;
  right: 14px;
  bottom: calc(var(--surface-h) - 2px);
  padding: 6px 12px;
  border-radius: var(--r-pill);
  background: var(--ink);
  color: #fff;
  font: 700 13px var(--font);
  z-index: 21;
  pointer-events: none;
  box-shadow: var(--shadow-tile);
}

.list-count[hidden] {
  display: none;
}

/* -------------------------------------------------------------------- deals */

.deals-head {
  flex: none;
  padding: 22px var(--pad-screen) 0;
}

.deals-title {
  font: 800 30px var(--font);
  letter-spacing: -0.8px;
  margin: 0;
  color: var(--ink);
}

.deals-tabs {
  display: flex;
  gap: 8px;
  margin-top: 14px;
  overflow-x: auto;
  scrollbar-width: none;
}

.deals-tabs::-webkit-scrollbar {
  display: none;
}

.deals-tab {
  flex: none;
  min-height: 44px;
  padding: 10px 16px;
  border-radius: var(--r-pill);
  border: 1px solid var(--line-strong);
  background: var(--paper);
  color: var(--ink-muted);
  font: 700 14px var(--font);
  cursor: pointer;
  white-space: nowrap;
}

.deals-tab[aria-selected='true'] {
  background: var(--ink);
  border-color: var(--ink);
  color: #fff;
}

.deals-list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 14px 22px calc(var(--surface-h) + 16px);
}

.deals-list .card__shot {
  width: 82px;
  height: 98px;
}

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

.sheet {
  position: absolute;
  inset: 0;
  background: var(--paper);
  z-index: 45;
  display: none;
  flex-direction: column;
  padding: 22px 20px 0;
}

.sheet[data-open='true'] {
  display: flex;
}

.sheet__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  flex: none;
}

.sheet__title {
  font: 800 26px var(--font);
  letter-spacing: -0.6px;
  color: var(--ink);
}

.sheet__sub {
  font: 500 14px/1.4 var(--font);
  color: var(--ink-faint);
  margin-top: 4px;
}

.sheet__body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 14px 0 30px;
}

.chip--on {
  background: var(--ink);
  border-color: var(--ink);
  color: #fff;
}

/* ------------------------------- ongoing lists in the capture sheet (C) */

.capture-pick__title {
  font: 700 13px var(--font);
  letter-spacing: 0.4px;
  text-transform: uppercase;
  color: var(--ink-faint);
  padding: 14px 2px 2px;
}

/* Visually secondary to the three capture routes above it: starting a list is the
 * sheet's purpose, resuming one is the escape hatch. */
.capture-pick {
  background: var(--paper-warm);
}

/* ------------------------------------------------- the store mark (item 7) */

/* The height is RESERVED from the intrinsic aspect ratio (1003x363 = 2.76), so the
 * block occupies its final size before the PNG decodes. Without it the headline below
 * — which has its own carefully measured reservation — is pushed down on load, which
 * is the one thing that reservation exists to prevent. */
.welcome-brand {
  padding: 52px 30px 0;
  display: flex;
  justify-content: center;
}

/* The home screen sits tighter than the welcome screen — it has tiles below it, and
 * the welcome screen has only the orb. */
.welcome-brand--home {
  padding: 30px 30px 0;
}

.welcome-brand__logo {
  width: 190px;
  height: auto;
  /* Belt and braces with the `width`/`height` attributes: either alone reserves the
   * box in a modern browser, and together they also survive a stylesheet that has
   * not loaded yet. */
  aspect-ratio: 1003 / 363;
}

/* --------------------------------------------------- see all (F, 9.3) */

.see-all-sub {
  font: 500 14px/1.35 var(--font);
  color: var(--ink-faint);
  padding: 4px 0 0;
}

/* ------------------------------------------ readback preferences (E, 3k) */

.readback-prefs__prompt {
  flex: none;
  font: 500 15px/1.35 var(--font);
  color: var(--ink-faint);
  padding: 2px var(--pad-screen) 0;
}

/* A single scrolling ROW, not a wrapping grid. Seven chips wrap to three lines on
 * a 375px screen, which pushes the items themselves off the bottom — and the items
 * are what the read-back exists to confirm. `flex: none` on the chips is what
 * makes them keep their width instead of being squeezed to fit. */
.readback-prefs {
  /* `flex: none` is load-bearing, not tidiness. The read-back body is a flex
   * COLUMN, so the default `flex: 0 1 auto` let this row be shrunk — measured at
   * 42px against a 62px natural height — and `overflow-y: hidden` then clipped the
   * bottom of every 48px chip. The rules were all present and the row still was
   * not readable: "the chips exist" is not "the chips are visible". */
  flex: none;
  display: flex;
  gap: 9px;
  padding: 12px var(--pad-screen) 2px;
  overflow-x: auto;
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}

.readback-prefs::-webkit-scrollbar {
  display: none;
}

.readback-prefs .chip {
  flex: none;
}

.readback-prefs__note {
  flex: none;
  font: 500 13.5px/1.35 var(--font);
  color: var(--ink-faint);
  padding: 8px var(--pad-screen) 0;
}

.readback-prefs__note[hidden] {
  display: none;
}

/* ------------------------------------------------------------ product screen */

.product-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 20px var(--pad-screen) 0;
}

.product-title {
  font: 800 26px/1.12 var(--font);
  letter-spacing: -0.8px;
  color: var(--ink);
  margin-top: 6px;
}

.product-price {
  font: 800 32px var(--font);
  color: var(--ink);
  margin-top: 10px;
}

.section {
  margin: 20px 22px 0;
  padding-top: 16px;
  border-top: 1px solid var(--line-faint);
}

.section__label {
  font: 700 12px var(--font);
  color: var(--ink-faint);
  letter-spacing: 0.7px;
  margin-bottom: 10px;
}

.section__body {
  font: 500 15px/1.5 var(--font);
  color: var(--ink-muted);
}

/* Values with a category-relative position. Never a score, never a verdict. */
.metric__rel {
  font: 700 13px var(--font);
  margin-top: 2px;
}

.metric__rel--high {
  color: var(--good);
}

.metric__rel--low {
  color: var(--good);
}

.metric__rel--moderate {
  color: #e9a100;
}

.claims {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.claim {
  font: 700 13px var(--font);
  padding: 7px 12px;
  border-radius: var(--r-pill);
  background: var(--good-tint);
  color: var(--good);
}





.orb--sm {
  width: 150px;
  height: 150px;
}


/* ---------------------------------------------------- the scanner (§8.19) */

.scan-head {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 22px var(--pad-screen) 0;
}

.scan-head__title {
  font: 800 22px var(--font);
  color: var(--ink);
  letter-spacing: -0.4px;
}

.scan-stage {
  flex: 1 1 auto;
  min-height: 0;
  position: relative;
  margin: 16px 20px 0;
  border-radius: var(--r-card);
  overflow: hidden;
  background: #0b0b0d;
}

.scan-video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* The guide matches the crop the decoder actually reads (80% x 32% of the frame),
 * so aiming at the guide is aiming at the decoder. It also does the job the torch
 * would do on Android: a clear target beats a brighter picture. */
.scan-guide {
  position: absolute;
  left: 10%;
  top: 34%;
  width: 80%;
  height: 32%;
  border-radius: 12px;
  box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.45);
}

.scan-guide__corner {
  position: absolute;
  width: 26px;
  height: 26px;
  border: 3px solid var(--accent);
}

.scan-guide__corner--tl {
  top: -2px;
  left: -2px;
  border-right: 0;
  border-bottom: 0;
  border-radius: 12px 0 0 0;
}

.scan-guide__corner--tr {
  top: -2px;
  right: -2px;
  border-left: 0;
  border-bottom: 0;
  border-radius: 0 12px 0 0;
}

.scan-guide__corner--bl {
  bottom: -2px;
  left: -2px;
  border-right: 0;
  border-top: 0;
  border-radius: 0 0 0 12px;
}

.scan-guide__corner--br {
  bottom: -2px;
  right: -2px;
  border-left: 0;
  border-top: 0;
  border-radius: 0 0 12px 0;
}

.scan-foot {
  flex: none;
  padding: 16px 22px calc(28px + env(safe-area-inset-bottom, 0));
}

.scan-hint {
  font: 600 16px/1.45 var(--font);
  color: var(--ink-muted);
  text-align: center;
}

.weighed-note {
  margin: 14px 22px 0;
  padding: 12px 14px;
  border-radius: var(--r-control);
  background: var(--accent-bg);
  border: 1px solid var(--accent-line);
  font: 600 14px/1.4 var(--font);
  color: var(--ink);
}


/* --------------------------------------------- product name and location */

/* Five lines, then a control. The full name is always in the DOM. */
.product-title--clamped {
  display: -webkit-box;
  -webkit-line-clamp: 5;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.name-more {
  appearance: none;
  border: 0;
  background: none;
  padding: 6px 0 0;
  font: 600 14px var(--font);
  color: var(--accent);
  cursor: pointer;
}

.name-more[hidden] {
  display: none;
}

/* The marker-pen highlight from the design. Zone names run long
 * ("Snacks & Beverages Corner"), so the mark wraps with the text instead of
 * forcing one line: `box-decoration-break` keeps the highlight painted on every
 * line rather than only the first. */
.where-mark {
  font: 800 14px/1.5 var(--font);
  color: var(--ink);
  letter-spacing: 0.3px;
  text-transform: uppercase;
  background: linear-gradient(
    180deg,
    rgba(255, 214, 0, 0) 12%,
    rgba(255, 214, 0, 0.72) 12%,
    rgba(255, 214, 0, 0.72) 92%,
    rgba(255, 214, 0, 0) 92%
  );
  padding: 2px 6px;
  border-radius: 3px;
  max-width: 62%;
  text-align: right;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}

.where-mark:empty {
  display: none;
}

/* ========================================================== Phase 4b ======
 * The list: capture sheet, read-back, walking screen, full list.
 *
 * Measured from the design import ("Add list" 3b, "Spoken list, read back for
 * confirmation" 3k, "Up next" 2c). The design owns type scale, colour, radii,
 * spacing and motion; the spec and the phase brief own behaviour.
 *
 * ERGONOMICS (§14) drive the sizes here more than taste does: the phone is on a
 * cart handle, read while walking, operated with one hand that is also steering.
 * One very large element per screen, everything else small, and controls in the
 * lower two-thirds where a reaching thumb lands.
 */

.btn--tall {
  min-height: 66px;
  font: 700 19px var(--font);
}

/* ---------------------------------------------------------- capture (§8.5) */

.capture-route {
  width: 100%;
  min-height: 88px;
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 0 20px;
  margin-bottom: 12px;
  border: 1px solid var(--line);
  border-radius: var(--r-tile);
  background: var(--paper);
  box-shadow: var(--shadow-tile);
  text-align: left;
}

/* The live route is the one the design emphasises. */
.capture-route:not(.capture-route--soon) {
  border: 1.5px solid var(--accent);
  background: var(--accent-bg);
}

.capture-route--soon {
  opacity: 0.55;
}

.capture-route__main {
  flex: 1;
  min-width: 0;
}

.capture-route__label {
  font: 700 19px var(--font);
  color: var(--ink);
}

.capture-route__sub {
  margin-top: 2px;
  font: 500 13.5px var(--font);
  color: var(--ink-muted);
}

.capture-dismiss {
  display: block;
  width: 100%;
  margin-top: 8px;
  padding: 14px;
  border: 0;
  background: none;
  font: 600 16px var(--font);
  color: var(--ink-muted);
}

/* --------------------------------------------------------- read-back (§8.6) */

.readback-head {
  flex: none;
  padding: 22px var(--pad-screen) 0;
}

.readback-said {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.readback-said__label {
  font: 500 14px var(--font);
  color: var(--ink-faint);
}

.readback-said__text {
  margin: 0;
  font: 600 15.5px/1.35 var(--font);
  color: var(--ink-muted);
}

.readback-heading {
  margin: 18px 0 0;
  font: 800 28px/1.2 var(--font);
  color: var(--ink);
  letter-spacing: -0.8px;
  text-wrap: pretty;
}

.readback-count {
  margin-top: 5px;
  font: 500 15px var(--font);
  color: var(--ink-muted);
}

.readback-rows {
  flex: 1 1 auto;
  min-height: 120px;
  overflow-y: auto;
  padding: 18px var(--pad-screen) 4px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.readback-actions {
  flex: none;
  padding: 14px var(--pad-screen) calc(16px + env(safe-area-inset-bottom, 0));
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* ------------------------------------------------------------- a list row */

/* WRAPS. "chicken breasts" beside "Poultry & Packaged Meats" does not fit on one
 * 375px row, and with `flex-wrap: nowrap` the two collided — the item text ran over
 * its own destination. Wrapping puts the destination on its own line instead, which
 * costs a row of height and is legible.
 *
 * `align-items: center` stays for the single-line case, which is most of them. */
.list-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px 14px;
  padding: 16px 18px;
  border: 1px solid var(--line);
  border-radius: 18px;
  background: var(--paper);
  box-shadow: var(--shadow-tile);
}

/* -------------------------------------------------------------------------
 * THE INFERRED MARKER. The visual half of invariant #5.
 *
 * These are items nobody said out loud — we added them because the shopper
 * named an occasion. Inference is allowed to be generous ONLY because this is
 * unmissable and removal is one tap, so it needs real contrast: it has to read
 * at a glance from a moving cart, not merely be present in a screenshot. Hence
 * an accent left edge AND a tinted row AND a labelled kicker, rather than a
 * subtle grey word.
 * ------------------------------------------------------------------------- */
.list-row--inferred {
  border-color: var(--accent-line);
  background: var(--accent-bg);
  box-shadow: inset 4px 0 0 var(--accent);
}

.inferred-mark {
  display: inline-block;
  margin-bottom: 4px;
  font: 800 11.5px var(--font);
  letter-spacing: 0.8px;
  text-transform: uppercase;
  color: var(--accent);
}

.list-row--done .list-row__text {
  color: var(--ink-faint);
  text-decoration: line-through;
}

.list-row__main {
  flex: 1;
  min-width: 0;
  border: 0;
  background: none;
  padding: 0;
  text-align: left;
}

.list-row__text {
  font: 700 21px var(--font);
  color: var(--ink);
  text-wrap: pretty;
}

.list-row__constraints {
  margin-top: 3px;
  font: 500 13.5px var(--font);
  color: var(--accent);
}

/* `flex: none` so it keeps its width, and NOT `text-align: right` once it has
 * wrapped to its own line — right-aligned under a left-aligned item reads as two
 * unrelated things. Left when wrapped, right when it shares the row: `margin-left:
 * auto` does both without a media query or a measurement. */
.list-row__where {
  flex: none;
  margin-left: auto;
  font: 500 14.5px var(--font);
  color: var(--ink-faint);
}

/* The item text may shrink and wrap; it must never be squeezed to zero by a long
 * destination. `min-width` is what stops the flex algorithm from doing that. */
.list-row__main {
  min-width: 8ch;
}

.list-row__remove,
.list-row__check {
  width: 40px;
  height: 40px;
  flex: none;
  border-radius: 50%;
  border: 0;
  background: var(--chip-bg);
  color: var(--ink-muted);
  font: 600 17px var(--font);
  display: flex;
  align-items: center;
  justify-content: center;
}

.list-row__check {
  border: 2px solid var(--line-strong);
  background: var(--paper);
}

.list-row__check[aria-pressed='true'] {
  border-color: var(--accent);
  background: var(--accent);
  color: #fff;
}

/* --------------------------------------------------- walking screen (§8.7) */

.walking-head {
  flex: none;
  padding: 22px var(--pad-screen) 0;
}

.walking-head__row {
  display: flex;
  align-items: center;
  gap: 12px;
}

.walking-head__pos {
  flex: 1;
  font: 600 15px var(--font);
  letter-spacing: 0.4px;
  color: var(--ink-faint);
}

.walking-head__list {
  border: 0;
  background: none;
  padding: 8px 4px;
  font: 600 16px var(--font);
  color: var(--accent);
}

.walking-progress {
  display: flex;
  gap: 5px;
  margin-top: 14px;
}

/* THREE STATES, FILLING LEFT TO RIGHT (design 2j).
 *
 * It filled right to left, and the cause was that only `--on` existed: every
 * segment was `flex: 1` and coloured purely by `done`, so the colour appeared
 * wherever the done items happened to sit. Each segment now carries its own state
 * and order is document order, which is left to right. */
.walking-progress__seg {
  flex: 1;
  height: 5px;
  border-radius: 3px;
  background: var(--line-faint);
}

/* Checked — the darker colour. */
.walking-progress__seg--done {
  background: var(--accent);
}

/* THE CURRENT item — lighter, so it reads as "you are here" against both a
 * finished segment and an empty one. It has to sit visibly between the two, which
 * is why --progress-now is more saturated than --accent-line. */
.walking-progress__seg--now {
  background: var(--progress-now);
}

/* The hero sits at the TOP and the suggestion follows it immediately; the slack
 * is absorbed below, by the suggestion region. Letting the hero block stretch
 * instead pushed the card to the bottom of the screen with 400px of nothing
 * between them, which reads as a layout that failed to load. */
/* The map FLOATS rather than sharing a flex row, and this is a measurement, not a
 * preference. As a flex sibling the 128px map left the hero column 181px wide on a
 * 375px screen — and at 181px the longest real aisle name ("Snacks & Beverages
 * Corner", 25 characters) cannot fit on one line even at the 24px floor. Design
 * `2c` runs the aisle full width under the item.
 *
 * Floating gets both: the item wraps beside the map, and the aisle line clears it
 * and gets the full 331px. */
/* FIXED HEIGHT — item 6's entire mechanism.
 *
 * The two product cards below must never move: identical y on every item, on every
 * name, on every zone name. Nothing about font ramps guarantees that on its own,
 * because a ramp still has a floor and a long enough name reaches it. So the
 * region is given a height and `render.fitWalkingBody` makes its CONTENT yield —
 * name ramp 54->30, then the aisle 34->24, then the position sentence 17->13, then
 * the name truncates. Position is the invariant; the text bends around it.
 *
 * `overflow: hidden` is the backstop, not the mechanism: if a future change adds a
 * row the fit cannot shrink, the cards still hold their place and the overflow is
 * visible in the browser rather than silently displacing them.
 *
 * The old float is gone (the map no longer floats right), so `::after` clearing
 * went with it. */
.walking-body {
  flex: none;
  /* 298px, and the number is arithmetic rather than taste. The viewport is 812,
   * the head takes 85 and the persistent surface reserves --surface-h (104), so
   * 623 is left for this region plus both cards. The cards need 16 + 98.9 + 44 +
   * 165.7 = 324.6, and 623 - 325 = 298.
   *
   * At 322 the pairing card ended at 731.7 with the surface starting at 708 — it
   * was COVERED by 24px, which item 6 forbids as explicitly as it forbids moving.
   * Locking the y is not enough on its own; the locked position has to be one
   * where both cards are actually visible.
   *
   * SOLVED, not tuned: with both cards at a FIXED 130px (see
   * `.walking-cards .suggestion`) the block below is 16 + 130 + 16 + 130 = 292, so
   * 85 + 326 + 292 = 703 and the pairing card's bottom lands exactly on the
   * surface's top edge. Trying to
   * reach this by shrinking the region alone failed twice, because the cards' own
   * heights were the variable — card 1 measured 99px on one item and 127px on
   * another, and the pairing card 166 to 191 as its reason line wrapped to three
   * lines. A locked y over a variable height locks the top and lets the bottom
   * move, which is the half of "never covered" that matters. */
  /* NO FIXED HEIGHT ANY MORE, and that is the fix rather than a relaxation.
   *
   * 326px was arithmetic against ONE fixed card height (130px for both cards),
   * and the two cards do not hold the same content: the pairing card carries a
   * REASON row the recommended card does not, and invariant #18 makes that row
   * mandatory. Forcing both to one height is what pushed 37px of the pairing
   * card's content outside its own border — measured 17 of 17 items, worst 41px.
   *
   * So the CARDS block below is `flex: none` and sized by its own bounded
   * content, and this region takes whatever is left. The cards still cannot move,
   * because their block's height is a constant; it is simply a constant derived
   * from the content rather than imposed on it. `fitWalkingBody` already ramps
   * this region's text against its measured `clientHeight`, so it needs no number
   * here and survives a viewport that is not 812 tall. */
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
  padding: 22px var(--pad-screen) 0;
}

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

.walking__kicker {
  font: 800 14px var(--font);
  letter-spacing: 0.8px;
  color: var(--accent);
}

/* One very large element per screen (§14 rule 2). This is it: the item, in the
 * customer's own words. The design's second line ("1 gallon, whole") is
 * deliberately absent — a size they never stated is a product-level claim on a
 * category-level intent. */
.walking__item {
  margin: 10px 0 0;
  font: 800 52px/0.98 var(--font);
  letter-spacing: -2px;
  color: var(--ink);
  /* NEVER BREAK OR HYPHENATE INSIDE A WORD.
   *
   * `overflow-wrap: break-word` rendered "Cheetos" as "Cheeto / s", and `hyphens:
   * auto` produced "bev-erages", "condi-ments" and "ba-nanas". A word split across
   * two lines is not a smaller word, it is two wrong words — and on the one string
   * the shopper is reading at arm's length while pushing a cart.
   *
   * The adaptive fit (`render.fitHero`) is what makes a long word fit: it shrinks the
   * type to the floor. Below the floor a word stays whole and overflows, which is
   * visible and reportable rather than silently mangled — `render.wordTooLong`
   * detects exactly that case. */
  text-wrap: balance;
  overflow-wrap: normal;
  word-break: keep-all;
  hyphens: none;
}

.walking__constraints {
  margin-top: 10px;
  font: 600 17px var(--font);
  color: var(--accent);
}

/* `clear: right` is what hands this line the full screen width. Without it the
 * aisle sits in the 181px column beside the map and wraps at every real name. */
.walking__where {
  clear: right;
  margin-top: 22px;
  display: flex;
  align-items: baseline;
  gap: 12px;
  flex-wrap: wrap;
}

/* BLOCK, and with an explicit line-height. Both are needed by the adaptive fit:
 * `scrollHeight` is not a line measure on an inline element, and a computed
 * `line-height: normal` parses to NaN — together they made the aisle shrink to its
 * floor on every string, including "Aisle 3". */
.walking__aisle {
  display: block;
  font: 800 34px/1.1 var(--font);
  letter-spacing: -1px;
  color: var(--accent);
}

/* Invariant #8: an unresolvable location states its own reason. It is never a
 * guessed aisle and never silently blank. */
.walking__nowhere {
  font: 600 17px/1.35 var(--font);
  color: var(--ink-muted);
}

.walking__done {
  font: 800 30px/1.2 var(--font);
  letter-spacing: -1px;
  color: var(--ink);
}

/* The completion screen's two actions, TOGETHER and below the headline.
 *
 * They used to be at opposite ends of the screen: "Create new list" rendered into the
 * card region and collided with the headline, while "Show the list" stayed in the
 * docked CTA pinned at the very bottom. Design `2c`'s spacing — a 57px primary, a
 * 12px gap, then the secondary. */
.walking-done-actions {
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin-top: 28px;
}

.walking-done-actions .btn {
  width: 100%;
}

/* The docked CTA is the WALKING screen's control, and the completion screen is not
 * the walking screen. Leaving it there is what put a primary action 300px below the
 * two it belongs with. */
[data-screen='walking'][data-done='true'] .walking-actions {
  display: none;
}

/* Nothing to scroll clear of once the CTA is gone. */
[data-screen='walking'][data-done='true'] .walking-cards {
  padding-bottom: calc(var(--surface-h) + 16px);
}

/* FULL WIDTH, ABOVE THE HERO — and the float is gone, which is what pays for it.
 *
 * MEASURED at 375x812 with the real `fitHero`: the 128px floated thumbnail cost
 * 0 px of height because it sat BESIDE the hero, but the hero then had to wrap
 * its 52px type around a 128x90 box. Removing the float reclaims a median of
 * 62 px — a whole line — across six item-name lengths, 0 px for a one-line name
 * like `milk` and 72 px for `bananas` and `cheddar cheese`.
 *
 * The strip is not free, so the net is small rather than zero, and the reason for
 * spending it is the LANDMARK: 128px cannot carry a neighbouring run, and "Dairy
 * Aisle, next to Aisle 2" is what makes the strip more than decoration.
 *
 * `.walking-cards` below is already scrolling — it overflows by 171 px at
 * baseline — so every pixel here lands on an existing clip. The real net is
 * recorded in docs/LOCATION.md. */
.walking-map {
  margin: 0 0 10px;
}


.walking__note {
  margin-top: 10px;
  font: 500 14px/1.35 var(--font);
  color: var(--ink-muted);
}

/* The suggested product. When there is none this region is EMPTY and collapses
 * — no placeholder, no dashed frame. The pairing slot is not here at all. */
/* The DEAL fallback, visually distinct from a pairing (item 6). A pairing claims two
 * things go together; a deal claims only that this is discounted on the way. Sharing
 * one treatment would let the fallback borrow the pairing's authority. */
.suggestion--deal {
  border-color: var(--accent);
  background: linear-gradient(180deg, rgba(255, 90, 0, 0.05), rgba(255, 90, 0, 0));
}

.suggestion--deal .suggestion__label {
  color: var(--accent);
}

/* Tier 3, visually distinct from BOTH the others. A pairing claims a relationship, a
 * deal claims a discount, and this claims only that the aisle sells a lot of it — so
 * it gets the quietest treatment of the three. */
.suggestion--popular .suggestion__label {
  color: var(--ink-faint);
}

/* The pairing's reason and its own aisle, on one line. A flex gap rather than a
 * punctuation character in the copy: the reason is built by the SERVICE from the
 * shopper's words, so gluing a separator onto it in the client would put customer-
 * facing punctuation outside copy.js. Rendered without a gap they read as one
 * string — "Goes with cerealDairy Aisle". */
.suggestion__reason {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 10px;
  margin-top: 6px;
  font: 500 13.5px/1.35 var(--font);
  color: var(--ink-muted);
}

/* The pairing's OWN aisle (§9.4) — a different category in a different place, so
 * this is deliberately not the current item's. */
.suggestion__reason-aisle {
  font-weight: 700;
  color: var(--accent);
  /* A MARGIN, NOT THE ROW'S `gap`, and the reason is CSS specificity rather than
     taste. `.suggestion__reason` is `display: flex` with `gap: 4px 10px`, but
     `.walking-cards .suggestion__reason` overrides `display` to `-webkit-box` for
     the two-line clamp — and `gap` does not apply to a `-webkit-box`. So the
     separation silently vanished on the walking screen only, rendering "Goes with
     vegan cream cheeseDairy Aisle": exactly the glued string the elements were
     wrapped in spans to prevent, reintroduced by a rule that changed the layout
     model out from under it. A margin survives both. */
  margin-left: 6px;
}

/* The scrolling card area: recommended, then pairing. The SCROLL lives here and
 * not on either card host, so the two move together and neither can be clipped
 * independently of the other. */
.walking-cards {
  /* NOTHING ON THIS PAGE SCROLLS VERTICALLY. The only scroll is horizontal,
   * between items, and it is a swipe rather than a scroll container.
   *
   * This region scrolled 207px, measured. Two causes, and neither was the cards
   * being too big: the block reserved `--surface-h` a SECOND time (
   * `.screen--with-surface .screen__body` already reserves it, which is the same
   * double-reservation `.rx-dots` and `.rx-body` each carry a note about), and it
   * reserved `--walking-cta-h` for a docked button that design 2j removed from
   * this state — `[data-region="walking-cta"]` is `hidden` while an item is
   * shown, so 68px was held for a control with no height.
   *
   * `flex: none` WITH AN EXPLICIT HEIGHT, and the height is not decoration.
   * Sized by its content instead, this region is 0 tall until the cards are
   * appended — so `fitWalkingBody` ran against a 607px body, found that a 54px
   * name fitted, and never ramped; the cards then landed, the body shrank to 283,
   * and the hero overflowed it by up to 79px with the type still at its maximum.
   * MEASURED on "vegan cream cheese": a 3-line 54px name in a 159px row.
   *
   * 16 + 125 + 16 + 167 = 324 — the two card heights and their margins, the same
   * arithmetic as the card rules below rather than a tuned number. Reserved even
   * when an item has no cards, so the hero's type size does not jump between an
   * item that has them and one that does not. */
  flex: none;
  height: 324px;
  overflow: hidden;
  /* Horizontal padding only. The surface clearance lives on
   * `.screen--with-surface .screen__body` and reserving it twice is what created
   * the dead space this region then scrolled through. */
  padding: 0 22px;
}

.walking-suggestion:empty,
.walking-pairing:empty {
  display: none;
}

.suggestion {
  width: 100%;
  margin-top: 16px;
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 14px 18px;
  border: 1px solid var(--line);
  border-radius: var(--r-tile);
  background: var(--paper);
  box-shadow: var(--shadow-tile);
  text-align: left;
}

.suggestion__shot {
  width: 56px;
  height: 68px;
  flex: none;
  border-radius: 12px;
  object-fit: contain;
  background: #fff;
  border: 1px solid var(--line-faint);
}

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

.suggestion__label {
  font: 500 12.5px var(--font);
  letter-spacing: 0.4px;
  color: var(--ink-faint);
}

.suggestion__name {
  margin-top: 4px;
  /* 20px per design 2j. */
  font: 700 20px/1.2 var(--font);
  color: var(--ink);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  /* TWO LINES ALWAYS RESERVED, and this is what locks the PAIRING card's y.
   *
   * MEASURED: with the name free to be one or two lines the recommended card was
   * 98.9px or 118.7px, so the pairing card below it sat at 538 or 558 depending on
   * how long the product happened to be called. The region above was already
   * fixed, so card 1 held at 423 while card 2 moved 20px — item 6 says BOTH cards
   * never move, and reserving the second line is what makes card 1's height a
   * constant instead of a function of its content. */
  min-height: 48px;
}

.suggestion__price {
  margin-top: 5px;
  font: 800 19px var(--font);
  color: var(--ink);
}

.suggestion__more {
  flex: none;
  font: 600 15px var(--font);
  color: var(--accent);
}

/* THE BAR MERGE (§F, design `2c`) — reverses amendment 21's stacking, not its
 * ruling. Amendment 21 kept the persistent surface on the walking screen because
 * §5.1 makes that screen list mode's home and a home with no way to ask a question
 * is a dead end. That stays true: the surface is still here, still reachable. What
 * changes is that "Got it" no longer occupies its OWN in-flow bar above it.
 *
 * Measured before: 214px of the viewport went to two stacked bars (a 94px in-flow
 * CTA block plus a 98px absolute surface). Docking the CTA directly above the
 * surface — 2c's 12px gap — hands the difference back to the suggestion area,
 * which is where the pairing card goes.
 *
 * `bottom` is the surface's own height plus the gap. It is expressed against
 * --surface-h rather than hardcoded so the two cannot drift apart and leave the
 * CTA floating over the controls. */
.walking-actions {
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(var(--surface-h) + 6px);
  padding: 0 20px;
  z-index: 21;
}

.walking-actions .btn {
  width: 100%;
}

/* -------------------------------------------------------- full list (§8.8) */

.list-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  /* 14 rather than 22 at the top: this screen's job is to show as many items as
     it can, and the title needs clearance from the status bar, not framing. */
  padding: 14px var(--pad-screen) 6px;
}

/* The list SCROLLS and the shop button does not. `flex: 1 1 auto` + `min-height: 0`
 * is what makes the scroll live here rather than on the screen body — a scrolling
 * body would carry the pinned button up and down with it, which is the opposite of
 * pinned. */
.list-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: 0 var(--pad-screen) 8px;
  display: flex;
  flex-direction: column;
  /* 10 -> 7. Twelve rows at 74px each carry eleven gaps; three pixels off each is
     a third of another row, and rows are what this screen is for. */
  gap: 7px;
}

/* D: the sticky "Shop list" button. Sits above the persistent surface rather than
 * over it — §6's four controls stay reachable, and a primary action overlapping them
 * is a mis-tap generator on a phone. */
.list-shop {
  flex: none;
  /* THE SURFACE IS RESERVED ONCE, and it was reserved twice.
   *
   * This held `calc(var(--surface-h) + 8px)` while
   * `.screen--with-surface .screen__body` was already holding
   * `calc(var(--surface-h) + 16px)` — 232px of reservation for a 104px surface.
   * MEASURED at 375x812: 128px of dead space below the button, which is 1.7 rows
   * of a list the shopper is trying to read. Exactly the double-reservation that
   * made the walking screen's card region scroll 207px.
   *
   * 8px is the gap between the button and the surface's top edge, nothing more. */
  padding: 12px var(--pad-screen) 8px;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, var(--paper-warm) 42%);
}

.list-shop .btn {
  width: 100%;
}

/* Nothing to shop, nothing to press. An empty list shows its own empty state and
 * this collapses rather than offering a button that leads to a blank walk. */
.list-shop[hidden] {
  display: none;
}

.list-group {
  margin-top: 12px;
  font: 700 14px var(--font);
  letter-spacing: 0.4px;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* --------------------------------------------------- §9.1 category browse ---
 * The answer to a department word: the next choice, above the grid.
 *
 * The chip row SCROLLS HORIZONTALLY rather than wrapping. A department can
 * return several subcategories, and a wrapping row pushes the product grid off
 * a 375x812 screen — the grid is still the answer for shoppers who ignore the
 * chips, and it must not be displaced by them.
 */
.browse {
  padding: 4px 0 2px;
}

.browse__prompt {
  padding: 0 22px 10px;
  font: 600 15px var(--font);
  color: var(--ink-soft);
}

.browse__chips {
  display: flex;
  gap: 10px;
  overflow-x: auto;
  padding: 0 22px 4px;
  scrollbar-width: none;
}

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

.browse__chip {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 0 0 auto;
  max-width: 70vw;
}

.browse__name {
  font: 600 15px var(--font);
  color: var(--ink);
}

/* The API's `aisle`, verbatim, and the only location string on this card.
 * `map_segment` is a polygon key and is never rendered (invariant #11). */
.browse__aisle {
  font: 500 13px var(--font);
  color: var(--accent);
}

/* ------------------------------------------------------------------ recipes
   Designs `3d` (Recipes — one at a time, big visuals) and `3l` (Recipe detail —
   ingredients, then two actions), imported 2026-08-12 into
   docs/design/coop-app-screens.html.

   Values are taken from the design's own inline styles and expressed through the
   existing tokens where a token already holds that exact value; where the design
   uses a one-off (452px card image, 178px hero, 62px action buttons) the literal
   is kept so a later design diff is legible. */

/* ---------------------------------------------------------------- 3d list */

.rx-head {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 22px 22px 0;
}

.rx-back,
.rx-hero__back {
  border: 0;
  background: none;
  font: 600 26px var(--font);
  color: var(--ink);
  padding: 0;
  cursor: pointer;
  line-height: 1;
}

.rx-head__title {
  font: 800 30px var(--font);
  color: var(--ink);
  letter-spacing: -0.8px;
  margin: 0;
}

.rx-filters {
  display: flex;
  gap: 9px;
  padding: 16px 22px 0;
  overflow-x: auto;
  scrollbar-width: none;
  flex: 0 0 auto;
}

.rx-filters::-webkit-scrollbar { display: none; }

.rx-filter {
  flex: none;
  padding: 12px 18px;
  border-radius: 24px;
  border: 1px solid var(--line-strong);
  background: var(--paper);
  color: var(--ink);
  font: 600 15px var(--font);
  cursor: pointer;
}

.rx-filter[aria-pressed='true'] {
  background: var(--ink);
  border-color: var(--ink);
  color: #fff;
}

.rx-subhead {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  padding: 28px 22px 0;
  flex: 0 0 auto;
}

.rx-subhead__label {
  font: 700 20px var(--font);
  color: var(--ink);
}

.rx-subhead__count {
  font: 500 14px var(--font);
  color: var(--ink-faint);
}

.rx-carousel {
  display: flex;
  gap: 16px;
  padding: 16px 22px 0;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  /* WITHOUT THIS, EVERY CARD AFTER THE FIRST SITS FLUSH AGAINST THE SCREEN EDGE.
     `padding-left` only insets the first card; `scroll-snap-align: start` snaps to
     the scrollport's start edge, which ignores padding unless scroll-padding says
     otherwise. So the carousel looked correctly inset until it was scrolled. */
  scroll-padding-left: 22px;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  flex: 1 1 auto;
  min-height: 0;
  align-items: flex-start;
}

.rx-carousel::-webkit-scrollbar { display: none; }

.rx-card {
  width: 300px;
  flex: none;
  scroll-snap-align: start;
  border: 0;
  background: none;
  padding: 0;
  text-align: left;
  font: inherit;
  color: inherit;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  /* Full height of the carousel track, so the image can take the slack above a
     body that always stays visible. */
  height: 100%;
  min-height: 0;
}

/* The design specifies a 452px image on a 390x844 phone, where the title, meta and
   availability line below it land in the last 90px and just fit.

   Here it is a FLEXIBLE height with 452px as the cap, because a hard 452 pushed all
   three text lines off the bottom on a 375x812 viewport — including the availability
   line, which is the one thing on this card a shopper needs before tapping. The card
   is a column, the image takes the slack, the body is never compressed. Measured in
   the browser at 812px: the image lands near 360px and every line is visible. */
.rx-card__shot {
  width: 300px;
  max-height: 452px;
  flex: 1 1 auto;
  min-height: 120px;
  object-fit: cover;
  border-radius: 28px;
  background: var(--skeleton-a);
  display: block;
}

.rx-card__body {
  padding: 16px 4px 0;
  flex: 0 0 auto;
}

.rx-card__title {
  font: 800 25px/1.15 var(--font);
  color: var(--ink);
  letter-spacing: -0.6px;
}

.rx-card__meta {
  font: 500 15.5px var(--font);
  color: var(--ink-muted);
  margin-top: 7px;
}

/* THE CONDITIONAL AVAILABILITY LINE.
   Green ONLY when `data-complete="true"`, which the client sets from the server's
   `coverage_complete`. 6 of 23 recipes have an unmatched ingredient, so an
   unconditional green all-clear would be a false availability claim on a quarter of
   the set. The incomplete form is deliberately NOT red or warning-coloured: nothing
   is wrong, we simply matched fewer rows, and colouring it as a problem would imply
   a stock claim. */
.rx-card__stock {
  font: 600 15.5px var(--font);
  color: var(--ink-muted);
  margin-top: 4px;
  min-height: 20px;
}

.rx-card__stock[data-complete='true'] { color: var(--good); }

/* No surface-height padding here: `.screen--with-surface .screen__body` already
   reserves it, and adding it again left ~160px of dead space below the dots with the
   card shrunk to match. Measured in the browser at 375x812. */
.rx-dots {
  display: flex;
  gap: 7px;
  justify-content: center;
  flex-wrap: wrap;
  padding: 18px 22px 6px;
  flex: 0 0 auto;
}

.rx-dot {
  width: 7px;
  height: 7px;
  border-radius: 4px;
  background: #dddee2;
}

.rx-dot[data-active='true'] {
  width: 22px;
  background: var(--ink);
}

/* -------------------------------------------------------------- 3l detail */

/* NO `[data-screen='recipe'] { display: flex }` HERE, and this comment is the
   guard. `.screen` is `display:none` and `.screen[data-active='true']` turns it on;
   a bare `[data-screen='recipe']` rule has equal specificity and sits LATER in the
   file, so it wins unconditionally and the recipe screen renders on top of whatever
   screen is actually active. Found in the browser as a stray back-button and a hero
   image over the welcome screen — the same `display`-outranks-the-hidden-state trap
   CLAUDE.md records for `[hidden]`.

   It was also unnecessary: `.screen` already sets `flex-direction: column`. */

.rx-hero {
  position: relative;
  height: 178px;
  flex: 0 0 auto;
  background: var(--skeleton-a);
  overflow: hidden;
}

.rx-hero__shot {
  width: 100%;
  height: 178px;
  object-fit: cover;
  display: block;
}

.rx-hero__back {
  position: absolute;
  top: 16px;
  left: 20px;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.92);
  display: flex;
  align-items: center;
  justify-content: center;
  font: 600 20px var(--font);
  z-index: 8;
}

/* `.rx-actions` is a flex SIBLING below this, not an overlay, so the body must not
   also reserve the surface height — `.screen--with-surface .screen__body` does that
   by default and the two together left ~100px of dead space above the buttons.
   The actions bar carries the surface clearance; the body just fills what is left. */
.rx-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

/* TWO CLASSES, TO MATCH THE RULE IT IS OVERRIDING. `.screen--with-surface
   .screen__body` sets `padding-bottom: calc(var(--surface-h) + 16px)` at specificity
   0,2,0; a bare `.rx-body` is 0,1,0 and loses, so the first attempt at this changed
   nothing and left 140px of dead space above the buttons (measured: computed
   padding-bottom stayed 120px). Third time this exercise that a `display`/padding
   rule lost on specificity — the lesson is to read the computed value, not the
   stylesheet. */
.screen--with-surface .rx-body {
  padding-bottom: 0;
}

.rx-detail {
  display: flex;
  flex-direction: column;
  padding: 18px 22px 20px;
}

.rx-title {
  font: 800 28px/1.15 var(--font);
  color: var(--ink);
  letter-spacing: -0.9px;
  margin: 0;
}

.rx-desc {
  font: 500 15px/1.45 var(--font);
  color: var(--ink-muted);
  margin: 6px 0 0;
}

.rx-chips {
  display: flex;
  gap: 8px;
  margin-top: 14px;
  flex-wrap: wrap;
}

.rx-chip {
  padding: 9px 14px;
  border-radius: 20px;
  background: #f1f2f4;
  font: 600 14px var(--font);
  color: var(--ink);
}

/* The total, in the design's green. It renders only once the SERVER has computed
   it — there is no placeholder and no client-side arithmetic behind it. */
.rx-chip--price {
  background: #eaf7f0;
  color: var(--good);
}

.rx-diet {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 10px;
}

.rx-diet__chip {
  font: 600 11px var(--font);
  color: var(--good);
  background: var(--good-tint);
  border-radius: var(--r-pill);
  padding: 3px 10px;
  text-transform: capitalize;
}

.rx-block__head {
  font: 700 19px var(--font);
  color: var(--ink);
  margin: 18px 0 0;
}

.rx-rows { display: flex; flex-direction: column; }

.rx-row {
  display: flex;
  align-items: center;
  gap: 13px;
  padding: 11px 0;
  border-bottom: 1px solid #f1f2f4;
}

.rx-row__thumb {
  width: 40px;
  height: 40px;
  flex: none;
  border-radius: 11px;
  background: var(--skeleton-a);
  overflow: hidden;
}

.rx-row__img {
  width: 40px;
  height: 40px;
  object-fit: cover;
  display: block;
}

.rx-row__body { flex: 1; min-width: 0; }

.rx-row__name {
  font: 600 17px var(--font);
  color: var(--ink);
}

.rx-row__sub {
  font: 500 14px var(--font);
  color: var(--ink-muted);
  margin-top: 2px;
}

.rx-row__deal {
  color: var(--good);
  font-weight: 600;
}

/* An unmatched row is DIMMED, never struck through and never marked unavailable:
   the item stays on the list (§8.5) and we are saying nothing about stock. */
.rx-row[data-unmatched='true'] .rx-row__name { color: var(--ink-muted); }

.rx-check {
  width: 30px;
  height: 30px;
  flex: none;
  border-radius: 50%;
  border: 1.5px solid var(--line-strong);
  background: var(--paper);
  display: flex;
  align-items: center;
  justify-content: center;
  font: 600 15px var(--font);
  color: var(--ink-faint);
  cursor: pointer;
  padding: 0;
}

.rx-check[data-checked='true'] {
  background: var(--ink);
  border-color: var(--ink);
  color: #fff;
}

.rx-steps {
  counter-reset: step;
  list-style: none;
  margin: 10px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.rx-step {
  counter-increment: step;
  display: flex;
  gap: 10px;
  font: 400 14px/1.45 var(--font);
  color: var(--ink);
}

.rx-step::before {
  content: counter(step);
  flex: 0 0 auto;
  width: 22px;
  height: 22px;
  border-radius: var(--r-pill);
  background: var(--accent-tint);
  color: var(--accent-hot);
  font: 700 12px var(--font);
  display: grid;
  place-items: center;
}

/* The two actions of design 3l, side by side and sticky. */
.rx-actions {
  flex: 0 0 auto;
  display: flex;
  gap: 11px;
  padding: 14px 22px calc(var(--surface-h) + 14px);
  background: var(--paper);
  box-shadow: 0 -1px 0 var(--line);
}

.rx-btn {
  flex: 1;
  height: 62px;
  border-radius: 18px;
  font: 700 18px var(--font);
  cursor: pointer;
  border: 1.5px solid transparent;
}

.rx-btn--primary {
  background: #0a0a0c;
  color: #fff;
}

.rx-btn--primary:disabled {
  background: var(--chip-bg);
  color: var(--ink-faint);
  cursor: default;
}

.rx-btn--ghost {
  background: var(--paper);
  border-color: var(--line-strong);
  color: var(--ink);
}

/* §10 ruling: the "cheapest options" toggle, beside the Ingredients heading.
   A pill rather than a switch — it is one of two named states, and both states are
   legitimate defaults for different shoppers. */
.rx-rows__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}

.rx-cheapest {
  flex: none;
  padding: 7px 13px;
  border-radius: var(--r-pill);
  border: 1px solid var(--line-strong);
  background: var(--paper);
  color: var(--ink);
  font: 600 13px var(--font);
  cursor: pointer;
}

.rx-cheapest[aria-pressed='true'] {
  background: var(--good-tint);
  border-color: var(--good-tint);
  color: var(--good);
}

.rx-cheapest:disabled {
  color: var(--ink-faint);
  cursor: default;
}

/* Ingredients | Instructions (item 5) — a segmented control below the description.
   It replaced an inline step expansion AND the "Save instructions" button, so the
   method is readable without an account. */
.rx-tabs {
  display: flex;
  gap: 4px;
  margin-top: 16px;
  padding: 4px;
  background: var(--chip-bg);
  border-radius: 14px;
}

.rx-tab {
  flex: 1;
  padding: 9px 12px;
  border: 0;
  border-radius: 11px;
  background: none;
  color: var(--ink-muted);
  font: 600 14px var(--font);
  cursor: pointer;
}

.rx-tab[aria-pressed='true'] {
  background: var(--paper);
  color: var(--ink);
  box-shadow: var(--shadow-card);
}

/* One primary action, full width (item 6). */
.rx-btn--block { flex: 1 1 100%; }

/* The post-add state, matching the answer screen's accent treatment: the control
   that added becomes the way out. */
.rx-btn--accent {
  background: var(--accent);
  color: #fff;
}

/* The read-back's close control. Positioned rather than in flow because the head is
   a stack of text blocks and the X belongs at its top-right corner. */
.readback-close {
  position: absolute;
  top: 14px;
  right: 18px;
  z-index: 4;
}

[data-screen='list-readback'] .readback-head { position: relative; }

/* THE HOME-SCREEN CARD. Over the walking screen, once, in a browser tab only. */
.pwa-gate {
  position: absolute;
  inset: 0;
  z-index: 80;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: rgba(255, 255, 255, 0.96);
}

.pwa-gate[hidden] {
  display: none;
}

.pwa-gate__card {
  width: 100%;
  max-width: 320px;
  padding: 24px 22px;
  border: 1px solid var(--line);
  border-radius: var(--r-tile);
  background: var(--paper);
  box-shadow: var(--shadow-tile);
}

.pwa-gate__title {
  margin: 0 0 10px;
  font: 800 24px/1.15 var(--font);
  letter-spacing: -0.5px;
  color: var(--ink);
}

.pwa-gate__body {
  margin: 0 0 16px;
  font: 500 14px/1.45 var(--font);
  color: var(--ink-muted);
}

.pwa-gate__steps {
  margin: 0 0 20px;
  padding-left: 20px;
  font: 500 14px/1.5 var(--font);
  color: var(--ink);
}

.pwa-gate__steps li + li {
  margin-top: 8px;
}

/* --------------------------------------------- flow (a): the one-item card
 *
 * The same overlay shape as `.pwa-gate` above, and deliberately so: both are
 * one-off cards over the walking screen, and a second geometry for the same
 * relationship would be a second thing to keep in step. It sits BELOW the gate's
 * z-index because the gate is a decision about the whole session and this is a
 * decision about one word — but in practice they are never both up, because
 * `openAddConfirm` hides the gate.
 *
 * NO `--surface-h` RESERVATION. The persistent surface is reserved once, by
 * `.screen--with-surface .screen__body`, and this card is a full-bleed overlay
 * that deliberately covers it: the mic and the keyboard are how the shopper got
 * here, and offering them again over a question they have not answered is a
 * second capture on top of the first. The property to assert is that the card's
 * controls clear the bottom of the screen, which the centred flex does.
 */
.add-confirm {
  position: absolute;
  inset: 0;
  z-index: 70;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: rgba(255, 255, 255, 0.96);
}

.add-confirm[hidden] {
  display: none;
}

.add-confirm__card {
  width: 100%;
  max-width: 320px;
  padding: 22px;
  border: 1px solid var(--line);
  border-radius: var(--r-tile);
  background: var(--paper);
  box-shadow: var(--shadow-tile);
}

.add-confirm__said {
  margin-bottom: 14px;
}

.add-confirm__said[hidden] {
  display: none;
}

.add-confirm__said-label {
  font: 700 11px var(--font);
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: var(--ink-muted);
}

/* The shopper's own words, byte-identical. Quiet, because the ITEM is the thing
   being decided about and the echo is only proof of being heard. */
.add-confirm__said-text {
  margin: 4px 0 0;
  font: 500 14px/1.4 var(--font);
  color: var(--ink-muted);
}

.add-confirm__title {
  margin: 0 0 14px;
  font: 800 22px/1.15 var(--font);
  letter-spacing: -0.5px;
  color: var(--ink);
}

.add-confirm__item {
  margin-bottom: 20px;
}

/* THE ITEM IS THE LARGEST THING ON THE CARD. It is what the question is about,
   and it is read by someone holding a phone in one hand and a cart in the other. */
.confirm-item {
  padding: 16px;
  border: 1px solid var(--line);
  border-radius: var(--r-tile);
  background: var(--chip-bg);
}

.confirm-item__text {
  font: 800 26px/1.15 var(--font);
  letter-spacing: -0.6px;
  color: var(--ink);
  /* A long single word shrinks the card rather than breaking mid-word: "Cheetos"
     as "Cheeto / s" is two wrong words, the same rule the walking hero keeps. */
  overflow-wrap: break-word;
  word-break: keep-all;
}

.confirm-item__constraints {
  margin-top: 6px;
  font: 600 13px var(--font);
  color: var(--ink-muted);
}

/* The destination, or the block closes up — no dash, no placeholder, and never a
   guessed aisle (§8.10). */
.confirm-item__where {
  margin-top: 10px;
  font: 700 14px var(--font);
  color: var(--accent-hot);
}

.confirm-item .inferred-mark {
  margin-bottom: 6px;
}

.add-confirm__card .btn + .btn {
  margin-top: 10px;
}

.add-confirm__card .btn {
  width: 100%;
}

/* THE BROWSER-TOLERANT VARIANT. The strip and the also-line are the two elements
   that yield, in that order — see `applyBrowserVariant`. The aisle name, the
   position SENTENCE and the full-map link all stay, so the shopper loses the
   drawing of where along the run they are and keeps the words for it.

   `display: none` rather than not rendering them: the location block is
   `location.js`'s component and only it may build the strip's internals, so the
   walking screen expresses this as a rule about its own scope. The ramp measures
   the region after this applies, so the reclaimed height is real. */
/* THE BROWSER SHOWS EVERYTHING AND SCROLLS. Product ruling 2026-08-16, reversing
 * the earlier one: completeness beats the locked-card invariant here, because the
 * browser tab is the FIRST thing every tester sees and a screen with its content
 * cut reads as broken before it reads as compact.
 *
 * THE INVARIANT NOW APPLIES TO STANDALONE ONLY. There, the cards stay locked at a
 * constant y and nothing scrolls — that is `[data-variant='standalone']`, which is
 * the base rules untouched. Everything below is scoped to the browser branch and
 * changes nothing about the other one.
 *
 * Nothing is dropped and nothing is clipped: the strip and the also-line are back,
 * the cards take the height their content needs, and the page scrolls to reach
 * whatever does not fit. */
[data-screen='walking'][data-variant='browser'] .screen__body {
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

/* The region above the cards stops being a fixed box: with the page scrolling
   there is nothing to protect below it, so its content is shown rather than
   ramped into a height it does not have. */
[data-screen='walking'][data-variant='browser'] .walking-body {
  flex: none;
  height: auto;
  overflow: visible;
}

[data-screen='walking'][data-variant='browser'] .walking-cards {
  height: auto;
  overflow: visible;
}

/* EQUAL MINIMUM HEIGHT, NATURAL GROWTH, NOTHING CUT.
 *
 * `min-height` rather than `height` is the whole of it: the two cards do not hold
 * the same rows — the pairing card carries a REASON that invariant #18 makes
 * mandatory — so an equal FIXED height either clips that row or leaves 42px of
 * dead space in the other card. An equal FLOOR gives them the same height
 * whenever the content allows and lets the pairing card grow when it does not,
 * which is the only reading of "the same height" that is also "uncut". */
[data-screen='walking'][data-variant='browser'] .walking-cards .suggestion,
[data-screen='walking'][data-variant='browser'] .walking-cards .suggestion--pair {
  height: auto;
  min-height: 125px;
  overflow: visible;
}

/* And the clamps come off the text inside them, or "uncut" is only true of the
   box. Two lines was a budget imposed by a fixed height that is now gone. */
[data-screen='walking'][data-variant='browser'] .walking-cards .suggestion__name,
[data-screen='walking'][data-variant='browser'] .walking-cards .suggestion__reason {
  -webkit-line-clamp: unset;
  display: block;
  overflow: visible;
}

/* WHAT WAS HERE, AND WHY IT IS GONE. This variant used to squeeze: it tightened
 * the head, the region padding and the Got it control, then spent 20px of the
 * cards' own measured slack, all to fit everything into a screen that would not
 * scroll. The ruling reversed — the browser shows everything and scrolls — so the
 * squeeze has nothing left to buy, and keeping it would shrink a control and a
 * heading for no reason at all. Removed rather than left dormant: a rule that no
 * longer has a purpose is the next person's puzzle.
 *
 * The height reductions in particular MUST go, not merely be overridden. They sat
 * after the auto-height rules above and would have won on source order. */

/* THE SWIPE, between items. The two content regions move together and the head
   does not: the position line and the progress bar are chrome that describe where
   you are in the list, and a shopper dragging the item should not be dragging the
   thing that tells them which item it is.

   `--swipe-dx` is written by `bindWalkSwipe` on every `pointermove`, and
   `--swipe-ms` is 0 while the finger is down — an eased transform during a drag is
   what makes a carousel feel like it is arguing with you. It becomes 180ms only
   for the release, which always lands on exactly one of three offsets. */
[data-screen='walking'] .walking-body,
[data-screen='walking'] .walking-cards {
  transform: translateX(var(--swipe-dx, 0px));
  transition: transform var(--swipe-ms, 0ms) cubic-bezier(0.22, 0.61, 0.36, 1);
}

/* NOTHING VERTICAL FOR THE BROWSER TO CLAIM — **AND THAT IS TRUE OF STANDALONE
   ONLY, NOW.** The original comment ended "nothing on this screen scrolls, so
   nothing is given up by saying so", which was true when it was written and was
   falsified two commits later by the browser variant being made to scroll
   (`baec9e2`). A blanket `touch-action: none` then told the browser the app
   handles every pan on a screen that had become a scroll container — so on a real
   touch screen the two contend, the address bar starts hiding, Safari takes the
   pointer stream and fires `pointercancel`, and the swipe is abandoned. That is
   the whole of "swipe barely works in a browser tab", and it is a conflict
   between two of our own rules rather than anything Safari does wrong.

   THE PREMISE IS THE THING TO SCOPE, not the value. Standalone still scrolls
   nothing, so it still gives up nothing by claiming every axis. */
[data-screen='walking'] {
  touch-action: none;
}

/* The browser variant DOES scroll, so it hands the vertical axis back and claims
   only the horizontal one. `pan-y` is precisely "the browser pans vertically, the
   app handles the rest" — the standard shape for a carousel inside a scroller,
   and the one that stops the two from fighting over the same finger. */
[data-screen='walking'][data-variant='browser'] {
  touch-action: pan-y;
}

/* The build stamp on the welcome screen. A DEVELOPER surface deliberately kept
   quiet — it sits under the consent line in the same faint grey, because it must
   be readable by a tester holding the phone and invisible to a shopper who is not
   looking for it. `pre-wrap` because the expanded state is one file per line. */
.buildstamp {
  border: 0;
  background: none;
  padding: 2px 4px;
  margin: 0;
  font: 500 10px/1.5 var(--font-mono, ui-monospace, monospace);
  color: var(--ink-faint);
  text-align: center;
  white-space: pre-wrap;
  /* NO `word-break: break-all` HERE, and the suite was right to reject it. It was
     defensive and unnecessary — the longest expanded row is a 26-character
     `name:hex` token, which fits at 10px monospace — and the property the rule
     protects is that no CUSTOMER surface splits a word, which the welcome screen
     is regardless of how faint this text is. */
  max-width: 300px;
  cursor: pointer;
  opacity: 0.75;
}

/* `?build=1` — pinned over every screen, so a tester already mid-trip can answer
   "am I on the current build" without getting back to the welcome screen. */
.buildstamp--pinned {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 95;
  max-width: none;
  background: rgba(255, 255, 255, 0.92);
  padding: 4px 8px calc(4px + env(safe-area-inset-bottom, 0));
  opacity: 1;
}

/* Expanded, it is a diagnostic and stops pretending otherwise. */
.buildstamp[aria-expanded='true'] {
  opacity: 1;
  text-align: left;
  color: var(--ink-soft, var(--ink-faint));
}

/* The API-version mismatch banner. A DEVELOPER message, shown in place of an empty
   screen — pinned to the top, above every screen, and dismissible because it must not
   block the app it is describing. Deliberately loud: three debugging cycles were lost
   to this failure looking like a client bug. */
.stale-api {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 90;
  padding: 12px 44px 12px 16px;
  background: #2b1a00;
  color: #ffe3d0;
  font: 500 12.5px/1.5 var(--font);
}

.stale-api__title {
  display: block;
  font: 700 13px var(--font);
  color: var(--accent-warm);
  margin-bottom: 2px;
}

.stale-api__x {
  position: absolute;
  top: 8px;
  right: 10px;
  border: 0;
  background: none;
  color: #ffe3d0;
  font-size: 15px;
  cursor: pointer;
  padding: 4px 6px;
}

/* ==========================================================================
   THE LOCATION COMPONENT (docs/LOCATION.md)
   ==========================================================================
   One component on every screen, in compact or standard mode. The rules that
   shape this block:

     * ONE ORANGE ELEMENT. Orange means "your item is here" and nothing else, so
       the run, the sibling shelves, the neighbour landmark and every label are
       neutral grey. Product content stays visually dominant.
     * The strip renders HORIZONTALLY with the CHECKOUT END ON THE LEFT. That is
       a fixed physical landmark rather than a side, so the orange segment means
       the same thing whichever end of the aisle the shopper walked in from.
     * `See exactly where` is orange TEXT, not a button.
   ========================================================================== */

.loc {
  margin: 0;
}

/* The product screen's map section. 22 px is that screen's own convention —
 * `.cards`, `.chips` and `.metrics` all use the literal, and there is no token
 * for it — so this matches its neighbours rather than introducing a third value. */
.product-map {
  padding: 8px 22px 0;
}

.loc__where {
  margin-bottom: 10px;
}

.loc__aisle {
  font: 800 27px/1.15 var(--font);
  letter-spacing: -0.6px;
  color: var(--accent);
}

.loc-strip__frame {
  position: relative;
}

/* `preserveAspectRatio: none` on the SVG: the strip is a SCHEMATIC, deliberately
 * not to scale. A 23 m aisle at true proportions is a 6.6:1 hairline while a
 * 9.3 m fridge run is 2.2:1 and three times taller, so honouring aspect would
 * make the component's height depend on which zone the shopper happens to be
 * going to. Length resolution is what the strip is for; true size is the full
 * map's job.
 *
 * ONE HEIGHT NOW, not two: the neighbour-run row is gone (design 2j replaced it
 * with the aisle's own two sides), so there is no rows-1/rows-2 variance and the
 * block's height no longer depends on whether a landmark happened to exist. */
.loc-strip {
  display: block;
  width: 100%;
  /* `--strip-h` is the LAST step of `render.js::fitWalkingBody`'s ramp, set on
     the region rather than on this element: only `location.js` may build or select
     the strip's internals (a second renderer is how the two location paths once
     diverged), so the fit reaches it through a custom property instead. The
     default is the unramped height. */
  height: var(--strip-h, 54px);
}

/* THE TWO SIDES OF THE AISLE. Grey, with a real border so they read as fixtures.
 * --strip-line is darker than --line-strong on purpose: at 16 viewBox units
 * scaled into ~54 px these have to hold their own against the orange marker. */
.loc-line {
  fill: var(--strip-line);
}

/* THE ONE ORANGE ELEMENT. Taller than the line it sits on, so it reads as a
 * marker ON a shelf rather than a segment OF it (design 2j). */
.loc-here {
  fill: var(--accent);
}

/* Zone level: the whole line, washed, so it cannot be mistaken for a
 * shelf-precise marker. 9 of 280 segments land here and it is an honest state. */
.loc-here-zone {
  fill: rgba(255, 90, 0, 0.3);
  stroke: var(--accent-hot);
  stroke-width: 1.6;
  stroke-dasharray: 5 4;
}

/* The end caps and the landmark name, INSIDE the SVG. Sized in viewBox units, so
 * they scale with the strip instead of adding rows to the block — which is what
 * they did as sibling divs, at 18 and 19 px on a screen with none to spare.
 *
 * A LANDMARK LABEL, never a direction — no left, no right, no front, no back
 * (invariant #11). */
.loc-cap {
  fill: var(--ink-faint);
  font-family: var(--font);
  font-weight: 700;
  letter-spacing: 0.3px;
}


/* The position sentence and the way to the full map share ONE row: two stacked
 * rows cost 31 + 44 px, side by side they cost the taller. The sentence is the
 * subject so it takes the space; the link sits at the end of the line. */
.loc__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin-top: 7px;
}

/* The position sentence. In BUCKETS, never a percentage: the graphic is allowed
 * to be more precise than the words, never the other way round. */
.loc__position {
  flex: 1 1 auto;
  font: 600 15px/1.3 var(--font);
  color: var(--ink);
}

/* Other places, gated on `name_share` — fires on 24% of located items rather
 * than the 90% the old note managed. Secondary weight: it qualifies the
 * destination, it is not a second destination. */
.loc__also {
  margin-top: 3px;
  font: 500 13px/1.3 var(--font);
  color: var(--ink-muted);
}

/* Orange text, no button (design instruction). Still a real <button> for the
 * accessibility tree and for a 44px touch target. */
.loc__open {
  flex: 0 0 auto;
  padding: 4px 0;
  border: 0;
  background: none;
  font: 700 14px var(--font);
  color: var(--accent);
  white-space: nowrap;
  cursor: pointer;
}

/* COMPACT: the walking screen, whose hero already carries the aisle in 34px
 * type. The heading is dropped there; the position line is NOT — it is the thing
 * that screen was missing. */
.walking-map .loc__position {
  font-size: 15px;
}

/* ==========================================================================
   THE GOT IT CONTROL (design 2j / 2k)
   ==========================================================================
   Navy pill beside the item name, with the white circle inside it as the tap
   target. On tap it becomes the green checked state and holds for 1s before
   advancing — the check IS the confirmation, so advancing instantly would mean
   the shopper never sees what they confirmed.

   One <button> wrapping both, not a decorative pill plus a mystery circle: the
   accessibility tree gets a single control with a single label. */
/* ROTATED 90 DEGREES: the label on top, the circle below it.
 *
 * Horizontally the pill was 62px tall and roughly 150px wide, and width is the
 * scarce axis on this row — the item name is the largest element on the screen
 * and shares the row with it. Measured at 375px: the name's TEXT ran into the
 * pill on 6 of 28 items, worst 97px ("Sourdough bread"), while the name's own BOX
 * never overlapped it at all, because flex had already shrunk the box and the
 * glyphs simply overflowed. Standing the control up gives that width back. */
.gotit {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  flex: none;
  /* Narrow enough that the name gets the width, wide enough that "Got it" sits on
     one line at 17px and the circle clears its own padding.
     HEIGHT IS AS SCARCE AS WIDTH ON THIS SCREEN — the first rotation used 92px
     and cost the region 30px it did not have, which showed up as the location
     block being clipped rather than as anything wrong with the control. 74px is
     8 + 21 (label) + 5 + 32 (circle) + 8. */
  width: 82px;
  height: 74px;
  padding: 8px 0;
  border: 1.5px solid transparent;
  border-radius: 24px;
  background: var(--ink);
  cursor: pointer;
  transition: background 0.18s ease, border-color 0.18s ease;
}

.gotit__label {
  font: 800 17px var(--font);
  color: var(--paper);
  white-space: nowrap;
  transition: color 0.18s ease;
}

.gotit__circle {
  width: 32px;
  height: 32px;
  flex: none;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font: 700 20px var(--font);
  color: var(--paper);
  background: var(--paper);
  transition: background 0.18s ease;
}

/* A SLOW, STEADY, SUBTLE PULSE. The shopper looks at this screen fifteen times a
   trip, so the brief is "draws the eye without being annoying" — which rules out
   anything fast, anything that moves the layout, and anything that repeats a
   sharp attack.

   A HALO rather than a scale: `box-shadow` is composited and does not reflow, so
   it cannot nudge the item name beside it, and a soft ring reads as attention
   rather than as a button twitching. 2.6s with a symmetric ease is slow enough to
   be felt rather than watched.

   It stops once CHECKED — the control has been pressed and is about to advance;
   continuing to ask for attention there would be the interface talking over
   itself. */
@keyframes gotit-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(255, 90, 0, 0); }
  50% { box-shadow: 0 0 0 9px rgba(255, 90, 0, 0.16); }
}

.gotit:not(.gotit--checked) {
  animation: gotit-pulse 2.6s ease-in-out infinite;
}

/* Respected, not approximated. A shopper who has asked their phone to stop moving
   things has asked for exactly this. */
@media (prefers-reduced-motion: reduce) {
  .gotit:not(.gotit--checked) {
    animation: none;
  }
}

/* 2k. --good and --good-tint are the design's own values now, so there is one
   success green in the app rather than two near-misses. */
.gotit--checked {
  background: var(--good-tint);
  border-color: var(--good);
}

.gotit--checked .gotit__label {
  color: var(--good);
}

.gotit--checked .gotit__circle {
  background: var(--good);
  color: var(--paper);
}

/* The item name and its control on one row. `min-width: 0` on the name is what
   lets it shrink and truncate instead of pushing the pill off the screen. */
.walking__row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  margin-top: 12px;
}

/* TWO LINES, ALWAYS, AND AN ELLIPSIS BEYOND — not only as a last resort.
 *
 * This was `.walking__item--clipped`, applied by `fitWalkingBody` once every ramp
 * had bottomed out. That made the two-line rule conditional on the ramp reaching
 * its floor, and the ramp is driven by VERTICAL fit — so a name that was too WIDE
 * ran to three lines and into the control while the vertical check was satisfied.
 * MEASURED: `maxLines` 3 across the list, with the type still at 54px.
 *
 * Clamping unconditionally makes "at most two lines" a property of the element
 * rather than an outcome of the fit, and the fit is then only about size. */
.walking__row .walking__item {
  margin: 0;
  min-width: 0;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  /* ROOM FOR THE INK, and this is a REGRESSION I INTRODUCED with the clamp above.
   *
   * `.walking__item` is `52px/0.98` — a line box deliberately TIGHTER than the
   * font's own ink, which is what gives the display type its packed look. Before
   * the clamp there was no `overflow: hidden` here, so the ink simply overflowed
   * the box and was visible. Adding `overflow: hidden` to get "at most two lines"
   * started clipping it: MEASURED at 375x812, the glyphs sit 8px ABOVE and 7px
   * BELOW the box at 54px, and 5/4px at the 30px floor — every item, every size,
   * with descenders ("oranges", "yogurt") the most obviously beheaded.
   *
   * In `em` because the overhang is proportional to the type size, which is also
   * why RAMPING can never fix it: shrinking the font shrinks the box and the ink
   * together, so a fit predicate that included ink overflow could never return
   * false and would drive every name to the floor. The box has to be right.
   *
   * 0.17/0.15 against a measured 0.15/0.13 — the margin is for another weight or
   * a font that falls back, not for tuning.
   *
   * THE RIGHT EDGE IS THE SAME DEFECT ON A DIFFERENT AXIS, and it needed a
   * different instrument to see. A Range's client rects are LINE BOXES, which end
   * at the last glyph's ADVANCE WIDTH — they do not include ink that overhangs it,
   * so measuring with them reported 0 overflow on all four sides while the final
   * letter was still being shaved. Canvas `TextMetrics.actualBoundingBoxRight`
   * measures true ink: MEASURED at 375x812 across six words and four ramp sizes,
   * ink runs up to 1.0px past the advance.
   *
   * FLAT px, NOT em, and deliberately the opposite choice from the vertical pad
   * above: the overhang does not scale with the type — it is 0.2px at 54px and
   * 1.0px at 30px, which is grid-fitting, not geometry. A proportional value would
   * be too small exactly where the overhang is largest. Left overhang is negative
   * at every size (ink starts inside the origin), so only the right needs it. */
  padding: 0.17em 2px 0.15em 0;
}

/* Retained: `fitWalkingBody` still adds it, and a name that has actually been cut
   is worth being able to select in a test and in the DOM. The clamp above is now
   unconditional, so this marks the CASE rather than causing the behaviour. */
.walking__item--clipped {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* UP NEXT becomes RESUME LIST when the shopper has swiped away from next-up.
   Same slot, same type scale, so the label and the control cannot both be on
   screen competing for the same job. */
.walking__resume {
  display: inline-block;
  padding: 0;
  border: 0;
  background: none;
  font: 800 14px var(--font);
  letter-spacing: 0.8px;
  color: var(--accent);
  cursor: pointer;
}

/* "See all" to match 2j: plain orange text at 15px, inside the card's own row
   rather than a bordered control below it. It stays a <button> with
   stopPropagation — a tap here must not open the single product screen. */
.suggestion__more {
  align-self: center;
  flex: none;
  padding: 6px 0 6px 8px;
  border: 0;
  background: none;
  font: 600 15px var(--font);
  color: var(--accent);
  white-space: nowrap;
  cursor: pointer;
}

/* ==========================================================================
   BOTH CARDS AT A FIXED HEIGHT — the other half of item 6
   ==========================================================================
   Locking the region above the cards fixes their TOP. It does nothing about their
   BOTTOM, and "never covered" is a statement about the bottom: measured, card 1
   ranged 99-127px as its name wrapped and the pairing card 166-191px as its reason
   line ran to three lines, so the pairing card's bottom moved 25px and crossed
   into the persistent surface.

   So both cards are given the same fixed height and their text is clamped to fit
   inside it. The card is the invariant; the text bends. */
/* TWO CARDS, TWO HEIGHTS, EACH FROM THE CONTENT IT ACTUALLY HOLDS.
 *
 * One shared height was the defect. The two cards do not hold the same rows: the
 * pairing card carries a REASON ("Goes with frozen peas · Poultry & Packaged
 * Meats") that invariant #18 makes mandatory, and the recommended card has no
 * such row. Measured inside the cards at 375x812:
 *
 *   recommended  14 + 16 (label) + 4 + 48 (name, 2 lines) + 5 + 24 (price) + 14
 *                = 125, and it overflowed on 0 of 17 items — it always fitted.
 *   pairing      the same 97 of content + 6 + 36 (reason, 2 lines) = 139 + 28
 *                = 167, against a 130px box: 37px of content rendered BELOW the
 *                card's own border, on 17 of 17 items, worst 41px. That is the
 *                "Popular in Produce FridgesProduce Fridges" orphan text.
 *
 * `overflow: hidden` so nothing can ever paint outside the card again, and
 * `render.js::fitCard` guarantees the content fits so that hiding never clips.
 * Both together, deliberately: the fit function is the property and the overflow
 * rule is the backstop, and a backstop that is never reached costs nothing.
 *
 * Two name lines rather than one, unchanged. Design 2j shows single-line names
 * because its copy is placeholder ("Milam's Whole Milk"); the real catalogue has
 * "Grass Fed Ground Beef 90/10", where one line drops the grade. */
.walking-cards .suggestion {
  height: 125px;
  overflow: hidden;
  align-items: flex-start;
}

.walking-cards .suggestion--pair {
  height: 167px;
}

/* The pairing card's reason. REQUIRED and non-empty (invariant #18), so it cannot
   be dropped to save space — it is clamped instead. Two lines, then an ellipsis,
   never a mid-word break. */
.walking-cards .suggestion__reason {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* The packshot must not stretch to the card's fixed height — design 2j keeps it a
   square-ish thumbnail with the text beside it. */
.walking-cards .suggestion__shot {
  align-self: center;
}

/* The card's own text column, so the clamps have something bounded to work in. */
.walking-cards .suggestion__main {
  min-width: 0;
  overflow: hidden;
}
