/* =========================================================================
   Pet Club — storefront client.

   The palette is IMPORTED, never restated. brand/tokens/undercoat.css is
   generated from brand/tokens/undercoat.tokens.json; web/tokens/undercoat.css
   is a symlink to it. If a value is missing here, add it to the tokens JSON
   and re-run tools/build_tokens.py — do not paste a hex code into this file.

   Everything below is the *application* layer: how Undercoat's components are
   assembled for a shop. Component styling follows brand/positioning-site.html
   so the two surfaces cannot drift apart visually.

   ---------------------------------------------------------------------------
   MOBILE FIRST — which direction the queries point, and why it is load-bearing.

   The file this replaces was a desktop page made narrow: a `max-width: 1140px`
   wrapper with six `max-width` media queries hanging off it, breakpoints at
   860/820/460, and not one rule anywhere enforcing a touch target. Measured at
   390px against the seeded catalogue, that page put 46 of its 57 controls (81%)
   under 44px — the nav links were 36px tall, the wordmark 28px, "Sign in" 29px
   — every one of its four form fields at 15px, which is the size that makes iOS
   Safari zoom the page on focus and never zoom back, and the masthead alone
   stood 143px tall: 17% of the viewport, sticky, permanently.

   The same measurement against this file: not one of its ~47 controls (the
   count moves by one or two with cart and filter state) is under 44px, no field
   is below 16px, and the masthead is 105px and scrolls away. Re-run it by
   serving `git archive HEAD apps/store` beside the working tree and counting
   `getBoundingClientRect()` on every focusable node at a 390px viewport.

   In India the mobile web IS the storefront: a link from WhatsApp, a search
   result, a shared product. A design that is only correct once it has 1140px is
   a design that is wrong for almost every visit.

   So:
     - Everything OUTSIDE a media query is the phone. Read this file top to
       bottom with no queries applied and you have the 390px design.
     - Every media query is `min-width` and only ever ADDS. There is no
       `max-width` query in this file and there should not be one again: a
       `max-width` query is a desktop rule being taken back, which means the
       desktop rule was the base.
     - Three breakpoints, named once here and written literally below because a
       custom property cannot appear in a media query:
           600px   large phone / small tablet
           900px   multi-column layouts; the masthead becomes sticky
          1200px   wide desktop

   WHAT IS PINNED, AND WHERE. On a phone exactly one thing is pinned to the
   viewport, and it is at the BOTTOM: `.stickybar`, holding the one action the
   screen exists for. The top is left to the content. The masthead scrolls away
   and only becomes sticky at 900px, where there is height to spend on it; the
   catalogue's filter bar is the single exception, because on a results screen
   the search box is the control you want while you scroll. Two sticky bars top
   and bottom on an 844px phone is a quarter of the screen given to chrome.

   THE CLASS CONTRACT. js/ui.js documents the vocabulary this stylesheet has to
   define, and the screens carry no inline layout at all — the only inline style
   left in the client is the width of a progress fill, which is data. Every
   section below is named for the block of that contract it implements.
   ========================================================================= */
/* THE PALETTE IS LINKED BY THE PAGE, NOT IMPORTED HERE.

   There used to be an `@import "./tokens/undercoat.css";` on this line, while
   every shell ALSO carries `<link rel="stylesheet" href="./tokens/undercoat.css?v=…">`.
   Two different URLs for one 4 KB file: two cache entries and two requests on
   every page. The @import was the expensive one — invisible to the preload
   scanner, so it was not even discovered until this 77 KB file had been
   downloaded and parsed, adding a round trip to the render-blocking chain — and
   its URL carried no ?v= stamp, because version-store-assets.mjs stamps <link>
   and <script> and cannot reach inside a stylesheet.
   
   Keep the tag in the shells ABOVE this file. tools/check-css-vars.mjs is what
   enforces that every property this file consumes is defined by a sheet the
   page actually links. */

/* -------------------------------------------------------------------------
   The application scale.

   Spacing is NOT in undercoat.tokens.json — the design system publishes colour,
   type, radius, texture and duration and nothing else. Rather than keep
   guessing per element (the old file had 26 distinct hand-typed paddings) the
   scale lives here, in the application layer: one 4px ramp, plus the handful of
   semantic names the layout actually reasons about.

   The semantic values are the brand's own rhythm, measured off
   brand/positioning-site.html: 15–22px inside a card, 12–16px between cards,
   ~20px to the screen edge.

   RADII AND COLOUR ARE NOT REDEFINED HERE. Those come from the token file, and
   a literal in this section would be the exact drift the token pipeline exists
   to prevent.
   ------------------------------------------------------------------------- */
:root {
  /* The ramp. */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 20px;
  --space-6: 24px;
  --space-7: 32px;
  --space-8: 40px;
  --space-9: 56px;

  /* Semantic spacing — phone values. */
  --gutter: var(--space-5);       /* page edge to content */
  --pad-card: 18px;               /* inside a card, panel or sheet */
  --pad-tile: var(--space-3);     /* inside a catalogue tile — it is 165px wide */
  --pad-tight: var(--space-4);    /* inside a well, notice, toast, error box */
  --gap-card: var(--space-3);     /* between sibling cards in a grid */
  --gap-block: var(--space-5);    /* between blocks stacked in one column */
  --gap-section: var(--space-7);  /* between page sections */

  /* Touch. 44px is the floor for every control at every width, and it is always
     reached with min-height + padding rather than a fixed height, so a label
     can still grow with the reader's type size. A control that is 44px because
     it says `height: 44px` stops being 44px the moment someone zooms text. */
  --tap: 44px;

  --page-max: 1140px;

  /* Type roles, phone first. Reading text is >= 15px and body copy is 17px;
     only labels — eyebrows, tags, mono metadata — go below that, and never
     below 11px.

     --fs-field is 16px at EVERY width and has to stay there: iOS Safari zooms
     the whole page when a focused field is smaller than 16px and never zooms
     back out, which reads to a customer as a layout bug. */
  --fs-body: 17px;
  --fs-lede: 18px;
  --fs-small: 15px;    /* secondary reading text: captions, card copy, tables */
  --fs-micro: 14px;    /* hints and fine print */
  --fs-label: 12px;    /* eyebrows, legends, mono metadata */
  --fs-control: 16px;  /* button and nav labels */
  --fs-field: 16px;    /* form inputs — a floor, not a preference */
}

@media (min-width: 600px) {
  :root {
    --gutter: var(--space-7);
    --pad-card: 20px;
    --pad-tile: var(--space-4);
    --gap-card: var(--space-4);
  }
}

@media (min-width: 900px) {
  :root {
    --gutter: var(--space-8);
    --pad-card: 22px;
    --gap-block: var(--space-6);
    --gap-section: var(--space-8);

    /* Denser editorial type once there is a mouse and a wide column. Body and
       field sizes do not move. */
    --fs-lede: 20px;
    --fs-small: 13.5px;
    --fs-micro: 12.5px;
    --fs-label: 11px;
    --fs-control: 14px;
  }
}

@media (min-width: 1200px) {
  :root { --gutter: var(--space-9); }
}

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

html {
  /* iOS inflates text in landscape unless told not to; the page is already
     sized for the device. */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  /* An in-page jump — the skip link, #main after a pager move — must land below
     whatever is sticky rather than under it. */
  scroll-padding-top: var(--space-3);
}

html, body { margin: 0; padding: 0; }

body {
  background: var(--undercoat);
  color: var(--ink);
  font-family: var(--serif);
  font-size: var(--fs-body);
  line-height: 1.62;
  -webkit-font-smoothing: antialiased;
  /* Clear whatever `stickyBar()` measured itself to be, so the last line of
     every page stays readable. `--stickybar-h` is written by ui.js and reset by
     `clearStickyBar()`; this is the only place it becomes padding, which is why
     ui.js deliberately does not also set `body.style.paddingBottom` — an inline
     value would clobber the `max(…, env(…))` that keeps a page with NO bar
     clear of the home indicator. */
  padding-bottom: max(var(--stickybar-h, 0px), env(safe-area-inset-bottom, 0px));
}

/* Vertical scrolling belongs to the browser. Nothing in this client sets
   `touch-action` on a scrolling surface, `overscroll-behavior` on the page, or
   a full-viewport overlay, so momentum, rubber-banding and pull-to-refresh all
   behave natively. `touch-action: manipulation` appears only on buttons, where
   it drops the double-tap delay and nothing else — pinch-zoom stays everywhere,
   on every surface. */

img, svg, video, canvas { max-width: 100%; }
img { height: auto; }

/* NOTE: scoped to `div` deliberately. `.wrap` is the page column — used as
   `div.wrap` by the masthead, the footer, the shell, the filter bar and the
   sticky bar — but product.js also renders `p.hint.wrap`, meaning "this hint
   wraps", which would otherwise inherit a 1140px column and a 20px inset. The
   `div` narrows the collision away; renaming that one class in product.js is
   the real fix and this qualifier can go with it. */
div.wrap {
  width: 100%;
  max-width: var(--page-max);
  margin: 0 auto;
  /* max() rather than a plain gutter: a notched phone held sideways puts the
     camera cutout inside the page box. */
  padding-inline: max(var(--gutter), env(safe-area-inset-left, 0px))
                  max(var(--gutter), env(safe-area-inset-right, 0px));
  position: relative;
}

/* Break out of the page column and run to the screen edge — the filter bar and
   the product media strip. Mirrors the gutter expression above exactly, so the
   two cannot drift apart. */
.bleed {
  margin-inline: calc(-1 * max(var(--gutter), env(safe-area-inset-left, 0px)))
                 calc(-1 * max(var(--gutter), env(safe-area-inset-right, 0px)));
}

.measure { max-width: 62ch; }
.pad { padding: var(--space-6) 0 var(--space-8); }
.section { margin-top: var(--gap-section); }

@media (min-width: 600px) { .pad { padding: var(--space-7) 0 var(--space-9); } }
@media (min-width: 900px) { .pad { padding: var(--space-8) 0 72px; } }

/* The generic "two things side by side when there is room" grid. One column on
   a phone, always. */
.cols { display: grid; grid-template-columns: 1fr; gap: var(--gap-block); align-items: start; }
@media (min-width: 900px) {
  .cols { grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
}

.grid { display: grid; grid-template-columns: 1fr; gap: var(--gap-card); }
@media (min-width: 600px) {
  .grid { grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); }
}

/* Stacks and rows. These replace the `style="display:flex; gap:8px"` that used
   to be written at 40-odd call sites. */
.stack-sm > * + * { margin-top: var(--space-2); }
.stack > * + * { margin-top: var(--space-4); }

/* Only the modifiers ui.js's vocabulary actually names. A stylesheet that
   carries alignment variants nothing renders is a stylesheet nobody trusts to
   describe the client. */
.hstack { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); }
.hstack.tight { gap: var(--space-2); }
.hstack.baseline { align-items: baseline; }

.spring { flex: 1 1 auto; }
.grow { flex: 1 1 auto; }
.mt0 { margin-top: 0; }
[hidden] { display: none !important; }

.skip {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  background: var(--vellum); color: var(--ink);
  border: 1px solid var(--rule); border-radius: var(--radius-md);
  font-family: var(--sans); font-size: var(--fs-small);
  min-height: var(--tap); padding: 0 var(--space-4);
  display: inline-flex; align-items: center;
}
.skip:focus { left: var(--gutter); top: var(--space-2); }

:focus-visible { outline: 2px solid var(--silver); outline-offset: 2px; border-radius: var(--radius-sm); }

/* -------------------------------------------------------------------------
   Type
   ------------------------------------------------------------------------- */
