:root{
  --sea:#1f6f5c;         /* deep sea green */
  --sea-dark:#164f42;
  --sea-tint:#e4efe9;
  --paper:#f7f4ec;       /* warm paper */
  --paper-line:#e9e2d2;
  --ink:#25302c;         /* near-black green-grey */
  --ink-soft:#5d6b64;
  --gold:#c99a3a;        /* brass accent */
  --teal:#3fb99e;        /* only on the dark toast, where the sea green sinks */
  --alert:#e8833a;       /* the "something is waiting" dot; not the brass gold */
  --danger:#b4462f;
  --done:#9aa8a0;
  --radius:14px;
  --shadow:0 2px 8px rgba(22,79,66,.10),0 8px 24px rgba(22,79,66,.06);
  --tap:48px;
}

*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
html,body{margin:0;padding:0}
body{
  font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  background:var(--paper);
  color:var(--ink);
  min-height:100dvh;
  -webkit-font-smoothing:antialiased;
}
.view{min-height:100dvh;display:flex;flex-direction:column}
/* The hidden attribute always wins, whatever display a class sets. Here since
   the first commit — which makes it the disproof of a claim a later commit
   made, that .icon-btn's grid had beaten [hidden]. It cannot: !important is
   above any specificity. Do not add per-class [hidden] guards; they are noise. */
[hidden]{display:none !important}

/* ---- brand ---- */
.brand{display:flex;align-items:center;gap:.55rem}
.brand h1{font-size:1.9rem;margin:0;letter-spacing:-.02em;font-weight:800}