h1 {
  font-family: var(--sans); font-weight: 700;
  font-size: clamp(29px, 7.4vw, 44px); line-height: 1.05; letter-spacing: -.042em;
  margin: 0 0 var(--space-4); text-wrap: balance;
}
h2 {
  font-family: var(--sans); font-weight: 660;
  font-size: clamp(21px, 5vw, 27px); line-height: 1.15; letter-spacing: -.03em;
  margin: 0 0 var(--space-3); text-wrap: balance;
}
h3 {
  font-family: var(--sans); font-size: 17px; font-weight: 660;
  letter-spacing: -.012em; line-height: 1.35; margin: 0 0 var(--space-2);
}
p { margin: 0 0 var(--space-4); }
a { color: var(--silver); text-underline-offset: 2px; }

.eyebrow {
  font-family: var(--mono); font-size: var(--fs-label); letter-spacing: .14em;
  text-transform: uppercase; color: var(--ink-2); margin: 0 0 var(--space-3);
}
.lede { font-size: var(--fs-lede); line-height: 1.5; color: var(--ink-2); }
.sub { font-family: var(--sans); font-size: var(--fs-small); color: var(--ink-2); line-height: 1.5; }
p.sub { margin: 0 0 var(--space-2); }
.sans { font-family: var(--sans); }
.hint { font-family: var(--sans); font-size: var(--fs-micro); color: var(--ink-2); margin: var(--space-2) 0 0; line-height: 1.5; }

/* A hint whose ENTIRE content is one link is not prose — it is the only route
   out of the sheet to the product page, and it was 37.5px tall, so a thumb
   missed it. WCAG 2.5.8 exempts a target "in a sentence, or whose size is
   otherwise constrained by the line-height of non-target text"; that exemption
   is about targets sharing a line with words around them. This one has no
   words around it, so it never qualified.

   Reached with padding and min-height, never `height`, so the label still
   grows with the reader's type size.

   THE `:only-child` GUARD IS LATER-PROOF, NOT RETROACTIVE. It reads "the
   anchor is the only ELEMENT in this hint", which is true of the one usage
   today (shop.js builds `p.hint > a` and nothing else). A hint that mixes
   prose with a link — `<p class=hint>See <a>the standard</a>.</p>` — would
   still match, because CSS cannot see the text nodes beside the anchor, and
   promoting it to a block would break that sentence in half. If such a hint is
   ever written, give it its own class rather than widening this rule. */
.hint > a:only-child {
  display: block; min-height: var(--tap);
  padding: var(--space-2) 0; box-sizing: border-box;
}

/* The "data" type role: mono, tracked, tabular. Every number a customer might
   compare against another number goes through this. */
.data, .num {
  font-family: var(--mono); font-size: var(--fs-micro); letter-spacing: .04em;
  font-variant-numeric: tabular-nums; line-height: 1.5;
}

/* A reading that a vet would want to see — a double-digit weight swing either
   way — marked rather than left as body text. */
.alarming { color: var(--alert); font-weight: 640; }

/* One number, said loudly: a portion in grams a day, a pet's weight. */
.figure {
  font-family: var(--sans); font-size: 30px; font-weight: 700;
  letter-spacing: -.03em; line-height: 1.1; color: var(--ink);
  margin: 0 0 var(--space-1);
}

/* An API path, an error code or a SKU can be longer than a phone is wide, and
   one unbreakable string is all it takes to give the whole page a horizontal
   scrollbar. */
code { overflow-wrap: anywhere; }

@media (min-width: 900px) { h3 { font-size: 16px; } .figure { font-size: 26px; } }

/* -------------------------------------------------------------------------
   Grain. Opacity comes from the texture ceiling tokens — FR-R2 is enforced by
   the token, not by review. Surfaces where texture is BANNED (FR-R3: the
   standard, the diet calculator, money) never get this class at all, so the
   ban is structural rather than a zero someone can edit.
   ------------------------------------------------------------------------- */
.grain { position: relative; }
.grain > .grain-layer {
  position: absolute; inset: 0; pointer-events: none; z-index: 0;
  opacity: var(--texture-web-content);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23g)'/%3E%3C/svg%3E");
}
.grain > .wrap { position: relative; z-index: 1; }

/* -------------------------------------------------------------------------
   The sign-in band.

   Undercoat Grain across the top of the way in, per the dosage table's row for
   "Sign-in & onboarding": a full-bleed band, above the form, never behind a
   field. It is a SIBLING of <main>, inserted by shell(..., {band}) — being
   outside the page column is what makes it full-bleed, and being outside <main>
   is what makes "never behind a field" structural instead of a z-index somebody
   can undo later.

   The height is in vh so the band's share of the screen stays roughly constant,
   which is the quantity the 8% ceiling is a percentage of. js/fur.js measures
   the real box anyway and derives the dose from it, so the clamp cannot push
   the surface over its ceiling at either extreme.

   The background is the ground colour, painted here rather than by script: this
   is what the band looks like before — and if — the coat arrives.
   ------------------------------------------------------------------------- */
.furband {
  height: clamp(88px, 18vh, 160px);
  background: var(--undercoat);
  overflow: hidden;
  pointer-events: none;
}
.furband > canvas { display: block; width: 100%; height: 100%; pointer-events: none; }

/* The sign-in GROUND: the same coat, filling the page's main instead of a
   strip after the masthead. The centred card floats on it — the card, not the
   fur, is what sits behind every field. Height comes from the positioned
   parent, so the band's clamp is overridden. */
.furband.furband-ground { position: absolute; inset: 0; height: auto; }
/* Only the ground is ever a valid drag target — see fur.js requirement 3 for
   why band and crown must stay click-through and this must not. */
.furband-ground > canvas { pointer-events: auto; touch-action: pan-y; }
/* .wrap is a plain full-bleed content column everywhere else in this client,
   and it is NOT visually transparent there — it holds real content edge to
   edge. On the sign-in page it holds only the centred card with empty space
   around it, and that empty space is exactly where a drag on the ground has
   to land. Scoped to .signin-main so no other page's .wrap loses its normal
   hit-testing; the card opts back in, since it is the one real control inside
   the now-transparent column. */
.signin-main > .wrap { pointer-events: none; }
.signin-card { pointer-events: auto; }
.signin-main { position: relative; }
.signin-main > .wrap { position: relative; }
.signin-stage { display: grid; place-items: center; min-height: clamp(360px, 62dvh, 640px); padding: var(--space-6) 0; }
.signin-card { width: min(430px, 100%); }

/* The sheet's CROWN: the band cut down to a cap above the title. Its top
   corners follow the panel's. */
.furband-crown { height: 76px; flex: 0 0 auto; border-radius: var(--radius-3xl) var(--radius-3xl) 0 0; }

/* -------------------------------------------------------------------------
   The HERO banner — the catalogue's masthead.

   A full-bleed section whose entire backdrop is live coat, with the heading
   sitting on it. `.hero-banner` is the positioned parent the `hero` variant
   fills; the body is a normal page column layered over it.

   The canvas takes pointer events so the coat can be dragged, and `touch-action:
   pan-y` hands vertical gestures back to the page — js/fur.js claims only the
   horizontal ones. The BODY above it is click-through except for its own
   controls, for the same reason the sign-in card is: a banner this size is
   mostly empty space, and that empty space is exactly where a drag has to land.
   ------------------------------------------------------------------------- */
.hero-banner {
  position: relative;
  overflow: hidden;
  margin-bottom: var(--gap-section);
  /* PARITY WITH `.coat-hero` (landing.css), which this is the short cut of.
     The coat here is the interactive one, so the cursor has to say so — and a
     drag across the headline must part the coat rather than highlight the
     words, which is what a text selection starting mid-gesture looks like. */
  cursor: grab;
  user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
}
.hero-banner:active { cursor: grabbing; }
/* The banner supplies this page's top rhythm, so <main> must not also. Without
   this the coat starts a --space-8 gap below the masthead and the two read as
   two separate strips of chrome. */
.shop-main { padding-top: 0; }
.furband.furband-hero { position: absolute; inset: 0; height: auto; }
/* The dissolve, identical to `.coat-hero .fade` in landing.css — one hero
   treatment in this client, not two. Only the bottom fades: legibility is the
   halo on the type's job, and veiling the whole banner would throw away the
   density that is the point of the surface. */
.hero-banner .fade {
  position: absolute; inset: 0; pointer-events: none; z-index: 1;
  background: linear-gradient(180deg, transparent 0%, transparent 58%, var(--undercoat) 100%);
}
/* The flat block a hero cut degrades to, and shows for the two frames before
   the renderer arrives. Must match the ground js/fur.js paints. */
.furband.furband-cut-hero { background: var(--hero-1); }
.furband-hero > canvas { pointer-events: auto; touch-action: pan-y; }
/* Composed as `div.wrap.hero-banner-body`, so the page column and the gutter
   come from .wrap and only the vertical rhythm is set here. `padding-block`,
   never the `padding` shorthand: that would reset .wrap's padding-inline to 0
   and run the heading into the screen edge. */
.hero-banner-body {
  position: relative;
  padding-block: var(--space-8);
  pointer-events: none;
}
.hero-banner-body a, .hero-banner-body button { pointer-events: auto; }
.hero-banner-body h1 { margin: var(--space-3) 0 0; }
.hero-banner-body .lede { margin-top: var(--space-3); max-width: 46ch; }
/* Copy over live fur, exactly as the landing hero does it (landing.css,
   `.hero-in h1`). A halo on the type, NOT a veil over the coat: veiling the
   whole banner to rescue two lines of lede throws away the density that is the
   entire point of the surface. The eyebrow needs none of this — .hero-badge
   already carries its own opaque ground for the same reason.

   --ink, NOT --ink-2. A lede is --ink-2 everywhere else in this client and that
   is right on paper; over an 86%-ink coat it measured 3.05:1 at 18px, under the
   4.5:1 floor for body text and visibly muddy. The halo alone could not save it
   because a halo raises local contrast at the glyph edge and does nothing for
   the stroke interiors. Darkening the type is the fix; the halo is what keeps
   it from sitting ON the texture. Together: 6.4:1. */
.hero-banner-body h1,
.hero-banner-body .lede {
  color: var(--ink);
  text-shadow:
    0 1px 0 var(--undercoat),
    0 0 6px var(--undercoat),
    0 0 20px var(--undercoat);
}

/* The reference's pill-shaped eyebrow: a bordered chip rather than the plain
   letterspaced label the rest of the client uses, because on a hero it has to
   hold its own against the coat behind it. */
.hero-badge {
  display: inline-flex; align-items: center; gap: var(--space-2);
  margin: 0; padding: 6px 14px;
  border: 1px solid var(--ink); border-radius: var(--radius-pill);
  background: var(--undercoat);
  font-family: var(--mono); font-size: var(--fs-label);
  letter-spacing: .12em; text-transform: uppercase; color: var(--ink);
}

@media (min-width: 600px) { .hero-banner-body { padding-block: var(--space-9); } }
@media (min-width: 900px) {
  .hero-banner-body { padding-block: 88px; }
  .hero-banner-body h1 { max-width: 18ch; }
}

/* -------------------------------------------------------------------------
   Masthead — chrome

   Two rows: the wordmark, the basket and the account control; then the four
   destinations. On a phone it scrolls away with the page. That is a deliberate
   change from the version this replaces, which was sticky AND wrapped to three
   rows: 143px of permanent chrome on an 844px screen.

   `masthead()` measures the bar and publishes `--top-h`, which is what the
   filter bar pins itself under once the masthead is sticky.
   ------------------------------------------------------------------------- */
header.top {
  background: var(--undercoat);
  border-bottom: 1px solid var(--rule);
  padding-top: env(safe-area-inset-top, 0px);
}

/* ONE STRIP ON A DESKTOP, TWO ROWS ON A PHONE, from one flex container.

   ui.js ships the masthead as a single `.topbar` whose children are wordmark,
   nav, basket and account. What that renders as is decided here and only here:

     under 900px  `order: 2; flex: 1 0 100%` drops the nav onto a full-width
                  row of its own beneath the wordmark, where it scrolls
                  sideways. This is not a compromise, it is the only honest
                  answer: six destinations, a wordmark, a basket and an account
                  control do not fit across 390px, and the version this
                  replaces answered that by wrapping to THREE rows — 143px of
                  sticky chrome on an 844px screen. Squeezing the nav into the
                  ~100px left between the wordmark and the basket would be a
                  scroller nobody can aim at.
     900px and up `order: 0; flex: 0 1 auto` pulls it up beside the wordmark
                  and the strip is one row, as the reference design has it.

   Nothing here is inert any more: the two-row shape this replaced had
   `nav.primary` inside a plain `.navwrap` block, so the `order` and `flex`
   declarations could not bite and the masthead was two rows at EVERY width —
   about 105px of chrome above the first product on a desktop too.

   Measure rather than believe: `--top-h` is published by masthead() and is
   what anything pinned underneath actually reads. */
.topbar {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2);
  padding: var(--space-1) 0;
}
.mark {
  font-family: var(--sans); font-weight: 700; letter-spacing: -.035em; font-size: 19px;
  color: var(--ink); text-decoration: none; white-space: nowrap;
  display: inline-flex; align-items: center; min-height: var(--tap);
  flex: 0 0 auto; margin-right: auto;
}
.mark em { font-style: normal; color: var(--silver); }
.topbar > .cta, .account { flex: 0 0 auto; }
/* The theme switch. `aria-pressed` carries the state, so the label stays the
   word "Dark" rather than flipping to name the other mode — a control whose
   label changes to its opposite is the classic ambiguity here ("Dark" meaning
   "you are in dark" or "press for dark"?). Pressed styling answers it. */
.themetoggle[aria-pressed="true"] {
  background: var(--silver-soft); border-color: var(--silver-lit); color: var(--silver);
}
/* Icon-only, so it is square to the tap floor rather than text-width. That also
   bought back 24px in the masthead — see the width note below. */
.themetoggle {
  width: var(--tap); min-width: var(--tap); padding: 0;
  align-items: center; justify-content: center;
}
.themetoggle-ico { display: block; }

/* WHICH OF THE TWO COPIES IS VISIBLE, and it is exactly one at every width.

   The masthead is at its width budget on a phone: at 375px the strip is
   wordmark 67 + basket 116 + account 83, and a fourth control wrapped it to
   three rows — --top-h went 53px to 161px. So below 600px the footer carries
   it, and at 600px and up the masthead does, where a preference is reachable
   from the home screen without scrolling past a 660px hero.

   The exception is marketing chrome, which is what index.html wears before
   launch: applyChrome hides the basket AND the account control there, so the
   strip is a wordmark and a nav and there is room at any width. */
/* SCOPED TO THEIR CONTAINERS FOR SPECIFICITY, not for tidiness. A bare
   `.themetoggle-top { display: none }` is (0,1,0) and so is the `.cta`
   rule below it that sets `display: inline-flex` — equal weight, and `.cta`
   wins on source order. The effect was both copies visible at 375px and the
   masthead back to three rows, which is the exact regression these two rules
   exist to prevent. */
.topbar .themetoggle-top { display: none; }
.foot .themetoggle-foot { display: inline-flex; }
body[data-chrome="marketing"] .topbar .themetoggle-top { display: inline-flex; }
body[data-chrome="marketing"] .foot .themetoggle-foot { display: none; }

@media (min-width: 600px) {
  .topbar .themetoggle-top { display: inline-flex; }
  .foot .themetoggle-foot { display: none; }
}

/* The basket, when ui.js renders it as its own control rather than a nav item:
   it carries state and is the one destination a customer checks mid-task, so it
   reads as a control beside the account button. */
.basket {
  display: inline-flex; align-items: center; gap: var(--space-1);
  min-height: var(--tap); padding: 0 var(--space-4);
  border: 1px solid var(--rule); border-radius: var(--radius-pill);
  background: var(--vellum); color: var(--ink); text-decoration: none;
  font-family: var(--sans); font-size: var(--fs-control); font-weight: 640;
  white-space: nowrap; flex: 0 0 auto; touch-action: manipulation;
}
.basket[aria-current="page"] { background: var(--silver-soft); border-color: var(--silver-lit); color: var(--silver); }
.basket:active { background: var(--sunk); }

.cart-count {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 21px; height: 21px; padding: 0 6px; margin-left: 5px;
  border-radius: var(--radius-pill); background: var(--silver); color: var(--undercoat);
  font-family: var(--mono); font-size: 11px; letter-spacing: .02em;
  font-variant-numeric: tabular-nums;
}

nav.primary {
  /* `order: 2` + a 100% basis is what puts the nav on its own row inside the
     strip without any markup to hang it on — see the note above. */
  order: 2; flex: 1 0 100%; min-width: 0;
  display: flex; flex-wrap: nowrap; gap: var(--space-1);
  /* Sideways rather than taller. Every destination is a top-level page also
     reachable from the footer and from the pages themselves, and the
     alternative — wrapping — is what made the old masthead a third of a phone
     screen.

     A ROW THAT SCROLLS HAS TO CLIP AT THE SCREEN EDGE. The negative margin and
     the matching padding below are what put "The standard" half off the glass
     instead of cut short with a tidy 20px margin after it: the first reads as
     "there is more", the second reads as a bug. Same pair on `.chiprow`. */
  overflow-x: auto; overscroll-behavior-x: contain; scrollbar-width: none;
  scroll-snap-type: x proximity;
  margin-inline: calc(-1 * max(var(--gutter), env(safe-area-inset-left, 0px)))
                 calc(-1 * max(var(--gutter), env(safe-area-inset-right, 0px)));
  padding: 0 max(var(--gutter), env(safe-area-inset-right, 0px)) var(--space-1)
             max(var(--gutter), env(safe-area-inset-left, 0px));
  scroll-padding-inline: max(var(--gutter), env(safe-area-inset-left, 0px));
}
nav.primary::-webkit-scrollbar { display: none; }
/* The other half of the pair the comment above describes. The bleed margins
   clip the row at the glass; these say the clipping is on purpose.

   `data-at` is published by the row itself (reportScrollEdges in ui.js), so
   only the edge with something behind it fades, and a row that fits — every
   width from 900px up, where the nav stops scrolling — reports "none" and gets
   no fade at all. `currentColor` is the mask's paint because only its alpha is
   read; it keeps a raw colour out of the file. Same three rules as .chiprow,
   deliberately identical: two rows that overflow for the same reason should
   not signal it two different ways. */
nav.primary[data-at="start"] {
  -webkit-mask-image: linear-gradient(to right, currentColor calc(100% - 36px), transparent);
          mask-image: linear-gradient(to right, currentColor calc(100% - 36px), transparent);
}
nav.primary[data-at="end"] {
  -webkit-mask-image: linear-gradient(to left, currentColor calc(100% - 36px), transparent);
          mask-image: linear-gradient(to left, currentColor calc(100% - 36px), transparent);
}
nav.primary[data-at="mid"] {
  -webkit-mask-image: linear-gradient(to right, transparent, currentColor 36px, currentColor calc(100% - 36px), transparent);
          mask-image: linear-gradient(to right, transparent, currentColor 36px, currentColor calc(100% - 36px), transparent);
}
nav.primary a {
  flex: 0 0 auto; scroll-snap-align: start;
  display: inline-flex; align-items: center; justify-content: center;
  min-height: var(--tap); padding: 0 var(--space-3);
  border-radius: var(--radius-md);
  font-family: var(--sans); font-size: var(--fs-small); line-height: 1.2;
  color: var(--ink-2); text-decoration: none; white-space: nowrap;
  touch-action: manipulation;
}
nav.primary a[aria-current="page"] { background: var(--silver-soft); color: var(--silver); font-weight: 640; }
nav.primary a:active { background: var(--sunk); color: var(--ink); }

@media (min-width: 900px) {
  /* Height to spend, and a pointer that does not care how far from the bottom
     of the window a control is — so the masthead can stay. */
  header.top { position: sticky; top: 0; z-index: 40; }
  .topbar { gap: var(--space-3); flex-wrap: nowrap; }
  .mark { margin-right: var(--space-2); }
  nav.primary {
    /* `margin-inline: 0` is the load-bearing one in BOTH shapes: it gives back
       the negative bleed margins the phone row needs, which would otherwise
       hang the nav outside the page column. `overflow-x` and `padding` retire
       the sideways scroller; `order`/`flex` close the strip up. */
    order: 0; flex: 0 1 auto; margin-inline: 0 auto;
    overflow-x: visible; padding: 0;
  }
}

@media (hover: hover) and (pointer: fine) {
  nav.primary a:hover { background: var(--sunk); color: var(--ink); }
  nav.primary a[aria-current="page"]:hover { background: var(--silver-soft); color: var(--silver); }
  .basket:hover { border-color: var(--ink-3); }
}

/* -------------------------------------------------------------------------
   Buttons, chips, tags, steppers — controls

   THE TAP FLOOR. `min-height: var(--tap)` plus real padding on every
   interactive thing, at every width. Never `height`: the box has to grow when
   someone raises their text size, and a fixed height turns that into clipped
   text.

   HOVER IS DECORATIVE AND IS BEHIND `@media (hover: hover)`. On a touch screen
   a `:hover` style applied by a tap STICKS until something else is tapped,
   which is how a phone ends up with two chips that both look selected. Nothing
   in this client is reachable only on hover; `:active` carries the feedback.
   ------------------------------------------------------------------------- */
.cta {
  font-family: var(--sans); font-size: var(--fs-control); font-weight: 640;
  text-decoration: none; text-align: center; line-height: 1.25;
  background: var(--silver); color: var(--undercoat);
  min-height: var(--tap); padding: var(--space-2) var(--space-5);
  border-radius: var(--radius-pill); border: 1px solid var(--silver);
  display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2);
  cursor: pointer; touch-action: manipulation;
  /* A label that breaks in half ("Sign / in") is what a shrinking flex item
     does before it admits it has no room. Say no and let the row scroll or
     wrap instead. */
  white-space: nowrap;
}
.cta.ghost { background: none; color: var(--silver); }
.cta.small { font-size: calc(var(--fs-control) - 1px); padding: var(--space-2) var(--space-4); }
.cta.danger { background: none; color: var(--alert); border-color: var(--alert); }
.cta:active { background: var(--ink); border-color: var(--ink); color: var(--undercoat); }
.cta.ghost:active { background: var(--silver-soft); border-color: var(--silver); color: var(--silver); }
.cta.danger:active { background: var(--alert); border-color: var(--alert); color: var(--undercoat); }
.cta[disabled], .cta[aria-disabled="true"] {
  opacity: .5; cursor: not-allowed; background: none;
  color: var(--ink-3); border-color: var(--rule);
}
.cta[disabled]:active, .cta[aria-disabled="true"]:active { background: none; color: var(--ink-3); border-color: var(--rule); }

.chip {
  font-family: var(--sans); font-size: var(--fs-small); line-height: 1.25;
  min-height: var(--tap); padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-pill);
  border: 1px solid var(--rule); background: none; color: var(--ink-2);
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer; touch-action: manipulation; white-space: nowrap;
}
.chip:active { border-color: var(--ink-3); color: var(--ink); background: var(--sunk); }
.chip[aria-pressed="true"] { background: var(--silver); border-color: var(--silver); color: var(--undercoat); }
.chip[aria-pressed="true"]:active { background: var(--silver); border-color: var(--silver); color: var(--undercoat); }
.chip[disabled] { opacity: .5; cursor: not-allowed; }

@media (hover: hover) and (pointer: fine) {
  .cta:hover { background: var(--ink); border-color: var(--ink); color: var(--undercoat); }
  .cta.ghost:hover { background: var(--silver-soft); border-color: var(--silver); color: var(--silver); }
  .cta.danger:hover { background: var(--alert); border-color: var(--alert); color: var(--undercoat); }
  .cta[disabled]:hover, .cta[aria-disabled="true"]:hover { background: none; color: var(--ink-3); border-color: var(--rule); }
  .chip:hover { border-color: var(--ink-3); color: var(--ink); }
  .chip[aria-pressed="true"]:hover { background: var(--silver); border-color: var(--silver); color: var(--undercoat); }
  .chip[disabled]:hover { border-color: var(--rule); color: var(--ink-2); }
}