/* ---- login ---- */
#login-view{align-items:center;justify-content:center;padding:1.5rem 1.5rem 4.5rem}
.login-card{
  background:#fff;border-radius:var(--radius);box-shadow:var(--shadow);
  padding:2rem 1.6rem;width:100%;max-width:380px;
  border:1px solid var(--paper-line);
}
.login-card .brand{justify-content:center;margin-bottom:.25rem}
.login-sub{text-align:center;color:var(--ink-soft);margin:.2rem 0 1.4rem}
label{display:block;font-size:.82rem;font-weight:600;color:var(--ink-soft);margin-bottom:1rem}
input[type=email],input[type=password],input[type=text]{
  width:100%;margin-top:.35rem;padding:.8rem .9rem;font-size:1rem;
  border:1.5px solid var(--paper-line);border-radius:10px;background:var(--paper);
  color:var(--ink);
}
input:focus{outline:none;border-color:var(--sea);background:#fff}

/* ---- password fields ---- */
/* The wrapper is what the eye is positioned against; the input keeps its own
   styling and stays a real password field. */
.pw-wrap{position:relative;display:block}
.pw-wrap input{padding-right:3rem}
.pw-eye{
  position:absolute;right:.35rem;top:50%;transform:translateY(-50%);
  width:38px;height:38px;display:grid;place-items:center;
  background:transparent;border:none;border-radius:9px;
  color:var(--ink-soft);cursor:pointer;padding:0;
}
.pw-eye:active{background:var(--paper-line)}
.pw-eye[aria-pressed="true"]{color:var(--sea)}
.btn-primary{
  width:100%;padding:.85rem;font-size:1rem;font-weight:700;color:#fff;
  background:var(--sea);border:none;border-radius:10px;cursor:pointer;
  min-height:var(--tap);
}
.btn-primary:active{background:var(--sea-dark)}
.form-error{color:var(--danger);font-size:.85rem;margin:.2rem 0 .8rem;font-weight:600}

/* ---- topbar ---- */
/* The sticky block is the wrapper, so the chips travel with the title bar.
   It must outrank .scrim (8) — it is a stacking context, so #menu-flyout's
   z-index is scoped inside it, and at a lower value the scrim painted over the
   flyout and swallowed clicks meant for the menu. */
.topwrap{position:sticky;top:0;z-index:10}
.topbar{
  position:relative;          /* anchors #menu-flyout to the title bar itself */
  display:flex;align-items:center;justify-content:space-between;
  padding:calc(env(safe-area-inset-top,0) + .7rem) 1rem .7rem;
  background:var(--sea);color:#fff;
}
.brand-name{font-weight:800;font-size:1.15rem;letter-spacing:-.01em}
/* Two lines in the bar, on a narrow screen.
 *
 * "Basket Buddy" on one line is 167px of a 375px bar, and every control added
 * to that bar has been paid for out of the same 375. Wrapped it costs about
 * ninety, which is what makes room for Add to sit beside the switch instead of
 * floating over the list.
 *
 * The bar's copy of the name carries an ordinary space rather than the nbsp the
 * sign-in card still uses — a wrap has to be possible before any rule here can
 * ask for one. Tightened leading so two lines are barely taller than one was. */
@media (max-width:599px){
  .brand-sm .brand-name{
    white-space:normal;font-size:1.08rem;line-height:1.06;
    max-width:5.3em;
  }
}
/* 44px rather than 48 in the bar alone: at 48 with a 21px glyph there is 27px
   of air around each one, and they read as scattered rather than as a set. 44
   is still a comfortable target — it is Apple's own floor — and the menu items
   keep the full 48.
 *
 * The gap was 0 for the same reason, back when the bar held four icons of one
 * kind. It now holds a switch, a gold action and two icons, and packed edge to
 * edge those read as one object rather than four controls. There is room for it:
 * with the name wrapped, a 375px bar has about 36px to spare, and three gaps of
 * 5.6px spend half of that.
 *
 * align-items:center is the alignment fix. The default is stretch, which left
 * the 34px Add pinned to the top of a 48px row — a misalignment easy to see and
 * hard to name. */
.topbar-actions{display:flex;align-items:center;gap:.35rem}
/* 44 SQUARE, not 44 wide. Only the width was set here, so the height stayed at
   --tap (48) and border-radius:50% on a 44x48 box draws an ellipse. Invisible
   for as long as the background was transparent, and plainly an oval the moment
   Find lit one up. */
.topbar-actions .icon-btn{width:44px;height:44px}

/* ...and the lit disc is drawn a few pixels inside that target rather than
   filling it. The button stays 44 because that is what a thumb needs — it is
   Apple's own floor — so the circle you see and the circle you press are
   deliberately different sizes: 36 and 44.
 *
   A radial-gradient rather than a pseudo-element, because a background paints
   behind the icon for nothing. An absolutely-positioned ::before would sit on
   TOP of the glyph and need every child lifted back out from under it, which is
   three more rules and a stacking context to get one smaller circle. The 99%
   stop is where the edge is; the last 1% is what keeps it from stairstepping. */
.topbar-actions .icon-btn:active{
  background:radial-gradient(circle 18px at 50% 50%,
    rgba(255,255,255,.15) 99%, transparent 100%);
}
/* More air where there is plenty of it — and a name to match.
 *
 * 1.15rem is a size chosen for a 375px bar, where the name competes with a
 * switch, a gold action and two icons for the same few hundred pixels. On a
 * desktop it was still wearing that compromise: the same small title against
 * half a metre of empty green, which reads less like restraint than like the
 * page forgot to finish loading.
 *
 * It costs nothing to grow. The bar's height is set by the 44px controls, and
 * the mark beside the name is 2.3em, so the pair only start pushing the bar
 * taller past about 1.9rem — both steps here stay under that, and the bar is
 * exactly as tall at 1200px as it is at 375. */
@media (min-width:600px){
  .topbar-actions{gap:.5rem}
  .brand-sm .brand-name{font-size:1.45rem}
}
@media (min-width:900px){
  .brand-sm .brand-name{font-size:1.7rem}
}
.icon-btn{
  width:var(--tap);height:var(--tap);display:grid;place-items:center;
  /* padding:0 matters. A <button> carries 6px of it from the user agent, which
     leaves a 44px button with a 32px content box — and the 36px avatar then
     centres in THAT and overflows to one side, sitting 2px right of the button
     it lives in. Invisible until something had to be drawn concentric with it. */
  background:transparent;border:none;padding:0;color:#fff;border-radius:50%;cursor:pointer;
}
.icon-btn:active{background:rgba(255,255,255,.15)}

/* ---- add item ---- */
/* Floats just below the chrome, wherever that currently ends. Below .scrim (8)
   so the menu still covers it, above the list so it stays reachable. */
.add-fab{
  position:fixed;right:.85rem;z-index:6;
  top:calc(var(--chrome-h,56px) + .5rem);
  width:42px;height:42px;border-radius:50%;
  display:grid;place-items:center;
  background:var(--gold);color:#fff;border:none;cursor:pointer;
  box-shadow:0 3px 10px rgba(0,0,0,.22);
  transition:top .18s ease;
}
.add-fab:active{filter:brightness(.94)}
.add-fab-icon{transition:transform .18s ease}
/* Beside the add button, not under it: stacked, the second disk landed on the
   first store band and covered its count. Quieter than the add button, because
   refreshing is the second thing you want here, not the first. */
.refresh-fab{
  top:calc(var(--chrome-h,56px) + .5rem + 2px);   /* +2 centres 38px against 42 */
  right:calc(.85rem + 42px + .55rem);
  width:38px;height:38px;
  background:#fff;color:var(--sea);
  box-shadow:0 2px 8px rgba(0,0,0,.16);
}
.refresh-fab:active{background:var(--sea-tint)}
/* Between the .7s that read as waiting and the .45s that read as frantic. A
   spinner is meant to say "working", and past a certain speed it stops saying
   that and starts saying something is wrong. */
.refresh-fab.spinning .refresh-icon{animation:spin .6s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}

/* Refreshing is a desktop habit. On a phone the list is pulled on focus and on
   reconnect anyway, and the gesture for "get me the latest" is the pull the
   whole platform already has — so the disk was one more thing sitting over the
   list for something nobody needed to ask for. */
@media (pointer:coarse){
  .add-fab.refresh-fab{display:none}
}

/* On a computer they belong in the bar, not floating over the list.
 *
 * A disc parked over the top-right corner is a phone idiom: it is where a thumb
 * reaches and there is no room for a toolbar. With a pointer there is a bar
 * already, with room in it, and a control that hovers over the content it acts
 * on is just something to move the mouse around.
 *
 * They sit in .topbar-actions in the markup, which changes nothing anywhere the
 * rule below does not apply — position:fixed takes them out of the flex flow
 * entirely, so the bar measures the same 375px it always did.
 *
 * The width matters as much as the pointer. Two more buttons do not fit a
 * 375px bar, and a desktop window dragged that narrow is no roomier than a
 * phone — so a narrow window gets the floating pair back rather than a bar with
 * the avatar pushed off the end of it, which is exactly the regression the
 * 375px budget test exists to catch. */
/* Add is in the bar everywhere. It stays gold there: it is the one ACTION among
   controls that only change how the list is looked at, and the bar has no other
   way to say so. */
.add-fab{
  position:static;top:auto;right:auto;
  width:34px;height:34px;
  box-shadow:none;transition:none;
}

/* Refresh only joins it on a computer with room. It is already hidden outright
   on a touch screen — the platform's own pull gesture is the way to ask for the
   latest — and a window dragged narrow is no roomier than a phone, so it falls
   back rather than pushing the avatar off the end of a 375px bar. */
@media (pointer:fine) and (min-width:600px){
  .add-fab.refresh-fab{
    position:static;top:auto;right:auto;
    width:34px;height:34px;
    background:transparent;color:#fff;box-shadow:none;
  }
  .add-fab.refresh-fab:active{background:rgba(255,255,255,.16)}
}
/* Narrow, but with a pointer: refresh has nowhere in the bar to go, so it keeps
   the floating disc it has always had. */
@media (pointer:fine) and (max-width:599px){
  .add-fab.refresh-fab{
    position:fixed;
    top:calc(var(--chrome-h,56px) + .5rem + 2px);
    right:calc(.85rem + 42px + .55rem);
    width:38px;height:38px;
    background:#fff;color:var(--sea);
    box-shadow:0 2px 8px rgba(0,0,0,.16);
  }
}

/* ---- the shopping marker ---- */
/* The avatar is the marker. It was a cart of its own, which cost 44px of a bar
   that had none to spare on a 375px phone — and the avatar is already the thing
   you look at, so a ring around it is read without being hunted for. Gold, the
   same as everywhere else something is switched on that will not stay on.
   While it runs the first tap ends the trip; the next opens the menu. */

/* The track it runs on, and the whole of the state when nothing is moving. */
.avatar-btn.shopping{position:relative}
.avatar-btn.shopping .avatar{box-shadow:0 0 0 4px rgba(201,154,58,.30)}

@media (prefers-reduced-motion:no-preference){
  /* A segment going round, rather than a ring going out. Both say "running",
     and a lap reads as continuous where an expanding ring reads as a series of
     events — this is one thing carrying on, not something happening over and
     over. It also stays inside the button at every moment, where the expanding
     one cleared the top of the bar by two pixels and had to be measured.

     It cannot live on .avatar: that has overflow:hidden for the photo, and
     would crop the arc into a chord the moment it came round. */
  .avatar-btn.shopping::after{
    content:'';position:absolute;
    /* Square, and centred by hand rather than inset. The bar's buttons are
       44px wide since it closed up in 3.43.0, but still 48 tall from --tap, so
       an inset box inherits that difference and draws an ellipse. It did:
       35 by 39, which reads as a wobble rather than a lap. */
    top:50%;left:50%;width:44px;height:44px;margin:-22px 0 0 -22px;
    border-radius:50%;
    /* Three quarters of a turn, brightest at the leading edge and fading out
       behind it. A border can only draw quarter-circles, and only with hard
       ends — this needs both a length and a tail, so it is a conic gradient
       masked into a ring instead.

       Angles run clockwise from twelve, and the element turns the same way, so
       the high-angle end is the one out in front. Full gold there, fading back
       to nothing by 130 degrees, then a gap. color-mix keeps --gold the single
       source of the colour; where it is unsupported the gradient is dropped
       whole and the steady ring underneath still says a trip is on. */
    background:conic-gradient(from 0deg,
      transparent 0deg 88deg,
      color-mix(in srgb, var(--gold) 10%, transparent) 130deg,
      color-mix(in srgb, var(--gold) 55%, transparent) 250deg,
      var(--gold) 358deg);
    -webkit-mask:radial-gradient(farthest-side, transparent calc(100% - 4px), #000 0);
    mask:radial-gradient(farthest-side, transparent calc(100% - 4px), #000 0);
    pointer-events:none;
    animation:trip-orbit 1.4s linear infinite;
  }
}
/* No animation, no segment — so the ring becomes solid instead, or the state
   would be a faint circle that says nothing at all. */
@media (prefers-reduced-motion:reduce){
  .avatar-btn.shopping .avatar{box-shadow:0 0 0 4px var(--gold)}
}
@keyframes trip-orbit{to{transform:rotate(360deg)}}

/* No number stored yet: the button still works — it asks for one — but it
   should not look as ready as the one that dials straight away. */
#call-btn.unset .flyout-icon{color:var(--ink-soft);opacity:.55}
/* Same reasoning as the camera button: a tel: link only means something on a
   device that can place a call. Keyed on pointer:fine, so a tablet — coarse
   pointer, and often able to dial — keeps it. The number itself stays editable
   from a computer, under your name in the menu, since setting it up sitting at
   a desk is easier than doing it one-handed in a shop. */
@media (pointer:fine){
  #call-btn{display:none}
}

/* ---- who added it, and whether it should be organic ---- */
/* 12px against the 22px it used to be — a bit over a quarter of the area. At
   this size it is a mark on the line rather than a control, which is the
   point: it should be legible in passing without asking to be read. Initials
   no longer fit, so a member with no picture gets a coloured disc instead of
   two letters at a size nobody can make out; the name is in the title.

   inline-block, sitting inside the name itself, so it trails the last word of
   a name that wraps instead of being pushed out to the right margin. The
   nudge off the baseline centres it against lower-case letters. */
/* A line of its own under the name: who wants it, and whether it should be
   organic. Both answer "what sort of thing is this row" and neither is a
   control, so they sit with the name rather than out among the buttons. */
.row-meta{
  display:flex;align-items:center;gap:.4rem;min-width:0;
  margin-top:.15rem;line-height:1;
}
/* 18px, not the 12 it was inline. On the name it had to fit inside a line of
   text; on its own line there is room for a face you can actually recognise,
   which is the only reason to store a picture rather than a coloured dot. */
.row-who{
  flex:none;
  width:18px;height:18px;border-radius:50%;overflow:hidden;
  background:var(--sea);
  font-size:0;                     /* any stray text must not force it open */
}
.row-who img{width:100%;height:100%;object-fit:cover;display:block}
/* Home Assistant is a thing rather than a person, so its mark is set on a disc
   and inset, not cropped to fill one like a photograph. The logo is a house
   with square shoulders and a round clip would cut both of them off. The hairline
   keeps a white disc from dissolving into a pale row background. */
.row-who.ha{
  background:#fff;
  display:grid;place-items:center;
  box-shadow:inset 0 0 0 1px rgba(0,0,0,.12);
}
.row-who.ha svg{width:78%;height:78%;display:block}
/* Your own additions do not need pointing out; still shown, just quieter. */
/* Dimmed, not greyed: the colour IS the identity now, and washing it out would
   take the one thing the mark has to say. */
.row-who.mine{opacity:.55}
/* A ticked row is dimmed as a whole, but this mark is the thing a ticked row
   is now FOR — who has it in the basket — so it does not fade away with the
   text. It was .35 back when it only ever meant "who wrote this down", which
   is old news by the time the item is in the trolley. */
.row.done .row-who{opacity:.9}
.row.done .row-who.mine{opacity:.45}
.row-organic{flex:none;font-size:.9rem;line-height:1}

/* ---- showing or hiding what has been bought ---- */
/* A switch, not an icon. Position and colour both carry the state, which is
   more than a glyph with a slash through it can manage at this size — and a
   switch cannot be mistaken for something that acts on an item. */
/* No margin of its own: .topbar-actions has a gap now, and a margin on top of
   it is a second opinion — it made the space after the switch twice the space
   after everything else. */
.sw-btn{width:auto;padding:0 .3rem}
.sw{
  position:relative;display:block;width:46px;height:27px;border-radius:999px;
  background:rgba(255,255,255,.26);
}
/* The two boxes, pinned to the ends. The knob slides over one of them, so the
   one you can see is the one you are in. */
.sw-box{position:absolute;top:7px}
.sw-on{left:5px}
.sw-off{right:5px}
.sw-knob{
  position:absolute;top:3px;left:3px;width:21px;height:21px;border-radius:50%;
  transform:translateX(19px);background:#fff;
}
@media (prefers-reduced-motion:no-preference){
  .sw,.sw-knob{transition:transform .17s ease,background .17s ease}
}
/* Hiding: the knob slides back over the ticked box and turns gold, leaving the
   empty one showing. Gold is what the shopping marker uses and means the same
   thing here — something is switched on that will not stay on, and the list in
   front of you is not all of it. */
#show-done-btn[aria-pressed="false"] .sw{background:rgba(0,0,0,.22)}
#show-done-btn[aria-pressed="false"] .sw-knob{transform:translateX(0);background:var(--gold)}
/* Only the exposed one is drawn. Leaving the covered box underneath would show
   through the moment the knob's colour or size changed. */
#show-done-btn[aria-pressed="true"]  .sw-off{visibility:hidden}
#show-done-btn[aria-pressed="false"] .sw-on{visibility:hidden}
#show-done-btn:active .sw{background:rgba(255,255,255,.4)}

/* ---- count and unit ---- */
/* The count keeps its width and the unit sits beside it at its own size —
   stretched across the row it read as the important half of the pair, which it
   is not: how many is the answer, what one IS is the qualifier. */
.qty-unit{display:flex;gap:.5rem;align-items:center;flex:1;min-width:0}
select.unit-in,
select.price-unit{
  /* Same treatment as the store and category selects. Without appearance:none
     the browser draws its own chevron, which sits wherever it likes and does
     not line up with the ones directly beneath it. */
  appearance:none;-webkit-appearance:none;-moz-appearance:none;
  /* min-width, because a select sized `auto` measures its widest option
     WITHOUT the padding reserved for the chevron, so the word ran underneath
     it. The moment the default gained a glyph of its own there was no option
     short enough to hide that. */
  flex:none;width:auto;min-width:7rem;margin:0;
  padding:.5rem 2rem .5rem .6rem;
  font:inherit;font-size:.95rem;line-height:1.3;
  border:1.5px solid var(--paper-line);border-radius:10px;
  background-color:#fff;color:var(--ink);
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%235d6b64' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  background-repeat:no-repeat;
  background-position:right .55rem center;
  background-size:17px 17px;
}
select.unit-in:focus,
select.price-unit:focus{outline:none;border-color:var(--sea)}

/* ---- organic ---- */
/* One box, two states: clear is whatever they have, a tick is organic. Drawn
   here rather than left to the platform so the tick matches the app's own
   checkboxes rather than the OS's. */
.organic-field{flex:1;min-width:0;display:flex;align-items:center;gap:.55rem}
/* The sprout the row will carry, beside the box that decides it — the same
   glyph the list uses, so this is a preview and not a second notation. */
.organic-mark{flex:none;font-size:1rem;line-height:1}
/* No preference set: the row shows nothing, so this shows nothing. Removed
   rather than left empty, or the flex gap beside the box would still be there
   holding a space open for a glyph that is not coming. */
.organic-mark:empty{display:none}

input.organic-box{
  appearance:none;-webkit-appearance:none;margin:0;
  width:1.35rem;height:1.35rem;flex:none;cursor:pointer;
  border:1.5px solid var(--paper-line);border-radius:7px;background:#fff;
  background-repeat:no-repeat;background-position:center;background-size:100% 100%;
}
input.organic-box:focus-visible{outline:none;border-color:var(--sea);
  box-shadow:0 0 0 3px rgba(31,122,90,.25)}
/* Organic or come home without it. Filled, because this one does change what
   happens at the shelf. */
input.organic-box:checked{
  border-color:var(--sea);background-color:var(--sea);
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='3.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='5 12.5 10 17 19 7.5'/%3E%3C/svg%3E");
}

/* ---- a red bell pepper ----
   Unicode has only the green one (U+1FAD1), so this is two copies of it: a
   red-filtered body with the original stem clipped over the top. Sized in em
   so it travels to every place an icon is drawn — the row, the picker, the
   panel title — at whatever size that place uses. */
.pepper{
  position:relative;display:inline-block;
  width:1em;height:1em;line-height:1;vertical-align:-.125em;
}
.pepper i{position:absolute;inset:0;display:block;font-style:normal}
.pepper i.body{filter:sepia(1) saturate(8) hue-rotate(-42deg) brightness(.95) contrast(1.18)}
/* Wide across the curl, where there is nothing but transparent background to
   over-cover, then narrowing to the width of the stalk where the flesh closes
   in either side of it. Measured off the glyph rather than guessed. */
.pepper i.stem{clip-path:polygon(46% 0%, 64% 0%, 64% 9.5%, 55% 9.5%, 55% 14.5%, 49.5% 14.5%, 49.5% 9.5%, 46% 9.5%)}

/* ---- call for help ---- */
.phone-wrap{flex:1 1 100%;margin-top:.35rem}
/* One field now, but the caption stays: a placeholder disappears the moment
   the box has a number in it, which is exactly when you come back wondering
   what the number was for. */
.phone-cap{
  display:block;margin:0 0 .15rem .15rem;
  font-size:.68rem;font-weight:700;letter-spacing:.02em;
  text-transform:uppercase;color:var(--ink-soft);
}
input.phone-in{
  width:100%;margin:0;padding:.45rem .6rem;
  font:inherit;font-size:.88rem;
  border:1.5px solid var(--paper-line);border-radius:9px;background:#fff;color:var(--ink);
}
input.phone-in:focus{outline:none;border-color:var(--sea)}

/* ---- store chips ---- */
/* ---- find an item ---- */
/* Sits in the pinned block above the chips, so it stays put while the list
   scrolls under it — the whole point is to type and watch the list narrow. */
/* The clear button is laid over the field rather than beside it. Beside it,
   the field shortened the moment you typed the first letter — the text you
   were still writing shuffling left to make room for a button that had just
   appeared. */
.find-bar{
  display:flex;align-items:center;
  padding:.5rem .8rem;background:var(--sea-dark);
  position:relative;
}
#find-input{
  flex:1;min-width:0;margin:0;padding:.45rem 2.4rem .45rem .7rem;
  font:inherit;font-size:.95rem;
  border:1px solid rgba(255,255,255,.28);border-radius:999px;
  background:rgba(255,255,255,.14);color:#fff;
}
#find-input::placeholder{color:rgba(255,255,255,.6)}
#find-input:focus{outline:none;border-color:rgba(255,255,255,.65);background:rgba(255,255,255,.2)}
/* The browser's own search reset is a grey X on a dark field, and on a phone
   it is not there at all. */
#find-input::-webkit-search-cancel-button{display:none}
.find-clear{
  position:absolute;right:1.15rem;top:50%;transform:translateY(-50%);
  width:26px;height:26px;border-radius:50%;border:none;cursor:pointer;
  display:grid;place-items:center;
  background:rgba(255,255,255,.22);color:#fff;font-size:1rem;line-height:1;
}
.find-clear:active{background:rgba(255,255,255,.3)}
/* Lit while the bar is open, so it is obvious which button closes it again. */
/* Same disc as the press, held while the bar is open. */
#find-btn.on{
  background:radial-gradient(circle 18px at 50% 50%,
    rgba(255,255,255,.2) 99%, transparent 100%);
}

.store-chips{
  display:flex;gap:.4rem;align-items:center;
  padding:.5rem .8rem;background:var(--sea-dark);
  overflow-x:auto;overflow-y:hidden;
  scrollbar-width:none;-webkit-overflow-scrolling:touch;
}
.store-chips::-webkit-scrollbar{display:none}
.chip{
  flex:none;display:inline-flex;align-items:center;gap:.4rem;
  padding:.35rem .7rem;border-radius:999px;cursor:pointer;
  background:rgba(255,255,255,.14);border:1px solid rgba(255,255,255,.22);
  color:#fff;font:inherit;font-size:.82rem;font-weight:700;
  white-space:nowrap;
}
.chip:active{background:rgba(255,255,255,.3)}
.chip .chip-count{
  font-family:ui-monospace,"SF Mono",Menlo,monospace;
  font-size:.72rem;font-weight:700;opacity:.75;
}
/* Everything in this store is ticked off. */
.chip.done{opacity:.5}

/* Closing time. Solid fills rather than a tinted border, because this has to
   register from a glance at a phone held at hip height in a bright aisle.
   Amber inside 90 minutes, red inside 30 — the useful question is not "when
   does it shut" but "can I still finish this list", and half an hour with a
   trolley is already tight. */
/* Dark ink on the amber: --alert is a light orange, and white on it is the
   one pairing in this palette that fails to read outdoors. */
.chip.shut-soon{
  background:var(--alert);border-color:rgba(255,255,255,.55);color:#3a2410;opacity:1;
}
.chip.shut-closing{
  background:var(--danger);border-color:rgba(255,255,255,.6);color:#fff;opacity:1;
}
/* Shut already: stated, not shouted. A muted red rather than the solid one
   above — closing soon is a reason to hurry, closed is a reason not to go, and
   the two must not look like the same alarm. Dimming with a strike-through
   said "crossed off the list", which is what a bought item looks like. */
.chip.shut-closed{
  background:rgba(180,70,47,.34);
  border-color:rgba(180,70,47,.55);
  color:#f2bfb2;
  opacity:1;
}
/* An active filter still has to look chosen even when it is also closing, so
   the ring goes outside the fill rather than replacing it. */
.chip.active.shut-soon,
.chip.active.shut-closing,
.chip.active.shut-closed{box-shadow:0 0 0 2px rgba(255,255,255,.85)}
/* ---- dragging the band into a running order ----
   The dragged chip follows the finger; the others slide by exactly one
   chip-width to open a gap for it. Two different jobs, so two different
   transition rules — the dragged one must NOT ease, or it lags behind the
   finger by the length of the easing. */
/* Sideways belongs to the browser, which scrolls the band with it — the shops
   stop fitting on a phone long before a household runs out of them. Reordering
   is behind a hold, and only once it has started does the chip take the
   gesture over, below. */
.store-chips .chip{
  touch-action:auto;
  cursor:grab;
  will-change:transform;
  -webkit-touch-callout:none;
  user-select:none;-webkit-user-select:none;
}
/* Held long enough to lift: now the drag arranges rather than scrolls. */
.store-chips.chips-dragging .chip{touch-action:none}

/* "All" is the way back out of a filter, so it has to stay reachable however
   far the band has been scrolled. Sticky rather than lifted out of the
   scroller: it keeps its place at the head of the running order and simply
   stops at the edge. */
/* left:0, not left:.8rem — a sticky offset is measured from the scrollport's
   padding box, so .8rem was added to the band's own .8rem padding and pinned
   the chip 25.6px in, twice as far as the mask below covers. */
.chip.chip-all{position:sticky;left:0;z-index:2}
/* Two layers, because one cannot do it.
 *
 * Masking only the strips either side left the pill's four rounded corners
 * uncovered — a pill is round, its box is not — and the chip sliding past
 * showed through those corners. That is what made a round chip read as a
 * square block.
 *
 * So a rectangle has to cover the whole box, and the pill has to sit on top of
 * that rectangle. It cannot go underneath: a pseudo-element with z-index:-1
 * paints ABOVE its own parent's background, not below it, so a backdrop drawn
 * that way swallowed the gold of an active All. The chip therefore keeps no
 * fill of its own — ::before is the backdrop, ::after is the pill, and the
 * label is lifted over both. */
.chip.chip-all{background:transparent;border-color:transparent}
/* The box, plus the band's own left padding, plus the gap on the right so
   nothing arrives welded to it. */
.chip.chip-all::before{
  content:'';position:absolute;
  left:-.8rem;right:-.4rem;top:-.5rem;bottom:-.5rem;
  background:var(--sea-dark);
}
/* inset:-1px so it covers where the chip's own border used to be drawn. */
.chip.chip-all::after{
  content:'';position:absolute;inset:-1px;
  border:1px solid rgba(255,255,255,.22);border-radius:999px;
  background:rgba(255,255,255,.14);
}
.chip.chip-all.active::after{background:var(--gold);border-color:var(--gold)}
.chip.chip-all:active::after{background:rgba(255,255,255,.3)}
.chip.chip-all.active:active::after{background:var(--gold)}
/* Above the backdrop and the pill both. */
.chip.chip-all > *{position:relative;z-index:1}
@media (prefers-reduced-motion:no-preference){
  /* Only the neighbours animate. A gentle overshoot-free curve: this is a
     gap opening, not something bouncing. */
  .chip{transition:transform .22s cubic-bezier(.22,.68,.24,1)}
}
/* Under the finger: no transform transition at all, so it tracks exactly. */
.chip.dragging{
  transition:box-shadow .16s ease, background-color .16s ease;
  position:relative;z-index:3;
  cursor:grabbing;
  box-shadow:0 8px 20px rgba(0,0,0,.45);
  /* Slightly brighter as well as bigger — on a dark band, a shadow alone is
     nearly invisible and the lift does not read. */
  background:rgba(255,255,255,.28);
}
/* Let go: the one moment the dragged chip is allowed to ease, as it drops
   into its slot. */
.chip.settling{transition:transform .2s cubic-bezier(.22,.68,.24,1), box-shadow .2s ease}
.chip.settling{box-shadow:0 2px 6px rgba(0,0,0,.25)}
.store-chips.chips-dragging{cursor:grabbing;scroll-behavior:auto}
/* Nothing else should respond to a pointer crossing it mid-drag. */
.store-chips.chips-dragging .chip:not(.dragging){pointer-events:none}

/* ---- what moved while you were out ----
   Gold, because gold is what this app uses for "look here" — the add button,
   the chip you have picked. Not red: nothing has gone wrong, somebody has
   simply changed their mind while you were in an aisle. */
.trip-band{
  display:flex;align-items:center;gap:1rem;
  margin:0 0 1.1rem;padding:.85rem 1rem;
  background:#fdf6e7;border:1px solid #e8d3a4;border-radius:var(--radius);
  box-shadow:0 1px 2px rgba(201,154,58,.10);
}
.trip-text{flex:1;min-width:0}
.trip-eyebrow{
  display:block;font-size:.66rem;font-weight:800;letter-spacing:.09em;
  text-transform:uppercase;color:#8a6a20;
}
.trip-said{margin:.2rem 0 0;font-size:.98rem;font-weight:700;color:var(--ink)}
.trip-which{
  margin:.1rem 0 0;font-size:.82rem;color:var(--ink-soft);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.trip-band .btn-primary{flex:none;background:var(--gold)}

/* A row that moved. The dot carries it; the tint is only enough to find the
   row again after you look away. */
.row.moved{background:#fdf9f0}
.row-moved-dot{
  flex:none;width:8px;height:8px;border-radius:50%;background:var(--gold);
  box-shadow:0 0 0 3px rgba(201,154,58,.18);
}
/* And on the chip, so you can see which shop it happened in without scrolling
   the whole list to find out. */
.chip.moved .chip-moved{
  color:var(--gold);font-size:1.1rem;line-height:0;margin-left:.1rem;
}
.chip.active.moved .chip-moved{color:#fff}

/* ---- add item dialog ---- */
.add-inner{width:100%;max-width:420px}
/* Laid out like the item panel — same rows, same label column, same field
   styles — so the two screens that ask the same questions stop looking like
   two different applications. Aligned with the title above it: 1rem here plus
   the .6rem the modal itself carries. */
.add-body{
  display:flex;flex-direction:column;gap:.7rem;
  padding:0 1rem;margin:.7rem 0 .2rem;
}
/* Matches input.qty-in and select.unit-in exactly — same padding, same font,
   same border — which is what keeps the three fields the same height down the
   column. It used to inherit the ask dialog's input styling and stood taller
   than the unit picker beside it. */
input.add-item-in{
  flex:1;min-width:0;margin:0;padding:.5rem .6rem;
  font:inherit;font-size:.95rem;line-height:1.3;
  border:1.5px solid var(--paper-line);border-radius:10px;
  background:#fff;color:var(--ink);
}
input.add-item-in:focus{outline:none;border-color:var(--sea)}
/* The count reads left-to-right here rather than right-aligned as in the
   panel: it is a field being filled in, not a value being corrected. */
.add-body input.qty-in{text-align:center}

/* ---- list ---- */
/* Enough head room that the floating button sits in the gap above the first
   store band at rest. Scrolled, content passes under it, as a floating button
   implies. */
.list{flex:1;padding:3.2rem 1rem 6rem;max-width:640px;width:100%;margin:0 auto}
.store-group{margin-top:1.4rem}
/* The list already reserves room for the floating disk, so the first group
   must not add its own gap on top of it. */
.store-group:first-child{margin-top:0}
/* A tinted band, so the store clearly outranks the category labels beneath it
   without repeating heavy colour down a long list. */
.store-head{
  display:flex;align-items:center;gap:.5rem;
  font-size:1rem;font-weight:800;letter-spacing:0;text-transform:none;
  color:var(--sea-dark);background:var(--sea-tint);
  padding:.6rem .8rem;border-radius:10px;
}
.store-head .count{
  margin-left:auto;font-family:ui-monospace,"SF Mono",Menlo,monospace;
  font-size:.78rem;color:var(--sea-dark);opacity:.75;letter-spacing:0;font-weight:700;
}
/* The way in to a shop's opening hours, shaped like the aisle tag on a
   category heading — the same idea in the same place, one level up. */
.hours-tag{
  flex:none;padding:.15rem .5rem;border-radius:999px;border:none;cursor:pointer;
  font:inherit;font-size:.72rem;font-weight:700;letter-spacing:.01em;
  background:rgba(22,79,66,.10);color:var(--sea-dark);
}
.hours-tag.unset{
  background:none;border:1px dashed rgba(22,79,66,.35);
  color:var(--sea-dark);opacity:.6;font-weight:600;
}
/* The same pill as a <span> while the shop's settings are put away: it is
   telling you something, not offering to be pressed. Only the cursor and the
   inline-block need saying — a span carries neither by default. */
.hours-tag.static{display:inline-block;cursor:default}
/* Everything about the shop that is not its hours. Quieter than the hours tag
   on purpose: hours are read at a glance every trip, this is opened a few times
   in the life of a shop. Sized to the 44px tap target even though the glyph is
   small — it sits next to the hours tag and both get thumbed at in a shop. */
.store-cog{
  flex:none;display:grid;place-items:center;
  width:28px;height:28px;margin:-6px 0;padding:0;
  background:none;border:none;border-radius:50%;cursor:pointer;
  color:var(--sea-dark);opacity:.55;
}
.store-cog:hover,.store-cog:focus-visible{opacity:1}
.store-cog:active{background:rgba(22,79,66,.10)}
/* A shop with nothing in it is still a shop — it keeps its heading so its
   settings stay reachable — but it should not read as somewhere to go. */
.store-group.empty-store .store-head{opacity:.62}
.hours-tag.shut-soon{background:var(--alert);color:#3a2410}
.hours-tag.shut-closing{background:var(--danger);color:#fff}
/* The word itself carries the colour, on the same red as the chip above. */
.hours-tag.shut-closed{background:rgba(180,70,47,.12);color:var(--danger)}

/* ---- photo editor ---- */
/* Full screen and black. The picture is the whole job here, and any chrome
   competing with it makes the framing harder to judge — which is the one thing
   this screen exists to help with. Above the scrim (8) and the FABs (6). */
.photoed{
  position:fixed;inset:0;z-index:60;
  display:flex;flex-direction:column;background:#000;
}
.photoed-stage{
  flex:1;min-height:0;display:grid;place-items:center;overflow:hidden;
  /* The stage owns every gesture: the browser must not steal a drag to scroll
     or a pinch to zoom the page. */
  touch-action:none;cursor:grab;
}
.photoed-stage:active{cursor:grabbing}
#photoed-canvas{
  display:block;max-width:100%;max-height:100%;
  /* A ring rather than a shadow: on black, a shadow is invisible and the crop
     edge has to be unambiguous. */
  box-shadow:0 0 0 1px rgba(255,255,255,.35);
  touch-action:none;
}
.photoed-bar{
  flex:none;padding:.7rem 1rem calc(env(safe-area-inset-bottom,0) + .9rem);
  background:#111;color:#fff;display:flex;flex-direction:column;gap:.7rem;
}
.photoed-tools{display:flex;align-items:center;gap:.5rem}
.photoed-btn{
  flex:none;min-width:var(--tap);height:38px;padding:0 .7rem;border-radius:9px;
  display:grid;place-items:center;
  background:rgba(255,255,255,.12);color:#fff;border:none;cursor:pointer;
  font:inherit;font-size:.8rem;font-weight:700;
}
.photoed-btn:active{background:rgba(255,255,255,.28)}
.photoed-aspect[aria-pressed="true"]{background:var(--teal);color:#06211b}
.photoed-zoom{flex:1;min-width:0;accent-color:var(--teal)}
.photoed-actions{display:flex;align-items:center;justify-content:flex-end;gap:.8rem}
.photoed-actions .btn-primary{width:auto}
.photoed-cancel{color:#fff;opacity:.85}

/* ---- opening hours sheet ---- */
.hours-inner{width:100%;max-width:380px}
.hours-row{display:flex;align-items:center;gap:.5rem;margin-bottom:.4rem}
.hours-day{
  flex:none;width:2.6rem;font-size:.8rem;font-weight:700;color:var(--ink-soft);
}
.hours-dash{flex:none;color:var(--ink-soft)}
input.hours-in{
  flex:1;min-width:0;margin:0;padding:.35rem .5rem;
  font:inherit;font-size:.9rem;
  border:1.5px solid var(--paper-line);border-radius:9px;background:#fff;color:var(--ink);
}
input.hours-in:focus{outline:none;border-color:var(--sea)}

/* ---- the receipt button on a shop's heading ---- */
/* Appears when the last thing at that shop is ticked and goes with the trip.
   Gold, because it is the one thing on that heading you are being asked to do
   rather than told — the hours are information and the cog is housekeeping. */
.store-receipt{
  flex:none;display:grid;place-items:center;
  width:28px;height:28px;margin:-6px 0;padding:0;
  background:var(--gold);color:#fff;border:none;border-radius:50%;cursor:pointer;
}
.store-receipt:active{filter:brightness(.94)}

/* ---- receipts ---- */
.receipt-add{display:flex;align-items:center;gap:.6rem;padding:0 1rem}
#receipt-hint{margin:.5rem 0 1rem}
.receipt-card{
  margin:0 1rem 1rem;padding:.7rem .8rem;
  background:#fff;border-radius:12px;box-shadow:var(--shadow);
}
.receipt-head{display:flex;align-items:baseline;gap:.5rem}
.receipt-store{font-weight:800;color:var(--sea-dark)}
.receipt-total{
  margin-left:auto;font-family:ui-monospace,"SF Mono",Menlo,monospace;
  font-weight:700;color:var(--ink);
}
.receipt-meta{font-size:.8rem;color:var(--ink-soft);margin:.1rem 0 .5rem}
/* A receipt is tall and thin, so a thumbnail that keeps its shape would be a
   sliver. Cropped to a band instead, and tapped to see the whole thing —
   object-position:top because the shop and the date are at the top of the paper
   and that is what identifies it in a pile. */
.receipt-shot{
  display:block;width:100%;height:130px;object-fit:cover;object-position:top;
  border-radius:8px;background:var(--paper);cursor:zoom-in;
}
.receipt-shot.big{height:auto;object-fit:contain;cursor:zoom-out}
.receipt-actions{display:flex;justify-content:flex-end;margin-top:.3rem}

/* ---- price ---- */
/* Sized to a price rather than stretched across the row: money is four or five
   characters, and a field the width of the panel invites something else. */
/* The price row IS the quantity row: a number, then a picker saying what the
   number counts. So it is built out of the same two rules rather than a pair of
   near-copies — see input.qty-in and select.unit-in, which these now share.

   They sat one row apart with their fields split at different x and their right
   edges 4px out, which is the worst of both: too close to read as a deliberate
   difference, too far to look aligned. Sharing the rules makes them one grid,
   and makes it impossible for one to drift when the other is adjusted. */
.price-field{display:flex;gap:.5rem;align-items:center;flex:1;min-width:0}

/* ---- returns ---- */
/* A category at the top of the shop it goes back to, not a section of its own.
   The list is organised by shop because that is the unit of a trip, and these
   are dealt with at that shop's desk on the way in. Alert orange rather than
   the ordinary grey heading: it is the one group here that is not about
   putting something into the basket. */
.returns-cat .cat-head span{color:#7a3c10;font-weight:800}
.returns-cat .cat-head::after{
  content:'';flex:1;height:1px;
  background:color-mix(in srgb, var(--alert) 35%, transparent);
}
.return-note{font-size:.78rem;color:var(--ink-soft)}

/* ---- one shop's settings ---- */
/* Its own sheet now. It began as a disclosure inside the hours sheet, which had
   the two the wrong way round: hours are checked in passing, mid-trip, and
   everything here is the once-a-year work of naming or removing a shop. */
.store-inner{width:100%;max-width:380px;overflow-y:auto}
.store-sub-h{
  margin:.9rem 1rem .1rem;font-size:.72rem;font-weight:800;
  letter-spacing:.06em;text-transform:uppercase;color:var(--ink-soft);
}
.store-sub-h:first-of-type{margin-top:.4rem}
.store-inner .sheet-sub{margin:.15rem 0 .6rem}
.store-inner .alias-list,
.store-inner .alias-add,
.store-inner .alias-merge,
.store-inner .alias-bin{margin-left:1rem;margin-right:1rem}
.alias-list{list-style:none;margin:0 0 .55rem;padding:0;display:flex;flex-wrap:wrap;gap:.35rem}
.alias-item{
  display:inline-flex;align-items:center;gap:.1rem;
  background:var(--sea-tint);color:var(--ink);
  border-radius:999px;padding:.15rem .2rem .15rem .55rem;font-size:.85rem;
}
.alias-x{
  background:none;border:none;padding:0 .3rem;cursor:pointer;
  color:var(--ink-soft);font:inherit;font-size:1.1rem;line-height:1;
}
.alias-x:hover{color:var(--danger)}
.alias-none{color:var(--ink-soft);font-size:.85rem}
.alias-add,.alias-merge{display:flex;gap:.4rem;align-items:center;margin-bottom:.45rem}
.alias-merge{flex-wrap:wrap}
/* Its own line. Label, shop and button on one row leaves nothing any of them
   can give up on a 375px phone, and the button is what gets pushed off — it
   wrapped alone underneath, reading as an action belonging to nothing. */
.alias-merge-label{flex:1 0 100%;font-size:.85rem;color:var(--ink-soft)}
input.alias-in{flex:1 1 7rem}
.alias-merge select{flex:1 1 6rem}
.alias-add .link-btn,.alias-merge .link-btn{flex:none}
input.alias-in,.alias-merge select{
  min-width:0;margin:0;padding:.35rem .5rem;
  font:inherit;font-size:.9rem;
  border:1.5px solid var(--paper-line);border-radius:9px;background:#fff;color:var(--ink);
}
input.alias-in:focus,.alias-merge select:focus{outline:none;border-color:var(--sea)}
.alias-merge .link-btn:disabled,.alias-merge select:disabled{opacity:.45;cursor:default}
/* The cost of deleting, in front of the button rather than only in the confirm
   — it is the entire decision, and a confirm is read after the mind is made up. */
.alias-bin{display:flex;flex-wrap:wrap;gap:.1rem .5rem;align-items:baseline}
.alias-bin-note{font-size:.8rem;color:var(--ink-soft)}
/* The sheet grew a second section, so on a short screen it has to be able to
   scroll rather than push its own Save button off the bottom. */
#hours-modal .modal-inner{overflow-y:auto}
.row{
  display:flex;align-items:center;gap:.85rem;
  padding:.85rem .2rem;border-bottom:1px solid var(--paper-line);
  min-height:var(--tap);
}
.check{
  width:26px;height:26px;flex:none;border-radius:8px;
  border:2px solid var(--sea);background:#fff;cursor:pointer;
  display:grid;place-items:center;padding:0;
}
.check svg{width:16px;height:16px;stroke:#fff;opacity:0}
.row.done .check{background:var(--sea);border-color:var(--sea)}
.row.done .check svg{opacity:1}
.row-icon{
  flex:none;width:1.5rem;text-align:center;font-size:1.1rem;line-height:1.25;
  /* Reserved even when empty so item names line up down the column. */
}
/* No frame, and no ground under it.
 *
 * The border was drawing a box round something that already has edges, and on a
 * cut-out it drew a box round mostly nothing — a rectangle in mid-air with a
 * logo floating inside it. The paper fill went with it for the same reason:
 * a row has no background of its own, so on an ordinary photo it was invisible
 * and on a transparent one it was quietly undoing the transparency. */
img.row-thumb{
  width:1.9rem;height:1.9rem;object-fit:cover;border-radius:6px;
}
.row.done .row-icon{opacity:.4}
/* Name (and note) share the flexible column; the label itself must not grow,
   or the editor input stretches vertically inside this column flexbox. */
.row-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:.1rem}
.row-item{font-size:1.05rem;line-height:1.25;cursor:text;text-align:left}
/* Capitalisation is done in JS (displayName), not with ::first-letter, which
   would turn "iPhone charger" into "IPhone charger". */
/* Shares the meta line with the face and the sprout, so it takes what is left
   and truncates there rather than pushing them off the row. */
.row-note{
  font-size:.78rem;color:var(--ink-soft);line-height:1.3;
  flex:1 1 auto;min-width:0;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.row-sep{font-size:.78rem;color:var(--ink-soft);opacity:.7;line-height:1;flex:none}
.row.done .row-note{opacity:.6}
/* A button, because it is the shortest way into the count field. */
/* Read at arm's length in a shop, so it is a size you can take in without
   looking for it. */
.row-qty{
  flex:none;font-family:ui-monospace,"SF Mono",Menlo,monospace;
  font-size:1rem;font-weight:800;color:var(--sea-dark);
  background:var(--sea-tint);border:none;border-radius:8px;padding:4px 10px;
  cursor:pointer;line-height:1.15;
}
button.row-qty:active{background:var(--sea);color:#fff}
.row-qty:empty{display:none}
/* The drawn unit glyph, sized to sit where an emoji would.
   1.05em rather than 1em: the emoji in this badge are drawn to fill their box
   and this one keeps clear margins inside its 24x24, so matching the font size
   exactly would leave it visibly the smaller of the two. currentColor makes it
   follow the badge through :active, where the text goes white on green and a
   fixed fill would have stayed dark. */
.row-qty .unit-mark{
  /* Height, and let the width follow the shape. A square box would pad a tall
     narrow bottle with air on both sides and push the count away from it. */
  height:1.15em;width:auto;display:inline-block;vertical-align:-.2em;
  margin-left:.15em;
}
.row-qty:has(.unit-mark:only-child){padding:3px 7px}
.more{
  width:34px;height:34px;flex:none;display:grid;place-items:center;
  background:transparent;border:none;color:var(--ink-soft);cursor:pointer;border-radius:8px;
}
.more:active{background:var(--paper-line)}
/* With a mouse there is a hover state to use, so the row carries no permanent
   furniture: the pencil and the trash both appear when you point at it. */
@media (pointer:fine){
  .more{opacity:0;transition:opacity .12s ease}
  .row:hover .more,
  .more:focus-visible{opacity:1}
}
/* And on a touch screen it goes entirely. There is no hover to reveal it, so
   it sat there permanently on every row — a column of pencils down the list,
   each one a small target beside a large one that does the same thing, since
   tapping the row opens the same panel. */
@media (pointer:coarse){
  .row .more{display:none}
}

/* ---- category sub-groups (nested inside a store) ---- */
.cat-group{margin-top:.5rem}
.cat-head{
  display:flex;align-items:center;gap:.6rem;
  font-size:.7rem;font-weight:800;letter-spacing:.1em;text-transform:uppercase;
  color:var(--ink-soft);padding:.45rem .2rem .15rem;
}
.cat-head::after{content:"";flex:1;height:1px;background:var(--paper-line)}

/* ---- the item editor ---- */
.item{border-bottom:1px solid var(--paper-line)}
.item .row{border-bottom:none}
#edit-body{display:flex;flex-direction:column;gap:.7rem;margin-bottom:.2rem}
/* The label column is fixed so the fields line up down the dialog rather than
   stepping in and out with the length of each word. */
.detail-row{display:flex;align-items:center;gap:.8rem}
.detail-row .detail-label{flex:none;width:5.2rem}
.detail-label{font-size:.78rem;font-weight:800;letter-spacing:.08em;
  text-transform:uppercase;color:var(--ink-soft)}
textarea.notes{
  width:100%;margin:0;padding:.6rem .7rem;font:inherit;font-size:.95rem;
  border:1.5px solid var(--paper-line);border-radius:10px;background:#fff;
  color:var(--ink);resize:vertical;
}
textarea.notes:focus{outline:none;border-color:var(--sea)}
/* The label sits at the top of a box that can grow, not floating beside its
   middle. */
.notes-row{align-items:flex-start}
.notes-row .detail-label{margin-top:.6rem}
.link-btn{
  background:transparent;border:none;padding:.3rem 0;cursor:pointer;
  color:var(--sea);font:inherit;font-size:.88rem;font-weight:700;
}
.link-btn.danger{color:var(--danger)}
.link-btn:active{opacity:.6}
.row-item:focus-visible{outline:2px solid var(--sea);outline-offset:3px;border-radius:4px}
.row.done .row-item{color:var(--done);text-decoration:line-through}
/* ---- swipe a row to delete ---- */
/* The track clips; the row slides over Delete sitting behind its right edge.
   pan-y leaves vertical scrolling to the browser and gives us the horizontal
   axis, which is what keeps a swipe from fighting the list. */
.swipe{position:relative;overflow:hidden}
.swipe .row{
  position:relative;background:var(--paper);
  touch-action:pan-y;transition:transform .18s ease;
}
/* One disc, wherever delete appears. Discreet enough to sit in a list without
   shouting, unmistakable enough not to be pressed by accident. */
.trash-btn{
  width:38px;height:38px;flex:none;border-radius:50%;
  display:grid;place-items:center;
  background:var(--danger);color:#fff;border:none;cursor:pointer;
}
.trash-btn:active{filter:brightness(.9)}
.trash-btn:focus-visible{outline:2px solid var(--danger);outline-offset:2px}
/* Behind the row, centred in the strip the swipe uncovers. */
.row-delete{position:absolute;top:50%;right:15px;transform:translateY(-50%)}

/* Delete on a computer: nothing until the pointer is over the row, then a
   small trash at its edge. Hidden outright on touch, where the swipe is the
   gesture and this would just sit there being pressed by accident. */
.row-hover-del{display:none}
@media (pointer:fine){
  .row-hover-del{
    display:grid;width:30px;height:30px;margin-left:.1rem;
    opacity:0;transition:opacity .12s ease;
  }
  .row:hover .row-hover-del,
  .row-hover-del:focus-visible{opacity:1}
}

/* Unreachable until the row has moved, so a stray tap on a closed row cannot
   land on it. */
.swipe:not(.open) .row-delete{pointer-events:none}

/* ---- changing your mind ---- */
/* The mirror of delete, in every sense. Delete is parked behind the right edge
   and uncovered by swiping left; this is parked behind the left edge and
   uncovered by swiping right. Both take the row off the list, and which one you
   reach for is whether the errand was wrong or is simply not happening today.
 *
 * Solid, and a muted slate rather than a colour that shouts. Red is the delete
 * on the other side and orange is Returns; this is neither an alarm nor an
 * errand, it is a shrug. Solid still matters though — an earlier disc on this
 * side was sea-tint on paper, which is two near-identical creams, and it was
 * correctly positioned, fully opaque and effectively invisible. */
.skip-btn{
  width:38px;height:38px;flex:none;border-radius:50%;
  display:grid;place-items:center;padding:0;cursor:pointer;
  border:none;background:var(--ink-soft);color:#fff;
}
.skip-btn:active{filter:brightness(.9)}
.skip-btn:focus-visible{outline:2px solid var(--ink-soft);outline-offset:2px}
.row-skip{position:absolute;top:50%;left:15px;transform:translateY(-50%)}
/* Unreachable until the row has moved, so a stray tap on a closed row cannot
   land on it — the same guard the trash has on the other side. */
.swipe:not(.open-skip) .row-skip{pointer-events:none}

/* On a computer: nothing until the pointer is over the row. Hidden outright on
   touch, where the swipe is the gesture and this would sit there being pressed
   by accident — the same bargain the trash makes. */
.row-hover-skip{display:none}
@media (pointer:fine){
  .row-hover-skip{
    display:grid;width:30px;height:30px;margin-left:.1rem;
    opacity:0;transition:opacity .12s ease;
  }
  .row:hover .row-hover-skip,
  .row-hover-skip:focus-visible{opacity:1}
}

/* The footnote a skipped row keeps, on the meta line beside the face and the
   sprout. Muted and small on purpose: the row has already left the list, and
   this only answers which of the two ways it left. */
.row-skipmark{
  flex:none;display:inline-grid;place-items:center;
  color:var(--ink-soft);opacity:.75;
}

/* ---- empty ---- */
.empty{
  text-align:center;color:var(--ink-soft);padding:3.5rem 1.5rem;
}
.empty .mark{font-size:2.4rem;margin-bottom:.4rem;opacity:.5}
.empty h2{margin:.2rem 0;color:var(--ink);font-weight:800}

/* ---- menu flyout ---- */
/* Invisible click-catcher: a dropdown this small doesn't warrant a dim scrim. */
.scrim{position:fixed;inset:0;background:transparent;z-index:8}
/* Anchored to .topbar, which is position:sticky and so is a containing block. */
.flyout{
  position:absolute;top:100%;right:.6rem;z-index:9;margin-top:.3rem;
  min-width:232px;max-width:calc(100vw - 1.2rem);
  background:#fff;color:var(--ink);
  border:1px solid var(--paper-line);border-radius:12px;
  box-shadow:0 6px 28px rgba(22,79,66,.20);
  padding:.3rem;
}
.flyout-item{
  width:100%;text-align:left;padding:.75rem 1rem;font-size:.95rem;font-weight:600;
  background:transparent;border:none;border-radius:9px;color:var(--ink);cursor:pointer;
  min-height:var(--tap);
}
.flyout-item:active{background:var(--paper)}
.flyout-item.danger{color:var(--danger)}
/* The one row in here that is an offer rather than a setting, and the one
   nobody thinks to go looking for — installing is what turns this from a tab
   into an app that opens from the home screen and works with no signal. So it
   is tinted rather than left to sit among the settings looking like one.
 *
 * Sea tint, not gold: gold already means something in this app — the trip
 * marker and the All chip — and a second meaning would cost more than it buys.
 * The width is spelled out rather than left to auto: a <button> shrink-to-fits
 * its content even at display:flex, so auto gave a tinted block the width of
 * the words sitting in a menu twice as wide. */
#install-btn{
  width:calc(100% - 1rem);margin:.25rem .5rem;
  background:var(--sea-tint);color:var(--sea-dark);
  box-shadow:inset 0 0 0 1.5px rgba(31,111,92,.16);
}
#install-btn:active{background:#d5e6dc}
/* The tint already separates it, so the group's own rule above it just boxes a
   card in between two lines. The one below belongs to the next group and still
   does its job whether this row is showing or not. */
#install-group .flyout-divider{display:none}
.flyout-divider{height:1px;background:var(--paper-line);margin:.3rem .5rem}
/* A button now: tapping the name is how you change your picture. */
button.flyout-user{
  width:100%;display:flex;align-items:center;gap:.7rem;
  padding:.7rem 1rem .6rem;font:inherit;text-align:left;
  background:transparent;border:none;cursor:pointer;
}
button.flyout-user:active{background:var(--paper)}
/* The list that drops out of the name. These styles went with the household
   switcher that used to use them; the avatar picker uses the same shape. */
.hh-menu{display:flex;flex-direction:column;padding:.1rem .5rem .4rem}
/* Same controls as an item's photo, laid out in a row under the name. */
#avatar-menu{flex-direction:row;align-items:center;flex-wrap:wrap;gap:.45rem;padding:.1rem 1rem .7rem}
/* Scoped to the picker so it beats img.photo-thumb's 10px radius, which is
   defined later in the file and would otherwise win on source order. */
#avatar-menu img.photo-thumb{
  border-radius:50%;
  /* The one that keeps an edge. A face cropped to a circle on a white sheet
     has no corners to end at, and this is a picture OF somebody rather than a
     picture of a thing on a list. */
  border:1px solid var(--paper-line);
}
.flyout-user-text{display:flex;flex-direction:column;line-height:1.2;min-width:0}
.flyout-user-name{font-weight:800;font-size:.98rem;color:var(--ink);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* The address and the household below the name. Either is hidden when empty,
   so the row never leaves a blank line behind. */
.flyout-user-sub{font-size:.76rem;color:var(--ink-soft);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.flyout-user-sub:empty{display:none}
/* The household gets a little air above it and a little more weight. The
   address identifies the account and is read once; the household says which
   list you are looking at, which is the line you check at a glance. */
#menu-household-name{margin-top:3px;font-weight:600}
/* The "Hide completed" checkbox that used to sit here moved into the top bar in
   3.41.0, where the thing it changes is in view while you change it. */
/* Shared icon column so labels line up across the buttons. */
.flyout-icon{flex:none;color:var(--ink-soft)}
.flyout-item.danger .flyout-icon{color:var(--danger)}
.flyout-item{display:flex;align-items:center;gap:.7rem}
/* One of them is an <a> now. Everything above was written for a <button>, which
   carries none of a link's own decoration, and box-sizing keeps the padding
   inside the 100% width rather than pushing the row past the flyout's edge. */
a.flyout-item{text-decoration:none;box-sizing:border-box}
/* The leave-the-app arrow, pushed to the far end and quieter than the label:
   it qualifies the item rather than competing with it. */
.flyout-out{margin-left:auto;flex:none;color:var(--ink-soft)}

/* ---- avatar ---- */
.avatar{
  display:grid;place-items:center;flex:none;overflow:hidden;
  width:36px;height:36px;border-radius:50%;
  background:var(--sea-tint);color:var(--sea-dark);
  font-size:.82rem;font-weight:800;letter-spacing:.02em;
}
/* Already square from the crop; object-fit is the guard against an older
   picture uploaded before the cropping existed. */
img.avatar-img{width:100%;height:100%;object-fit:cover;display:block}
/* The tint would show as a ring around a photo that does not quite fill it. */
.avatar.has-photo{background:transparent}
.avatar-btn{position:relative}
/* A hairline ring standing off the picture rather than a band drawn on it:
   the first shadow is the gap, painted in whatever is behind, and the second is
   the ring itself. Nothing to clip, so a photo keeps its full circle. */
.avatar-btn .avatar{
  background:rgba(255,255,255,.18);color:#fff;
  box-shadow:0 0 0 2px var(--sea), 0 0 0 3px rgba(255,255,255,.45);
}
/* A new version is waiting. A dot, not a banner — it can be ignored for a week
   without costing anything. The ring is the bar's own colour, so the dot reads
   as sitting on the avatar rather than floating near it. */
.avatar-btn.has-update::after{
  content:"";position:absolute;top:7px;right:7px;
  width:9px;height:9px;border-radius:50%;
  background:var(--alert);box-shadow:0 0 0 2px var(--sea);
}
/* Same dot in the menu, so the row that resolves it is the row that is marked. */
#update-btn.has-update::after{
  content:"";width:8px;height:8px;border-radius:50%;
  background:var(--alert);margin-left:.1rem;
}
/* In the menu the gap is white, not sea green, because that is what is behind
   it there. */
.avatar-lg{
  width:46px;height:46px;font-size:1rem;
  box-shadow:0 0 0 2px #fff, 0 0 0 3px var(--paper-line);
}

/* ---- footer ---- */
.app-footer{
  position:fixed;left:0;right:0;bottom:0;z-index:3;
  display:flex;align-items:center;justify-content:center;gap:.4rem;flex-wrap:wrap;
  padding:.5rem 1rem calc(env(safe-area-inset-bottom,0) + .5rem);
  background:rgba(247,244,236,.94);
  border-top:1px solid var(--paper-line);
  font-size:.72rem;color:var(--ink-soft);
}
@supports (backdrop-filter:blur(6px)){
  .app-footer{backdrop-filter:blur(6px)}
}
.foot-name{font-weight:800;color:var(--ink)}
.foot-version{font-family:ui-monospace,"SF Mono",Menlo,monospace;font-weight:600;color:var(--ink-soft)}
.foot-sep{color:var(--sea);font-weight:900;opacity:.85;padding:0 .1rem}
.app-footer a{color:var(--sea);font-weight:700;text-decoration:none}
.app-footer a:hover,.app-footer a:focus-visible{text-decoration:underline}

/* ---- toast ---- */
.toast{
  position:fixed;left:50%;transform:translateX(-50%);
  bottom:calc(env(safe-area-inset-bottom,0) + 3.6rem);z-index:20;
  background:var(--ink);color:#fff;padding:.55rem .7rem .55rem 1.1rem;border-radius:10px;
  font-size:.9rem;font-weight:600;box-shadow:var(--shadow);max-width:90vw;
  display:flex;align-items:center;gap:.9rem;
}
.toast-msg{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* A filled pill, not tinted text. Undo has five seconds to be noticed and
   pressed, and as flat text on a dark toast it read as part of the sentence.
   Dark ink on the teal rather than white: at this size and weight white would
   sit around 3:1 against it, and this is nearer 7:1. */
.toast-action{
  flex:none;background:var(--teal);border:none;cursor:pointer;
  color:#0d2b24;font:inherit;font-weight:800;
  text-transform:uppercase;letter-spacing:.07em;font-size:.78rem;
  padding:.42rem .85rem;border-radius:999px;
}
.toast-action:active{filter:brightness(.92)}

/* A newly added item can be filed anywhere once it lands in its category, so
   it is scrolled into view and briefly tinted to say "it went here". */
.item.just-added{background:var(--sea-tint);border-radius:8px}

@media (prefers-reduced-motion:no-preference){
  .row{transition:opacity .15s}
  .toast{animation:rise .2s ease}
  @keyframes rise{from{opacity:0;transform:translate(-50%,8px)}to{opacity:1;transform:translate(-50%,0)}}
  .item.just-added{animation:settle 2.5s ease forwards}
  @keyframes settle{0%,55%{background:var(--sea-tint)}100%{background:transparent}}
}

/* category corrector inside the details panel */
select.cat-select{
  flex:1;min-width:0;margin:0;padding:.55rem .6rem;font:inherit;font-size:.92rem;
  border:1.5px solid var(--paper-line);border-radius:10px;background:#fff;color:var(--ink);
}
select.cat-select:focus{outline:none;border-color:var(--sea)}

/* offline / pending-sync status strip */
.sync-bar{
  padding:.4rem .9rem;font-size:.74rem;font-weight:700;letter-spacing:.02em;
  color:var(--sea-dark);background:var(--sea-tint);
  border-bottom:1px solid var(--paper-line);text-align:center;
}
.sync-bar.off{color:#fff;background:var(--danger)}
/* an item that exists only in the local queue so far */
.item .row.pending .row-item{opacity:.75}
.row-pending-dot{
  flex:none;width:6px;height:6px;border-radius:50%;background:var(--gold);
}

/* a selected store filter */
.chip.active{
  background:var(--gold);border-color:var(--gold);color:#fff;
  box-shadow:0 0 0 1px rgba(0,0,0,.05) inset;
}
.chip.active .chip-count{opacity:.9}
/* dimming is for "nothing left to buy"; never dim the chip you just picked */
.chip.active.done{opacity:1}
.empty .btn-primary{max-width:220px;margin:1.2rem auto 0}

/* ---- emoji picker ---- */
.row-icon-btn{
  background:transparent;border:none;cursor:pointer;padding:0;
  font-size:1.1rem;line-height:1.25;border-radius:6px;
}
.row-icon-btn.empty{
  color:var(--ink-soft);opacity:.45;font-weight:700;
  border:1px dashed var(--paper-line);
}
.row-icon-btn:active{background:var(--paper-line)}
.scrim.dim{background:rgba(22,79,66,.35)}
.sheet-title{margin:.2rem 0 0;font-size:1.05rem;font-weight:800;padding:0 1rem}
.sheet-sub{margin:.15rem 0 .8rem;padding:0 1rem;color:var(--ink-soft);font-size:.85rem}
.emoji-grid{
  display:grid;grid-template-columns:repeat(auto-fill,minmax(46px,1fr));gap:.3rem;
  padding:0 .6rem;max-height:42vh;overflow-y:auto;
}
.emoji-opt{
  aspect-ratio:1;display:grid;place-items:center;font-size:1.4rem;
  background:var(--paper);border:1px solid var(--paper-line);border-radius:10px;cursor:pointer;
}
.emoji-opt:active{background:var(--sea-tint)}
.emoji-opt.current{border-color:var(--sea);background:var(--sea-tint)}
.emoji-any{
  display:block;margin:.9rem 1rem .2rem;font-size:.8rem;font-weight:600;color:var(--ink-soft);
}
.emoji-any input{font-size:1.3rem;text-align:center}
.sheet-actions{display:flex;justify-content:space-between;padding:.4rem 1rem .2rem}

/* ---- app icon in the brand ---- */
/* Inline SVG, not the PNG: the PNG brings its green tile and blob with it,
   which read as a background and shadow once placed on the green bar. */
svg.brand-mark{
  width:2.3em;height:2.3em;flex:none;display:block;
  color:#fff;                 /* body and handle; the rim stays brass */
  background:none;box-shadow:none;border-radius:0;
}
svg.brand-mark::after{content:none}
.login-card svg.brand-mark{width:2.8em;height:2.8em;color:var(--sea)}

/* ---- store field in the details panel ---- */
/* Type into it or pick from it. The chevron is drawn here, matching the two
   real selects beside it, because a datalist gives no affordance of its own —
   without it the field looks like somewhere you must type the name in full. */
.store-combo{flex:1;min-width:0;display:flex;position:relative}
/* The chevron is a real button now, not a painted-on hint: a datalist opened
   itself only for what had been typed, so it could not offer the full list. */
.store-open{
  position:absolute;right:1px;top:1px;bottom:1px;width:2.1rem;
  display:flex;align-items:center;justify-content:center;
  border:none;background:transparent;cursor:pointer;border-radius:0 9px 9px 0;
}
.store-open::after{
  content:'';width:17px;height:17px;
  background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%235d6b64' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E") no-repeat center/17px 17px;
}
.store-open:active{background:var(--paper)}
/* Every shop, every time it opens. Scrolls rather than growing without limit —
   a household that shops in a dozen places should not get a menu taller than
   the panel it is opening inside. */
/* Fixed, and placed by storeCombo() — see place() there. Both modals clip an
   absolutely positioned child, so hanging it off the field cut it in half.
   Above the modal's z-index of 9, below the full-size photo viewer. */
.store-menu{
  position:fixed;z-index:20;
  overflow-y:auto;
  background:#fff;border:1.5px solid var(--paper-line);border-radius:10px;
  box-shadow:0 8px 22px rgba(0,0,0,.16);
  padding:.25rem;
}
.store-opt{
  display:block;width:100%;text-align:left;
  padding:.5rem .6rem;border:none;background:transparent;border-radius:7px;
  font:inherit;font-size:.92rem;color:var(--ink);cursor:pointer;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.store-opt:hover{background:var(--paper)}
.store-opt.on{background:var(--sea-tint);font-weight:700}
/* Not a shop — the way back to an item that belongs to none of them. */
.store-opt.none{color:var(--ink-soft);font-style:italic;
  border-top:1px solid var(--paper-line);border-radius:0 0 7px 7px;margin-top:.2rem}
/* The same field in the add box, where it is a block in a label rather than a
   flex item, so it has to claim the width itself. */
.ask-field input.store-input{width:100%}
input.store-input{
  flex:1;width:auto;min-width:0;margin:0;
  padding:.55rem 2.2rem .55rem .6rem;font-size:.95rem;
  border:1.5px solid var(--paper-line);border-radius:10px;background:#fff;color:var(--ink);
}
input.store-input:focus{outline:none;border-color:var(--sea)}

/* ---- help ---- */
.help{
  position:fixed;inset:0;z-index:30;background:var(--paper);
  display:flex;flex-direction:column;
}
.help-bar{
  display:flex;align-items:center;justify-content:space-between;
  padding:calc(env(safe-area-inset-top,0) + .7rem) .6rem .7rem 1rem;
  background:var(--sea);color:#fff;flex:none;
}
.help-bar h2{margin:0;font-size:1.1rem;font-weight:800}
/* The scroller spans the full width and the column is centred with padding,
   rather than the scroller BEING the 640px column — that put its scrollbar at
   the column's edge, which on a wide screen is the middle of the page. */
.help-body{
  flex:1;overflow-y:auto;width:100%;
  padding:1rem max(1rem, calc((100% - 640px) / 2))
          calc(env(safe-area-inset-bottom,0) + 2rem);
}
.help-body h3{
  margin:1.4rem 0 .5rem;font-size:.76rem;font-weight:800;letter-spacing:.12em;
  text-transform:uppercase;color:var(--sea-dark);
}
.help-body h3:first-child{margin-top:.2rem}
/* .help, .help-bar and .help-body outlive the help sheet they were named for:
   other panels are built out of them too. The list styles that went with the
   sheet's own content left with it in 3.37.1. */

/* ---- waiting ---- */
/* One spinner, used by every panel that fetches before it can draw. It was the
   word "Loading…" in the top-left corner, which reads as a failed heading more
   than as progress. Centred on both axes: a panel that is empty apart from this
   has nowhere else for the eye to go. */
/* 100%, not a vh guess: .help-body is flex:1 inside a fixed panel, so its
   height is definite and the percentage resolves against it. A viewport
   fraction centres on the viewport, which is not the box anyone is looking at. */
.loading{display:grid;place-items:center;min-height:100%}
.spinner{
  width:34px;height:34px;border-radius:50%;
  border:3px solid var(--sea-tint);border-top-color:var(--sea);
}
@media (prefers-reduced-motion:no-preference){
  .spinner{animation:spin .8s linear infinite}
}
/* Still says something when the animation is off, or it is a grey ring that
   never resolves. */
@media (prefers-reduced-motion:reduce){
  .spinner{border-top-color:var(--sea);opacity:.65}
}
@keyframes spin{to{transform:rotate(360deg)}}

/* ---- centred modal (emoji picker) ---- */
.modal{
  position:fixed;inset:0;z-index:9;
  display:flex;align-items:center;justify-content:center;
  padding:1rem;pointer-events:none;      /* the scrim behind takes the taps */
}
.modal-inner{
  pointer-events:auto;
  background:#fff;border-radius:16px;
  padding:1rem .6rem .8rem;
  width:100%;max-width:420px;max-height:86vh;
  display:flex;flex-direction:column;
  box-shadow:0 18px 50px rgba(22,79,66,.28);
}
.modal-inner .emoji-grid{flex:1;max-height:none}
.emoji-preview{
  font-size:2.2rem;line-height:1;text-align:center;
  margin:.2rem 0 .7rem;min-height:2.4rem;color:var(--ink-soft);
}
.sheet-actions-right{display:flex;align-items:center;gap:.9rem}
.btn-inline{width:auto;min-height:38px;padding:.45rem 1.1rem;font-size:.9rem;border-radius:9px}
.btn-primary:disabled{opacity:.45;cursor:default}


/* The picker must clear the sticky header (.topwrap is z-index 10), or the
   title bar paints over the top of the modal and hides its heading. */
/* Above the item editor (42/43), which is what opens it — at 20 it appeared
   behind the dialog and looked like nothing had happened. */
#emoji-scrim{z-index:46}
#emoji-sheet.modal{z-index:47}

/* ---- details panel: consistent control sizing ---- */

/* One custom chevron for every select, so it sits where we put it instead of
   wherever the platform decides. The native arrow was the misaligned one. */
select.cat-select{
  appearance:none;-webkit-appearance:none;-moz-appearance:none;
  flex:1;min-width:0;margin:0;
  padding:.5rem 2.2rem .5rem .6rem;
  font:inherit;font-size:.95rem;line-height:1.3;
  border:1.5px solid var(--paper-line);border-radius:10px;
  background-color:#fff;color:var(--ink);
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%235d6b64' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  background-repeat:no-repeat;
  background-position:right .65rem center;
  background-size:17px 17px;
}
select.cat-select:focus{outline:none;border-color:var(--sea)}

.emoji-any-row{display:flex;align-items:baseline;justify-content:space-between;gap:1rem}
.emoji-any-row a{
  color:var(--sea);font-weight:700;text-decoration:none;font-size:.78rem;white-space:nowrap;
}
.emoji-any-row a:hover{text-decoration:underline}

/* ---- full-size photo ---- */
.photo-viewer{
  /* Above everything, including the item editor at 43 that usually opens it —
     at 40 the editor painted straight over the photo it had just launched.
     It is a full-screen overlay that any tap dismisses, so nothing needs to
     sit on top of it. */
  position:fixed;inset:0;z-index:48;
  background:rgba(18,26,23,.94);
  display:flex;align-items:center;justify-content:center;
  padding:1.2rem;cursor:zoom-out;
}
.photo-viewer img{
  max-width:100%;max-height:82vh;border-radius:12px;display:block;
  box-shadow:0 20px 60px rgba(0,0,0,.5);
}
.photo-close{
  position:absolute;top:calc(env(safe-area-inset-top,0) + .6rem);right:.6rem;
  width:var(--tap);height:var(--tap);display:grid;place-items:center;
  background:transparent;border:none;color:#fff;cursor:pointer;border-radius:10px;
}
.photo-close:active{background:rgba(255,255,255,.15)}
.photo-caption{
  position:absolute;left:0;right:0;
  bottom:calc(env(safe-area-inset-bottom,0) + 1rem);
  display:flex;flex-direction:column;align-items:center;gap:.15rem;
  text-align:center;padding:0 1.5rem;
}
.photo-caption-name{color:#fff;font-size:.9rem;font-weight:700;opacity:.9}
/* Quieter than the name, and wrapping, because a note can be a sentence. */
.photo-caption-notes{
  color:#fff;font-size:.82rem;font-weight:500;opacity:.72;
  line-height:1.35;max-width:34rem;
}
img.row-thumb{cursor:zoom-in}
img.row-thumb:focus-visible{outline:2px solid var(--sea);outline-offset:2px}

/* ---- aisle labels ---- */
/* font:inherit matters — a bare <input> uses the system UI font, so without it
   the count sat in a visibly different typeface from the Category select
   directly beneath it. Same size and line-height as the selects, too. */
input.qty-in,
input.price-in{
  flex:1;min-width:0;margin:0;padding:.5rem .6rem;
  font:inherit;font-size:.95rem;line-height:1.3;
  border:1.5px solid var(--paper-line);border-radius:10px;background:#fff;color:var(--ink);
}
/* Right-aligned, as numbers in a field usually are. The box measures exactly
   the same as the Category select beneath it — 38.76px, padding 8/8, border
   1.5 — it only reads taller because a short box next to wide ones does. */
input.qty-in,
input.price-in{width:4.5rem;max-width:4.5rem;flex:none;text-align:right}
input.qty-in:focus,
input.price-in:focus{outline:none;border-color:var(--sea)}
/* On the category heading, and editable there. */
.aisle-tag{
  flex:none;font:inherit;font-size:.66rem;font-weight:800;letter-spacing:.04em;
  text-transform:none;color:var(--sea-dark);background:var(--sea-tint);
  border:none;border-radius:5px;padding:2px 6px;cursor:pointer;
}
.aisle-tag:active{background:var(--sea);color:#fff}
/* Unset: present enough to find, quiet enough not to clutter every heading. */
.aisle-tag.empty{background:transparent;color:var(--ink-soft);opacity:.55;font-weight:700}
.aisle-tag.empty:active{background:var(--paper-line);color:var(--ink);opacity:1}
input.aisle-tag-edit{
  flex:none;width:8.5rem;margin:0;padding:2px 6px;
  font:inherit;font-size:.72rem;font-weight:700;
  border:1.5px solid var(--sea);border-radius:5px;background:#fff;color:var(--ink);
}
input.aisle-tag-edit:focus{outline:none}
.cat-head::after{order:9}

/* ---- accounts & households ---- */
.login-switch{
  margin:.9rem 0 0;text-align:center;font-size:.85rem;color:var(--ink-soft);
}
.login-switch .link-btn{font-size:.85rem}
.join-blurb{
  margin:0 0 1.2rem;font-size:.95rem;line-height:1.5;color:var(--ink);
  background:var(--sea-tint);padding:.8rem .9rem;border-radius:10px;
}
/* The active household doubles as the switcher. The chevron only appears when
   tapping actually opens something — see renderHouseholds(). */

/* The usage screen's own styling is NOT here. It is injected by /admin.php,
   behind the same check as the data, for the reason that file gives: a
   stylesheet every visitor downloads should not describe a screen almost none
   of them can open. */

/* Count of pending invites, so they are visible without opening Households. */
.flyout-count{
  margin-left:auto;flex:none;
  font-size:.68rem;font-weight:800;
  color:var(--gold);border:1px solid var(--gold);border-radius:999px;
  min-width:1.2rem;padding:0 5px;text-align:center;
}

/* ---- unconfirmed email ---- */
.verify-bar{
  display:flex;align-items:center;justify-content:center;gap:.7rem;flex-wrap:wrap;
  padding:.45rem .9rem;font-size:.76rem;font-weight:700;
  color:#7a5a12;background:#f6e6bf;border-bottom:1px solid #e6d3a4;
}
.verify-bar .link-btn{font-size:.76rem;color:#7a5a12;text-decoration:underline}
.login-switch .dim{opacity:.5}

/* ---- in-app prompt / confirm ---- */
/* Above .topwrap (10), like every other modal here — the base .modal z-index
   of 9 puts it UNDER the sticky title bar. Below the prompt at 44/45 so a
   confirm can still appear over it. */
#add-scrim{z-index:40}
#add-modal.modal{
  z-index:41;
  /* Confined to what the keyboard has left, and moving with it: the layout
     viewport does not shrink when the keyboard opens, so centring in it put
     these fields behind the keyboard. bottom:auto because .modal sets inset:0
     and a height cannot win against an opposing bottom. */
  top:var(--vv-top,0px);height:var(--vv-h,100%);bottom:auto;
  /* Top-aligned rather than centred: as the keyboard grows the space shrinks
     from the bottom, and a top-aligned dialog simply stays where it is instead
     of sliding up under the title bar. */
  align-items:flex-start;
  padding-top:max(1rem, env(safe-area-inset-top,0));
  transition:top .15s ease, height .15s ease;
}
/* Room enough to centre it on a desktop, where there is no keyboard to dodge. */
@media (min-height:700px) and (pointer:fine){
  #add-modal.modal{align-items:center;padding-top:1rem}
}
/* The dialog itself scrolls if the remaining space is genuinely too small. */
#add-modal .modal-inner{max-height:100%;overflow-y:auto}

/* Same treatment as the add dialog: above the sticky bar, inside the visible
   viewport so the keyboard cannot bury the field you are typing in. */
#edit-scrim{z-index:42}
#edit-modal.modal{
  z-index:43;
  top:var(--vv-top,0px);height:var(--vv-h,100%);bottom:auto;
  align-items:flex-start;
  padding-top:max(1rem, env(safe-area-inset-top,0));
  transition:top .15s ease, height .15s ease;
}
@media (min-height:700px) and (pointer:fine){
  #edit-modal.modal{align-items:center;padding-top:1rem}
}
.edit-inner{width:100%;max-width:460px;max-height:100%;overflow-y:auto}
/* A hairline between the item and its fields. Enough to say "this names what
   you are editing" without drawing a box around it. */
.edit-head{
  display:flex;align-items:center;gap:.7rem;
  margin:0 0 1rem;padding-bottom:.9rem;
  border-bottom:1px solid var(--paper-line);
}
/* Big enough to read as the item's icon rather than as decoration, and
   obviously pressable. */
/* NOT called .empty — that class is the empty-basket block, whose 3.5rem
   padding turned this into a tall dashed pill. */
.edit-emoji{
  flex:none;width:46px;height:46px;padding:0;border-radius:12px;
  display:grid;place-items:center;font-size:1.5rem;line-height:1;
  background:var(--sea-tint);border:none;cursor:pointer;
}
.edit-emoji:active{background:var(--sea);color:#fff}
/* No icon yet: a dashed well, the same language the list uses. */
.edit-emoji.no-icon{
  background:transparent;border:1.5px dashed var(--paper-line);
  color:var(--ink-soft);font-size:1.15rem;font-weight:700;
}
/* The title IS the name field. Looks like a heading until you touch it. */
input.edit-name{
  flex:1;min-width:0;width:auto;margin:0;padding:.35rem .5rem;
  font:inherit;font-size:1.25rem;font-weight:800;letter-spacing:-.01em;
  color:var(--ink);background:transparent;
  border:1.5px solid transparent;border-radius:9px;
}
input.edit-name:hover{border-color:var(--paper-line)}
input.edit-name:focus{outline:none;background:#fff;border-color:var(--sea)}
/* Photo controls on the left, Done on the right. Deleting is not here —
   that is the swipe on touch and the hover trash with a pointer. */
/* Room between the last field and the buttons, so Done does not read as part
   of the form it closes. */
.edit-actions{align-items:center;justify-content:flex-end;margin-top:1.1rem}
#edit-photo{margin-right:auto;display:flex;align-items:center;gap:.45rem}
.photo-btn{
  width:36px;height:36px;flex:none;padding:0;border-radius:10px;
  display:grid;place-items:center;
  background:var(--sea-tint);color:var(--sea-dark);border:none;cursor:pointer;
}
.photo-btn:active{background:var(--sea);color:#fff}
/* The camera button is shown wherever there IS a camera, which is a question
   for enumerateDevices() rather than for the pointer: a laptop has one and a
   desktop often does not. It starts hidden and JS reveals it, so a machine
   without one never sees a button that cannot work. */

/* ---- taking a picture with a webcam ---- */
.cam-inner{width:100%;max-width:460px}
.cam-stage{
  margin:.8rem 1rem 0;border-radius:14px;overflow:hidden;background:#0d1714;
  aspect-ratio:1/1;display:grid;place-items:center;
}
/* Mirrored, because a preview of your own face that moves the wrong way is
   the one thing everybody notices. The saved frame is mirrored to match, so
   what you framed is what you get. */
#cam-video{width:100%;height:100%;object-fit:cover;transform:scaleX(-1)}
/* Small on purpose: it is a reminder of which packet, not the subject of the
   screen. Full size is one tap away. */
.photo-thumb-wrap{position:relative;flex:none;width:36px;height:36px;margin-right:.25rem}
/* Unframed, like the one on the row. See img.row-thumb. */
img.photo-thumb{
  width:36px;height:36px;object-fit:cover;border-radius:10px;display:block;
  cursor:zoom-in;
}
.photo-del{
  position:absolute;top:-7px;right:-7px;width:20px;height:20px;padding:0;
  border-radius:50%;background:var(--danger);color:#fff;border:2px solid #fff;
  display:grid;place-items:center;cursor:pointer;
  font:inherit;font-size:.8rem;font-weight:700;line-height:1;
}
.photo-del:active{filter:brightness(.9)}

#ask-scrim{z-index:44}
#ask-modal.modal{z-index:45}
.ask-inner{max-width:380px}
.ask-field{display:block;margin:0 1rem .4rem;font-size:.8rem;font-weight:600;color:var(--ink-soft)}
.ask-field input{margin-top:.35rem}
.btn-primary.danger-btn{background:var(--danger)}

/* ---- households screen ---- */
.hh-card{
  background:#fff;border:1px solid var(--paper-line);border-radius:12px;
  padding:.9rem 1rem;margin-bottom:.8rem;
}
.hh-card.current{border-color:var(--sea);box-shadow:0 0 0 1px var(--sea) inset}
.hh-head{display:flex;align-items:baseline;gap:.6rem}
.hh-name{display:flex;align-items:baseline;gap:.5rem;font-weight:800;font-size:1.02rem}
.hh-badge{
  font-size:.62rem;font-weight:800;letter-spacing:.06em;text-transform:uppercase;
  color:var(--sea);border:1px solid var(--sea);border-radius:5px;padding:0 5px;
}
.hh-role{margin-left:auto;font-size:.72rem;color:var(--ink-soft);text-transform:capitalize}
.hh-line{margin:.45rem 0 0;font-size:.86rem;color:var(--ink-soft);line-height:1.4}
/* One member per row, address under the name: names repeat within a family,
   addresses do not, so the address is what tells two accounts apart. */
.hh-member{display:flex;flex-direction:column;gap:.05rem;margin-top:.5rem;min-width:0}
.hh-member-name{
  display:flex;align-items:center;gap:.4rem;
  font-size:.86rem;color:var(--ink);line-height:1.3;
}
.hh-member-email{
  font-size:.76rem;color:var(--ink-soft);line-height:1.3;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.hh-invite{
  display:flex;align-items:center;gap:.6rem;margin-top:.5rem;
  font-size:.82rem;color:var(--ink-soft);
}
/* The address gets its own line: on a phone it is the one thing you need to
   read, and sharing a row with a badge and two buttons truncated it to
   "someone@ex...". Actions sit under it, pushed right. */
.hh-invite{flex-wrap:wrap;row-gap:.15rem}
.hh-invite-to{flex:1 1 100%;min-width:0;word-break:break-all;color:var(--ink)}
.hh-invite .link-btn{flex:none;font-size:.8rem}
.hh-invite .link-btn:first-of-type{margin-left:auto}
.link-btn.danger-link{color:var(--danger)}
.hh-sub{
  margin:1rem 0 0;font-size:.68rem;font-weight:800;letter-spacing:.06em;
  text-transform:uppercase;color:var(--ink-soft);
}
.hh-pending{
  font-size:.62rem;font-weight:800;text-transform:uppercase;letter-spacing:.06em;
  color:var(--gold);border:1px solid var(--gold);border-radius:5px;padding:0 5px;
}
.hh-actions{display:flex;align-items:center;gap:1rem;margin-top:.8rem;flex-wrap:wrap}
.hh-actions .btn-inline{width:auto}
.hh-add{margin-top:1.2rem}
.hh-empty{color:var(--ink-soft);font-size:.9rem}