/* A tag is a label, not a control: it takes the label size, not the tap floor. */
.tag {
  font-family: var(--mono); font-size: calc(var(--fs-label) - 1px);
  letter-spacing: .07em; text-transform: uppercase; line-height: 1.5;
  color: var(--ink-2); border: 1px solid var(--rule); border-radius: var(--radius-pill);
  padding: 3px var(--space-2); display: inline-block;
  /* Wraps rather than clips. A tile is 132px wide on a 320px phone and
     "LIMITED INGREDIENT" is 150px at this size — with `nowrap` it ran past the
     card and `.card { overflow: hidden }` cut it in half. It only ever wraps
     when it has run out of room, which is the right moment for it to. */
  white-space: normal; max-width: 100%; text-align: center;
}
.tag.pass { color: var(--moss); border-color: var(--moss); }
.tag.warn { color: var(--alert); border-color: var(--alert); }
.tag.accent { color: var(--silver); border-color: var(--silver); }
.tag.plain { color: var(--ink-2); }

/* The quantity stepper. Two 44px targets and a tabular readout — the
   `<input type="number">` it replaces had 12px spinners on desktop and none at
   all on a phone, where the only way to change a quantity was the keyboard. */
.stepper {
  display: inline-flex; align-items: center; flex: 0 0 auto;
  border: 1px solid var(--rule); border-radius: var(--radius-pill);
  background: var(--vellum); overflow: hidden;
}
.stepper-btn {
  min-width: var(--tap); min-height: var(--tap); padding: 0;
  border: 0; background: none; color: var(--ink);
  font-family: var(--sans); font-size: 21px; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer; touch-action: manipulation;
}
.stepper-btn:active { background: var(--sunk); }
.stepper-btn:disabled { color: var(--ink-3); cursor: not-allowed; background: none; }
.stepper-btn:focus-visible { outline-offset: -3px; }
.stepper-val {
  min-width: 34px; padding: 0 var(--space-1); text-align: center;
  font-size: var(--fs-control); color: var(--ink);
}

/* A <summary> is a control, so it gets the floor through padding — `display`
   is left alone so the disclosure marker survives. */
summary { padding: var(--space-3) 0; cursor: pointer; touch-action: manipulation; }
details.disclose { border-top: 1px solid var(--rule-2); margin-top: var(--space-4); }
details.disclose > summary {
  font-family: var(--sans); font-size: var(--fs-small); font-weight: 640; color: var(--silver);
}
details.disclose[open] > summary { margin-bottom: var(--space-2); }

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

   Every field is 44px tall and 16px. The 16px is not a preference: below it,
   iOS Safari zooms the entire page the moment the field takes focus and does
   not zoom back out. The floor is set on the ELEMENT, so a new input added
   anywhere in this client inherits it without anyone having to remember.
   ------------------------------------------------------------------------- */
input, select, textarea { font-size: var(--fs-field); }

.field { margin: 0 0 var(--space-4); }
.field label, .flabel {
  display: block; font-family: var(--sans); font-size: var(--fs-small); font-weight: 640;
  color: var(--ink); margin: 0 0 var(--space-2);
}
.field input, .field select, .field textarea, input.inp, select.inp, textarea.inp {
  width: 100%; min-height: var(--tap);
  font-family: var(--sans); font-size: var(--fs-field); line-height: 1.4;
  color: var(--ink);
  background: var(--undercoat); border: 1px solid var(--rule);
  border-radius: var(--radius-md); padding: var(--space-3) var(--space-4);
}
.field input:focus, .field select:focus, .field textarea:focus,
input.inp:focus, select.inp:focus, textarea.inp:focus {
  border-color: var(--silver); outline: none; box-shadow: 0 0 0 3px var(--silver-soft);
}

.row { display: flex; gap: var(--space-3); flex-wrap: wrap; align-items: flex-end; }
.row > * { flex: 1 1 160px; min-width: 0; }
.row > .shrink { flex: 0 0 auto; }
/* A chip in a `.row` is a choice, not a field: the 160px basis is for inputs,
   and applying it to "Dog" / "Cat" stretched each one to its own full-width
   line and made a pair of pills read as two buttons. */
.row > .chip { flex: 0 1 auto; }

fieldset { border: 0; margin: 0; padding: 0; min-width: 0; }
legend {
  font-family: var(--mono); font-size: var(--fs-label); letter-spacing: .12em; text-transform: uppercase;
  color: var(--ink-2); padding: 0; margin: 0 0 var(--space-2);
}

/* -------------------------------------------------------------------------
   Cards and surfaces
   ------------------------------------------------------------------------- */
.card {
  background: var(--vellum); border: 1px solid var(--rule); border-radius: var(--radius-lg);
  box-shadow: var(--shadow); overflow: hidden; display: flex; flex-direction: column;
}
.card-body { padding: var(--pad-card); flex: 1; }
/* Scoped to `.card-body`, NOT to `.card`. A catalogue tile is a `.card` without
   a `.card-body`, and `.card p` (0,1,1) outranks `.tile-brand` (0,1,0) — which
   silently took the brand line's size AND its horizontal padding away, so it
   sat flush against the tile edge at 15px while the name it labels sat indented
   at 15px. Every rule that dresses card COPY belongs under the body. */
.card-body p { margin: 0 0 var(--space-2); font-family: var(--sans); font-size: var(--fs-small); line-height: 1.5; color: var(--ink-2); }
.card-body > :last-child { margin-bottom: 0; }
.meta {
  margin-top: var(--space-3); padding-top: var(--space-3); border-top: 1px solid var(--rule-2);
  display: flex; gap: var(--space-2); flex-wrap: wrap; align-items: center;
}

.panel {
  background: var(--vellum); border: 1px solid var(--rule); border-radius: var(--radius-lg);
  box-shadow: var(--shadow); padding: var(--pad-card);
}
.panel + .panel, .panel + .cols, .cols + .panel { margin-top: var(--gap-block); }
.panel > :last-child { margin-bottom: 0; }
/* The basket's line list: each line brings its own vertical padding, so the
   panel must not add a second one. */
.panel.lines { padding-block: 0; }
.panel > form { margin: 0 0 var(--space-5); }

/* A well is recessed rather than raised: reference data, not a thing you act on. */
.well { background: var(--sunk); border: 1px solid var(--rule); border-radius: var(--radius-md); padding: var(--pad-tight); }
.well > :last-child { margin-bottom: 0; }

/* The notice/callout pattern from the positioning site: a left rule in a
   meaning-bearing colour, and a squared-off left edge so it reads as attached
   to the flow rather than floating. */
.notice {
  border-left: 3px solid var(--ginger); background: var(--vellum);
  border-radius: 0 var(--radius-md) var(--radius-md) 0; padding: var(--pad-tight);
  margin: var(--gap-block) 0 0; box-shadow: var(--shadow);
}
.notice p { margin: 0 0 var(--space-2); font-family: var(--sans); font-size: var(--fs-small); line-height: 1.55; color: var(--ink-2); }
.notice p:last-child, .notice > :last-child { margin-bottom: 0; }
/* `margin: var(--gap-block) 0 0` above buys space ABOVE the notice and none
   below it, so a notice followed by another panel butts into it — measured 0px
   between .notice and .doc on standard.html while every other gap on that page
   was 20px, and both carry their own background and border, so they read as one
   merged block. Only when something follows, so a trailing notice adds no
   dangling space. Sibling margins collapse in normal flow, so this takes the
   larger of the two rather than summing with whatever comes next. */
.notice:not(:last-child) { margin-bottom: var(--gap-block); }
.notice b { color: var(--ink); }
.notice.alert { border-left-color: var(--alert); }
.notice.pass { border-left-color: var(--moss); }
.notice.accent { border-left-color: var(--silver); }

/* The address form's live delivery verdict, which sits between the PIN code
   field and the label of the field under it.

   `.notice` buys its own space ABOVE, and spaces itself BELOW only when it has
   a following sibling — see `:not(:last-child)` above. The verdict has none:
   it lives inside a host element the form swaps content into, so it is always
   the last child of that host however much is on screen. Measured 20px above
   and 0px below at 402px, which put "We do not deliver to 110001 yet" flush
   against "Call this address (optional)" and read as one block.

   `:not(:empty)` rather than a flat margin, because the host is in the form
   from the start and holds nothing until six digits have been typed. A margin
   on the empty state is a gap where there is nothing to space. */
.verdict:not(:empty) { margin-bottom: var(--gap-block); }

/* A list that is a list of statements, not a bulleted aside. */
.plainlist { list-style: none; margin: var(--space-2) 0 0; padding: 0; }
.plainlist > li + li { margin-top: var(--space-2); }
.steps { margin: 0 0 var(--space-4); padding-left: 1.15em; }
.steps > li { margin-bottom: var(--space-2); font-family: var(--sans); font-size: var(--fs-small); color: var(--ink-2); line-height: 1.55; }

.state { font-family: var(--sans); font-size: var(--fs-small); color: var(--ink-2); padding: var(--space-6) 0; }

/* The error block is deliberately loud and deliberately specific. Nothing in
   this client substitutes placeholder data for a failed call — a shop that
   invents a price is worse than a shop that is down — so a failure shows the
   method, the path and the server's own message. */
.errbox {
  border-left: 3px solid var(--alert); background: var(--vellum);
  border-radius: 0 var(--radius-md) var(--radius-md) 0; padding: var(--pad-tight);
  margin: var(--gap-block) 0; box-shadow: var(--shadow);
}
.errbox h3 { font-family: var(--sans); font-size: 16px; color: var(--alert); margin: 0 0 var(--space-2); }
.errbox p { font-family: var(--sans); font-size: var(--fs-small); color: var(--ink-2); line-height: 1.55; margin: 0 0 var(--space-2); overflow-wrap: anywhere; }
.errbox > :last-child { margin-bottom: 0; }
.errbox code {
  font-family: var(--mono); font-size: var(--fs-micro); background: var(--sunk);
  border: 1px solid var(--rule); border-radius: var(--radius-sm); padding: 2px 5px; color: var(--ink-2);
}

/* The iOS "add me to your home screen" nudge — see iosInstallBanner() in
   ui.js for who gets shown this and why. Same shelf as the toast below it:
   fixed, above the sticky bar, spanning the gutters on a phone. Unlike a
   toast it is not transient, so it needs its own dismiss control rather than
   a timeout. */
.ios-install {
  position: fixed;
  left: var(--gutter); right: var(--gutter);
  bottom: calc(var(--stickybar-h, 0px) + max(var(--space-3), env(safe-area-inset-bottom, 0px)));
  z-index: 89; /* one under .toasts, so a confirmation is never hidden behind this */
  display: flex; align-items: center; gap: 12px;
  background: var(--vellum); border: 1px solid var(--rule);
  border-radius: var(--radius-lg); box-shadow: var(--shadow);
  padding: 12px 8px 12px 12px;
  /* A hairline of the app's own ink along the top edge — the one deliberate
     accent on an otherwise neutral surface, so this reads as a card FROM the
     app rather than a generic browser notice. */
  border-top: 2px solid var(--ink);
}
.ios-install-icon {
  width: 44px; height: 44px; border-radius: 11px; flex: 0 0 auto;
  /* iOS itself puts a hairline ring around every home-screen icon it
     composites onto a light wallpaper; matching it here is what keeps this
     preview from reading as a plain inline <img>. */
  box-shadow: 0 0 0 1px var(--rule-2), var(--shadow);
}
.ios-install-body { flex: 1 1 auto; min-width: 0; }
.ios-install-title {
  font-family: var(--sans); font-size: var(--fs-small); font-weight: 640; color: var(--ink);
  margin: 0 0 2px; letter-spacing: -0.01em;
}
.ios-install-sub {
  font-family: var(--sans); font-size: var(--fs-micro); color: var(--ink-2); line-height: 1.4; margin: 0;
}
.ios-install-glyph { vertical-align: -2px; color: var(--ink-2); margin: 0 1px; }
.ios-install-close {
  flex: 0 0 auto; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center;
  background: none; border: 0; border-radius: var(--radius-pill); color: var(--ink-3); cursor: pointer;
}
.ios-install-close::before {
  content: ''; width: 9px; height: 9px;
  background: currentColor;
  /* An × drawn as a masked box rather than the glyph "×": every font sets its
     baseline differently, so a text × never sits optically centred against a
     44px icon the way a shape does. */
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3' stroke-linecap='round'%3E%3Cpath d='M5 5l14 14M19 5L5 19'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3' stroke-linecap='round'%3E%3Cpath d='M5 5l14 14M19 5L5 19'/%3E%3C/svg%3E") center / contain no-repeat;
}
.ios-install-close:hover { background: var(--sunk); color: var(--ink-2); }

@media (min-width: 600px) {
  .ios-install { left: auto; right: var(--space-5); max-width: 340px; }
}

/* Toast: a transient result of an action the user just took. It sits ABOVE the
   sticky bar — `--stickybar-h` is measured and published by `stickyBar()`, so a
   confirmation never lands under the checkout button — and on a phone it spans
   the gutters rather than being squeezed into a corner. */
.toasts {
  position: fixed;
  left: var(--gutter); right: var(--gutter);
  bottom: calc(var(--stickybar-h, 0px) + max(var(--space-3), env(safe-area-inset-bottom, 0px)));
  z-index: 90; display: flex; flex-direction: column; gap: var(--space-2);
}
.toast {
  font-family: var(--sans); font-size: var(--fs-small); line-height: 1.45;
  background: var(--vellum); color: var(--ink-2); border: 1px solid var(--rule);
  border-left: 3px solid var(--moss); border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4); box-shadow: var(--shadow);
}
.toast.bad { border-left-color: var(--alert); }

@media (min-width: 600px) {
  .toasts { left: auto; right: var(--space-5); max-width: min(380px, calc(100vw - var(--space-8))); }
}

/* -------------------------------------------------------------------------
   Tables

   A table is the one thing here allowed to be wider than the phone, and it
   scrolls inside its own box. `overscroll-behavior-x: contain` stops a swipe
   that runs off the end of the table from turning into a browser back-swipe.
   ------------------------------------------------------------------------- */
.table-scroll { overflow-x: auto; overscroll-behavior-x: contain; -webkit-overflow-scrolling: touch; }
table.tbl { width: 100%; border-collapse: collapse; font-family: var(--sans); font-size: var(--fs-small); }
table.tbl caption {
  text-align: left; font-family: var(--mono); font-size: var(--fs-label); letter-spacing: .13em;
  text-transform: uppercase; color: var(--ink-2); padding: 0 0 var(--space-2);
}
table.tbl th {
  text-align: left; background: var(--sunk); color: var(--ink-2);
  font-weight: 640; font-size: var(--fs-label); letter-spacing: .04em; text-transform: uppercase;
  padding: var(--space-3); border-bottom: 1px solid var(--rule); white-space: nowrap;
}
table.tbl td { padding: var(--space-3); border-bottom: 1px solid var(--rule-2); color: var(--ink-2); vertical-align: top; }
table.tbl tr:last-child td { border-bottom: 0; }
table.tbl td.name { color: var(--ink); }
table.tbl th.right, table.tbl td.right { text-align: right; }
table.tbl td.right { font-family: var(--mono); font-size: var(--fs-micro); font-variant-numeric: tabular-nums; letter-spacing: .02em; }
table.tbl tr.total td { border-top: 1px solid var(--rule); border-bottom: 0; color: var(--ink); font-weight: 640; padding-top: var(--space-4); }

/* -------------------------------------------------------------------------
   The sheet

   A bottom sheet on a real <dialog>: `showModal()` supplies the focus trap, the
   inert background, Escape-to-close and the top layer. The panel is anchored to
   the BOTTOM because that is where the thumb is and, on the sign-in flow, where
   the software keyboard is not — a box centred in the viewport is a box the
   keyboard covers.
   ------------------------------------------------------------------------- */
.sheet {
  position: fixed; inset: 0; margin: 0; padding: 0;
  width: 100%; max-width: 100%; height: 100%; max-height: 100%;
  border: 0; background: transparent; overflow: hidden;
}
/* Darkens whatever is behind it rather than painting a colour over it, so it
   works in both themes without a literal. Browsers without it keep their own
   default backdrop. */
.sheet::backdrop { -webkit-backdrop-filter: brightness(.55); backdrop-filter: brightness(.55); }

.sheet-panel {
  position: absolute; left: 0; right: 0; bottom: 0;
  max-height: 92dvh; display: flex; flex-direction: column;
  background: var(--vellum); border-top: 1px solid var(--rule);
  border-radius: var(--radius-3xl) var(--radius-3xl) 0 0;
  box-shadow: var(--shadow);
  padding-bottom: env(safe-area-inset-bottom, 0px);
}
.sheet-grip {
  flex: 0 0 auto; width: 40px; height: 4px; border-radius: var(--radius-pill);
  background: var(--rule); margin: var(--space-3) auto var(--space-1);
}
.sheet-head { flex: 0 0 auto; display: flex; align-items: center; gap: var(--space-2); padding: var(--space-1) var(--pad-card); }
.sheet-title { flex: 1 1 auto; margin: 0; font-family: var(--sans); font-size: 20px; font-weight: 700; letter-spacing: -.025em; }
.sheet-close {
  flex: 0 0 auto; min-width: var(--tap); min-height: var(--tap); padding: 0;
  border: 0; border-radius: var(--radius-pill); background: none; color: var(--ink-2);
  /* 24px because the glyph is × (U+00D7), which sits optically smaller in its em
     box than the ✕ (U+2715) this replaced — see the note in ui.js sheet(). The
     TAP target is --tap above and does not move with this. */
  font-size: 24px; line-height: 1; cursor: pointer; touch-action: manipulation;
  display: inline-flex; align-items: center; justify-content: center;
}
.sheet-close:active { background: var(--sunk); color: var(--ink); }
.sheet-body {
  flex: 1 1 auto; overflow-y: auto; overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  padding: var(--space-2) var(--pad-card) var(--pad-card);
}
.sheet-body > :last-child { margin-bottom: 0; }
.sheet-foot {
  flex: 0 0 auto; display: flex; gap: var(--space-3); align-items: center;
  padding: var(--space-3) var(--pad-card);
  border-top: 1px solid var(--rule); background: var(--vellum);
}
.sheet-actions { display: flex; gap: var(--space-3); flex: 1 1 auto; }

/* Sign-in's centre-focus sheet. Phones keep the bottom dock — the software
   keyboard owns the bottom half of a phone screen, and a centred box is a box
   the keyboard covers — but where there is room, the way in is a card in the
   middle of the screen with the coat as its crown. */
@media (min-width: 700px) {
  .sheet-focus .sheet-panel {
    left: 50%; right: auto; top: 50%; bottom: auto;
    transform: translate(-50%, -50%);
    width: min(420px, calc(100vw - 2 * var(--space-6)));
    border: 1px solid var(--rule);
    border-radius: var(--radius-3xl);
    max-height: 86dvh;
    padding-bottom: 0;
  }
  /* The grip is a drag affordance for a bottom dock; a centred card has
     nothing to drag. */
  .sheet-focus .sheet-grip { display: none; }
}
.sheet-foot .cta, .sheet-actions .cta { min-width: 0; }

@media (min-width: 600px) {
  /* Wide enough to centre it: a full-width bar of controls at 900px is a lot of
     travel for one button. */
  .sheet-panel {
    left: 50%; right: auto; transform: translateX(-50%);
    width: min(520px, 100%);
    border-radius: var(--radius-3xl) var(--radius-3xl) 0 0;
  }
}

@media (hover: hover) and (pointer: fine) {
  .sheet-close:hover { background: var(--sunk); color: var(--ink); }
}

/* -------------------------------------------------------------------------
   The sticky action bar

   The one thing pinned to a phone's viewport, and it is at the bottom, in the
   thumb zone: the pack and its price with "Add to basket" on the product page,
   the basket total with checkout in the cart.

   `position: fixed`, and the screens deliberately hang the host off <body> so
   no ancestor can turn `fixed` into `absolute` with a transform or a filter.
   Nothing in this file introduces one above them.

   The height is MEASURED by stickyBar() and published as `--stickybar-h`,
   because a bar is one line on the product page and two in the cart and grows
   again with the reader's type size. `body`'s bottom padding and the toast host
   both read that variable rather than guessing a number.
   ------------------------------------------------------------------------- */
.stickybar {
  position: fixed; inset: auto 0 0 0; z-index: 35;
  background: var(--vellum); border-top: 1px solid var(--rule);
  box-shadow: var(--shadow);
  padding-bottom: env(safe-area-inset-bottom, 0px);
}
.stickybar-inner {
  display: flex; align-items: center; gap: var(--space-3);
  padding-block: var(--space-3);
}
.stickybar-inner > .cta { flex: 1 1 auto; }

@media (min-width: 600px) {
  /* Full-width is right on a phone and absurd on a desktop, where it produces a
     1000px pill. Past the phone the label holds the left and the action takes a
     readable width on the right. */
  .stickybar-inner > .cta { flex: 0 1 auto; min-width: 220px; }
  .buy-what, .checkout-total { margin-right: auto; }
}

/* What the bar is buying, on two tight lines beside the button. */
.buy-what, .checkout-total { display: flex; flex-direction: column; min-width: 0; flex: 0 1 auto; }
.buy-pack, .checkout-label { font-family: var(--sans); font-size: var(--fs-micro); color: var(--ink-2); line-height: 1.25; }
.buy-price, .checkout-figure { line-height: 1.2; white-space: nowrap; }

/* -------------------------------------------------------------------------
   Catalogue — the filter bar

   The sidebar this replaces was a desktop pattern that survived onto a phone by
   stacking: at 390px the filter panel WAS the first screen. The bar carries the
   two controls people reach for constantly; everything else is in the sheet.

   It is the one thing allowed to stick to the TOP on a phone, because on a
   results screen the search box is what you want while you scroll. Once the
   masthead becomes sticky at 900px, the bar pins under it using the `--top-h`
   the masthead measured for itself.
   ------------------------------------------------------------------------- */
.filterbar {
  position: sticky; top: 0; z-index: 20;
  background: var(--undercoat);
  border-bottom: 1px solid var(--rule);
  padding-block: var(--space-2);
  margin-top: var(--gap-block); margin-bottom: var(--gap-block);
}
.filterbar-row { display: flex; gap: var(--space-2); align-items: center; }
.filterbar-search { flex: 1 1 auto; min-width: 0; }

.filter-btn {
  flex: 0 0 auto; display: inline-flex; align-items: center; gap: var(--space-2);
  min-height: var(--tap); padding: 0 var(--space-4);
  border: 1px solid var(--rule); border-radius: var(--radius-pill);
  background: var(--vellum); color: var(--ink);
  font-family: var(--sans); font-size: var(--fs-control); font-weight: 640;
  white-space: nowrap; cursor: pointer; touch-action: manipulation;
}
.filter-btn:active { background: var(--sunk); }
.filter-count {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 20px; height: 20px; padding: 0 5px;
  border-radius: var(--radius-pill); background: var(--silver); color: var(--undercoat);
  font-family: var(--mono); font-size: 11px; font-variant-numeric: tabular-nums;
}

/* The quick row. `chiprow()` reports its own scroll position as
   `data-at="none|start|mid|end"`, so only the edge with something behind it is
   faded — a permanent fade on both ends is a lie at the two moments it matters.
   `currentColor` is the mask's paint because only its alpha is read; it keeps a
   raw colour out of the file. */
.chiprow {
  display: flex; flex-wrap: nowrap; gap: var(--space-2);
  overflow-x: auto; overscroll-behavior-x: contain; -webkit-overflow-scrolling: touch;
  scroll-snap-type: x proximity; scrollbar-width: none;
  margin-inline: calc(-1 * max(var(--gutter), env(safe-area-inset-left, 0px)))
                 calc(-1 * max(var(--gutter), env(safe-area-inset-right, 0px)));
  padding: var(--space-2) max(var(--gutter), env(safe-area-inset-right, 0px))
           var(--space-2) max(var(--gutter), env(safe-area-inset-left, 0px));
  scroll-padding-inline: max(var(--gutter), env(safe-area-inset-left, 0px));
}
.chiprow::-webkit-scrollbar { display: none; }
.chiprow > * { flex: 0 0 auto; scroll-snap-align: start; }
.chiprow[data-at="start"] {
  -webkit-mask-image: linear-gradient(to right, currentColor calc(100% - 36px), transparent);
          mask-image: linear-gradient(to right, currentColor calc(100% - 36px), transparent);
}
.chiprow[data-at="end"] {
  -webkit-mask-image: linear-gradient(to left, currentColor calc(100% - 36px), transparent);
          mask-image: linear-gradient(to left, currentColor calc(100% - 36px), transparent);
}
.chiprow[data-at="mid"] {
  -webkit-mask-image: linear-gradient(to right, transparent, currentColor 36px, currentColor calc(100% - 36px), transparent);
          mask-image: linear-gradient(to right, transparent, currentColor 36px, currentColor calc(100% - 36px), transparent);
}

.filter-group { margin: 0 0 var(--space-6); }
.filter-group > legend + .hint { margin-bottom: var(--space-2); }
.chipwrap { display: flex; flex-wrap: wrap; gap: var(--space-2); }

.results-count { margin: 0 0 var(--space-4); }
.pager {
  display: flex; gap: var(--space-3); align-items: center; justify-content: center;
  flex-wrap: wrap; padding: var(--gap-section) 0 0;
}

@media (min-width: 900px) {
  .filterbar { top: var(--top-h, 0px); }
}

/* -------------------------------------------------------------------------
   Catalogue — the tiles

   Two-up on a phone. The media takes the tile's full width with the words
   underneath; a thumbnail beside a paragraph is not a narrower version of this,
   it is a different product.

   A long name WRAPS and the price does not move, which is why the footer is
   below the name rather than beside it: "Cold-Pressed Lamb & Sweet Potato for
   Sensitive Stomachs" is a real length in this catalogue.
   ------------------------------------------------------------------------- */
.tiles { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--gap-card); }

.tile-link {
  display: block; flex: 1 1 auto;
  color: var(--ink); text-decoration: none;
  padding-bottom: var(--space-2);
}
/* Square, not 4:3. The reference's cards lead with a much larger image slot,
   and a bag of food photographed head-on is a square subject — the 4:3 crop was
   cutting the top and bottom off every pack shot. */
.tile-media { background: var(--sunk); }
.tile-media .shot, .tile-media .shot-empty { aspect-ratio: 1 / 1; }
.tile-media .shot, .tile-media .shot-empty { border-bottom: 1px solid var(--rule-2); }
/* --fs-micro, not --fs-label: a brand name is something a customer reads, not a
   chrome label, so it stays above the 14px phone floor even though it sits
   subordinate to the 15px title above it. */
/* `.tile .tile-brand`, not `.tile-brand`. This is a <p class="tile-brand sub">
   and `p.sub { margin: 0 0 var(--space-2) }` is specificity (0,1,1) — the
   element selector beats a lone class no matter which is declared later. The
   effect was the brand and the summary sitting flush against the card's left
   border at 1px while the name and the pills, which are an h3 and a div and so
   never matched `p.sub`, sat correctly at 13px. Two paddings in one card,
   which is what it looked like. */
.tile .tile-brand { margin: var(--space-3) var(--pad-tile) 0; font-size: var(--fs-micro); }
.tile-name { margin: var(--space-1) var(--pad-tile) 0; font-size: 15px; line-height: 1.3; }
.tile-pills { display: flex; flex-wrap: wrap; gap: var(--space-1); margin: var(--space-2) var(--pad-tile) 0; }

/* The line the reference design spends on a pack weight — see js/shop.js for
   why this carries the product summary instead. Two lines, clamped: a summary
   is written for a product page and some of them are three sentences long. */
.tile .tile-summary {
  margin: var(--space-2) var(--pad-tile) 0;
  font-size: var(--fs-micro); line-height: 1.4;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden;
}

/* A SIBLING of the link, never a child of it: a control nested inside an anchor
   is either unreachable or steals the anchor's activation. */
.tile-foot {
  display: flex; align-items: center; gap: var(--space-2);
  margin-top: auto; padding: var(--space-3) var(--pad-tile) var(--pad-tile);
  border-top: 1px solid var(--rule-2);
}
.tile-money { display: flex; flex-direction: column; min-width: 0; flex: 1 1 auto; }
.tile-stock {
  display: inline-flex; align-items: center; gap: 6px;
  margin-top: 2px; font-size: var(--fs-label); color: var(--ink-2);
}
.tile-stock .dot {
  width: 7px; height: 7px; border-radius: var(--radius-pill);
  background: var(--moss); flex: 0 0 auto;
}
.tile-from { font-size: var(--fs-label); line-height: 1.2; margin: 0; }
.tile-add {
  flex: 0 0 auto; width: var(--tap); height: var(--tap); padding: 0;
  border: 1px solid var(--silver); border-radius: var(--radius-pill);
  background: var(--silver); color: var(--undercoat);
  font-family: var(--sans); font-size: 23px; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer; touch-action: manipulation;
}
.tile-add:active { background: var(--ink); border-color: var(--ink); }

@media (min-width: 600px) {
  .tiles { grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); }
  .tile-name { font-size: 16px; }
}
@media (min-width: 900px) {
  .tile-name { font-size: 15px; }
}
@media (hover: hover) and (pointer: fine) {
  .tile-add:hover { background: var(--ink); border-color: var(--ink); }
  .tile-link:hover .tile-name { text-decoration: underline; text-underline-offset: 2px; }
}

/* The imagery slot. A product with no image gets a labelled, deliberate blank
   rather than a broken <img> — the catalogue is 200-400 curated SKUs and a
   missing photo is a content gap someone has to fix, not a rendering bug. */
.shot { background: var(--sunk); aspect-ratio: 4 / 3; display: block; width: 100%; object-fit: cover; }
.shot-empty {
  background: var(--sunk); aspect-ratio: 4 / 3;
  display: grid; place-items: center; text-align: center; padding: var(--space-3);
  font-family: var(--mono); font-size: var(--fs-label); letter-spacing: .12em; text-transform: uppercase; color: var(--ink-2);
}
.shot-empty.tall { aspect-ratio: 1 / 1; }

.price { font-family: var(--sans); font-size: 17px; font-weight: 660; color: var(--ink); letter-spacing: -.01em; }
.price .mrp { font-weight: 400; font-size: var(--fs-micro); color: var(--ink-2); text-decoration: line-through; margin-left: 6px; }
@media (min-width: 900px) { .price { font-size: 16px; } }

/* -------------------------------------------------------------------------
   Product page
   ------------------------------------------------------------------------- */
/* Breadcrumbs are navigation, not prose, so they take the tap floor rather than
   the inline-link exemption. `inline-block` + padding gets there without
   breaking the " / " that separates them. */
.crumbs { margin: 0 0 var(--space-1); }
/* Horizontal padding as well as vertical: "Shop" is 35px of text, so a target
   that is only padded top and bottom is 35x47 and still misses the floor on the
   axis a thumb is widest. */
.crumbs a { display: inline-block; padding: var(--space-3) var(--space-2); }

/* The media strip: full-bleed, scroll-snapping, with dots under it. Swiping is
   the gesture a phone owner already has, and it costs no chrome — the 58px
   thumbnail strip it replaces was under every touch-target floor. */
.pdp-media { margin-bottom: var(--gap-block); }
.pdp-shots {
  display: flex; flex-wrap: nowrap;
  overflow-x: auto; overscroll-behavior-x: contain; -webkit-overflow-scrolling: touch;
  scroll-snap-type: x mandatory; scrollbar-width: none;
}
.pdp-shots::-webkit-scrollbar { display: none; }
.pdp-shots:focus-visible { outline-offset: -3px; }
.pdp-shot { flex: 0 0 100%; scroll-snap-align: center; }
.pdp-shot .shot, .pdp-shot .shot-empty { aspect-ratio: 1 / 1; border-bottom: 0; }

/* A dot is a control, so it is a 44px box with an 8px dot painted inside it
   rather than an 8px control nobody can hit. */
.pdp-dots { display: flex; justify-content: center; gap: 0; padding-top: var(--space-1); }
.pdp-dot {
  width: var(--tap); height: var(--tap); padding: 0; border: 0;
  border-radius: var(--radius-pill); background-color: transparent;
  background-image: radial-gradient(circle at 50% 50%, var(--ink-3) 0 4px, transparent 4px);
  cursor: pointer; touch-action: manipulation;
}
.pdp-dot[aria-pressed="true"] {
  background-image: radial-gradient(circle at 50% 50%, var(--silver) 0 5px, transparent 5px);
}

.pdp-head { margin-bottom: var(--gap-block); }
.pdp-price { display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--space-2); margin: 0 0 var(--space-3); }
.pdp-price .price { font-size: 26px; }
.pdp-price-pack { margin: 0; }

/* FR-C2: the rationale is the loudest block on the page after the name, and it
   is never a <details>. If this ever collapses, the brand's whole claim
   collapses with it. */
.rationale {
  background: var(--silver-soft); border: 1px solid var(--silver-lit);
  border-left: 4px solid var(--silver); border-radius: 0 var(--radius-lg) var(--radius-lg) 0;
  padding: var(--pad-card); margin: 0 0 var(--gap-block);
}
.rationale .eyebrow { color: var(--silver); margin-bottom: var(--space-2); }
.rationale p { font-family: var(--serif); font-size: var(--fs-body); line-height: 1.55; color: var(--ink); margin: 0 0 var(--space-3); }
.rationale p:last-child { margin: 0; }
.rationale a { display: inline-flex; align-items: center; min-height: var(--tap); }

/* Pack sizes. A two-line chip — size over price — because a phone reads a
   stacked pair faster than "3 kg · ₹2,450" on one line, and because the note
   ("2 left", "Out of stock") needs a third line without pushing the row wide. */
.packs-block { margin-bottom: var(--gap-block); }
.packs { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.chip.pack {
  flex-direction: column; align-items: flex-start; justify-content: center;
  gap: 1px; padding: var(--space-2) var(--space-4); min-width: 104px; text-align: left;
}
.pack-size { font-family: var(--sans); font-size: var(--fs-control); font-weight: 640; }
/* A pack price is the number this row exists to compare — 1.5 kg against 3 kg
   is the whole point of the chip — so it takes the `.data`/`.num` role above
   ("every number a customer might compare against another number") rather than
   --fs-label, which this file reserves for eyebrows, tags and mono metadata.
   It was already mono and tracked; it was just two steps down the scale at
   12px. `.pack-note` stays at --fs-label because "2 left" really is a label. */
.pack-price { font-family: var(--mono); font-size: var(--fs-micro); letter-spacing: .03em; font-variant-numeric: tabular-nums; }
.pack-note { font-family: var(--sans); font-size: var(--fs-label); opacity: .8; }
.chip.pack.out { opacity: .55; }

.ingredients { font-family: var(--sans); font-size: 16px; line-height: 1.75; color: var(--ink-2); margin: 0; }
.allergens { margin-top: var(--space-5); }
.diet-result, .delivery-result { margin-top: var(--space-4); }

@media (min-width: 600px) {
  /* A full-bleed 1:1 shot is the right composition on a phone and a 1140px
     photograph on a desktop. Off the phone it stops bleeding, takes a sane
     width and gets its corners back. */
  .pdp-media { margin-inline: 0; max-width: 460px; }
  .pdp-shots { border: 1px solid var(--rule); border-radius: var(--radius-lg); }
  .pdp-dots { justify-content: flex-start; }
}
@media (min-width: 900px) {
  .ingredients { font-size: 14.5px; }
}

/* -------------------------------------------------------------------------
   Free-delivery threshold. A meter, not a decoration: the fill is the accent
   until it qualifies, then moss — the same green that marks "passes".
   ------------------------------------------------------------------------- */
.threshold {
  background: var(--vellum); border: 1px solid var(--rule); border-radius: var(--radius-lg);
  box-shadow: var(--shadow); padding: var(--pad-tight); margin-bottom: var(--gap-block);
}
.threshold-head { margin: 0 0 var(--space-2); font-family: var(--sans); font-size: var(--fs-small); color: var(--ink-2); line-height: 1.45; }
.threshold.done .threshold-head { color: var(--moss); font-weight: 640; }
.progress .bar { height: 10px; background: var(--sunk); border: 1px solid var(--rule); border-radius: var(--radius-pill); overflow: hidden; }
.progress .fill { height: 100%; background: var(--silver); border-radius: var(--radius-pill); }
.progress.done .fill { background: var(--moss); }
/* The caption is a SIBLING of `.progress`, not a child of it — cart.js emits
   head / bar / cap as three children of `.threshold`. Selecting it as
   `.progress .cap` matched nothing and left the line in 17px serif body copy.
   Plain `.cap` covers both nestings; `.cap` is used nowhere else. */
.cap { font-family: var(--sans); font-size: var(--fs-small); color: var(--ink-2); margin: var(--space-2) 0 0; }
.progress.done .cap, .threshold.done .cap, .progress.done + .cap { color: var(--moss); font-weight: 640; }

/* -------------------------------------------------------------------------
   Cart

   The line total is in the footer under the name rather than in a third column
   beside it, which is what lets a 60-character product name take two lines on a
   390px screen without pushing a price off the edge or squeezing the stepper
   into an unusable width.
   ------------------------------------------------------------------------- */
/* Three columns now, not two: thumbnail, the line itself, and the remove
   control pinned to the top-right corner. `auto` rather than a fixed width so
   the button contributes its own 44px target and nothing else. */
.line {
  display: grid; grid-template-columns: 64px minmax(0, 1fr) auto;
  gap: var(--space-3); align-items: start;
  padding: var(--space-4) 0; border-bottom: 1px solid var(--rule-2);
}
.line:last-child { border-bottom: 0; }
.line.blocked { background: var(--sunk); border-radius: var(--radius-md); padding-inline: var(--space-3); }
.line-media { flex: 0 0 auto; }
.line-main { min-width: 0; }
.line-name { margin: 0 0 var(--space-1); font-size: 16px; }
.thumb, .thumb-empty {
  width: 64px; height: 64px; border-radius: var(--radius-md);
  background: var(--sunk); border: 1px solid var(--rule); display: block;
}
.thumb { object-fit: cover; }

.line-foot { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; margin-top: var(--space-3); }
.linetotal { display: flex; flex-direction: column; align-items: flex-end; margin-left: auto; }
.line-each { line-height: 1.25; }
.line-actions { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; margin-top: var(--space-3); }
.line-weight { margin-left: auto; }

/* A dismiss drawn as a shape, not as the character "×".

   Every font sets that glyph on its own baseline and at its own size, so a
   text cross never sits optically centred in a square target — it rides high
   in one family and low in the next. A masked box is the same 10px cross
   everywhere, and `currentColor` keeps it honest in both themes and under the
   .danger modifier below.

   44px because that is this client's tap-target floor (see the note on .cta),
   even though the mark inside it is a quarter of that. */
.iconbtn {
  width: 44px; height: 44px; flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  background: none; border: 0; border-radius: var(--radius-pill);
  color: var(--ink-3); cursor: pointer; padding: 0;
}
.iconbtn::before {
  content: ''; width: 10px; height: 10px; background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3' stroke-linecap='round'%3E%3Cpath d='M5 5l14 14M19 5L5 19'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3' stroke-linecap='round'%3E%3Cpath d='M5 5l14 14M19 5L5 19'/%3E%3C/svg%3E") center / contain no-repeat;
}
/* Not red at rest. A basket of four lines would otherwise carry four alert-red
   marks competing with the prices; the colour is the confirmation that the
   press landed on the right thing, so it arrives on the reach. */
.iconbtn:disabled { opacity: .5; cursor: not-allowed; }
.iconbtn.danger:active { background: var(--sunk); color: var(--alert); }
@media (hover: hover) and (pointer: fine) {
  .iconbtn.danger:hover { background: var(--sunk); color: var(--alert); }
}
.low { color: var(--ginger); }

.modes { list-style: none; margin: 0; padding: 0; }
.mode {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  gap: 2px var(--space-3); align-items: baseline;
  padding: var(--space-3) 0; border-bottom: 1px solid var(--rule-2);
}
.mode:last-child { border-bottom: 0; }
.mode-name { grid-column: 1; font-family: var(--sans); font-size: var(--fs-small); font-weight: 640; color: var(--ink); }
.mode-fee { grid-column: 2; grid-row: 1; text-align: right; color: var(--ink); }
.mode-when { grid-column: 1 / -1; font-family: var(--sans); font-size: var(--fs-micro); color: var(--ink-2); line-height: 1.45; }

/* -------------------------------------------------------------------------
   Checkout

   THE CHOICE ROW is the only new control on this screen, and it is a NATIVE
   radio inside a <label>. Grouping, arrow-key navigation, Space to select, the
   disabled state and the focus ring all come from the platform. The alternative
   — a <button role="radio"> — needs every one of those reimplemented, and the
   version that skips them is the version that ships.

   The radio is left VISIBLE rather than hidden behind a `:has()` selector doing
   all the work. On a screen where the choice decides where a parcel goes and
   what it costs, the control that says which one is selected should be the one
   the customer already knows, and the row highlight should be reinforcement
   rather than the only signal.

   The row is a grid, not a flex line: the title, the note and the "why not"
   have to wrap under each other on a 390px screen while the price stays put in
   its own column. A flex row drops the price onto its own line the moment an
   address is long, which is always.
   ------------------------------------------------------------------------- */
.choices { display: grid; gap: var(--space-2); }

.choice {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
  gap: var(--space-3); align-items: start;
  /* The tap floor, reached with min-height and padding rather than a height, so
     the row still grows with the reader's type size. */
  min-height: var(--tap); padding: var(--space-3) var(--space-4);
  border: 1px solid var(--rule); border-radius: var(--radius-md);
  background: var(--vellum); cursor: pointer; touch-action: manipulation;
}
.choice:active { background: var(--sunk); }

/* The radio is drawn rather than native, for one reason: a native radio paints
   at the size of its box, so the only way to give it the 44px target this file
   demands of every control is to draw a 44px radio, which is absurd. Drawn, the
   BOX is 44x44 and the MARK is 18px — a real target with a normal-looking
   control in it. `.pdp-dot` above already does exactly this for the product
   page's variant dots; this is the same trade in the same file.

   The row's <label> is a target too, and a much bigger one, so nothing here is
   the difference between reachable and not. It is the difference between
   passing the rule and being argued about.

   `appearance: none` also drops the UA's own colours, which is why the forced-
   colors block below puts them back: with a high-contrast theme on, a control
   painted from `currentColor` and a border is legible where a hand-picked
   palette is not. */
.choice-input {
  appearance: none; -webkit-appearance: none;
  flex: 0 0 auto; width: var(--tap); height: var(--tap); margin: 0;
  /* The mark, centred in the box, drawn with a radial gradient so there is no
     extra element and no pseudo-element to fight the `:has()` rules below. */
  /* --ink-3, NOT --rule. tools/check-contrast.mjs is explicit that a control
     whose only edge is --rule or --rule-2 is a real 1.4.11 failure, and this
     ring is the whole of an unselected radio. */
  background:
    radial-gradient(circle at 50% 14px, var(--undercoat) 0 8px, transparent 9px),
    radial-gradient(circle at 50% 14px, var(--ink-3) 0 9px, transparent 10px);
  background-repeat: no-repeat;
  cursor: pointer;
}
.choice-input:checked {
  background:
    radial-gradient(circle at 50% 14px, var(--undercoat) 0 4px, transparent 5px),
    radial-gradient(circle at 50% 14px, var(--silver) 0 9px, transparent 10px);
}
.choice.out .choice-input {
  background:
    radial-gradient(circle at 50% 14px, var(--sunk) 0 8px, transparent 9px),
    radial-gradient(circle at 50% 14px, var(--ink-3) 0 9px, transparent 10px);
  cursor: not-allowed;
}
@media (forced-colors: active) {
  /* Hand back the control the UA knows how to paint for this user. */
  .choice-input { appearance: auto; -webkit-appearance: auto; width: 18px; height: 18px; margin: 2px 0 0; }
}
.choice-main { display: grid; gap: 2px; min-width: 0; }
.choice-title {
  font-family: var(--sans); font-size: var(--fs-small); font-weight: 640;
  color: var(--ink); line-height: 1.35;
}
.choice-note, .choice-meta, .choice-why {
  font-family: var(--sans); font-size: var(--fs-micro); line-height: 1.45; color: var(--ink-2);
}
.choice-meta { color: var(--ink-3); }
.choice-why { color: var(--alert); font-weight: 640; }
.choice-side { align-self: center; text-align: right; color: var(--ink); white-space: nowrap; }
.choice-fee { color: var(--ink); }

/* Selected. `:has()` is the only way to reach the label from the input inside
   it, and it is reinforcement here rather than the mechanism: where it is not
   supported the native radio still says which row is chosen. */
.choice:has(.choice-input:checked) {
  border-color: var(--silver); background: var(--silver-soft);
}
.choice:has(.choice-input:focus-visible) {
  outline: 2px solid var(--silver); outline-offset: 2px;
}

/* Unavailable, and SHOWN. A full delivery window comes back from the server
   rather than being hidden (FR-E5), because seeing that Saturday morning went
   is a different fact from Saturday morning never having been offered. Greyed
   and not selectable is what that looks like. */
.choice.out { background: var(--sunk); cursor: not-allowed; }
.choice.out .choice-title, .choice.out .choice-note { color: var(--ink-3); }

/* The basket, restated. Read-only here — the place to change it is the basket,
   and the link under the list says so. */
.colines { margin: 0; }
.plainlist.colines > li + li { margin-top: 0; }
.coline {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--space-3); align-items: start;
  padding: var(--space-3) 0; border-bottom: 1px solid var(--rule-2);
}
.coline:last-child { border-bottom: 0; }
.coline-main { display: grid; gap: 2px; min-width: 0; }
.coline-name {
  font-family: var(--sans); font-size: var(--fs-small); font-weight: 640;
  color: var(--ink); line-height: 1.4;
}
.coline-note { line-height: 1.4; }
.coline-flag { font-family: var(--sans); font-size: var(--fs-micro); color: var(--alert); font-weight: 640; }
.coline-total { white-space: nowrap; }

/* A field-level problem sits under the field it belongs to rather than in a
   summary at the top, so the answer is beside the question. */
.hint.problem { color: var(--alert); font-weight: 640; }

/* -------------------------------------------------------------------------
   Orders
   ------------------------------------------------------------------------- */
.orderlist { display: grid; gap: var(--gap-card); }
.orderrow {
  display: block; width: 100%; text-align: left; font: inherit; color: inherit;
  min-height: var(--tap); padding: var(--pad-card);
  background: var(--vellum); border: 1px solid var(--rule); border-radius: var(--radius-lg);
  box-shadow: var(--shadow); cursor: pointer; touch-action: manipulation;
}
.orderrow[aria-expanded="true"] { border-color: var(--silver); }
.orderrow:active { background: var(--sunk); }
.orderrow-head { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.orderrow-foot { display: flex; align-items: baseline; gap: var(--space-3); margin-top: var(--space-2); }
.orderrow .ordernumber { font-family: var(--mono); font-size: var(--fs-small); letter-spacing: .04em; color: var(--ink); }
h2.ordernumber { font-family: var(--mono); font-size: clamp(19px, 4.4vw, 24px); letter-spacing: 0; }

.orderitems { display: grid; gap: var(--space-3); margin: var(--space-4) 0; }
.orderitem {
  display: flex; gap: var(--space-3); align-items: flex-start; justify-content: space-between;
  padding-bottom: var(--space-3); border-bottom: 1px solid var(--rule-2);
}
.orderitem:last-child { border-bottom: 0; padding-bottom: 0; }
.orderitem-main { flex: 1 1 auto; min-width: 0; }
.orderitem-name { margin: 0 0 var(--space-1); font-size: 16px; }
.orderitem-total { flex: 0 0 auto; white-space: nowrap; }
.address { line-height: 1.6; }

@media (hover: hover) and (pointer: fine) {
  .orderrow:hover { border-color: var(--ink-3); }
}

/* Order status timeline */
.timeline { list-style: none; margin: 0; padding: 0 0 0 6px; }
.timeline li { position: relative; padding: 0 0 var(--space-5) var(--space-6); border-left: 1px solid var(--rule); }
.timeline li:last-child { border-left-color: transparent; padding-bottom: 0; }
.timeline li::before {
  content: ""; position: absolute; left: -6px; top: 5px; width: 11px; height: 11px;
  border-radius: var(--radius-pill); background: var(--undercoat); border: 1px solid var(--rule);
}
.timeline li.done { border-left-color: var(--silver); }
.timeline li.done::before { background: var(--silver); border-color: var(--silver); }
.timeline li.cancelled { border-left-color: var(--alert); }
.timeline li.cancelled::before { background: var(--alert); border-color: var(--alert); }
.timeline .tstep { font-family: var(--sans); font-size: var(--fs-small); font-weight: 640; color: var(--ink); display: block; }
.timeline li:not(.done) .tstep { color: var(--ink-2); font-weight: 400; }
.timeline .twhen { font-family: var(--mono); font-size: var(--fs-label); letter-spacing: .06em; color: var(--ink-2); }

/* -------------------------------------------------------------------------
   Pets
   ------------------------------------------------------------------------- */
.petcard.open { border-color: var(--silver); }
.prompt-next { margin-top: var(--space-4); }

/* -------------------------------------------------------------------------
   The standard document. ZERO texture by rule (FR-R3) — there is no .grain
   class anywhere on standard.html, so this cannot be turned on by tweaking a
   number. A decorated claim is a weaker claim.
   ------------------------------------------------------------------------- */
.doc {
  background: var(--vellum); border: 1px solid var(--rule); border-radius: var(--radius-lg);
  padding: var(--space-5); box-shadow: var(--shadow); max-width: 74ch;
}
.doc h2 { font-size: clamp(20px, 4.6vw, 24px); margin: var(--space-7) 0 var(--space-3); }
.doc h2:first-child { margin-top: 0; }
.doc h3 { font-size: 17px; margin: var(--space-6) 0 var(--space-2); }
.doc p { font-size: var(--fs-body); line-height: 1.62; margin: 0 0 var(--space-4); }
.doc ul, .doc ol { margin: 0 0 var(--space-4); padding-left: 1.25em; }
.doc li { margin-bottom: var(--space-2); line-height: 1.6; }
.doc blockquote {
  margin: 0 0 var(--space-4); padding: var(--space-3) var(--space-4); border-left: 3px solid var(--alert);
  background: var(--sunk); border-radius: 0 var(--radius-md) var(--radius-md) 0;
  font-family: var(--sans); font-size: var(--fs-small); color: var(--ink-2);
}
.doc code { font-family: var(--mono); font-size: var(--fs-micro); background: var(--sunk); border: 1px solid var(--rule); border-radius: var(--radius-sm); padding: 1px 5px; }
/* `p.doc-source`, not `.doc-source`. The element is `<p class="sub doc-source">`,
   and `p.sub` (0,1,1) sets `margin: 0 0 var(--space-2)` — a shorthand, so it
   also writes margin-top: 0. A bare `.doc-source` (0,1,0) loses that cascade
   and the caption sat flush against the document with a 0px gap. Matching the
   element+class specificity and winning on source order restores the 24px this
   rule always meant to set, without an !important. */
p.doc-source { margin-top: var(--space-6); }

@media (min-width: 600px) { .doc { padding: var(--space-7); } }
@media (min-width: 900px) { .doc { padding: 44px; } .doc h3 { font-size: 16px; } }

.sigblock { margin-top: var(--space-7); padding-top: var(--space-6); border-top: 2px solid var(--ink); display: flex; gap: var(--space-4); align-items: flex-start; flex-wrap: wrap; }
.sigmark {
  width: 44px; height: 44px; border-radius: var(--radius-pill); border: 1.5px solid var(--silver);
  display: grid; place-items: center; font-family: var(--mono); font-size: 9px;
  color: var(--silver); letter-spacing: .06em; flex: 0 0 auto;
}
.sigtext { font-family: var(--sans); font-size: var(--fs-small); line-height: 1.55; color: var(--ink-2); flex: 1 1 200px; min-width: 0; }
.sigtext b { color: var(--ink); display: block; font-size: 16px; margin-bottom: 2px; }

/* -------------------------------------------------------------------------
   Footer

   These links sit inside sentences. WCAG 2.5.8 exempts inline links from the
   target-size rule for exactly this reason — a 44px box around a word in a
   paragraph breaks the paragraph — so they get a generous line box and an
   always-on underline rather than a hover-only one.
   ------------------------------------------------------------------------- */
footer.foot {
  border-top: 1px solid var(--rule); padding: var(--space-6) 0;
  font-family: var(--sans); font-size: var(--fs-micro); color: var(--ink-2); line-height: 1.7;
}
footer.foot .footrow { display: flex; gap: var(--space-5); flex-wrap: wrap; justify-content: space-between; align-items: flex-start; }
footer.foot a { color: var(--ink-2); text-decoration: underline; text-underline-offset: 3px; }
footer.foot code { font-family: var(--mono); font-size: var(--fs-label); }

/* The policy row is the exception to the paragraph above it.

   These five are not links inside a sentence — each one is the whole row, a
   standalone control someone taps on a phone while cross, so the WCAG 2.5.8
   inline exemption does not cover them and they take the --tap floor. Scoped
   to .footlegal so the "The standard" link in the sentence beside them keeps
   its inline line box. */
footer.foot .footlegal {
  display: flex; flex-wrap: wrap; column-gap: var(--space-5);
  margin-top: var(--space-3);
}
footer.foot .footlegal a {
  display: inline-flex; align-items: center;
  min-height: var(--tap); min-width: var(--tap);
}

@media (hover: hover) and (pointer: fine) {
  footer.foot a:hover { color: var(--silver); }
}

/* The cross-links between the policy documents — same reasoning as
   .footlegal: a list where every row is a link, not prose with links in it. */
.policylinks { margin: 0; padding: 0; list-style: none; }
.policylinks > li { margin: 0; }
.policylinks a {
  display: flex; align-items: center; min-height: var(--tap);
  font-family: var(--sans); font-size: var(--fs-small); color: var(--ink-2);
  text-decoration: underline; text-underline-offset: 3px;
}
.policylinks > li + li { border-top: 1px solid var(--rule); }

@media (hover: hover) and (pointer: fine) {
  .policylinks a:hover { color: var(--ink); }
}

/* -------------------------------------------------------------------------
   Navigation between pages.

   This client is a multi-page app on purpose — every screen is its own URL and
   its own document, and the masthead is rebuilt by shell() on each load. The
   cost of that is the hard cut a browser makes between two documents: the old
   page is discarded, the new one paints from nothing, and moving between Shop
   and Pets reads as a flicker rather than a change of screen.

   A cross-document view transition is the whole fix, and it is one rule: the
   browser holds the old frame, cross-fades to the new one, and the navigation
   reads as continuous without a line of JavaScript or a router. Browsers that
   do not implement it ignore the at-rule entirely and keep today's behaviour,
   so there is nothing to feature-detect and nothing to fall back to.

   Deliberately NOT in the reduced-motion block below: a cross-fade is exactly
   the kind of motion that block exists to suppress, and the rule that follows
   turns it off for anyone who has asked for less movement.
   ------------------------------------------------------------------------- */
@view-transition { navigation: auto; }

/* ------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; scroll-behavior: auto !important; }
  /* The `*` rule above cannot reach these: the cross-fade is played by the
     browser on pseudo-elements outside the document tree, so it survives
     unless it is named. */
  ::view-transition-old(*), ::view-transition-new(*) { animation: none !important; }
}

/* -------------------------------------------------------------------------
   The basket, in the reference design's two-column shape.

   The lines and the summary are SIBLINGS IN A GRID, declared in reading order
   and collapsed to one column below 900px. Nothing is duplicated per breakpoint
   and nothing is moved between hosts by a media query, so the phone layout is
   the same DOM the desktop one is.
   ------------------------------------------------------------------------- */
.cart-head { display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--space-3); }
.cart-head > .measure { flex: 1 1 auto; }
.cart-pin {
  margin: 0; padding: 10px 16px;
  border: 1px solid var(--rule); border-radius: var(--radius-pill);
  background: var(--undercoat);
}
.cart-pin .sub { margin: 0; }
.cart-banner { margin-bottom: var(--space-5); }

.cart-cols { display: grid; gap: var(--gap-card); align-items: start; margin-top: var(--space-6); }
.cart-lines-col > * + * { margin-top: var(--gap-card); }
.cart-aside > * + * { margin-top: var(--gap-card); }

.cart-summary-title { margin: 0 0 var(--space-4); font-size: 20px; }
/* The card's own checkout control is for the two-column layout only. Below
   900px the summary sits at the foot of the page with the PINNED bar floating
   over it, so both buttons land in the same viewport a few pixels apart — one
   control, twice, and the pinned one is the one that is always reachable. */
.cart-summary-go { display: none; }
.cart-summary-go > .cta { width: 100%; }

.trust { display: grid; gap: var(--space-4); }
.trust-item { display: grid; gap: 2px; }
.trust-title { font-size: var(--fs-micro); }
.trust .hint { margin: 0; }

@media (min-width: 900px) {
  /* 360px, not a fraction: a money column should not grow with the window —
     the figures in it are short and a wide one just spreads a total away from
     its own label. */
  .cart-cols { grid-template-columns: minmax(0, 1fr) 360px; }
  /* Clears the sticky masthead, whose measured height ui.js publishes. */
  .cart-aside { position: sticky; top: calc(var(--top-h, 0px) + var(--space-4)); }
  .cart-summary-go { display: block; margin-top: var(--space-4); }
}

/* "Empty the basket", under the lines rather than under the checkout button.
   Right-aligned and small: it is the one destructive control on this screen and
   it should not read as a peer of the thing it sits below. */
.cart-empty { margin: 0; display: flex; justify-content: flex-end; }

/* A tile whose product is already in the basket. The control keeps its size and
   position — only its fill and its label change — so a grid does not reflow
   when something is added from a sheet floating over it. */
.tile-add.in-basket {
  background: var(--moss); border-color: var(--moss); color: var(--undercoat);
  font-family: var(--mono); font-size: 15px; font-variant-numeric: tabular-nums;
}
.tile-add.in-basket:active { background: var(--ink); border-color: var(--ink); }
@media (hover: hover) and (pointer: fine) {
  .tile-add.in-basket:hover { background: var(--ink); border-color: var(--ink); }
}

/* The tile's basket control: a "+", a stepper, or a count, swapped in place by
   js/catalog.js. Fixed height so the grid does not reflow when a product moves
   between those states — a card that changes height on add makes every card
   below it jump. */
.tile-cta { flex: 0 0 auto; min-height: var(--tap); display: flex; align-items: center; }
.tile-cta .stepper { margin: 0; }
